В прошлом посте я показал как можно изменить поведение DataGrid’а с помощью кастомного behavior’а. Давайте, взяв за основу созданное там приложениe, решим еще одну задачку.
Сейчас название тега в гриде представлено как простейший DataGridTextColumn. Т.е. в режиме редактирования мы получаем TextBox и можем изменять название как нам угодно. Давайте ограничим свободу ввода тегов, разрешив пользователю только выбирать теги из некоторого списка допустимых значений.
Для начала во ViewModel’ь добавим коллекцию AllTags, которая будет содержать все допустимые названия тегов.
public MainViewModel(IDataProvider provider)
{
Tags = new ObservableCollection<TagInfo>(provider.Tags);
AllTags = new ObservableCollection<string>(Tags.Select(t => t.Name));
}
public ObservableCollection<TagInfo> Tags
{
get;
private set;
}
public ObservableCollection<string> AllTags
{
get;
private set;
}
Теперь заменим колонку в гриде:
<DataGridComboBoxColumn
Header="Tag"
ItemsSource="{Binding AllTags}"
SelectedValueBinding="{Binding Name}" />
Запускаем приложение и видим вот такую картину.
Что случилось? Где потерялись названия тегов? Давайте разберемся. Для начала, как всегда в ситуациях, когда Binding ведет себя не так как мы ожидали, смотрим в окно Output, где находим ошибку:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=AllTags; DataItem=null; target element is 'DataGridComboBoxColumn' (HashCode=29758132); target property is 'ItemsSource' (type 'IEnumerable')
Проблема в том, что колонки грида не принадлежат его визуальному дереву. Соответственно они не могут наследовать от грида никакие свойства. Поэтому не смотря на то, что мы устанавливаем DataContext для самого грида, DataContext для любого из столбцов будет null.
Решение состоит с том, чтобы при изменении DataContext’а для грида, руками устанавливать DataContext для каждого столбца. Т.к. тип DataGridColumn (и интересующие нас производные от него) не содержит свойства DataContext, то мы воспользуемся DataContext’ом из типа FrameworkElement. Новую функциональность мы не будем добавлять в codebehind, а определим как отдельный behavior.
public sealed class PushDataContextToColumnsBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.DataContextChanged += DataGrid_DataContextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.DataContextChanged -= DataGrid_DataContextChanged;
}
private void DataGrid_DataContextChanged(
object sender, DependencyPropertyChangedEventArgs e)
{
foreach (var column in AssociatedObject.Columns)
{
column.SetValue(
FrameworkElement.DataContextProperty,
e.NewValue);
}
}
}
Теперь наш грид будет выглядеть вот так:
<DataGrid
ItemsSource="{Binding Tags}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridComboBoxColumn
Header="Tag"
ItemsSource="{Binding (FrameworkElement.DataContext).AllTags,
RelativeSource={RelativeSource Self}}"
SelectedValueBinding="{Binding Name}" />
<DataGridTextColumn Header="Score" Binding="{Binding Score}" />
</DataGrid.Columns>
<i:Interaction.Behaviors>
<bhvr:PerCellCommitBehavior />
<bhvr:PushDataContextToColumnsBehavior />
</i:Interaction.Behaviors>
</DataGrid>
Запустив приложение мы увидим то, что и ожидали
Как обычно, проект можно скачать отсюда. А вот здесь можно найти альтернативный вариант решения.