3

动态资源真的是动态的吗?如果我定义了一个 DynamicResource,我意识到创建了一个表达式(在哪里?),直到运行时才转换为资源,但是,我不明白的是这个动态资源,一旦构建,现在是否是“静态的”

例如,如果我通过动态资源创建上下文菜单,那么在访问时创建的菜单项是否是静态的,即使它们是绑定的?

如果是这样,我如何在 XAML 中创建动态上下文菜单?

4

1 回答 1

18

这是一个非常复杂的主题,因为 WPF 中有很多种类的动态。我将从一个简单的示例开始,以帮助您理解您需要的一些基本概念,然后继续解释 ContextMenu 可以动态更新和/或替换的各种方式,以及 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 将:

  • 用三个 MenuItem 构造一个 ContextMenu 对象并将其添加到 Window.Resources
  • 构造一个引用 ContextMenu 的 Ellipse 对象
  • 使用对 ContextMenu 的引用构造一个 TextBox 对象

Since both the Ellipse and the TextBox have references to the same ContextMenu, updating the ContextMenu will change the options available on each. For example the following will add "Carrots" to the menu when a button is clicked.

public void Button_Click(object sender, EventArgs e)
{
  var menu = (ContextMenu)Resources["Vegetables"];
  menu.Items.Add(new MenuItem { Header = "Carrots" });
}

In this sense every ContextMenu is dynamic: Its items can be modified at any time and the changes will immediately take effect. This is true even when the ContextMenu is actually open (dropped down) on the screen.

Dynamic ContextMenu updated through data binding

Another way in which a single ContextMenu object is dynamic is that it responds to data binding. Instead of setting individual MenuItems you can bind to a collection, for example:

<Window.Resources>
  <ContextMenu x:Key="Vegetables" ItemsSource="{Binding VegetableList}" />
</Window.Resources>

This assumes VegetableList is declared as an ObservableCollection or some other type that implements the INotifyCollectionChanged interface. Any changes you make to the collection will instantly update the ContextMenu, even if it is open. For example:

public void Button_Click(object sender, EventArgs e)
{
  VegetableList.Add("Carrots");
}

Note that this kind of collection update need not be made in code: You can also bind the vegetable list to a ListView, DataGrid, etc so that changes may be made by the end-user. These changes will also show up in your ContextMenu.

Switching ContextMenus using code

You can also replace the ContextMenu of an item with a completely different ContextMenu. For example:

<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>

The menu can be replaced in code like this:

public void Button_Click(object sender, EventArgs e)
{
  Oval.ContextMenu = (ContextMenu)Resources.Find("Fruits");
}

Note that instead of modifying the existing ContextMenu we are switching to a completely different ContextMenu. In this situation both ContextMenus are built immediately when the window is first constructed, but the Fruits menu is not used until it is switched.

If you want to avoid constructing the Fruits menu until it was necessary you could construct it in the Button_Click handler instead of doing it in XAML:

public void Button_Click(object sender, EventArgs e)
{
  Oval.ContextMenu =
    new ContextMenu { ItemsSource = new[] { "Apples", "Bananas" } };
}

In this example, every time you click on the button a new ContextMenu will be constructed and assigned to the oval. Any ContextMenu defined in Window.Resources still exists but is unused (unless another control uses it).

Switching ContextMenus using DynamicResource

Using DynamicResource allows you to switch between ContextMenus without explicitly assigning it code. For example:

<Window>
  <Window.Resources>
    <ContextMenu x:Key="Vegetables">
      <MenuItem Header="Broccoli" />
      <MenuItem Header="Cucumber" />
    </ContextMenu>
  </Window.Resources>

  <Grid>
    <Ellipse ContextMenu="{DynamicResource Vegetables}" />
    ...
  </Grid>
</Window>

Because this XAML uses DynamicResource instead of StaticResource, modifying the dictionary will update the ContextMenu property of the Ellipse. For example:

public void Button_Click(object sender, EventArgs e)
{
  Resources["Vegetables"] =
    new ContextMenu { ItemsSource = new[] {"Zucchini", "Tomatoes"} };
}

The key concept here is that DynamicResource vs StaticResource only controls when the dictionary is lookup is done. If StaticResource is used in the above example, assigning to Resources["Vegetables"] will not update the Ellipse's ContextMenu property.

On the other hand, if you are updating the ContextMenu itself (by changing its Items collection or via data binding), it does not matter whether you use DynamicResource or StaticResource: In each case any changes you make to the ContextMenu will be immediately visible.

Updating individual ContextMenu ITEMS using data binding

