WPF, dynamiczne i statyczne menu

Ostatnio napotkałem problem stworzenia menu zawierającego zarówno dynamiczne elementy (binding) jak i statyczne. Okazało się, że wcale nie jest to takie proste jak to jest w przypadku większości rzeczy w WPF. Zacznijmy jednak od przedstawienia sposobów tworzenia menu w WPF. Pierwszy to oczywiste statyczne menu, w całości zdefiniowane w XAML:

<Menu Height="23" Name="menu1" Width="200">
    <MenuItem Header="Plik">
        <MenuItem Header="statyczny tekst 1"/>
        <MenuItem Header="statyczny tekst 2"/>
        <MenuItem Header="statyczny tekst 3"/>
    </MenuItem>
</Menu>

W powyższym kodzie nie ma nic nadzwyczajnego. Sytuacja jest również łatwa gdy chcemy w całości zdefiniować menu jako dynamiczne:

<Menu Height="23" Name="menu1" Width="200">
   <MenuItem Header="Plik" ItemsSource="{Binding Items}">        
       <MenuItem.ItemTemplate>
           <DataTemplate>
               <TextBlock Text="{Binding DisplayName}" />
           </DataTemplate>
       </MenuItem.ItemTemplate>
       <MenuItem.ItemContainerStyle>
           <Style TargetType="MenuItem">
               <Setter Property="Command" Value="{Binding ShowCmd}" />
           </Style>
       </MenuItem.ItemContainerStyle>
   </MenuItem>
</Menu>

Gdzie pojedynczy item to:

public class Item
{
   public string DisplayName { get; set; }
   public ICommand ShowCmd { get; set; }
}

Co jednak w przypadku gdy chcemy aby menu Plik zawierało część statycznych podmenu zdefiniowanych w XAML a część pochodziła z kolekcji (na dodatek z różnych kolekcji)? Sytuacja się trochę komplikuje ale możemy rozwiązań problem za pomocą CollectionContainer:

<Menu Height="23" Name="menu1" Width="200">
   <Menu.Resources>
       <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>            
   </Menu.Resources>
   
   <MenuItem Header="Plik">        
       <MenuItem.ItemsSource>
           <CompositeCollection>
               <MenuItem Header="Statyczny element 1"/>
               <MenuItem Header="Statyczny element 2"/>
               <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>               
           </CompositeCollection>
       </MenuItem.ItemsSource>
   </MenuItem>
   
</Menu>

Przede wszystkim przenieśliśmy elementy do ItemsSource. CompositeCollection pozwala zdefiniować kolekcje menu składającej się z statycznych i dynamicznych elementów. Dynamiczne pozycje zdefiniowane są za pomocą CollectionContainer, który wykorzystuje jako źródło danych CollectionViewSource, który z kolei powiązany jest z Items. Po uruchomieniu powyższego przykładu zobaczymy menu składające się z różnych submenu ale niestety musimy jeszcze określić jaka właściwość powinna być wyświetlana oraz jaka komenda z klasy Item powinna zostać powiązana:

<MenuItem Header="Plik">            
  <MenuItem.Resources>
      <Style TargetType="{x:Type MenuItem}">
          <Style.Triggers>
              <DataTrigger Binding="{Binding Converter={StaticResource objectToTypeConverter}}" Value="{x:Type itemsNamespace:Item }">
                  <Setter Property="Header" Value="{Binding DisplayName}"/>
                  <Setter Property="Command" Value="{Binding ShowCmd}"/>
              </DataTrigger>
          </Style.Triggers>
      </Style>
      <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>
  </MenuItem.Resources>
  <MenuItem.ItemsSource>
      <CompositeCollection>                    
          <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>
          <MenuItem Header="Statyczny element 1"/>
          <MenuItem Header="Statyczny element 2"/>
      </CompositeCollection>
  </MenuItem.ItemsSource>
</MenuItem>

Za pomocą DataTrigger sprawdzamy czy typ DataContext jest równy typeof(Item). Jeśli tak to znaczy, że jest to element dynamiczny powiązany z kolekcją Items i możemy ustawić dowolne właściwości(w tym komendę i header). Na zakończenie konwerter objectToTypeConverter:

public class ObjectToTypeConverter:IValueConverter
{
   #region IValueConverter Members

   public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       if (value == null)
           return null;
       else
           return value.GetType();
   }

   public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       throw new NotImplementedException();
   }

   #endregion
}

2 thoughts on “WPF, dynamiczne i statyczne menu”

  1. Bardzo ciekawy przykład 😉 Fajnie byłoby, gdybyś do postów dotyczących zagadnień UI dodawał jakieś graficzne przykłady np menu opisanego w aktualnym poście 😉

Leave a Reply

Your email address will not be published.