DataTemplate to generate Menu with MVVM
Asked Answered
M

2

5

I'm trying to use a DataTemplate to create a Menu from my ViewModels with respect to MVVM. Basically, I've created several classes which will store information about my Menu structure. I then want to realize that menu stucture as a WPF Menu using a DataTemplate.

I have a menu service which allows different components to register new menus and items within the menus. Here's how I've organized my menu information (ViewModel)

I have the following classes: MainMenuViewModel - Contains a TopLevelMenuViewModelCollection (a collection of top level menus)

TopLevelMenuViewModel - Contains a MenuItemGroupViewModelCollection (a collection of groups of menu items), and a name for the menu 'Text'

MenuItemGroupViewModel - Contains a MenuItemViewModelCollection (collection of menu items)

MenuItemViewModel - Contains text, image uri, command, children MenuItemViewModels

What I want to do is apply a DataTemplate to the previous classes to transform them into a normal Menu.

MainMenuViewModel -> Menu

TopLevelMenuViewModel -> MenuItems with header set

MenuItemGroupViewModel -> Separator followed by a MenuItem for each MenuItemViewModel

MenuItemViewModel -> MenuItem (HeirarchicalDataTemplate)

The problem is I don't see how to generate multiple MenuItems for the MenuItemGroupViewModel. The Menu template wants to always create an ItemContainer for each item which is a MenuItem. Therefore, I either end up with my MenuItems inside a MenuItem which obviously doesn't work, or it doesn't work at all. I've tried several things and still cannot figure out how to make a single item produce more than one MenuItem.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:local="clr-namespace:--">
<!-- These data templates provide the views for the menu -->

<!-- MenuItemGroupView -->
<Style x:Key="MenuItemGroupStyle" TargetType="{x:Type MenuItem}">
    <Setter Property="Header" Value="qqq" />
    <!-- Now what? I don't want 1 item here..
    I wanted this to start with a <separator /> and list the MenuItemGroupViewModel.MenuItems -->
</Style>

<!-- TopLevelMenuView -->
<Style x:Key="TopLevelMenuStyle" TargetType="{x:Type MenuItem}">
    <Setter Property="Header" Value="{Binding Text}" />
    <Setter Property="ItemsSource" Value="{Binding MenuGroups}" />
    <Setter Property="ItemContainerStyle" Value="{StaticResource MenuItemGroupStyle}"/>
</Style>

<!-- MainMenuView -->
<DataTemplate DataType="{x:Type local:MainMenuViewModel}">
    <Menu ItemsSource="{Binding TopLevelMenus}" ItemContainerStyle="{StaticResource TopLevelMenuStyle}" />
</DataTemplate>

<!-- MenuItemView -->
<!--<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}"
                              ItemsSource="{Binding Path=Children}"
                          >
    <HierarchicalDataTemplate.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="Command"
                        Value="{Binding Command}" />
        </Style>
    </HierarchicalDataTemplate.ItemContainerStyle>
    <StackPanel Orientation="Horizontal">
        <Image Source="{Binding ImageSource}" />
        <TextBlock Text="{Binding Text}" />
    </StackPanel>
</HierarchicalDataTemplate>-->

Please Click the links to see a better picture of what I'm trying to do

Class Diagram

Basic Menu I want to Make

Merciless answered 29/2, 2012 at 16:14 Comment(9)
way too confusing without a pictureFrasquito
I added some links to clear pictures, it is really quite simple when you see itMerciless
is it possible with a slightly different approach? instead of having "groups", can you just create a stand-in for a separator, like this article shows..Frasquito
Maybe.. I'm looking at that article now.. I was just thinking the whole point of the MVVM approach is the "data" is supposed to be unaware of the "view". I was imagining this menu could be realized with a DataTemplate to create any type of menu be it a Main Menu, Ribbon, or other menu navigation interface. Of course, I could create it as a UserControl and do everything in procedural code, but I thought DataTemplates were the way to go... I guess I may have to modify my data to accommodate the DataTemplates. The idea was each module would register a group of items it owns and controls.Merciless
the issue is that WPF sees TopLevel.MenuGroups and says "Okay, let's create a MenuItem for each group." when I first started WPF and MVVM I came across the "separation of data" many times as well. But, the complexity has to lie somewhere, whether you use the data in a converter or what have you..Frasquito
I understand... and yes that is exactly the problem. Do you know what is responsible for creating that MenuItem? How does a Menu know it has MenuItems, and a ListBox has ListBoxItems.. etc? It looks like I either have to A. flatten my data or B. Write procedural code to do it myself in a user control.Merciless
You know, I'm wondering now if I should just embrace the MenuItem within a MenuItem and instead apply a custom ItemContainerStyle (instead of a <Separator/>) to format the spacing between groups of commands?!Merciless
I would need to override the container's template or style to not show a popup, I wonder if that is possible. Instead of an arrow and popup, perhaps a separator with group name, and some spacing with the children in a stackpanelMerciless
How does a Menu know it has MenuItems, and a ListBox has ListBoxItems.. etc? Via the ItemsControl.ItemsSourceFrasquito
M
10

