Friday, September 17, 2010

WPF, Prism, and Creating Dynamic Menus

This blog post originated from a question on StackOverflow How to programmatically set MenuItem.Header in a dynamic menu that I asked.  The first (and currently, only) person responding to the question implied that they weren't sure how I ended up where I did.  In order to clarify, I decided to write up this post (first one in what, a year?)

I've been working for sometime now on an application that utilizes WPF and Prism. Originally, the application had a static menu hosted in a seperate module - when the user clicked an item in the menu, static CompositeCommand objects would route command data back to a presentation model where DelegateCommands would handle the events.  This was an easy thing to knock together, as shown below.

The Original

PresentationModel

The original code behind the PresentationModel (or ViewModel - I started off with PresentationModel, even though VM is probably a better way of stating it - for consistency's sake, I'm going to stick with PM):



public interface IToolBarPresentationModel
{
   /// <summary>
   /// View associated with this model
   /// </summary>
   IToolBarView View { get; set; }
}

internal sealed class ToolBarPresentationModel : IToolBarPresentationModel
{
   private readonly IUnityContainer _container;
   private readonly IEventAggregator _eventAggregator;
   public DelegateCommand<object> FileExitCommand { get; set; }
   public DelegateCommand<object> HelpCommand { get; set; }

   /// <summary>
   /// Default constructor
   /// </summary>
   /// <param name="container">The unity container</param>
   /// <exception cref="ArgumentNullException">Thrown if the container is null</exception>
   public ToolBarPresentationModel(IUnityContainer container, IEventAggregator eventAggregator)
   {
      if (container == null)
         throw new ArgumentNullException("container", "The IUnityContainer cannot be null");
      if (eventAggregator == null)
         throw new ArgumentNullException("eventAggregator", "The IEventAggregator cannot be null");

      _container = container;
      _eventAggregator = eventAggregator;

      View = _container.Resolve<IToolBarView>();
      View.Model = this;

      FileExitCommand = new DelegateCommand<object>(new Action<object>(OnFileExit));
      HelpCommand = new DelegateCommand<object>(new Action<object>(OnHelp));

      ToolBarCommands.FileExit.RegisterCommand(this.FileExitCommand);
      ToolBarCommands.Help.RegisterCommand(this.HelpCommand);
   }
}

   private void OnFileExit(Object obj)
   {
      Application.Current.Shutdown();
   }

   private void OnHelp(Object obj)
   {
      var presentationModel = _container.Resolve<IHelpPresentationModel>();
      // Notify that the help view should be shown
      _eventAggregator.GetEvent<ChangeViewEvent>().Publish(new ChangeViewEventArguments(ViewName.HelpView));
   }

   #region IToolBarPresentationModel Members

   public Modules.ToolBar.Views.IToolBarView View
   {
      get;
      set;
   }

   #endregion
}

This is fairly straight-forward. We have two commands - File->Exit and Help, each of which have a static CompositeCommand that we add our DelegateCommand objects to. The Action targets exit the application, and call an event that forces the Help view to be displayed.

View

The original code behind simply implemented the View's interface:


public interface IToolBarView
{
   /// <summary>
   /// The model associated with this view
   /// </summary>
   IToolBarPresentationModel Model { get; set; }


The XAML was almost equally vanilla - except for one small detail that ended up being the crux of the problem:


<UserControl x:Class="Modules.ToolBar.Views.ToolBarView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:Modules.ToolBar.PresentationModels"
xmlns:local="clr-namespace:Modules.ToolBar">
<Grid>
   <Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent"
ItemsSource="{Binding}">
      <MenuItem Name="MenuFile" AutomationProperties.AutomationId="File">
         <MenuItem.Header>
            <StackPanel>
               <Image Height="24" VerticalAlignment="Center" Source="../Resources/066.png"/>
               <ContentPresenter Content="Main"/>
            </StackPanel>
         </MenuItem.Header>
         <MenuItem AutomationProperties.AutomationId="FileExit" Command="{x:Static local:ToolBarCommands.FileExit}">
            <MenuItem.Header>
               <StackPanel>
                  <Image Height="24" VerticalAlignment="Center" Source="../Resources/002.png"/>
                  <ContentPresenter Content="Exit"/>
               </StackPanel>
            </MenuItem.Header>
         </MenuItem>
      </MenuItem>
      <MenuItem Name="MenuHelp" AutomationProperties.AutomationId="Help" Command="{x:Static local:ToolBarCommands.Help}">
         <MenuItem.Header>
            <StackPanel>
               <Image Height="24" VerticalAlignment="Center" Source="../Resources/152.png"/>
               <ContentPresenter Content="Help"/>
            </StackPanel>
         </MenuItem.Header>
      </MenuItem>
   </Menu>
</Grid>
</UserControl>


The part of this XAML layout of the menu that I liked in the first cut was the appearance of each <MenuItem> - the <StackPanel> surrounding the <Image> and <ContentPresenter> lays out content such that it has sort of a "poor man's Ribbon" look and feel to it.

So: the goal is to keep the original look and feel of the menu, while updating it with the capability to have other modules add items to the menu.  The first cut!

Attempt One

So the first attempt had me adding a new model class that would represent a single item in the menu, called ToolbarObject - shown below.



/// <summary>
/// Represents an object on the toolbar
/// </summary>

public sealed class ToolbarObject : INotifyPropertyChanged
{

