tags:

views:

919

answers:

1

I have a scenario in which I need to have both static and dynamic menu items. The static items will be defined in XAML and the dynamic ones supplied by a View Model. Each dynamic item will itself be represented by a VieModel, lets call it a CommandViewModel. A CommandViewModel has amongst other things a display name, it can also contain other CommandViewModels.

The MainViewModel that gets used as the datacontext for the menu is as follows:

public class MainMenuViewModel : INotifyPropertyChanged
{

  private ObservableCollection<CommandViewModel> m_CommandVMList;


  public MainMenuViewModel()
  {
    m_ CommandVMList = new ObservableCollection<CommandViewModel>();

    CommandViewModel cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 1";
    m_CommandVMList.Add(cmv);

    cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 2";
    m_CommandVMList.Add(cmv);

    cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 3";
    m_CommandVMList.Add(cmv);

  }

  public ObservableCollection<CommandViewModel> CommandList
  {
    get { return m_CommandVMList; }
    set
    {
      m_CommandVMList = value;
      OnPropertyChanged("CommandList");
    }
  }

… … …

The Menu XAML:

<Grid>
  <Grid.Resources>
    <HierarchicalDataTemplate DataType="{x:Type Fwf:CommandViewModel}" ItemsSource="{Binding Path=CommandViewModels}">
      <MenuItem Header="{Binding Path=DisplayName}"/>
    </HierarchicalDataTemplate>
  </Grid.Resources>

  <Menu VerticalAlignment="Top" HorizontalAlignment="Stretch">
    <MenuItem Header="Static Top Menu Item 1">
      <MenuItem Header="Static Menu Item 1"/>
        <MenuItem Header="Static Menu Item 2"/>
        <MenuItem Header="Static Menu Item 3"/>
        <ItemsControl ItemsSource="{Binding Path= CommandList}"/>
        <MenuItem Header="Static Menu Item 4"/>
      </MenuItem>
  </Menu>
</Grid>

All works fine except that whatever I try to represent the list of dynamic menus, in this case an ItemsControl it is being shown on the UI as ONE Menu Item conatining more menu items, so the entire collection of dynamic menu items get selected when you click on the item. The collection gets represented correctly in that each dynamic menu item is show as a menu item itself but within this bigger menu item. I think I see why as the Menu is simply creating a menu item for each of the contained items, static or dynamic it does not care. Is there a way to have each dynamic menu item be created on the same level and belonging to the parent menu item as the static ones on the example does ?

+1  A: 

Instead of hard-coding your "static" menu items on the XAML side, I would hard-code them on the VM side as CommandViewModel objects.

Since you're hard-coding it either way, you won't lose flexibility, and you'll gain added benefit of having keeping your static menu items synchronized with your HierarchicalDataTemplate should you choose to render them differently in the future.

Note that you may will have to change your bindings so that your Menu binds to a collection of menu items. You can find an example of this here.

EDIT: Code Sample

I was able to hack this up fairly quickly, and most of the class definitions are incomplete (e.g. INotifyPropertyChanged) but it should give you an idea of what you can do. I've added some command nesting on the third command to make sure that the Hierarchical DataTemplate works.

Here's the XAML

<Window
    x:Class="WPFDynamicMenuItems.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WPFDynamicMenuItems"
    Title="Window1" Height="300" Width="600">
    <Grid>
        <Grid.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:CommandViewModel}" ItemsSource="{Binding Path=CommandList}">
                <ContentPresenter
                    Content="{Binding Path=DisplayName}"
                    RecognizesAccessKey="True" />
            </HierarchicalDataTemplate>
        </Grid.Resources>
        <ToolBarTray>
            <ToolBar>
            <Menu>
                <Menu.ItemsSource>
                    <CompositeCollection>
                        <MenuItem Header="A"></MenuItem>
                        <MenuItem Header="B"></MenuItem>
                        <MenuItem Header="C"></MenuItem>

                        <CollectionContainer x:Name="dynamicMenuItems">
                        </CollectionContainer>

                        <MenuItem Header="D"></MenuItem>

                    </CompositeCollection>
                </Menu.ItemsSource>

            </Menu>
                </ToolBar>
        </ToolBarTray>
    </Grid>
