动态资源是如何构建的以及它们在上下文菜单中的使用

发布于 2024-08-29 05:47:09 字数 199 浏览 5 评论 0原文

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

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

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

Are dynamic resources truly dynamic? If I define a DynamicResource, I realise that an expression is created (where?) that is not translated into a resource until runtime, however, What I do not understans is whether this dynamicresouce, once built, is now "Static"

For instance, if I create a context menu via a dynamicresource, are the menuitems which are created at runtime on access then static, even if they are bound?

If so, how can i create a dynamic context menu in XAML?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

安稳善良 2024-09-05 05:47:09

这是一个非常复杂的主题,因为 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 对象

因为 Ellipse 和 TextBox 都引用相同的对象ContextMenu,更新 ContextMenu 将更改每个菜单上可用的选项。例如,当单击按钮时,以下内容会将“Carrots”添加到菜单中。

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

从这个意义上说,每个 ContextMenu 都是动态的:它的项目可以随时修改,并且更改将立即生效。即使 ContextMenu 实际上在屏幕上打开(下拉)也是如此。

通过数据绑定更新动态 ContextMenu

单个 ContextMenu 对象动态的另一种方式是它响应数据绑定。您可以绑定到一个集合,而不是设置单独的 MenuItem,例如:

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

这假设 VegetableList 被声明为 ObservableCollection 或实现 INotifyCollectionChanged 接口的其他类型。您对集合所做的任何更改都会立即更新上下文菜单,即使它是打开的。例如:

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

请注意,这种集合更新不需要在代码中进行:您还可以将蔬菜列表绑定到 ListView、DataGrid 等,以便最终用户可以进行更改。这些更改也会显示在您的上下文菜单中。

使用代码切换ContextMenu

您还可以用完全不同的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");
}

请注意,我们不是修改现有的 ContextMenu,而是切换到完全不同的 ContextMenu。在这种情况下,两个 ContextMenus 都会在窗口首次构建时立即构建,但 Fruits 菜单只有在切换时才会使用。

如果您想避免在必要时构建“水果”菜单,您可以在 Button_Click 处理程序中构建它,而不是在 XAML 中构建:

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

在此示例中,每次单击按钮时,都会构建一个新的 ContextMenu 并将其分配给椭圆形。 Window.Resources 中定义的任何 ContextMenu 仍然存在,但未使用(除非另一个控件使用它)。

使用 DynamicResource 切换 ContextMenus

使用 DynamicResource 允许您在 ContextMenus 之间切换,而无需显式分配代码。例如:

<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,因此修改字典将更新 Ellipse 的 ContextMenu 属性。例如:

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

这里的关键概念是 DynamicResource 与 StaticResource 控制何时完成字典查找。如果在上面的示例中使用 StaticResource,则分配给 Resources["Vegetables"] 将不会更新 Ellipse 的 ContextMenu 属性。

另一方面,如果您要更新 ContextMenu 本身(通过更改其 Items 集合或通过数据绑定),则使用 DynamicResource 还是 StaticResource 并不重要:在每种情况下,您对 ContextMenu 所做的任何更改都将立即可见。

使用数据绑定更新单个 ContextMenu ITEMS

根据右键单击的项目的属性更新 ContextMenu 的最佳方法是使用数据绑定:

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

这将导致“删除”菜单项被删除。除非该项目设置了 IsDeletable 标志,否则会自动变灰。在这种情况下,不需要(甚至不需要)任何代码。

如果您想隐藏该项目而不是简单地将其变灰,请设置 Visibility 而不是 IsEnabled:

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

如果您想根据您的数据从 ContextMenu 添加/删除项目,您可以使用 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”是一个 ObservableCollection(或任何其他实现 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

使用触发器切换ContextMenus

更新ContextMenus常用的另一种技术是使用触发器或DataTrigger根据触发条件在默认上下文菜单和自定义上下文菜单之间切换。这可以处理您想要使用数据绑定但需要替换整个菜单而不是更新其部分内容的情况。

下面是其外观的说明:

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

如果 ContextMenu 中使用的资源在 RAM 中保存起来很昂贵,您可能需要释放它们。如果您使用数据绑定,这可能会自动发生,因为当菜单关闭时 DataContext 将被删除。如果您使用代码,则可能必须捕获 ContextMenu 上的 Closed 事件,以释放您为响应 Opened 事件而创建的任何内容。

从 XAML 延迟构造 ContextMenu

如果您有一个非常复杂的 ContextMenu,想要在 XAML 中进行编码,但不想在需要时才加载,则可以使用两种基本技术:

  1. Put它在一个单独的资源字典中。必要时,加载该 ResourceDictionary 并将其添加到 MergedDictionaries。只要您使用DynamicResource,合并的值就会被拾取。

  2. 将其放入 ControlTemplate 或 DataTemplate 中。在第一次使用模板之前,菜单实际上不会被实例化。

然而,这些技术本身都不会导致在上下文菜单打开时发生加载 - 仅当包含模板被实例化或字典被合并时。要实现此目的,您必须使用带有空 ItemsSource 的 ContextMenu,然后在 Opened 事件中分配 ItemsSource。然而,ItemsSource 的值可以从单独文件中的 ResourceDictionary 加载:

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

在 Opened 事件中使用此代码:

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

在 Closed 事件中使用此代码:

menu.ItemsSource = null;

实际上,如果您只有一个 x:Array,您也可以跳过资源字典。如果 XAML 的最外层元素是 x:Array,则 Opened 事件代码很简单:

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

关键概念摘要

DynamicResource 仅用于根据加载的资源字典及其包含的内容切换值:更新字典的内容,DynamicResource 会自动更新属性。 StaticResource 仅在加载 XAML 时读取它们。

无论使用 DynamicResource 还是 StaticResource,ContextMenu 都是在加载资源字典时创建的,而不是在打开菜单时创建的。

上下文菜单非常动态,您可以使用数据绑定或代码来操作它们,并且更改会立即生效。

在大多数情况下,您应该使用数据绑定而不是代码来更新 ContextMenu。

可以使用代码、触发器或 DynamicResource 来完全替换菜单。

如果只有在菜单打开时才必须将内容加载到 RAM 中,则可以在 Opened 事件中从单独的文件加载它们,并在 Closed 事件中清除它们。

This is a very complex subject because there are so many kinds of dynamism within WPF. I will start with a simple example to help you understand some basic concepts you need, then proceed to explain the various ways in which a ContextMenu can be dynamically updated and/or replaced, and how DynamicResource fits into the picture.

Initial example: Dynamically updating ContextMenu referenced through StaticResource

Let's say you have the following:

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

** Note the use of StaticResource for now.

This XAML will:

  • Construct a ContextMenu object with three MenuItems and add it to Window.Resources
  • Construct an Ellipse object with a reference to the ContextMenu
  • Construct a TextBox object with a reference to the ContextMenu

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文