   public ToolbarObject() : this(String.Empty, String.Empty, null)
   {

   }
   public ToolbarObject(
      String name,
      String imageLocation,
      CompositeCommand command)
   {
      _name = name;
      _imageLocation = imageLocation;
      _command = command;
      Children = new ObservableCollection();
   }

   private String _name;
   public String Name
   {
      get { return _name; }
      set
      {
         _name = value;
         NotifyPropertyChanged("Name");
      }
   }

   private string _imageLocation;
   public String ImageLocation
   {
      get
      {
         return _imageLocation;
      }
      set
      {
         _imageLocation = value;
         NotifyPropertyChanged("ImageLocation");
      }
   }

   private CompositeCommand _command;
   public CompositeCommand Command
   {
      get
      {
         return _command;
      }
      set
      {
         _command = value;
         NotifyPropertyChanged("Command");
      }
   }

   private ObservableCollection _children;
   public ObservableCollection Children
   {
      get
      {
         return _children;
      }
      set
      {
         _children = value;
      }
   }
  
   #region INotifyPropertyChanged Members
      public event PropertyChangedEventHandler PropertyChanged;
   #endregion

   private void NotifyPropertyChanged(String name)
   {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (!String.IsNullOrEmpty(name)
         && handler != null)
      {
         handler(this, new PropertyChangedEventArgs(name));
      }
   }
}


Since I now have an object representing the menu, the PresentationModel was updated with an ObservableCollection of ToolbarItems - shown in the following (just showing the interface, as the implementation doesn't need explanation):



public interface IToolBarPresentationModel
{

/// <summary>
/// View associated with this model
/// </summary>

IToolBarView View { get; set; }

ObservableCollection<ToolbarObject> ToolbarItems { get; set; }

}


The view was updated to use a hierarchical data template, and the menu was bound to the data template and the new observable collection:


<UserControl x:Class="Modules.ToolBar.Views.ToolBarView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:Modules.ToolBar.PresentationModels"
xmlns:local="clr-namespace:Modules.ToolBar">
   <UserControl.Resources>
      <model:ToolBarPresentationModel x:Key="modelData" />
      <HierarchicalDataTemplate DataType="{x:Type model:ToolbarObject}"
         ItemsSource="{Binding Path=Children}">
         <MenuItem Command="{Binding Path=Command}">
            <MenuItem.Header>
               <StackPanel>
                  <Image Height="24" VerticalAlignment="Center" Source="{Binding Path=ImageLocation}"/>
                  <ContentPresenter Content="{Binding Path=Name}"/>
               </StackPanel>
            </MenuItem.Header>
         </MenuItem>
      </HierarchicalDataTemplate>
    </UserControl.Resources>
<UserControl.DataContext>
    <Binding Source="{StaticResource modelData}"/>
</UserControl.DataContext>
<Grid>
   <Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent"
ItemsSource="{Binding}">
   </Menu>
</Grid>
</UserControl>


This does a great job of rendering the first row in the menu - however, although the command routing works, the images are shown for the first items in the menu, any subitems aren't shown. Clicking on "File", for example, wouldn't collapse any submenu. In researching this, I came across two articles:

Building a Databound WPF Menu using a HierarchicalDataTemplate

WPF Sample Series - DataBound HierarchicalDataTemplate Menu Sample

In these examples, the code behind is responsible (in a load event) for constructing the hiearchy in the menu and ensuring that everything gets bound together correctly.  When this is just a menu, this works great - however, the moment you try to place the <StackPanel> into the <MenuItem.Header>, the whole thing doesn't get rendered (it is clickable, however).  The question on StackOverflow thus became the following:
  1. Admittedly, there is a "code-smell" in using the code-behind to construct what should just be a HierarchicalDataTemplate.  Is there a way to do this that doesn't involve the load event?
  2. If not, then how can you get the rendering of items placed into a MenuItem's Header to work?  What is preventing them from being drawn?
When I get more of a conclusion, I'll update this with the version that uses the advice in the menus and the actual solution (I'm confident there is a way to do this that isn't too ugly).  For now, the XAML and code-behind showing my version of the menu construction in the load event is available on the StackOverflow question here.

0 comments:

Post a Comment