Because this is sort of complicated, I've updated this answer with a downloadable example.

PrismMenuServiceExample

My goal was to allow different modules to register menu commands and group them together with a title and sort the menu items in a proper order. First of all, let's show an example of what the menu looks like.

Grouped Menu Example

This is useful, for example a "Tools" menu could have a "Module1" group that has menu items listed for each tool that belongs to Module1 which Module1 can register independently of the other modules.

I have a "menu service" which allows modules to register new menus and menu items. Each node has a Path property which informs the service where to place the menu. This interface is likely in the infrastructure project, so that all modules can resolve it.

public interface IMenuService
{
    void AddTopLevelMenu(MenuItemNode node);
    void RegisterMenu(MenuItemNode node);
}

I can then implement that MenuService wherever is appropriate. (Infrastructure project, Separate Module, maybe the Shell). I go ahead and add some "default" menus that are defined application wide, although any module can add new top level menus.

I could have created these menus in code, but I instead pulled them out of the resources because it was easier to write them out in XAML in a resource file. I'm adding that resource file to my application resources, but you could load it directly.

public class MainMenuService : IMenuService
{
    MainMenuNode menu;
    MenuItemNode fileMenu;
    MenuItemNode toolMenu;
    MenuItemNode windowMenu;
    MenuItemNode helpMenu;

    public MainMenuService(MainMenuNode menu)
    {
        this.menu = menu;

        fileMenu = (MenuItemNode)Application.Current.Resources["FileMenu"];
        toolMenu = (MenuItemNode)Application.Current.Resources["ToolMenu"];
        windowMenu = (MenuItemNode)Application.Current.Resources["WindowMenu"];
        helpMenu = (MenuItemNode)Application.Current.Resources["HelpMenu"];

        menu.Menus.Add(fileMenu);
        menu.Menus.Add(toolMenu);
        menu.Menus.Add(windowMenu);
        menu.Menus.Add(helpMenu);
    }

    #region IMenuService Members

    public void AddTopLevelMenu(MenuItemNode node)
    {
        menu.Menus.Add(node);
    }

    public void RegisterMenu(MenuItemNode node)
    {
        String[] tokens = node.Path.Split('/');
        RegisterMenu(tokens.GetEnumerator(), menu.Menus, node);
    }

    #endregion

    private void RegisterMenu(IEnumerator tokenEnumerator, MenuItemNodeCollection current, MenuItemNode item)
    {
        if (!tokenEnumerator.MoveNext())
        {
            current.Add(item);
        }
        else
        {
            MenuItemNode menuPath = current.FirstOrDefault(x=> x.Text == tokenEnumerator.Current.ToString());

            if (menuPath == null)
            {
                menuPath = new MenuItemNode(String.Empty);
                menuPath.Text = tokenEnumerator.Current.ToString();
                current.Add(menuPath);
            }

            RegisterMenu(tokenEnumerator, menuPath.Children, item);
        }
    }
}

Here's an example of one of those pre-defined menus in my resource file:

<!-- File Menu Groups -->
<menu:MenuGroupDescription x:Key="fileCommands"
                           Name="Files"
                           SortIndex="10" />
<menu:MenuGroupDescription x:Key="printerCommands"
                           Name="Printing"
                           SortIndex="90" />
<menu:MenuGroupDescription x:Key="applicationCommands"
                           Name="Application"
                           SortIndex="100" />

