[Silverlight] How to handle the contextual menu

silverlight_thumb5 Hello again,

There is a question that I often see on Silverlight’s forum. It’s about the contextual menu (right click). The current answer is that it’s not possible to surcharge them like we can do in Flash. Maybe in the next version of Silverlight…

However I’m gonna show you a solution I created for bypass the classic right click. Unfortunatly my method works well on Internet Explorer and quite bad on Firefox where the classic menu displays and then the custom menu. If someone knows a workaround for Firefox he can post a comment.

For an easy use I created a RightClickService class, which allow us to create contextual menus for a lot of controls.

First, the code for ContextMenuItem and ContextMenu.

[TemplateVisualState(Name = "Normal", GroupName = "CommonStates"),
TemplateVisualState(Name = "Focused", GroupName = "FocusStates"),
TemplateVisualState(Name = "MouseOver", GroupName = "CommonStates"),
TemplateVisualState(Name = "Disabled", GroupName = "CommonStates"),
TemplateVisualState(Name = "Unselected", GroupName = "SelectionStates"),
TemplateVisualState(Name = "Selected", GroupName = "SelectionStates"),
TemplateVisualState(Name = "SelectedUnfocused", GroupName = "SelectionStates"),
TemplateVisualState(Name = "Unfocused", GroupName = "FocusStates")]
public class ContextMenuItem : ContentControl
{
    public event MouseButtonEventHandler Click;
 
    public ContextMenuItem()
    {
        this.DefaultStyleKey = typeof(ContextMenuItem);
        this.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
        {
            if (Click != null) Click(this, e);
        };
    }
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        VisualStateManager.GoToState(this, "Normal", false);
    }
 
    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        if (!e.Handled)
        {
            base.OnMouseLeftButtonUp(e);
            e.Handled = true;
            if (RightClickService.Popup != null) RightClickService.Popup.IsOpen = false;
        }
    }
 
    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        VisualStateManager.GoToState(this, "MouseOver", false);
    }
 
    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        VisualStateManager.GoToState(this, "Normal", false);
    }
}

It’s just a simple control who looks like a ListBoxItem. I also created a Click event which is fired when a MouseLeftButtonDown occurs.

The theme’s control:

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ContextMenu"    
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
    <Style TargetType="local:ContextMenu">
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Background" Value="#FFFFFFFF" />
        <Setter Property="Foreground" Value="#FF000000"/>
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="TabNavigation" Value="Once" />
        <Setter Property="BorderBrush">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFA3AEB9" Offset="0"/>
                    <GradientStop Color="#FF8399A9" Offset="0.375"/>
                    <GradientStop Color="#FF718597" Offset="0.375"/>
                    <GradientStop Color="#FF617584" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:ContextMenu">
                    <Border CornerRadius="2" 
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <ItemsPresenter />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
    <Style TargetType="local:ContextMenuItem">
        <Setter Property="Padding" Value="3" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:ContextMenuItem">
                    <Grid Background="{TemplateBinding Background}">
                        <vsm:VisualStateManager.VisualStateGroups>
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualState x:Name="Normal" />
                                <vsm:VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0" To=".35"/>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" Duration="0" To=".55" />
                                    </Storyboard>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                            <vsm:VisualStateGroup x:Name="SelectionStates">
                                <vsm:VisualState x:Name="Unselected" />
                                <vsm:VisualState x:Name="Selected">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty="Opacity" Duration="0" To=".75"/>
                                    </Storyboard>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                            <vsm:VisualStateGroup x:Name="FocusStates">
                                <vsm:VisualState x:Name="Focused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Visibility" Duration="0">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Unfocused"/>
                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                        <Rectangle Fill="LightGray" IsHitTestVisible="False" RadiusX="1" RadiusY="1" />
                        <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                        <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
                        <ContentPresenter
                    x:Name="contentPresenter"
                    Content="{TemplateBinding Content}"
                    ContentTemplate="{TemplateBinding ContentTemplate}"
                    HorizontalAlignment="Left"
                    Margin="{TemplateBinding Padding}"/>
                        <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed" RadiusX="1" RadiusY="1" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
</ResourceDictionary>

 

The ContextMenu :

public class ContextMenu : ItemsControl
{
    public ContextMenu()
    {
        base.DefaultStyleKey = typeof(ContextMenu);
    }
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
    }
 
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ContextMenuItem();
    }
 
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is ContextMenuItem);
    }
}

This control inherits from ItemsControl, and overrides the GetContainerForItemOverride and IsItemItsOwnContainerOverride methods for define the control to use for the display if we don’t use a ContextMenuItem.

And now the big part: RightClickService.

public class RightClickService
{
    private static FrameworkElement mRootVisual = null;
 
    internal static FrameworkElement RootVisual
    {
        get
        {
            SetRootVisual();
            return mRootVisual;
        }
    }
 
    private static void SetRootVisual()
    {
        if (mRootVisual == null && Application.Current != null)
        {
            mRootVisual = Application.Current.RootVisual as FrameworkElement;
        }
    }
 
    #region Attached Property
 
    public static readonly DependencyProperty ContextMenuProperty =
        DependencyProperty.RegisterAttached("ContextMenu", typeof(ContextMenu), typeof(RightClickService), null);
 
    public static void SetContextMenu(UIElement element, ContextMenu value)
    {
        element.SetValue(ContextMenuProperty, value);
    }
    public static ContextMenu GetContextMenu(UIElement element)
    {
        return (ContextMenu)element.GetValue(ContextMenuProperty);
    }
 
    #endregion
 
    static RightClickService()
    {
        if (Application.Current.Host.Settings.Windowless == false) throw new Exception("Your SL plugin must be initialize with Windowless to true");
 
        if (HtmlPage.IsEnabled) HtmlPage.Document.AttachEvent("oncontextmenu", RightClickService.OnContextMenu);
    }
 
    private static void OnContextMenu(object sender, HtmlEventArgs e)
    {
        IEnumerable<UIElement> elements = GetControls(e.OffsetX, e.OffsetY);
 
        if (elements != null)
        {
            foreach (UIElement element in elements)
            {
                ContextMenu menu = RightClickService.GetContextMenu(element);
 
                if (menu != null)
                {
                    PerformPlacement(menu, e.OffsetX, e.OffsetY);
                    break;
                }
            }
        }
 
        e.PreventDefault();
    }
 
    internal static Popup Popup { get; set; }
 
    private static void PerformPlacement(FrameworkElement content, int x, int y)
    {
        Canvas elementOutside = new Canvas();
        Canvas childCanvas = new Canvas();
 
        elementOutside.Background = new SolidColorBrush(Colors.Transparent);
 
        if (Popup != null)
        {
            Popup.IsOpen = false;
            if (Popup.Child is Canvas) ((Canvas)Popup.Child).Children.Clear();
        }
        Popup = new Popup();
 
        Popup.Child = childCanvas;
 
        elementOutside.MouseLeftButtonDown += new MouseButtonEventHandler((o, e) => Popup.IsOpen = false);
        elementOutside.Width = Application.Current.Host.Content.ActualWidth;
        elementOutside.Height = Application.Current.Host.Content.ActualHeight;
 
        childCanvas.Children.Add(elementOutside);
        childCanvas.Children.Add(content);
 
        Canvas.SetLeft(content, x);
        Canvas.SetTop(content, y);
 
        Popup.IsOpen = true;
    }
 
    private static IEnumerable<UIElement> GetControls(int x, int y)
    {
        return VisualTreeHelper.FindElementsInHostCoordinates(new Point(x, y), RootVisual);
    }
}

I just use the oncontextmenu Javascript event.

For specify the contextual menu for a control I use an attached property. When the right click is detected I get the control by using the coordinate of the mouse and the VisualTreeHelper class, then the menu associated to the control. Then I display the menu in a Popup.

Trick: I use an invisible Canvas which fill the entire application. It allows to close the menu when the user clicks outside it.

It’s over for the control, now let’s see how to use it.

First there is one requirement: your Silverlight plugin must be initialize with the Windowless property to true.

<UserControl x:Class="TestSilverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cm="clr-namespace:ContextMenu;assembly=ContextMenu"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <Image Source="silverlight.png">
            <cm:RightClickService.ContextMenu>
                <cm:ContextMenu>
                    <cm:ContextMenuItem Content="Save As" Click="SaveImage" />
                    <cm:ContextMenuItem Content="View Image" Click="ViewImage" />
                </cm:ContextMenu>
            </cm:RightClickService.ContextMenu>
        </Image>
    </Grid>
</UserControl>

You see? It’s simple.

And the result:

screen

Have fun :)

No Comments