The very best way to update a ContextMenu based on properties of the item that is right-clicked is to use data binding:

<ContextMenu x:Key="SelfUpdatingMenu">
  <MenuItem Header="Delete" IsEnabled="{Binding IsDeletable}" />
    ...
</ContextMenu>

This will cause the "Delete" menu item to be automatically grayed out unless the item has its IsDeletable flag set. No code is necessary (or even desirable) in this case.

If you want to hide the item instead of simply graying it out, set Visibility instead of IsEnabled:

<MenuItem Header="Delete"
          Visibility="{Binding IsDeletable, Converter={x:Static BooleanToVisibilityConverter}}" />

If you want to add/remove items from a ContextMenu based on your data, you can bind using a CompositeCollection. The syntax is a bit more complex, but it is still quite straightforward:

<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>

Assuming "MyChoicesList" is an ObservableCollection (or any other class that implements INotifyCollectionChanged), items added/removed/updated in this collection will be immediately visible on the ContextMenu.

Updating individual ContextMenu ITEMS without data binding

When at all possible you should control your ContextMenu items using data binding. They work very well, are nearly foolproof, and greatly simplify your code. Only if data binding can't be made to work does it make sense to use code to update your menu items. In this case you can build your ContextMenu by handling the ContextMenu.Opened event and doing updates within this event. For example:

<ContextMenu x:Key="Vegetables" Opened="Vegetables_Opened">
  <MenuItem Header="Broccoli" />
  <MenuItem Header="Green Peppers" />
</ContextMenu>

With this code:

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" });
}

Alternatively this code could simply change menu.ItemsSource if you were using data binding.

Switching ContextMenus using Triggers

Another technique commonly used to update ContextMenus is to use a Trigger or DataTrigger to switch between a default context menu and a custom context menu depending on the triggering condition. This can handle situations where you want to use data binding but need to replace the menu as a whole rather than update parts of it.

Here is an illustration of what this looks like:

<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>

In this scenario it is still possible to use data binding to control individual items in both NormalMenu and AlternateMenu.

Releasing ContextMenu resources when the menu is closed

If resources used in a ContextMenu are expensive to keep in RAM you may want to release them. If you are using data binding this is likely to happen automatically, as the DataContext is removed when the menu is closed. If you are using code instead you may have to catch the Closed event on the ContextMenu to deallocate whatever you created in response to the Opened event.

Delayed construction of ContextMenu from XAML

If you have a very complex ContextMenu that you want to code in XAML but don't want to load except when it is needed, two basic techniques are available:

  1. Put it in a separate ResourceDictionary. When necessary, load that ResourceDictionary and add it to MergedDictionaries. As long as you used DynamicResource, the merged value will be picked up.

  2. Put it in a ControlTemplate or DataTemplate. The menu will not actually be instantiated until the template is first used.

However neither of these techniques by itself will cause the loading to happen when the context menu is opened - only when the containing template is instantiated or the dictionary is merged. To accomplish that you must use a ContextMenu with an empty ItemsSource then assign the ItemsSource in the Opened event. However the value of the ItemsSource can be loaded from a ResourceDictionary in a separate file:

<ResourceDictionary ...>
  <x:Array x:Key="ComplexContextMenuContents">
    <MenuItem Header="Broccoli" />
    <MenuItem Header="Green Beans" />
    ... complex content here ...
  </x:Array>
</ResourceDictionary>

with this code in the Opened event:

var dict = (ResourceDictionary)Application.LoadComponent(...);
menu.ItemsSource = dict["ComplexMenuContents"];

and this code in the Closed event:

menu.ItemsSource = null;

Actually if you have only a single x:Array, you may as well skip the ResourceDictionary. If your XAML's outermost element is the x:Array the Opened event code is simply:

menu.ItemsSource = Application.LoadComponent(....)

Summary of critical concepts

DynamicResource is used only for switching values based on which resource dictionaries are loaded and what they contain: When updating the contents of the dictionaries, DynamicResource automatically updates the properties. StaticResource only reads them when the XAML is loaded.

No matter whether DynamicResource or StaticResource is used, the ContextMenu is created when the resource dictionary is loaded not when the menu is opened.

ContextMenus are very dynamic in that you can manipulate them using data binding or code and the changes immediately take effect.

In most cases you should update your ContextMenu using data bindings, not in code.

Completely replacing menus can be done with code, triggers, or DynamicResource.

If contents must be loaded into RAM only when the menu is open, they can be loaded from a separate file in the Opened event and cleared out in the Closed event.

于 2010-04-19T14:35:24.427 回答