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は次のようになります。

  • 3つの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オブジェクトが動的である別の方法は、データバインディングに応答することです。個々の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を完全に異なる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に切り替えていることに注意してください。この状況では、ウィンドウが最初に作成された直後に両方のContextMenuが作成されますが、Fruitsメニューは切り替えられるまで使用されません。

必要になるまでFruitsメニューの作成を避けたい場合は、XAMLではなくButton_Clickハンドラーで作成できます。

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

この例では、ボタンをクリックするたびに、新しいContextMenuが作成され、楕円形に割り当てられます。Window.Resourcesで定義されたContextMenuはまだ存在しますが、使用されていません(別のコントロールがそれを使用しない限り)。

DynamicResourceを使用したContextMenuの切り替え

DynamicResourceを使用すると、コードを明示的に割り当てることなく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はStaticResourceではなくDynamicResourceを使用するため、ディクショナリを変更すると、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アイテムの更新

右クリックされたアイテムのプロパティに基づいてContextMenuを更新する最も良い方法は、データバインディングを使用することです。

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

これにより、アイテムにIsDeletableフラグが設定されていない限り、[削除]メニューアイテムが自動的にグレー表示されます。この場合、コードは必要ありません(または望ましい)。

アイテムを単にグレー表示するのではなく非表示にする場合は、IsEnabledの代わりにVisibilityを設定します。

<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または、データバインディングを使用している場合は、このコードを変更するだけで済みます。

トリガーを使用したContextMenuの切り替え

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の構築の遅延

XAMLでコーディングしたいが、必要な場合を除いてロードしたくない非常に複雑なContextMenuがある場合は、次の2つの基本的な手法を使用できます。

  1. 別のResourceDictionaryに入れてください。必要に応じて、その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が1つしかない場合は、ResourceDictionaryをスキップすることもできます。XAMLの最も外側の要素がx:Arrayの場合、Openedイベントコードは次のようになります。

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

重要な概念の要約

DynamicResourceは、ロードされるリソースディクショナリとその内容に基づいて値を切り替えるためにのみ使用されます。ディクショナリのコンテンツを更新すると、DynamicResourceはプロパティを自動的に更新します。StaticResourceは、XAMLがロードされたときにのみそれらを読み取ります。

DynamicResourceまたはStaticResourceのどちらを使用する場合でも、ContextMenuは、メニューを開いたときではなく、リソースディクショナリをロードしたときに作成されます。

ContextMenuは、データバインディングまたはコードを使用して操作でき、変更がすぐに有効になるという点で非常に動的です。

ほとんどの場合、コードではなくデータバインディングを使用してContextMenuを更新する必要があります。

メニューを完全に置き換えるには、コード、トリガー、またはDynamicResourceを使用します。

メニューが開いているときにのみコンテンツをRAMにロードする必要がある場合は、Openedイベントで別のファイルからコンテンツをロードし、Closedイベントでクリアすることができます。

于 2010-04-19T14:35:24.427 に答える