I want to create a context menu in my program, but I want 3rd parties to be able to extend it.

asked 01 Jun '10, 21:37

Scott%20Whitlock's gravatar image

Scott Whitlock ♦♦
696262833
accept rate: 50%


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.

link

answered 01 Jun '10, 21:41

Scott%20Whitlock's gravatar image

Scott Whitlock ♦♦
696262833
accept rate: 50%

Note: the next version of SoapBox Core will include contextMenuStyle in SoapBox.Core.Contracts which will make this a bit shorter. The only difference is you have to reference it with a DynamicResource binding rather than a StaticResource binding.

(27 Jun '10, 09:30) Scott Whitlock ♦♦
Your answer
toggle preview

Follow this question

By Email:

Once you sign in you will be able to subscribe for any updates here

By RSS:

Answers

Answers and Comments

Markdown Basics

  • *italic* or _italic_
  • **bold** or __bold__
  • link:[text](http://url.com/ "Title")
  • image?![alt text](/path/img.jpg "Title")
  • numbered list: 1. Foo 2. Bar
  • to add a line break simply add two spaces to where you would like the new line to be.
  • basic HTML tags are also supported

Tags:

×23
×7
×2
×1

Asked: 01 Jun '10, 21:37

Seen: 1,516 times

Last updated: 27 Jun '10, 09:30

powered by OSQA