这是一个非常复杂的主题,因为WPF中有很多种动态性。我将从一个简单的示例开始,帮助您理解所需的一些基本概念,然后继续解释动态更新和/或替换上下文菜单的各种方式,以及DynamicResource如何适应图片。
初始示例:动态更新通过StaticResource引用的ContextMenu
假设你有以下条件:
<Window>
<Window.Resources>
<ContextMenu x:Key="Vegetables">
<MenuItem Header="Broccoli" />
<MenuItem Header="Cucumber" />
<MenuItem Header="Cauliflower" />
</ContextMenu>
</Window.Resources>
<Grid>
<Ellipse ContextMenu="{StaticResource Vegetables}" />
<TextBox ContextMenu="{StaticResource Vegetables}" ... />
...
</Grid>
</Window>
**注意使用
StaticResource
此XAML将:
-
构造一个包含三个MenuItems的ContextMenu对象,并将其添加到Window.Resources
-
-
构造一个引用ContextMenu的TextBox对象
由于椭圆和文本框都引用了相同的ContextMenu,更新ContextMenu将更改每个文本框上可用的选项。例如,当点击一个按钮时,下面将在菜单中添加“胡萝卜”。
public void Button_Click(object sender, EventArgs e)
{
var menu = (ContextMenu)Resources["Vegetables"];
menu.Items.Add(new MenuItem { Header = "Carrots" });
}
从这个意义上说,每个ContextMenu都是动态的:它的项可以随时修改,更改将立即生效。即使ContextMenu在屏幕上实际打开(下拉)时也是如此。
单个ContextMenu对象是动态的另一种方式是它响应数据绑定。您可以绑定到集合,而不是设置单个菜单项,例如:
<Window.Resources>
<ContextMenu x:Key="Vegetables" ItemsSource="{Binding VegetableList}" />
</Window.Resources>
这假设VegetableList被声明为observeCollection或实现INotifyCollectionChanged接口的其他类型。对集合所做的任何更改都将立即更新ContextMenu,即使它已打开。例如:
public void Button_Click(object sender, EventArgs e)
{
VegetableList.Add("Carrots");
}
请注意,这种集合更新不需要在代码中进行:还可以将蔬菜列表绑定到ListView、DataGrid等,以便最终用户可以进行更改。这些更改也将显示在您的上下文菜单中。
也可以用完全不同的ContextMenu替换项目的ContextMenu。例如:
<Window>
<Window.Resources>
<ContextMenu x:Key="Vegetables">
<MenuItem Header="Broccoli" />
<MenuItem Header="Cucumber" />
</ContextMenu>
<ContextMenu x:Key="Fruits">
<MenuItem Header="Apple" />
<MenuItem Header="Banana" />
</ContextMenu>
</Window.Resources>
<Grid>
<Ellipse x:Name="Oval" ContextMenu="{StaticResource Vegetables}" />
...
</Grid>
</Window>
可以用如下代码替换菜单:
public void Button_Click(object sender, EventArgs e)
{
Oval.ContextMenu = (ContextMenu)Resources.Find("Fruits");
}
public void Button_Click(object sender, EventArgs e)
{
Oval.ContextMenu =
new ContextMenu { ItemsSource = new[] { "Apples", "Bananas" } };
}
在本例中,每次单击按钮时,都会构造一个新的上下文菜单并将其指定给椭圆。Window.Resources中定义的任何ContextMenu仍然存在,但未使用(除非其他控件使用它)。
<Window>
<Window.Resources>
<ContextMenu x:Key="Vegetables">
<MenuItem Header="Broccoli" />
<MenuItem Header="Cucumber" />
</ContextMenu>
</Window.Resources>
<Grid>
<Ellipse ContextMenu="{DynamicResource Vegetables}" />
...
</Grid>
</Window>
因为这个XAML使用DynamicResource而不是StaticResource,所以修改字典将更新椭圆的ContextMenu属性。例如:
public void Button_Click(object sender, EventArgs e)
{
Resources["Vegetables"] =
new ContextMenu { ItemsSource = new[] {"Zucchini", "Tomatoes"} };
}
这里的关键概念是DynamicResource与StaticResource
只有
控制何时查找字典。如果在上述示例中使用了StaticResource,则将
Resources["Vegetables"]
不会更新椭圆的ContextMenu属性。
使用数据绑定更新单个上下文菜单项
根据右键单击的项的属性更新ContextMenu的最佳方法是使用数据绑定:
<ContextMenu x:Key="SelfUpdatingMenu">
<MenuItem Header="Delete" IsEnabled="{Binding IsDeletable}" />
...
</ContextMenu>
如果要隐藏项目而不是简单地将其灰显,请设置可见性而不是IsEnabled:
<MenuItem Header="Delete"
Visibility="{Binding IsDeletable, Converter={x:Static BooleanToVisibilityConverter}}" />
如果要根据数据从上下文菜单中添加/删除项,可以使用CompositeCollection进行绑定。语法有点复杂,但仍然非常简单:
<ContextMenu x:Key="MenuWithEmbeddedList">
<ContextMenu.ItemsSource>
<CompositeCollection>
<MenuItem Header="This item is always present" />
<MenuItem Header="So is this one" />
<Separator /> <!-- draw a bar -->
<CollectionContainer Collection="{Binding MyChoicesList}" />
<Separator />
<MenuItem Header="Fixed item at bottom of menu" />
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
假设“MyChoicesList”是一个observeCollection(或实现INotifyCollectionChanged的任何其他类),那么在该集合中添加/删除/更新的项将立即显示在ContextMenu上。
在不绑定数据的情况下更新单个ContextMenu项
如果可能的话
您应该使用数据绑定来控制ContextMenu项
. 它们工作得非常好,几乎是万无一失的,并且大大简化了代码。只有当数据绑定无法工作时,使用代码更新菜单项才有意义。在这种情况下,您可以通过处理ContextMenu.Opened事件并在此事件中进行更新来构建ContextMenu。例如:
<ContextMenu x:Key="Vegetables" Opened="Vegetables_Opened">
<MenuItem Header="Broccoli" />
<MenuItem Header="Green Peppers" />
</ContextMenu>
public void Vegetables_Opened(object sender, RoutedEventArgs e)
{
var menu = (ContextMenu)sender;
var data = (MyDataClass)menu.DataContext
var oldCarrots = (
from item in menu.Items
where (string)item.Header=="Carrots"
select item
).FirstOrDefault();
if(oldCarrots!=null)
menu.Items.Remove(oldCarrots);
if(ComplexCalculationOnDataItem(data) && UnrelatedCondition())
menu.Items.Add(new MenuItem { Header = "Carrots" });
}
或者,这个代码可以简单地更改
menu.ItemsSource
如果您使用的是数据绑定。
使用触发器切换上下文菜单
下面是一个这样的示例:
<ControlTemplate ...>
<ControlTemplate.Resources>
<ContextMenu x:Key="NormalMenu">
...
</ContextMenu>
<ContextMenu x:Key="AlternateMenu">
...
</ContextMenu>
</ControlTemplate.Resources>
...
<ListBox x:Name="MyList" ContextMenu="{StaticResource NormalMenu}">
...
<ControlTemplate.Triggers>
<Trigger Property="IsSpecialSomethingOrOther" Value="True">
<Setter TargetName="MyList" Property="ContextMenu" Value="{StaticResource AlternateMenu}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
在这种情况下,仍然可以使用数据绑定来控制NormalMenu和AlternateMenu中的单个项。
关闭菜单时释放ContextMenu资源
如果上下文菜单中使用的资源在RAM中保存起来很昂贵,您可能需要释放它们。如果您使用的是数据绑定,这很可能会自动发生,因为关闭菜单时DataContext将被删除。如果您使用的是代码,则可能需要捕获ContextMenu上的Closed事件,以释放为响应打开的事件而创建的任何内容。
从XAML延迟构造上下文菜单
如果您有一个非常复杂的ContextMenu,您想用XAML编写代码,但除了需要时不想加载它,那么可以使用两种基本技术:
-
把它放在另一本字典里。必要时,加载ResourceDictionary并将其添加到MergedDictionaries。只要您使用DynamicResource,合并的值就会被提取出来。
-
开的
-仅当包含模板被实例化或字典被合并时。要实现这一点,必须使用带有空ItemsSource的ContextMenu,然后在打开的事件中分配ItemsSource。但是,ItemsSource的值可以从单独的文件中的ResourceDictionary加载:
<ResourceDictionary ...>
<x:Array x:Key="ComplexContextMenuContents">
<MenuItem Header="Broccoli" />
<MenuItem Header="Green Beans" />
... complex content here ...
</x:Array>
</ResourceDictionary>
在打开的事件中使用此代码:
var dict = (ResourceDictionary)Application.LoadComponent(...);
menu.ItemsSource = dict["ComplexMenuContents"];
menu.ItemsSource = null;
实际上,如果您只有一个x:Array,那么您也可以跳过ResourceDictionary。如果XAML的最外层元素是x:Array,那么打开的事件代码就是:
menu.ItemsSource = Application.LoadComponent(....)
关键概念概述
DynamicResource仅用于根据加载的资源字典及其包含的内容切换值:更新字典的内容时,DynamicResource会自动更新属性。StaticResource仅在加载XAML时读取它们。
无论使用DynamicResource还是StaticResource,都会创建ContextMenu
加载资源字典时
在大多数情况下,应该使用数据绑定而不是代码来更新ContextMenu。