Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly bind a ViewModel (which Include Separators) to WPF's Menu?

I'm using MVVM and I want to data bind my list of MenuViewModels to my maim menu. Which consists of a set of menu items and separators.

Here's my MenuItemViewModel code:

public interface IMenuItemViewModel
{
}

[DebuggerDisplay("---")]
public class SeparatorViewModel : IMenuItemViewModel
{
}

[DebuggerDisplay("{Header}, Children={Children.Count}")]
public class MenuItemViewModel : IMenuItemViewModel, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public MenuItemViewModel(string header, ICommand command, ImageSource imageSource)
    {
        Header = header;
        Command = command;
        ImageSource = imageSource;

        Children = new List<IMenuItemViewModel>();
    }

    public string Header { get; private set; }
    public ICommand Command { get; private set; }

    public ImageSource ImageSource { get; private set; }

    public IList<IMenuItemViewModel> Children { get; private set; }
}

And my Main window looks like this:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
        ItemsSource="{Binding Children}">
        <MenuItem Header="{Binding Header}"
                  Command="{Binding Command}"/>
    </HierarchicalDataTemplate>

    <DataTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
        <Separator />
    </DataTemplate>
</Window.Resources>

<DockPanel>
    <Menu DockPanel.Dock="Top"
          ItemsSource="{Binding MenuItems}">
    </Menu>
</DockPanel>

Should be very simple stuff. Unfortunately, either the menu item looks wrong or the separator is an empty menuItem (depending on what I've tried).

So, how do I get my Menu to find my two DataTemplates?

like image 304
Kabuo Avatar asked Jul 03 '15 20:07

Kabuo


3 Answers

Another approach is to:

  • have a Boolean property on your menu item ViewModel that indicates whether an item is a separator or not
  • use a trigger based on this property to change the ControlTemplate of the MenuItem so that it uses a Separator control instead

Like so:

<Menu ItemsSource="{Binding MenuItems}">
    <Menu.Resources>
        <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Header" Value="{Binding Header}" />
            <Setter Property="Command" Value="{Binding Command}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSeparator}" Value="True">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type MenuItem}">
                                <Separator />
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>

        <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
                                  ItemsSource="{Binding Children}" />
    </Menu.Resources>
</Menu> 
like image 148
Steven Rands Avatar answered Sep 28 '22 15:09

Steven Rands


A solution without the TemplateSelector:

provide ItemContainerTemplates instead of the DataTemplates :

<ContextMenu ItemsSource="{Binding Path=MenuItems}" UsesItemContainerTemplate="True">
              <ContextMenu.Resources>
                <ResourceDictionary>
                  <ItemContainerTemplate DataType="{x:Type ViewModel:MenuItemViewModel }">
                    <MenuItem Header="{Binding Path=Header}" Command="{Binding Path=Command}" UsesItemContainerTemplate="True">
                      <MenuItem.Icon>
                        <Image Source="{Binding Path=ImageSource}"/>
                      </MenuItem.Icon>
                    </MenuItem>
                  </ItemContainerTemplate>
                  <ItemContainerTemplate DataType="{x:Type ViewModel:SeparatorViewModel}">
                    <Separator >
                      <Separator.Style>
                        <Style TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
                      </Separator.Style>
                    </Separator>
                  </ItemContainerTemplate>
                </ResourceDictionary>
              </ContextMenu.Resources>
            </ContextMenu>

Notes:

  • I haven't tried Children
  • the separator styled wrong: I had to manually re-apply the style
like image 28
jan Avatar answered Sep 28 '22 13:09

jan


Solved my own question

After spending several hours searching the web, I found lots of examples that work against the WPF's natural intentions but none that worked with it.

Here's how to work with the Menu control and not against it...

A little Background

WPF's Menu control will normally auto create MenuItem objects for you when it is binded to a POCO collection, using the ItemsSource property.

However, this default behavior can be overridden! Here's how...

The Solution

First, you must create a class that derives from ItemContainerTemplateSelector. Or use the simple class I've created:

public class MenuItemContainerTemplateSelector : ItemContainerTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, ItemsControl parentItemsControl)
    {
        var key = new DataTemplateKey(item.GetType());
        return (DataTemplate) parentItemsControl.FindResource(key);
    }
}

Second, you must add a reference to the MenuItemContainerTemplateSelector class to your Windows resources object, like so:

<Window.Resources>
    <Selectors:MenuItemContainerTemplateSelector x:Key="_menuItemContainerTemplateSelector" />

Third, you must set two properties (UsesItemContainerTemplate, and ItemContainerTemplateSelector) on both the Menu and the MenuItem (which is defined in the HierarchicalDataTemplate).

Like so:

    <HierarchicalDataTemplate DataType="{x:Type ViewModel:MenuItemViewModel}"
        ItemsSource="{Binding Children}">
        <MenuItem Header="{Binding Header}"
                  Command="{Binding Command}"
                  UsesItemContainerTemplate ="true"
                  ItemContainerTemplateSelector=
                  "{StaticResource _menuItemContainerTemplateSelector}"/>
    </HierarchicalDataTemplate>

    <Menu DockPanel.Dock="Top"
          ItemsSource="{Binding MenuItems}"
          UsesItemContainerTemplate="True"
          ItemContainerTemplateSelector=
          "{StaticResource _menuItemContainerTemplateSelector}">
    </Menu>

Why it Works

For optimization purposes, the Menu uses the UsesItemContainerTemplate flag (which has a default value of false) to skip the DataTemplate lookup and just returns a normal MenuItem object. Therefore, we needed to set this value to true and then our ItemContainerTemplateSelector works as expected.

Happy Coding!

like image 40
Kabuo Avatar answered Sep 28 '22 14:09

Kabuo