将 属性 绑定到动态创建的上下文菜单项
Binding property to dynamicaly created item of context menu
我正在尝试制作上下文菜单,其中的项目取决于代码中的某些数据。
所以,我有简单的class,确定菜单的单个项目
class ContextMenuItem
{
public string ItemHeader {get; set;}
public Command ItemAction {get; set;
}
其中 Command 是 ICommand 的实现,并存储动作,一旦选择此项,就会触发该动作。然后我有 class,作为 DataContext
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems {get; set;}
public string SomeProperty {get; set;}
public string SomeAnotherProperty {get; set;}
}
因此,ContextMenuItems 是我在上下文菜单中需要的操作列表,可以使用不同的方法生成。
我正在创建动态上下文菜单,使用这个 approach。
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
所以,我怀疑这个效果很好。但是,出于某种原因,绑定不是我想要的方式。
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
不知何故,此行的数据上下文不是 ContextMenuItem
,而是 SomeClass
本身。所以,我可以在这里绑定 SomeProperty 和 SomeAnotherProperty,但不能绑定 ItemHeader 或 ItemAction。这破坏了动态创建上下文菜单的整个想法。
那么,我怎样才能让这个模板将 ContextMenuItem 识别为它的 DataContext?
我想做的事情可以用DataTemplate来完成,但是它给了我们MenuItem里面的MenuItem,这样不好。
更新
涉及 ListBox
的完整 xaml 代码
<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding ObjectName}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
完成这项工作有一个偷偷摸摸的技巧。通常我只是在绑定中使用一个 RelativeSource 来让它通过 DataContext 连接到某个东西。问题是 ContextMenu 不在可视树层次结构中,因此 RelativeSource 找不到任何东西。
Copy/paste 这个 class 到你的项目某处:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
然后在 Window/UserControl/whatever:
的顶部引用 BindingProxy 的名称空间
xmlns:local="clr-namespace:INSERTYOURNAMESPACEHERE"
将 BindingProxy 作为资源添加到您的 ListBox:
<ListBox.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</ListBox.Resources>
最后将 ContextMenu ItemsSource 的 Source 绑定到代理:
<ContextMenu ItemsSource="{Binding Data.ContextMenuItems, Source={StaticResource proxy}}" >
参考下面的代码。它对我来说很好用。
<Window x:Class="BindingListBox_Learning.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding SomeProperty}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
class MainViewModel
{
public List<SomeClass> SwitchAgents { get; set; }
public MainViewModel()
{
SwitchAgents = new List<SomeClass>();
SomeClass obj = new SomeClass();
obj.SomeProperty = "Test";
List<ContextMenuItem> lst = new List<ContextMenuItem>();
lst.Add(new ContextMenuItem() { ItemHeader = "Hi", ItemAction = new BaseCommand(MenuClick) });
obj.ContextMenuItems = lst;
SwitchAgents.Add(obj);
}
void MenuClick(object obj)
{
// Do Menu Click Stuff
}
}
class ContextMenuItem
{
public string ItemHeader { get; set; }
public ICommand ItemAction { get; set; }
}
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems { get; set; }
public string SomeProperty { get; set; }
public string SomeAnotherProperty { get; set; }
}
public class BaseCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
: this(method, null)
{
}
public BaseCommand(Action<object> method, Predicate<object> canExecute)
{
_method = method;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
您使用 MVVMLight 的 RelayCommand 或 PRISM 的 DelegateCommand,而不是 BaseCommand。
我正在尝试制作上下文菜单,其中的项目取决于代码中的某些数据。 所以,我有简单的class,确定菜单的单个项目
class ContextMenuItem
{
public string ItemHeader {get; set;}
public Command ItemAction {get; set;
}
其中 Command 是 ICommand 的实现,并存储动作,一旦选择此项,就会触发该动作。然后我有 class,作为 DataContext
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems {get; set;}
public string SomeProperty {get; set;}
public string SomeAnotherProperty {get; set;}
}
因此,ContextMenuItems 是我在上下文菜单中需要的操作列表,可以使用不同的方法生成。
我正在创建动态上下文菜单,使用这个 approach。
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
所以,我怀疑这个效果很好。但是,出于某种原因,绑定不是我想要的方式。
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
不知何故,此行的数据上下文不是 ContextMenuItem
,而是 SomeClass
本身。所以,我可以在这里绑定 SomeProperty 和 SomeAnotherProperty,但不能绑定 ItemHeader 或 ItemAction。这破坏了动态创建上下文菜单的整个想法。
那么,我怎样才能让这个模板将 ContextMenuItem 识别为它的 DataContext?
我想做的事情可以用DataTemplate来完成,但是它给了我们MenuItem里面的MenuItem,这样不好。
更新
涉及 ListBox
的完整 xaml 代码<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding ObjectName}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
完成这项工作有一个偷偷摸摸的技巧。通常我只是在绑定中使用一个 RelativeSource 来让它通过 DataContext 连接到某个东西。问题是 ContextMenu 不在可视树层次结构中,因此 RelativeSource 找不到任何东西。
Copy/paste 这个 class 到你的项目某处:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
然后在 Window/UserControl/whatever:
的顶部引用 BindingProxy 的名称空间xmlns:local="clr-namespace:INSERTYOURNAMESPACEHERE"
将 BindingProxy 作为资源添加到您的 ListBox:
<ListBox.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</ListBox.Resources>
最后将 ContextMenu ItemsSource 的 Source 绑定到代理:
<ContextMenu ItemsSource="{Binding Data.ContextMenuItems, Source={StaticResource proxy}}" >
参考下面的代码。它对我来说很好用。
<Window x:Class="BindingListBox_Learning.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding SomeProperty}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
class MainViewModel
{
public List<SomeClass> SwitchAgents { get; set; }
public MainViewModel()
{
SwitchAgents = new List<SomeClass>();
SomeClass obj = new SomeClass();
obj.SomeProperty = "Test";
List<ContextMenuItem> lst = new List<ContextMenuItem>();
lst.Add(new ContextMenuItem() { ItemHeader = "Hi", ItemAction = new BaseCommand(MenuClick) });
obj.ContextMenuItems = lst;
SwitchAgents.Add(obj);
}
void MenuClick(object obj)
{
// Do Menu Click Stuff
}
}
class ContextMenuItem
{
public string ItemHeader { get; set; }
public ICommand ItemAction { get; set; }
}
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems { get; set; }
public string SomeProperty { get; set; }
public string SomeAnotherProperty { get; set; }
}
public class BaseCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
: this(method, null)
{
}
public BaseCommand(Action<object> method, Predicate<object> canExecute)
{
_method = method;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
您使用 MVVMLight 的 RelayCommand 或 PRISM 的 DelegateCommand,而不是 BaseCommand。