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 by Aviv on December 15, 2011 - 2:38 pm
works like a charm
#2 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?
#3 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.