<menu:MenuItemNode x:Key="FileMenu"
                   x:Name="FileMenu"
                   Text="{x:Static inf:DefaultTopLevelMenuNames.File}"
                   SortIndex="10">
    <menu:MenuItemNode Group="{StaticResource fileCommands}"
                       Text="_Open File..."
                       SortIndex="10"
                       Command="{x:Static local:FileCommands.OpenFileCommand}" />
    <menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Recent _Files" SortIndex="20"/>
    <menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Con_vert..."  SortIndex="30"/>
    <menu:MenuItemNode Group="{StaticResource fileCommands}"
                       Text="_Export"
                       SortIndex="40"
                       Command="{x:Static local:FileCommands.ExportCommand}" />
    <menu:MenuItemNode Group="{StaticResource fileCommands}" Text="_Save" SortIndex="50"/>
    <menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Save _All" SortIndex="60"/>
    <menu:MenuItemNode Group="{StaticResource fileCommands}"
                       Text="_Close"
                       SortIndex="70"
                       Command="{x:Static local:FileCommands.CloseCommand}" />
    <menu:MenuItemNode Group="{StaticResource printerCommands}" Text="Page _Setup..." SortIndex="10"/>
    <menu:MenuItemNode Group="{StaticResource printerCommands}" Text="_Print..." SortIndex="10"/>
    <menu:MenuItemNode Group="{StaticResource applicationCommands}"
                       Text="E_xit"
                       SortIndex="10"
                       Command="{x:Static local:FileCommands.ExitApplicationCommand}" />
</menu:MenuItemNode>

OK, here lists the types that define the structure of my menu system... (Not what it looks like)

The MainMenuNode basically exists so that you can easily create a different template for it. You probably what a menu bar or something that represents the menu as a whole.

public class MainMenuNode
{
    public MainMenuNode()
    {
        Menus = new MenuItemNodeCollection();
    }

    public MenuItemNodeCollection Menus { get; private set; }
}

Here's the definition for each MenuItem. They include a Path which tells the service where to put them, a SortIndex which is sort of like TabIndex that allows them to be organized in the proper order, and a GroupDescription which allows you to put them into "groups" which can be styled differently and sorted.

[ContentProperty("Children")]
public class MenuItemNode : NotificationObject
{
    private string text;
    private ICommand command;
    private Uri imageSource;
    private int sortIndex;

    public MenuItemNode()
    {
        Children = new MenuItemNodeCollection();
        SortIndex = 50;
    }

    public MenuItemNode(String path)
    {
        Children = new MenuItemNodeCollection();
        SortIndex = 50;
        Path = path;
    }

    public MenuItemNodeCollection Children { get; private set; }

    public ICommand Command
    {
        get
        {
            return command;
        }
        set
        {
            if (command != value)
            {
                command = value;
                RaisePropertyChanged(() => this.Command);
            }
        }
    }

    public Uri ImageSource
    {
        get
        {
            return imageSource;
        }
        set
        {
            if (imageSource != value)
            {
                imageSource = value;
                RaisePropertyChanged(() => this.ImageSource);
            }
        }
    }

    public string Text
    {
        get
        {
            return text;
        }
        set
        {
            if (text != value)
            {
                text = value;
                RaisePropertyChanged(() => this.Text);
            }
        }
    }

    private MenuGroupDescription group;

    public MenuGroupDescription Group
    {
        get { return group; }
        set
        {
            if (group != value)
            {
                group = value;
                RaisePropertyChanged(() => this.Group);
            }
        }
    }

    public int SortIndex
    {
        get
        {
            return sortIndex;
        }
        set
        {
            if (sortIndex != value)
            {
                sortIndex = value;
                RaisePropertyChanged(() => this.SortIndex);
            }
        }
    }

    public string Path
    {
        get;
        private set;
    }

And a collection of menu items:

public class MenuItemNodeCollection : ObservableCollection<MenuItemNode>
{
    public MenuItemNodeCollection() { }
    public MenuItemNodeCollection(IEnumerable<MenuItemNode> items) : base(items) { }
}

Here's how I ended up grouping MenuItems.. Each one has a GroupDescription

public class MenuGroupDescription : NotificationObject, IComparable<MenuGroupDescription>, IComparable
{
    private int sortIndex;

