Monday, December 27, 2010

Creating Dynamic Menus in WPF and Prism - Part 4

So, last time I noted that my approach was less than, uhm, optimal.  In fact, it was pretty bad.  At least it didn't involve convoluted code behind!  Anyway.

So, I'd like to have a nice, dynamic menu or toolbar (yup, either or both) that Prism modules can add items to.  It'd be nice if they could place those items where they need them, such that the menus are still ordered in a useful manner for the user.  It'd also be nice if a module could add an item that was always displayed, versus an item that is only displayed when a view in the module is active. 

Whew.

Spec the code.  Design the code.  Implement the code.  (Okay, so I didn't do that completely, and I paid for it.  But let's pretend that I practice what I preach...)

  1. The menu / toolbar service should not be tied to its view.  That is, items added should be able to be displayed in a menu, toolbar, "ribbon", or some hybrid thereof.
  2. Modules should be able to differentiate between the various types however - I should know if I'm adding an item to a menu or a toolbar.
  3. Modules should be able to specify whether an item has a parent (which can be any previously added item), and place the item relative to some other child of that parent.
  4. Modules should be able to add a seperator.
  5. Items that are added can be either permanent (always shown), or tied to the activity of the module.
Design decisions time!

You'll note that I already defined that the dynamic menu / toolbar thingy should be a service - based on the shellacking of my previous attempt, its become pretty obvious that this thing should be a service interface in the Prism project's Infrastructure DLL.  I didn't list that in the requirements, but it probably should be.

The first requirement should be pretty easy to meet - the service can expose an ObservableCollection of items, and the various UI elements can use a HierarchicalDataTemplate to format the collection.  This does mean that I'll probably need multiple implementations of the service however (requirement #2) - that shouldn't be a problem however using Unity's Resolve(string name) overload.

Requirement three means that each item will have to have children items, and the service should:
  • expose a way to add an item that optionally includes a parent and a predecessor
  • expose an IEnumerable<> of items, so that modules can query for parents / predecessors
  • (maybe?) expose a way to query for items, since the hierarchical nature of the data means queries will have to be recursive (and writing recursive LINQ operators everywhere would kinda suck)
Requirement four: items can be just a separator.  Boolean field for the win!

Requirement five is tricky.  There's a couple of ways Prism handles what is viewed / active:
  1. IActiveAware.  Views can implement this interface, which gives you a boolean field indicating whether or not the view is active, and an event that can be subscribed to when that field changes.  This is tied to a Region's concept of what is active.  If we allow modules to provide an IActiveAware object, a menu item could be tied to that, and we could show / hide a menu item based on that object.
  2. The service could implement INavigationAware.  This would give us notification when a navigation request is made to some object.  I'm not sure what happens if we register an object that is INavigationAware that is not designed to handle a navigation URI request - in essence, we want our service to snoop navigation, not actually perform in it.
  3. A PresentationEvent, that modules can fire when they want their items shown or hidden.
Option #1 provides a lot of granularity, as each View can have menu items added / removed.  On the other hand, it may provide too much granularity, and we may find ourselves adding / removing items when we don't want to.  We'd have to be careful as to what object implementing IActiveAware is sent to the service.

Option #2 feels a little fishy.  While it seems ideal, I don't like our service being a navigation object when it isn't; that, and at least two of the methods (OnNavigatedTo and OnNavigatedFrom) would never be called.  Implmenting half of interfaces always feels a little bad, so I'm inclined not to pick that route.

Option #3 gives the most granularity, but also requires more out of the users of the service.  I want this to be as "fire and forget" as possible - I put in my menu items, and I move on.

So, I'm probably going to go with Option #1.

Lots of design decisions made, with a lot of tradeoffs.  Next time: more design decisions, and some initial implementation.

Saturday, December 25, 2010

Creating Dynamic Menus in WPF and Prism - Part 3

