Связывание WPF из ItemsControl - FindAncestor не работает
Добрый день,
У меня есть то, что кажется очень распространенной проблемой. У меня есть пользовательский элемент управления, который имеет модель представления в качестве контекста данных. Элементы в этом пользовательском элементе управления затем используют эту ViewModel для целей привязки и т. Д.
ViewModel
public class TicketDetailsViewModel : ViewModelBase
{
public DelegateCommand<object> HideSelectedText { get; private set; }
private Ticket _ticket;
public Ticket Ticket
{
get { return _ticket; }
set
{
_ticket = value;
this.RaisePropertyChanged(p => p.Ticket);
}
}
}
Моя ViewModel содержит один объект Ticket. К этому объекту заявки прикреплена коллекция комментариев, которые передаются в пользовательский элемент управления Ticket Display с использованием ItemsControl.
<ItemsControl ItemsSource="{Binding Path=Ticket.Comments}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="15" Background="{Binding Path=CommentType, ConverterParameter=CommentType, Converter={StaticResource ResourceKey=commentColorConverter}}" Padding="10" Margin="40,10,40,0">
<TextBox x:Name="tbComment" Text="{Binding CommentText}" IsReadOnly="True">
<TextBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Spam" Command="{Binding Path=DataContext.HideSelectedText,RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl} }}">
</MenuItem>
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Вы заметите, что к каждому TextBox, отображаемому этим ItemsControl, прикреплено ContextMenu. То, что я пытаюсь сделать, это связать команду этого ContextMenuItem с DelegateCommand в моей ViewModel. Конечно, просто используя;
<MenuItem Header="Spam" Command="{Binding HideSelectedText}">
Мы не получаем ничего полезного, так как "Binding" в этом контексте равен Ticket.Comment и, следовательно, не имеет представления о том, что такое HideSelectedText.
Кажется, есть много вопросов, похожих на этот, и все ответы, кажется, отклоняются от решения RelativeSource. Как вы можете видеть в моем исходном коде XAML, я попробовал это, а также многие другие его версии (с набором AncestorLevel и без него, с AncestorType={x:Type ItemsControl}, AncestorType={x:Type Window}, AncestorType={x:Type DataTemplate} ect) и ALL выдают ошибку вывода, аналогичную;
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='1''. BindingExpression:Path=DataContext.HideSelectedText; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')
или же
System.Windows.Data Error: 40 : BindingExpression path error: 'HideSelectedText' property not found on 'object' ''TicketComment' (HashCode=49290260)'. BindingExpression:Path=DataContext.HideSelectedText; DataItem='ContextMenu' (Name=''); target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')
Так почему же это решение работает для очень многих людей, и все же для меня, нет никакой разницы, что когда-либо просто печатать {Binding HideSelectedText}
?
3 ответа
ContextMenus на самом деле не является частью VisualTree WPF, поэтому привязки не работают так, как ожидается. В качестве альтернативы попробуйте эту привязку:
<MenuItem Header="Spam" Command="{Binding PlacementTarget.DataContext.HideSelectedText,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}" />
Лучшее решение - сделать это через ресурс. Если ContextMenu будет создано в ресурсе, он автоматически унаследует DataContext от родительского. Тогда нужно создать стиль со значением, передаваемым статическим ресурсом.
Пример:
<DataGrid.Resources>
<ContextMenu x:Key="test_ContextMenu" DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
<MenuItem Header="Test Header" Command="{Binding Path=TestCommand, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
</ContextMenu>
</DataGrid.Resources>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<Setter Property="ContextMenu" Value="{StaticResource test_ContextMenu}"/>
</Style>
</DataGrid.ItemContainerStyle>
Этот код был протестирован на.NET 4.5, но я думаю, что он будет работать и на более ранних версиях.
Поскольку контекстное меню не является частью визуального дерева, оно не наследует DataContext по умолчанию. Вам нужно будет установить DataContext контекстного меню на PlacementTarget, как показано ниже:
<ContextMenu DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext}">
<ContextMenuItem Header="Item1" Command="{Binding YourCommand}" />
</ContextMenu>
Надеюсь это поможет.
-VJ