代码之家  ›  专栏  ›  技术社区  ›  miguel

动态资源是如何构建的,以及它们在contextmenus中的使用

  •  3
  • miguel  · 技术社区  · 16 年前

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

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

    1 回复  |  直到 16 年前
        1
  •  18
  •   Ray Burns    16 年前

    这是一个非常复杂的主题,因为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编写代码,但除了需要时不想加载它,那么可以使用两种基本技术:

    1. 把它放在另一本字典里。必要时,加载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。

    推荐文章