Kabua
Kabua

Reputation: 1019

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?

Upvotes: 11

Views: 5995

Answers (3)

Steven Rands
Steven Rands

Reputation: 5421

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> 

Upvotes: 4

jan
jan

Reputation: 1601

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

Upvotes: 4

Kabua
Kabua

Reputation: 1019

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!

Upvotes: 22

Related Questions