c# – 如何构建DynamicResources并将其用于上下文菜单中

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

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

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

最佳答案
这是一个非常复杂的主题,因为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将更改每个上下文中可用的选项.例如,单击按钮时,以下内容会将“胡萝卜”添加到菜单中.

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接口的类型.您对集合所做的任何更改都会立即更新ContextMenu,即使它已打开.例如:

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

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

使用代码切换ContextMenus

您还可以使用完全不同的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菜单.

如果你想避免构造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 vs 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 ITEMS而不进行数据绑定

在可能的情况下,您应该使用数据绑定控制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的另一种技术是使用Trigger或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中进行编码但不想加载,除非需要时,可以使用两种基本技术:

>将它放在一个单独的ResourceDictionary中.必要时,加载ResourceDictionary并将其添加到MergedDictionaries.只要您使用DynamicResource,就会获取合并的值.
>将它放在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,你也可以跳过ResourceDictionary.如果您的XAML的最外层元素是x:Array,则Opened事件代码就是:

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

关键概念摘要

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

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

ContextMenus非常动态,因为您可以使用数据绑定或代码来操作它们,并且更改会立即生效.

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

可以使用代码,触发器或DynamicResource完全替换菜单.

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

转载注明原文:c# – 如何构建DynamicResources并将其用于上下文菜单中 - 代码日志