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:
- 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?
- 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?
0 comments:
Post a Comment