This may be the topic that won't die.  After the train wreck that was my first attempt at creating a dynamic menu in WPF / Prism (see the past two blog posts, plus my question on StackOverflow), I ended up getting a pretty nice dynamic menu working using what Robert Rossney suggested (again, see that post on StackOverflow...).  My approach generally followed the following:
  1. The menu (or pseudo-toolbar) was implemented in its own Prism Module.  Other modules obtained the service by referencing the module, and could add menu items by accessing the service.
  2. Menu items were added by specifying a command, menu name, a reference to an image to display, and optionally, a parent menu item.
The meat of this approach is below.  A ViewModel contains an ObservableCollection of ToolbarItems, while a View interprets those items as a menu.  The toolbar service exposes a method AddMenu that modules can call to add their items:


///
/// Add a menu item to the toolbar
///

public void AddMenu(Action<object>callbackFunction, string menuName, string iconLocation, string parentMenu)
{
   if (String.IsNullOrEmpty(menuName))
      throw new ArgumentNullException("menuName");
   if (ExistingMenuNames.Contains<String>(menuName))
      throw new InvalidOperationException("Cannot add a menu item with the same name as an existing menu item");

   ToolbarObject newObject = new ToolbarObject(
      menuName,
      iconLocation,
      new CompositeCommand());

   // Add a command, if available
   if (callbackFunction != null)
      newObject.Command.RegisterCommand(new DelegateCommand<Object>(callbackFunction));

   // Set up the parent menu, if available
   if (!String.IsNullOrEmpty(parentMenu))
   {
      if (!ExistingMenuNames.Contains<String>(parentMenu))
         throw new InvalidOperationException("Cannot add a menu to a parent menu item that doesn't exist");
      if (_model == null)
         _model = _container.Resolve<IToolBarPresentationModel>();

      var parent = (from item in _model.ToolbarItems
            where String.Equals(item.Name, parentMenu, StringComparison.InvariantCulture)
            select item).Single<ToolbarObject>();
      parent.Children.Add(newObject);
   }
   else
   {
      // Insert after Main, keeping Help last
      _model.ToolbarItems.Insert(1, newObject);
   }
}


The big issue I had run into before was actually displaying the items in the View appropriately. Enter the wonderful HierarchicalDataTemplate, and its little (very necessary) cousin, ItemContainerStyle. The view code using these two is below.


   <UserControl.Resources>
      <model:ToolBarPresentationModel x:Key="modelData" />
      <Style TargetType="MenuItem">
         <Setter Property="Command" Value="{Binding Path=Command}"/>
         <Setter Property="Foreground" Value="White"/>
      </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>
   <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>


This code creates a menu where each menu item in the parent menu can have children, or be a clickable menu item itself.  Each menu item provides its own path to a resource, such that each menu item shows an image and a text description.  Due to the limited nature of the application, the first menu is always kept as Main (or File), while the last is always kept as Help.

While this approach worked great for the application at hand, its not very useful for a general application.  To whit, the following are some of the obvious drawbacks:

  1. First come, first served.  If Module A adds an item to the menu without a parent, that will get placed after Main.  If Module B does the same, then it will get added after Main, pushing Module A's menu further down the line.  This may not be desireable.
  2. Nesting of menus only goes down one level.  If you specify a parent that is not in the first row of the menu, the parent won't be found.
  3. Specifying an Action as the parameter only gives us a callback, and doesn't allow multiple modules to tie a callback to a single menu option.  This works fine if a menu option should only be handled by a single module, but works poorly if a number of modules need to respond to a menu click, e.g., Save or Exit.
  4. Menu options that are added under a parent are added on a first come, first served basis.  This can lead to some odd and confusing orderings, depending on the Module loading.
  5. No separators.  Ugh.
  6. The toolbar service module should not have to be referenced directly by other modules.
  7. Does everything really need an image?  This approach was supposed to emulate (to a very, very limited extent) a Ribbon-style control, combining both menu / toolbar approaches.  While kind of fun and quirky, its not appropriate for a typical line of business application.
Okay, so this approach kind of, well, sucks.  But we learn from our mistakes.  Next time: doing it better, part 1!  (Or is it Part 4?)

Edit

Also: Merry Christmas!  The fact that I'm interested in writing about code again may be an "end of the year" thing, but I'll see where it takes me for now.