Я работаю над приложением WPF по шаблону MVVM и сталкиваюсь с проблемой, которую я абстрагировал в приведенном ниже коде.
Приложение содержит DataGrid с 2 столбцами ComboBox (каждый генерируется по-разному). Цель состоит в том, чтобы ComboBox представлял только те элементы, которые еще не были выбраны другими ComboBox в том же столбце.
Комбо-ящики привязаны к наблюдаемой коллекции профессий. Каждая профессия имеет логическое значение «Выбираемый», и ComboBox должен показывать только те записи со значением «true».
Список содержит:
Чтобы смоделировать интерактивную Команду из XAML в ViewModel, я поместил кнопку, которая установит для Выбираемого Ученого значение «ложь».
App.xaml:
<Application x:Class="wpf_ComboBoxColumn.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>
MainWindow.xaml.cs:
using System.Windows;
namespace wpf_ComboBoxColumn
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
MainWindow.xaml:
<Window x:Class="wpf_ComboBoxColumn.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_ComboBoxColumn"
xmlns:viewModel="clr-namespace:wpf_ComboBoxColumn"
Title="Combobox Column Binding" Height="350" Width="460">
<Window.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="ItemsSource"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Selectable}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Selectable}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.DataContext>
<viewModel:MainViewModel />
</Grid.DataContext>
<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridComboBoxColumn
Header="ComboBoxColumn"
SelectedValueBinding="{Binding Description, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
SelectedValuePath="Description"
DisplayMemberPath="Description"
>
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Professions}"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Professions}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<DataGridTemplateColumn Header="TemplateColumn">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=DataContext.Professions, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"
DisplayMemberPath="Description"
SelectedValue="{Binding Profession, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="210,290,0,0" VerticalAlignment="Top" Width="75" Command="{Binding DebugCommand}"/>
</Grid>
</Window>
CustomCommand.cs (реализация ICommand):
using System;
using System.Windows.Input;
namespace wpf_ComboBoxColumn
{
public class CustomCommand: ICommand
{
private readonly Action<object> execute;
public CustomCommand(Action<object> execute)
{
this.execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
MainViewModel.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace wpf_ComboBoxColumn
{
public class NotifyUIBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Profession
{
public string Description { get; set; }
public Boolean Selectable { get; set; }
}
public class Person
{
public string Name { get; set; }
public string Profession { get; set; }
}
public class MainViewModel : NotifyUIBase
{
public ObservableCollection<Person> People { get; set; }
public ObservableCollection<Profession> Professions { get; set; }
public ICommand DebugCommand { get; set; }
public MainViewModel()
{
DebugCommand = new CustomCommand(Debug);
People = new ObservableCollection<Person>
{
new Person{Name="Tom", Profession="" },
new Person{Name= "Dick", Profession="" },
new Person{Name= "Harry", Profession="" }
};
Professions = new ObservableCollection<Profession>
{
new Profession{ Description="Painter", Selectable=true},
new Profession{ Description="Poet", Selectable=true},
new Profession{ Description="Scientist", Selectable=true},
};
}
private void Debug(object obj)
{
Professions[2].Selectable = false;
}
}
}
Теперь рассмотрим следующий сценарий (я все еще пытаюсь выяснить, как включить снимки экрана):
- Откройте приложение: это покажет сетку с 3 столбцами:
Первый столбец показывает имена «Том», «Дик» и «Гарри».
Второй столбец содержит ComboBox для каждого человека. Требуется несколько кликов, чтобы открыть.
Третий столбец также содержит ComboBox для каждого человека. Этот узнаваем как таковой.
- Выберите "Ученый" для Тома
- Нажмите кнопку (чтобы подделать, что мы выполнили код, который изменил Profession.Selectable)
- Нажмите на поле со списком для Дика
- Это действительно покажет оставшиеся Профессии (без Ученого) для самого правого столбца ComboBoxes. В крайнем левом столбце по-прежнему отображаются все параметры, поэтому этот параметр сразу не работает.
- Снова нажмите на комбобокс для Тома
- Это, даже для самого правого столбца ComboBox, снова покажет все опции (или, скорее: все еще)!
Оказывается, что отображенный список не обновляется динамически. Пока мы не нажмем на это, это (заставляет меня думать о квантовой механике, но это уже другая история)
Вопрос: есть ли способ принудительно обновить ItemsSource? Желательно, конечно, уважать MVVM, но на этом этапе я остановлюсь на любом рабочем решении, использующем любой тип ComboBox.
Спасибо!