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.
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;
}
}
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.. – FrasquitoHow does a Menu know it has MenuItems, and a ListBox has ListBoxItems.. etc?
Via theItemsControl.ItemsSource
– Frasquito