Saturday, September 18, 2010

WPF, Prism, and Creating Dynamic Menus - Part 2

Continuing my post from last time...

So my question on StackOverflow ended up getting a great answer that worked perfectly.  Where did I go wrong?

I listened to advice on teh internets (and I'm still learning WPF, which, if I knew it better, would have precluded me from going down this wonky path at all).  The "path" I ended up going down had a dynamic menu being created in a HierarchicalDataTemplate (good), but the actual content of the menu being generated in a load event in the code-behind (bad).

It's not that I'm against code in the code-behind.  I think there are times where it does make sense - take click event handlers in a ListView, for example.  There isn't a great way to route that through an ICommand object, at least that I'm aware of.  Often you aren't going to really do anything in the click handler - you just need to pass it back to the PresentationModel / ViewModel, which has all the logic to do the action of the click anyway.  You just need to handle the click.  And, unlike a menu, toolbar, or other button, you probably have a one-to-one relationship between the click handler and the method in PM/VM that will handle the action (so the benefit of an ICommand object is lessened somewhat).  Anyway.

That rant over, I pretty much knew I was hosing myself when I had written this blob of code:





private void ContentPresenter_Loaded(object sender, System.Windows.RoutedEventArgs e)
{

   ContentPresenter presenter = sender as ContentPresenter;

   if (sender != null)
   {
      DependencyObject parentObject = VisualTreeHelper.GetParent(presenter);
      bool bContinue = true;

      while (bContinue
         || parentObject == null)
      {
         if (parentObject is MenuItem)
            bContinue = false;
         else
            parentObject = VisualTreeHelper.GetParent(parentObject);
      }
      var menuItem = parentObject as MenuItem;

      if (menuItem != null)
      {

         ToolbarObject toolbarObject = menuItem.DataContext as ToolbarObject;
         StackPanel panel = new StackPanel();
         if (!String.IsNullOrEmpty(toolbarObject.ImageLocation))
         {
            Image image = new Image();
            image.Height = 24;
            image.VerticalAlignment = System.Windows.VerticalAlignment.Center;

            Binding sourceBinding = new Binding("ImageLocation");
            sourceBinding.Mode = BindingMode.TwoWay;
            sourceBinding.Source = toolbarObject;

            image.SetBinding(Image.SourceProperty, sourceBinding);

            panel.Children.Add(image);

         }

         ContentPresenter contentPresenter = new ContentPresenter();
         Binding contentBinding = new Binding("Name");
         contentBinding.Mode = BindingMode.TwoWay;
         contentBinding.Source = toolbarObject;
         contentPresenter.SetBinding(ContentPresenter.ContentProperty,
            contentBinding);
        
         panel.Children.Add(contentPresenter);

         menuItem.Header = panel;

         Binding commandBinding = new Binding("Command");
         commandBinding.Mode = BindingMode.TwoWay;
         commandBinding.Source = toolbarObject;
         menuItem.SetBinding(MenuItem.CommandProperty, commandBinding);
      }
   }
}


Yeah, that's no good at all. The end result of this was a "blank" menu, where the non-rendered items were clickable, but no rendering was done on the StackPanel embedded in the MenuItem's Header. Close, but no cigar.

Luckily, you can handle this pretty easily using the ItemContainerStyle property of a HierarchicalDataTemplate.  (I added the additional Style directive to handle the case where the command needs to occur on an item in the first row of the Menu)



<UserControl.Resources>

   <model:RecapToolBarPresentationModel x:Key="modelData" />
   <Style TargetType="MenuItem">
      <Setter Property="Command" Value="{Binding Path=Command}"/>
   </Style>
   <HierarchicalDataTemplate DataType="{x:Type model:ToolbarObject}"
      ItemsSource="{Binding Path=Children}">
      <HierarchicalDataTemplate.ItemContainerStyle>
         <Style TargetType="MenuItem">
            <Setter Property="Command" Value="{Binding Path=Command}"/>
         </Style>
      </HierarchicalDataTemplate.ItemContainerStyle>
      <StackPanel VerticalAlignment="Top" Margin="5,0,5,0">
         <Image Height="24" VerticalAlignment="Center" Source="{Binding Path=ImageLocation}"/>
         <ContentPresenter Content="{Binding Path=Name}" HorizontalAlignment="Center"/>
      </StackPanel>
   </HierarchicalDataTemplate>
</UserControl.Resources>



Lessons Learned:
  1. I've got a lot to learn in WPF still.  Knowing enough to get some basic ListViews displayed (and enough to get Prism more or less working for me) is great and all, but the more complex DataTemplating is still a learning ground.  Good stuff though - the power of WPF when compared against WinForms (or worse, MFC) is simply astounding.
  2. Watch out for advice coming teh internets.  StackOverflow's ratings really do provide a nice level of confidence on the advice you get (which, incidentally, you certainly aren't getting by reading this blog)

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.