c# - 如何构建DynamicResources及其在contextmenus中的使用

标签 c# wpf xaml dynamicresource

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

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

如果是这样,如何在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标志。在这种情况下,不需要代码(甚至不需要)。

如果要隐藏项目而不是简单地将其变灰,请设置可见性而不是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中。在第一次使用模板之前,实际上不会实例化菜单。


但是,这些技术本身都不会使打开上下文菜单时发生加载-仅在实例化包含模板或合并字典时才发生。为此,您必须将ContextMenu与空的ItemsSource一起使用,然后在Opened事件中分配ItemsSource。但是,可以从ResourceDictionary的单独文件中加载ItemsSource的值:

<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及其在contextmenus中的使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2648849/

相关文章:

c# - 列表框中部分可见的项目不会触发绑定(bind)命令

c# - Entity Framework 错误为“不允许新事务,因为 session 中还有其他线程正在运行

javascript - 使用 WebDriver 扩展 JavaScript 菜单

c# - 在同一类的构造函数中传递类的对象

c# - 枚举上的数据触发以更改图像

c# - OnMouseMove 不会在 WPF 中的 Canvas 上触发

c# - 仅在子项上带有复选框的 TreeView

c# - 我如何知道我使用的是什么 Windows 主题?

.net - wpf 绑定(bind)到索引器

c# - WPF 中带有文本 block 的动态工具提示