    public int SortIndex
    {
        get { return sortIndex; }
        set
        {
            if (sortIndex != value)
            {
                sortIndex = value;
                RaisePropertyChanged(() => this.SortIndex);
            }
        }
    }

    private String name;

    public String Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                RaisePropertyChanged(() => this.Name);
            }
        }
    }

    public MenuGroupDescription()
    {
        Name = String.Empty;
        SortIndex = 50;

    }

    public override string ToString()
    {
        return Name;
    }

    #region IComparable<MenuGroupDescription> Members

    public int CompareTo(MenuGroupDescription other)
    {
        return SortIndex.CompareTo(other.SortIndex);
    }

    #endregion

    #region IComparable Members

    public int CompareTo(object obj)
    {
        if(obj is MenuGroupDescription)
            return sortIndex.CompareTo((obj as MenuGroupDescription).SortIndex);
        return this.GetHashCode().CompareTo(obj.GetHashCode());
    }

    #endregion
}

I then can design what my menu looks like with the following templates:

<local:MenuCollectionViewConverter x:Key="GroupViewConverter" />

<!-- The style for the header of a group of menu items -->
<DataTemplate x:Key="GroupHeaderTemplate"
              x:Name="GroupHeader">
    <Grid x:Name="gridRoot"
          Background="#d9e4ec">
        <TextBlock Text="{Binding Name}"
                   Margin="4" />
        <Rectangle Stroke="{x:Static SystemColors.MenuBrush}"
                   VerticalAlignment="Top"
                   Height="1" />
        <Rectangle Stroke="#bbb"
                   VerticalAlignment="Bottom"
                   Height="1" />
    </Grid>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Name}"
                     Value="{x:Null}">
            <Setter TargetName="gridRoot"
                    Property="Visibility"
                    Value="Collapsed" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

<!-- Binds the MenuItemNode's properties to the generated MenuItem container -->
<Style x:Key="MenuItemStyle"
       TargetType="MenuItem">
    <Setter Property="Header"
            Value="{Binding Text}" />
    <Setter Property="Command"
            Value="{Binding Command}" />
    <Setter Property="GroupStyleSelector"
            Value="{x:Static local:MenuGroupStyleSelectorProxy.MenuGroupStyleSelector}" />
</Style>

<Style x:Key="TopMenuItemStyle"
       TargetType="MenuItem">
    <Setter Property="Header"
            Value="{Binding Text}" />
    <Setter Property="Command"
            Value="{Binding Command}" />
    <Setter Property="GroupStyleSelector"
            Value="{x:Static local:MenuGroupStyleSelectorProxy.MenuGroupStyleSelector}" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=Children.Count}"
                     Value="0">
            <Setter Property="Visibility"
                    Value="Collapsed" />
        </DataTrigger>
        <DataTrigger Binding="{Binding}"
                     Value="{x:Null}">
            <Setter Property="Visibility"
                    Value="Collapsed" />
        </DataTrigger>
    </Style.Triggers>
</Style>

<!-- MainMenuView -->
<DataTemplate DataType="{x:Type menu:MainMenuNode}">
    <Menu ItemsSource="{Binding Menus, Converter={StaticResource GroupViewConverter}}"
          ItemContainerStyle="{StaticResource TopMenuItemStyle}" />
</DataTemplate>

<!-- MenuItemView -->
<HierarchicalDataTemplate DataType="{x:Type menu:MenuItemNode}"
                          ItemsSource="{Binding Children, Converter={StaticResource GroupViewConverter}}"
                          ItemContainerStyle="{StaticResource MenuItemStyle}" />

A key to make this work was figuring out how to inject my CollectionView with proper sorting definitions and grouping definitions into my DataTemplate. This is how I did it:

[ValueConversion(typeof(MenuItemNodeCollection), typeof(IEnumerable))]
public class MenuCollectionViewConverter : IValueConverter
{

    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (targetType != typeof(IEnumerable))
            throw new NotImplementedException();

        CollectionViewSource src = new CollectionViewSource();
        src.GroupDescriptions.Add(new PropertyGroupDescription("Group"));
        src.SortDescriptions.Add(new SortDescription("Group", ListSortDirection.Ascending));
        src.SortDescriptions.Add(new SortDescription("SortIndex", ListSortDirection.Ascending));
        src.Source = value as IEnumerable;
        return src.View;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value.GetType() != typeof(CollectionViewSource))
            throw new NotImplementedException();
        return (value as CollectionViewSource).Source;
    }

    #endregion
}

