Create the ContextMenu and ContextMenuEnabled Properties
Create these properties on your ViewModel:
#region " ContextMenu "
public IEnumerable<IMenuItem> ContextMenu
{
get
{
return m_ContextMenu;
}
protected set
{
if (m_ContextMenu != value)
{
m_ContextMenu = value;
NotifyPropertyChanged(m_ContextMenuArgs);
}
}
}
private IEnumerable<IMenuItem> m_ContextMenu = null;
static readonly PropertyChangedEventArgs m_ContextMenuArgs =
NotifyPropertyChangedHelper.CreateArgs<MyViewModelType>(o => o.ContextMenu);
#endregion
#region " ContextMenuEnabled "
/// <summary>
/// Allows control of whether or not the context menu is enabled.
/// True by default.
/// </summary>
public bool ContextMenuEnabled
{
get
{
return m_ContextMenuEnabled;
}
protected set
{
if (m_ContextMenuEnabled != value)
{
m_ContextMenuEnabled = value;
NotifyPropertyChanged(m_ContextMenuEnabledArgs);
}
}
}
private bool m_ContextMenuEnabled = true;
static readonly PropertyChangedEventArgs m_ContextMenuEnabledArgs =
NotifyPropertyChangedHelper.CreateArgs<MyViewModelType>(o => o.ContextMenuEnabled);
static readonly string m_ContextMenuEnabledName =
NotifyPropertyChangedHelper.GetPropertyName<MyViewModelType>(o => o.ContextMenuEnabled);
#endregion
Import the Context Menu Items
The ViewModel that abstracts the control that will have the context menu has to import the context menu items, just like the Workbench imports the main menu (make sure your ViewModel class is implementing IPartImportsSatisfiedNotification):
[Import(SoapBox.Core.Services.Host.ExtensionService)]
private IExtensionService extensionService { get; set; }
[ImportMany(ExtensionPoints.Workbench.Pads.MyViewModelType.ContextMenu,
typeof(IMenuItem), AllowRecomposition = true)]
private IEnumerable<IMenuItem> contextMenu { get; set; }
public void OnImportsSatisfied()
{
ContextMenu = extensionService.Sort(contextMenu);
}
Create a View Including a Context Menu
Put this in a new resource dictionary file:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Your.Namespace"
xmlns:contracts="clr-namespace:SoapBox.Core;assembly=SoapBox.Core.Contracts"
x:Class="Your.Namespace.ViewClassname">
<DataTemplate DataType="{x:Type local:ViewModelClassname}">
<DataTemplate.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<Style x:Key="contextMenuStyle">
<Setter Property="MenuItem.Header"
Value="{Binding Path=(contracts:IMenuItem.Header)}"/>
<Setter Property="MenuItem.ItemsSource"
Value="{Binding Path=(contracts:IMenuItem.Items)}"/>
<Setter Property="MenuItem.Icon"
Value="{Binding Path=(contracts:IMenuItem.Icon)}"/>
<Setter Property="MenuItem.IsCheckable"
Value="{Binding Path=(contracts:IMenuItem.IsCheckable)}"/>
<Setter Property="MenuItem.IsChecked"
Value="{Binding Path=(contracts:IMenuItem.IsChecked)}"/>
<Setter Property="MenuItem.Command"
Value="{Binding}"/>
<Setter Property="MenuItem.Visibility"
Value="{Binding Path=(contracts:IControl.Visible),
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Setter Property="MenuItem.ToolTip"
Value="{Binding Path=(contracts:IControl.ToolTip)}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(contracts:IMenuItem.IsSeparator)}"
Value="true">
<Setter Property="MenuItem.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Separator
Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</DataTemplate.Resources>
<StackPanel Orientation="Horizontal"
ContextMenuOpening="stackPanel_ContextMenuOpening">
<Whatever you want goes here.../>
<StackPanel.ContextMenu>
<ContextMenu x:Name="contextMenu"
ItemsSource="{Binding Path=(local:YourViewModelClassname.ContextMenu)}"
IsEnabled="{Binding Path=(local:YourViewModelClassname.ContextMenuEnabled)}"
ItemContainerStyle="{StaticResource contextMenuStyle}"
/>
</StackPanel.ContextMenu>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
Create the Resource Dictionary CodeBehind
We need a CodeBehind to export the resource dictionary into the Views collection of the host, but we also need to handle the ContextMenuOpening event on the StackPanel. I've been trying to figure out a way to do this without using the CodeBehind, but it escapes me. At any rate, the point of the event handler is to get a reference to the ViewModel of the control that was clicked on, and pass that to the menu before it opens, so it has a "context". The context has to be the ViewModel, not the View or the control. Here's what the CodeBehind for the View looks like:
namespace Your.Namespace
{
[Export(SoapBox.Core.ExtensionPoints.Host.Views, typeof(ResourceDictionary))]
public partial class ViewClassname : ResourceDictionary
{
public ViewClassname()
{
InitializeComponent();
}
/// <summary>
/// Tell the context menu items about the ViewModel that is the "context"
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void stackPanel_ContextMenuOpening(
object sender, ContextMenuEventArgs e)
{
StackPanel sp = sender as StackPanel;
if (sp != null)
{
YourViewModelClassname vm =
sp.DataContext as YourViewModelClassname;
if (vm != null)
{
IEnumerable<IMenuItem> items =
vm.ContextMenu as IEnumerable<IMenuItem>;
if (items != null)
{
foreach (IMenuItem item in items)
{
// will automatically set all
// child menu items' context as well
item.Context = vm;
}
}
else
{
e.Handled = true;
}
}
else
{
e.Handled = true;
}
}
else
{
e.Handled = true;
}
}
}
}
That way, you can have lots of instances of your ViewModel floating around, and if the user right clicks on one to open a context menu, it's always the same context menu that gets opened, but the Context property on the menu items will always be set to the ViewModel of whatever the user clicked on.
answered
01 Jun '10, 21:41
Scott Whitlock ♦♦
696●25●28●33
accept rate:
52%