</Window>

And here's the code-behind:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace WPFDynamicMenuItems
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
     private MainMenuViewModel _mainMenuVM = new MainMenuViewModel();

     public Window1()
     {
      InitializeComponent();

      this.dynamicMenuItems.Collection = this._mainMenuVM.CommandList;
     }
    }


    public class MainMenuViewModel : INotifyPropertyChanged
    {
     private ObservableCollection<CommandViewModel> m_CommandVMList;

     public MainMenuViewModel()
     {
      m_CommandVMList = new ObservableCollection<CommandViewModel>();
      CommandViewModel cmv = new CommandViewModel();
      cmv.DisplayName = "Dynamic Menu 1";
      m_CommandVMList.Add(cmv);
      cmv = new CommandViewModel();
      cmv.DisplayName = "Dynamic Menu 2";
      m_CommandVMList.Add(cmv);
      cmv = new CommandViewModel();
      cmv.DisplayName = "Dynamic Menu 3";
      m_CommandVMList.Add(cmv);

      CommandViewModel nestedCMV = new CommandViewModel();
      nestedCMV.DisplayName = "Nested Menu 1";
      cmv.CommandList.Add(nestedCMV);

      nestedCMV = new CommandViewModel();
      nestedCMV.DisplayName = "Nested Menu 2";
      cmv.CommandList.Add(nestedCMV);
     }
     public ObservableCollection<CommandViewModel> CommandList
     {
      get { return m_CommandVMList; }
      set { m_CommandVMList = value; OnPropertyChanged("CommandList"); }
     }

     protected void OnPropertyChanged(string propertyName)
     {
      // Hook up event...
     }

     #region INotifyPropertyChanged Members

     public event PropertyChangedEventHandler PropertyChanged;

     #endregion
    }

    public class CommandViewModel : INotifyPropertyChanged
    {
     private ObservableCollection<CommandViewModel> m_CommandVMList;

     public CommandViewModel()
     {
      this.m_CommandVMList = new ObservableCollection<CommandViewModel>();
     }

     public string DisplayName { get; set; }

     public ObservableCollection<CommandViewModel> CommandList
     {
      get { return m_CommandVMList; }
      set { m_CommandVMList = value; OnPropertyChanged("CommandList"); }
     }

     protected void OnPropertyChanged(string propertyName)
     {
      // Hook up event...
     }

     #region INotifyPropertyChanged Members

     public event PropertyChangedEventHandler PropertyChanged;

     #endregion
    }
}
micahtan
Yeah I wish I could do that but unfortunately I can not, the reason primarily being that the XAML is pretty much out of my hands. It is created / managed by application developers, they simply use my model to augment their menus with stuff that needs to be driven from code.
That is unfortunate. Is there any way that you could change the XAML to take advantage of CompositeCollection (http://msdn.microsoft.com/en-us/library/system.windows.data.compositecollection.aspx)?It's not ideal, but it may allow you to inject your dynamic items. The only drawback I can see is how the resulting MenuItems are sorted, but you may be able to do that by treating the MenuItems that come before as one collection, and the MenuItems that come afterwards as another.
micahtan
Micahtan - maybe you should write the solution with the CompositeCollection class up to your post? 'Cause it seems to be really a right way to do it. Thanks, I've been having the same problem.
arconaut
arconaut - enjoy. But please clean it up before putting it into production :)
micahtan
thanks :) but i wasn't really asking the code for myself, just for the idea to make this a nice question-answer topic and also to stimulate marking your post as answer by Adrian. He doesn't seem to bother or the answer is not suitable for him, though :)
arconaut