WPF: DataContext for ContextMenu


Problem

Often times WPF  developers want to throw their computer out the window because of a small and very irritating truth about the ContextMenu control. I’m talking about the fact that ContextMenu is not in the same visual tree as its containing parent, resulting in many data binding issues. Since the ContextMenu is not in the same visual tree, ElementName, RelativeSouce (FindAncestor), etc bindings will not work. You can get around this through the use of the PlacementTarget property and some complex DataContext re-routing, but it is a pain, confusing, and does not scale well (at all). I will present a very simple attached property that relieves the situation.

Solution

The idea is simple and follows the pattern set fourth in the built-in ContextMenuService class. ContextMenuService has several attached properties that you set on the containing element that affect the behavior of its ContextMenu. The ContextMenuServiceExtensions class adds one more attached property to this tool-set: ContextMenuServiceExtensions.DataContext. When you set this attached property on any FrameworkElement or derivative, it immediately sets the DataContext property on the element’s ContextMenu to the value set in the attached property, which can be a binding. Its easier to explain in code.

public static class ContextMenuServiceExtensions
{
    public static readonly DependencyProperty DataContextProperty =
        DependencyProperty.RegisterAttached("DataContext",
        typeof(object), typeof(ContextMenuServiceExtensions),
        new UIPropertyMetadata(DataContextChanged));

    public static object GetDataContext(FrameworkElement obj)
    {
        return obj.GetValue(DataContextProperty);
    }

    public static void SetDataContext(FrameworkElement obj, object value)
    {
        obj.SetValue(DataContextProperty, value);
    }

    private static void DataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement frameworkElement = d as FrameworkElement;
        if (frameworkElement == null)
            return;

        if (frameworkElement.ContextMenu != null)
            frameworkElement.ContextMenu.DataContext = GetDataContext(frameworkElement);
    }
}

Example Usage

Below is an example of how to use the attached property. The example shows setting the DataContext property on the ContextMenu to the DataContext of the Button using the attached property, and then binding to the source within the ContextMenu.

<Button Content="Right-click me!"
        local:ContextMenuServiceExtensions.DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Button.ContextMenu>
        <ContextMenu ItemsSource="{Binding SomeItemsSourcePropertyOnTheButton}">
            <ContextMenu.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ItemDisplayName}" />
                </DataTemplate>
            </ContextMenu.ItemTemplate>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Conclusion

The attached property demonstrated here saves a lot of time and allows for much cleaner xaml markup. The only limitation here is that logic for the ContextMenu property being reset or being set after the attached property is evaluated is not currently supported; although adding support would be rather simple.

, , , , , , ,

  1. #1 by Anonymous on April 2, 2015 - 6:05 am

    Great work !
    I’ve done some changes to manage hidden columns, by displaying the columns before constructing the context Menu and hiding them after
    Your work is very useful, can i have your permission to push it on MahApps project ?
    http://mahapps.com/

  2. #2 by Anonymous on April 26, 2013 - 12:53 pm

    sorry, i have tried your sample code, but he doesn’t work. Context Menu on button is always null

    • #3 by Tim Valentine on May 5, 2013 - 4:49 pm

      You seem to be correct. I know this was working in production code and has been for a while.. so I investigated a bit.

      It seems I was only using this code inside of DataTemplates. That fact is significant because in my case the ContextMenu was being set after the attachedproperty, not before like shown in this example.

      So, this article needs to be cleaned up a little bit. I’ll see if I can work up something to account for the scenario shown in this example. In the meantime, if anyone comes up with something.. feel free to share :) thanks

      • #4 by Anonymous on July 18, 2014 - 6:05 pm

        The issue appears to be that the DataContext gets assigned BEFORE the ContextMenu does. As such, it’s NULL when the handler is invoked.

        A solution is to create a “dummy” binding on the target’s ContextMenu property and re-fire the DataContext copy whenever the ContextMenu changes.

  3. #5 by joancomasfdz on April 17, 2012 - 4:31 am

    Can you provide any guideline about ho to add support for a logic setted after the attached property is evaluated, please?

    • #6 by Tim Valentine on April 22, 2012 - 3:44 am

      attach an event handler to ContextMenuOpening from within DataContextChanged, then in the the handler get a reference to the object the ContextMenu is on, and set it’s ContextMenu property to the value of the attached property. But you’d probably want to put some logic in to prevent attaching to the ContextMenuOpening event multiple times on the same object.

  4. #7 by Aviv on December 15, 2011 - 2:38 pm

    works like a charm

    • #8 by Kostevichi on September 2, 2013 - 4:59 pm

      I’ve used your idea and creates this Base class, which do the cnlieang for you and you just to implement the hook and unhook methods. public abstract class BehaviorBase : Behavior where T : FrameworkElement { #region Setup protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += AssociatedObjectLoaded; AssociatedObject.Unloaded += AssociatedObjectUnloaded; } private void AssociatedObjectLoaded(object sender, RoutedEventArgs e) { OnAttachedObjectLoaded(); // Hook up a bunch of events to the AssociatedObject OnHookEvents(); } #endregion #region Cleanup private bool _isCleanedUp; private void Cleanup() { if (!_isCleanedUp) { _isCleanedUp = true; AssociatedObject.Loaded -= AssociatedObjectLoaded; AssociatedObject.Unloaded -= AssociatedObjectUnloaded; // Unhook the other events from the AssociatedObject OnUnHookEvents(); } } protected override void OnDetaching() { Cleanup(); base.OnDetaching(); } void AssociatedObjectUnloaded(object sender, RoutedEventArgs e) { OnAttachedObjectUnLoaded(); Cleanup(); } protected virtual void OnAttachedObjectLoaded() { // if the derived class wants to do something when the AssociatedObject load. } protected virtual void OnAttachedObjectUnLoaded() { // if the derived class wants to do something when the AssociatedObject unload. } protected abstract void OnHookEvents(); protected abstract void OnUnHookEvents(); #endregion } you may find it useful.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: