views:

33

answers:

1

I want to display several actions in a ContextMenu attached to a Hyperlink in a FlowDocument. Some of these actions depend on the value of the Hyperlink object's NavigateUri property. How can I get a reference to the Hyperlink that the user right-clicked?

Unfortunately, it's not as simple as using the PlacementTarget property. As this (unanswered) question in the MSDN forums noted, the PlacementTarget of the ContextMenu does not point to the Hyperlink element, but to the entire FlowDocumentScrollViewer: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/3ab90017-dea8-497c-a937-87a403cb24e0

So I need another way to figure out which Hyperlink the user clicked.

Note that my context menu is defined as a resource in the UserControl containing my FlowDocumentScrollViewer, and attached to each Hyperlink using a style property setter, like so:

<UserControl.Resources>
    <ContextMenu x:Key="contextMenu">
        <MenuItem Name="mnuOpen" Header="_Open Link" Click="mnuOpen_Click" />
        <MenuItem Name="mnuView" Header="_View Properties" Click="mnuView_Click" />
    </ContextMenu>
    <Style TargetType="Hyperlink">
        <Setter Property="ContextMenu" Value="{DynamicResource contextMenu}" />
    </Style>
</UserControl.Resources>

Any hints would be greatly appreciated!

+1  A: 

The framework actually tracks that value in the PopupControlService.Owner property, but it's an internal class. If you're willing to use undocumented features, you could iterate over the properties on the ContextMenu and pull it out:

private static object GetOwner(ContextMenu menu)
{
    var prop = menu.GetLocalValueEnumerator();
    while (prop.MoveNext())
    {
        if (prop.Current.Property.Name == "Owner" &&
            prop.Current.Property.OwnerType.Name == "PopupControlService")
        {
            return prop.Current.Value;
        }
    }
    return null;
}

The other approach is that ContextMenuService.ContextMenuOpeningEvent will be raised by the Hyperlink, so you could take the sender parameter and put it in your own attached property. Something like this:

public class ContextMenuOwnerTracker
{
    private static bool isInitialized;
    public static void Initialize()
    {
        if (!isInitialized)
        {
            isInitialized = true;
            EventManager.RegisterClassHandler(typeof(ContentElement), 
                ContextMenuService.ContextMenuOpeningEvent, 
                new ContextMenuEventHandler(OnContextMenuOpening));
            EventManager.RegisterClassHandler(typeof(ContentElement), 
                ContextMenuService.ContextMenuClosingEvent, 
                new ContextMenuEventHandler(OnContextMenuClosing));
        }
    }

    private static void OnContextMenuOpening
        (object sender, ContextMenuEventArgs args)
    {
        var menu = ContextMenuService.GetContextMenu((DependencyObject)sender);
        if (menu != null)
        {
            SetOwner(menu, sender);
        }
    }

    private static void OnContextMenuClosing
        (object sender, ContextMenuEventArgs args)
    {
        var menu = ContextMenuService.GetContextMenu((DependencyObject)sender);
        if (menu != null)
        {
            ClearOwner(menu);
        }
    }

    private static readonly DependencyPropertyKey OwnerKey =
        DependencyProperty.RegisterAttachedReadOnly(
            "Owner",
            typeof(object),
            typeof(ContextMenuOwnerTracker),
            new PropertyMetadata(null));
    public static readonly DependencyProperty OwnerProperty =
        OwnerKey.DependencyProperty;
    public static object GetOwner(ContextMenu element)
    {
        return element.GetValue(OwnerProperty);
    }
    private static void SetOwner(ContextMenu element, object value)
    {
        element.SetValue(OwnerKey, value);
    }
    private static void ClearOwner(ContextMenu element)
    {
        element.ClearValue(OwnerKey);
    }
}

Note that there may be multiple ContentElements with ContextMenu properties and this will actually set Owner on all of them, even though it only matters for the one that is actually displayed.

To get the value for a specific MenuItem, you will have to walk up the tree or use a binding with {RelativeSource AncestorType=ContextMenu}. You could also mark the Owner property inherited to have it propagate automatically to the MenuItems.

If you attach the context menu at a higher level than the Hyperlink, you can use OriginalSource instead of sender, but that will normally give you a Run so you'll have to walk up the tree to find a Hyperlink.

Quartermeister
Thanks, @Quartermeister. I knew there had to be a way to do it.
dthrasher