public static class MenuGroupStyleSelectorProxy
{
    public static GroupStyleSelector MenuGroupStyleSelector { get; private set; }

    private static GroupStyle Style { get; set; }

    static MenuGroupStyleSelectorProxy()
    {
        MenuGroupStyleSelector = new GroupStyleSelector(SelectGroupStyle);
        Style = new GroupStyle()
        {
            HeaderTemplate = (DataTemplate)Application.Current.Resources["GroupHeaderTemplate"]
        }; 
    }

    public static GroupStyle SelectGroupStyle(CollectionViewGroup grp, int target)
    {
        return Style;
    }
}
Merciless answered 11/10, 2012 at 15:47 Comment(0)
M
2

I think the biggest problem with what you have now is the way you're treating groups of menu items. All the MenuItems inside your groups need to belong to the same parent, so you can't use something like an ItemsControl for them.

Instead, I'd have each TopLevelMenuItems expose a property of ObservableCollection<MenuItems>, which is a read-only collection containing all menu items from all groups, with the groups separated by a null value which can be used for identifying a separator.

For example,

public class TopLevelMenu
{
    public ObservableCollection<MenuItem> MenuItems
    {
        get
        {
            // Would be better to maintain a private collection for this instead of creating each time
            var collection = new ObservableCollection<MenuItem>();

            foreach(MenuGroup group in MenuGroups)
            {
                if (collection.Length > 0)
                    collection.Add(null); // Use null as separator placeholder

                foreach(MenuItem item in group.MenuItems)
                    collection.Add(item);
            }

            // Will return a collection containing all menu items in all groups, 
            // with the groups separated by a null value
            return collection; 
        }
    }
}

Then your DataTemplates can bind your Menu to the flattened collections, and use a trigger to identify which items are null and should be drawn with a separator.

I probably have this syntax wrong, but here's an example. The default template should be a regular menu item, and a DataTrigger to used to display a different template for MenuItems with child objects, or that are bound to null objects.

<Style TargetType="{x:Type MenuItem}">
    <Setter Property="Template" Value="{StaticResource DefaultMenuItemTemplate}" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding }" Value="{x:Null}">
            <Setter Property="Template" Value="{StaticResource SeparatorTemplate}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding HasItems}" Value="True">
            <Setter Property="Template" Value="{StaticResource SubMenuItemTemplate}" />
        </DataTrigger>
    </Style.Triggers>
</Style>

Of course, you could use an actual object instead of a null value for identifying your Separators, however I found nulls work just fine in other projects I've done so don't see why I should create more work for myself.

Memnon answered 29/2, 2012 at 21:3 Comment(4)
Thanks for your help. I understand what you are saying, I kinda realize that the Menu it setup to have a flat list of other MenuItems. I could flatten my data so that I could bind a HeirarchalDataTemplate to the MenuItemViewModel (which I have done), but it is counter to what I'm trying to achieve. I'm trying to implement the MVVM pattern in a modular application using Prism V4. Basically as each module loads, it uses a menu service to register a group of MenuCommands that belong to the module under a defaul top level menu.Merciless
So to clarify, the MenuService would expose an interface to other modules that allow them to create a group of menu commands and add them to the menu. Then, the service would create the MenuItemGroupViewModel objects, name them, and store them in the TopLevelMenuViewModel. These "ViewModels" only represent the data belonging to the service, which are commands that have a picture and name that can be executed. It is supposed to be agnostic of the DataTemplate which is used to create the GUI that the user can use to activate the commands.Merciless
Therefore, modifying my ViewModel classes to make the DataTemplate work is not really correct in keeping with the MVVM design practices. If I can't figure out how to do it with a DataTemplate, I think I'll create a UserControl add a Menu to the user control, and just write procedural code to add the items instead of using a DataTemplate. I just really wanted to use a DataTemplate because they should be simpler, cleaner, and less prone to bugs (?)Merciless
Thanks for your help Rachel, I'm finally getting back to you with what I ended up doing. You were basically right, the way I'm treating my groups is wrong. There is no good way to create multiple menu items from a single group with the template. I'll post my solution as well..Merciless

© 2022 - 2024 — McMap. All rights reserved.