Thursday 15 September 2011

SmartMenuItem - The Power of Binding

In my previous post I mentioned an issue that a lot of people come across with WPF regarding Commands raised by MenuItems.  The problem being that when a MenuItem is nested in a ContextMenu, the Commands the MenuItem raises are not always heard by the Window or its inhabitants.  This is because a ContextMenu is on a separate Visual Tree and the Commands don't get seen by the Window or its Visual Tree.

The way around this is to bind the object reference that the MenuItem sends its Commands (its CommandTarget) to the ContextMenu's PlacementTarget object reference (the object that you right-clicked to bring up the context menu).  You could do this in your xaml for every MenuItem by writing the following each time you add a MenuItem:

CommandTarget="{Binding Path="PlacementTarget", RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}">
Curly braces tell xaml that this is a MarkupExtension - a link to some background code.  Markup works like this:  {MarkupExtensionToRun Parameter1 Parameter2 etc.}. Note the use of curly braces { }.  These indicate to xaml that the the bit in between is a Markup Extension, a piece of code to be run in it's own right.

Alternatively you can write a tiny helper class that means you'll never have to worry about it again.  The below does exactly the same as the above but it does it for us.  All you have to do is start using SmartMenuItem instead of MenuItem.

The Code
public class SmartMenuItem : MenuItem
{
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (!BindingOperations.IsDataBound(this, MenuItem.CommandTargetProperty))
        {
            Binding b = new Binding("PlacementTarget");
            b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ContextMenu), 1);
            b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            this.SetBinding(MenuItem.CommandTargetProperty, b);
        }
    }
}

Step by Step Breakdown
Run this code when the template for a SmartMenuItem is applied
public override void OnApplyTemplate()
    {
        Do all the normal template application things first
        base.OnApplyTemplate();

        Check whether the CommandTarget was bound by the Template
        If it was then we don't apply our binding as we assume the user was trying to override our behaviour
        if (!BindingOperations.IsDataBound(thisMenuItem.CommandTargetProperty))
        {
            The Path is the name of the source property on our RelativeSource
            Binding b = new Binding("PlacementTarget");

            When looking for the Path of "PlacementTarget", look for it in our first ancestor of type ContextMenu
            b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ContextMenu), 1);

            Update our source whenever the target property changes (i.e. re-find our relative source)
            b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

            Apply the binding
            this.SetBinding(MenuItem.CommandTargetProperty, b);
        }
    }

Why Use Binding?
If we were to have written code that dug up the ancestor of type ContextMenu and then said something like mySmartMenuItem.CommandTarget = myParentContextMenu.PlacementTarget then this link would have been Static.  What does this mean?  It means that even if we moved our SmartMenuItem into a different menu then it would still reference the original ContextMenu's placement target and, fundamentally, would present behaviour that is "broken".

Binding, on the other hand, is Dynamic.  The above code doesn't find our SmartMenuItem's parent ContextMenu and use its PlacementTarget as the CommandTarget at compile time.  It does it when the code is executed during runtime.  As you can imagine; this is much more flexible and intuitive.

It's worth mentioning in closing that as a typical rule of thumb, all things dynamic are slower than their static equivalent.  You're not going to see a huge performance hit the second you start using Binding but don't go using it just for the sake of it.

1 comment:

  1. Wow

    It's amazing. I found the code, which i was trying from last few days.

    It's really worth.

    ReplyDelete