There are a lot of good examples online, about how to make Silverlight applications respond to mouse right-clicks. So, I am not going to rehash it in detail here. Instead, I will attempt to describe a pattern that I found useful, when I implemented right-click context menus for controls on a page. The requirements for the functionality are:
1. There is a unified and consistent way to display context menu.
2. The control provides the data for the display (image / text / data).
3. The control is responsible for responding to the user selection.
Here is a demo application that uses the pattern I describe. Just as before, there is something about the script on the silverlight live site that doesn't make it work right for firefox. Works fine in IE, and when hosted on another site, on Firefox too. Since the app works ok when code is compiled and run on any browser, I am not going to sweat it.
Link to app here: http://silverlight.services.live.com/invoke/105917/Control-Specific%20Right-click%20Context%20Menu%20Demo/iframe.html
The codebase can be downloaded here: http://www.filefactory.com/file/ah498ca/n/ContextMenuDemo_zip
The main component of the functionality is the right-click interceptor class, which interacts with the DOM and intercepts the right mouse down, mouseclick, oncontextmenu events. Make sure your app is set as "windowless". All these aspects, it shares with other examples online. We will come to some extra functionality of this class later. The RootVisual of the app has an instance of this class.
The second component is the "ISupportCOntextMenu" interface.
Any control that needs a context menu needs to implement this interface, and the control needs to be registered with the rootvisual. Thre registration process brings us to the extra stuff in the interceptor. Interceptor maintains a list of references to ISupportContextMenu. Registration just adds to this list. When an interceptable event happens, the interceptor checks to see if it happened within the bounds of any of the items in the list (Big part of the whole thing).
If it did, then it asks the control to provide data for the context menu (GetContextMenuContent()).
This brings us to the third component: ContextMenuItemInfo.
The third component is a popup control that holds a list box. This is in the root visual. The RootVisual responds to an event raised by the interceptor and takes the List of ContextMenuItemInfo, populates the listbox, moves the popup to where the mouse is, and displays it. There is a DispatcherTimer and MouseEnter/MouseLeave events that make sure the popup is displayed and hidden in a graceful manner. In the RootVisual:
This ensures that a control has total say if it wants a context menu, and if so, what the content should be, and how the application should respond to user selection. I know that a lot of things are not elaborated here, but I am hoping it will make sense when you go through the code. If needed, I can augment this with some diagrams. Let me know.
1. There is a unified and consistent way to display context menu.
2. The control provides the data for the display (image / text / data).
3. The control is responsible for responding to the user selection.
Here is a demo application that uses the pattern I describe. Just as before, there is something about the script on the silverlight live site that doesn't make it work right for firefox. Works fine in IE, and when hosted on another site, on Firefox too. Since the app works ok when code is compiled and run on any browser, I am not going to sweat it.
Link to app here: http://silverlight.services.live.com/invoke/105917/Control-Specific%20Right-click%20Context%20Menu%20Demo/iframe.html
The codebase can be downloaded here: http://www.filefactory.com/file/ah498ca/n/ContextMenuDemo_zip
The main component of the functionality is the right-click interceptor class, which interacts with the DOM and intercepts the right mouse down, mouseclick, oncontextmenu events. Make sure your app is set as "windowless". All these aspects, it shares with other examples online. We will come to some extra functionality of this class later. The RootVisual of the app has an instance of this class.
1privatevoid OnMouseDown(object sender, HtmlEventArgs e)
2 {
3if (e.MouseButton == MouseButtons.Right)
4 {
5foreach (FrameworkElement ei in _ElementsToIntercept)
6 {
7if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei))
8 {
9 e.PreventDefault();
10if (MouseRightButtonDown != null)
11 MouseRightButtonDown(this, e);
12 RightClickedControl = ei;
13 _IsButtonDown = true;
14break;
15 }
16 }
17 }
18 }
19privatevoid OnMouseUp(object sender, HtmlEventArgs e)
20 {
21if (e.MouseButton == MouseButtons.Right && _IsButtonDown)
22 {
23 e.PreventDefault();
24 _IsButtonDown = false;
25if (MouseRightButtonUp != null)
26 MouseRightButtonUp(this, e);
27 }
28 }
29
30privatevoid OnContextMenu(object sender, HtmlEventArgs e)
31 {
32foreach (FrameworkElement ei in _ElementsToIntercept)
33 {
34if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei))
35 {
36 e.PreventDefault();
37break;
38 }
39 }
40 }
41
The second component is the "ISupportCOntextMenu" interface.
1publicinterface ISupportContextMenu
2 {
3void HandleContextMenuClick(object sender, ContextMenuClickEventArgs e);
4 List<ContextMenuItemInfo> GetContextMenuContent(HtmlEventArgs e);
5 }
Any control that needs a context menu needs to implement this interface, and the control needs to be registered with the rootvisual. Thre registration process brings us to the extra stuff in the interceptor. Interceptor maintains a list of references to ISupportContextMenu. Registration just adds to this list. When an interceptable event happens, the interceptor checks to see if it happened within the bounds of any of the items in the list (Big part of the whole thing).
1publicstaticbool IsPointWithinBounds(Point p, FrameworkElement element)
2 {
3try
4 {
5 GeneralTransform gt = element.TransformToVisual(Application.Current.RootVisual as FrameworkElement);
6 Point cp = gt.Transform(new Point(0, 0));
7
8if (p.X <= cp.X + element.ActualWidth && p.X >= cp.X && p.Y <= cp.Y + element.ActualHeight && p.Y >= cp.Y)
9returntrue;
10else
11returnfalse;
12 }
13//if the control has been cleared from parent 'transformtovisual' will throw an error.
14//In that case return false
15catch { returnfalse; }
16 }
If it did, then it asks the control to provide data for the context menu (GetContextMenuContent()).
1//From instantiating control:
2
3public Home()
4 {
5 InitializeComponent();
6 ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx1);
7 ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx2);
8 }
9
10//From RootVisual:
11
12publicvoid RegisterControlWithContextMenu(ISupportContextMenu control)
13 {
14 _RightClickIntrcpt.AddElementToIntercept(control);
15 }
16
17//From Interceptor:
18
19publicvoid AddElementToIntercept(ISupportContextMenu fe)
20 {
21if (!_ElementsToIntercept.Contains(fe))
22 _ElementsToIntercept.Add(fe);
23 }
This brings us to the third component: ContextMenuItemInfo.
1publicclass ContextMenuItemInfoYou can provide a list of items, each of which contains an image (optional), text, and any kind of data that makes sense in the context (optional) to the interceptor. The text and data tag is passed back to the control as a KeyValuePair if user click on it.
2 {
3public Image Image { get; set; }
4publicstring Command { get; set; }
5public Object Tag { get; set; }
6 }
The third component is a popup control that holds a list box. This is in the root visual. The RootVisual responds to an event raised by the interceptor and takes the List of ContextMenuItemInfo, populates the listbox, moves the popup to where the mouse is, and displays it. There is a DispatcherTimer and MouseEnter/MouseLeave events that make sure the popup is displayed and hidden in a graceful manner. In the RootVisual:
1privatevoid InitializeContextMenuIntercept()
2 {
3 InitializeContextMenuTimer();
4 _RightClickIntrcpt = new ContextMenuInterceptor();
5 _RightClickIntrcpt.MouseRightButtonUp += new EventHandler<System.Windows.Browser.HtmlEventArgs>(_RightClickIntrcpt_MouseRightButtonUp);
6 }
7
8privatevoid InitializeContextMenuTimer()
9 {
10 _ContextMenuTimer = new DispatcherTimer();
11 _ContextMenuTimer.Interval = TimeSpan.FromSeconds(2);
12 _ContextMenuTimer.Tick += (s, e) => ContextMenu_MouseLeave(null, null);
13 }
14
15void _RightClickIntrcpt_MouseRightButtonUp(object sender, System.Windows.Browser.HtmlEventArgs e)
16 {
17 ISupportContextMenu cntrl = _RightClickIntrcpt.RightClickedControl as ISupportContextMenu;
18if (cntrl != null)
19 {
20 List<ContextMenuItemInfo> mnuContent = cntrl.GetContextMenuContent(e);
21if (mnuContent != null&& mnuContent.Count > 0)
22 {
23 ContextMenuListBox.ItemsSource = from mc in mnuContent
24 select new ListBoxItem
25 {
26 Content = GetContextMenuItemVisual(mc.Command, mc.Image),
27 Tag = new KeyValuePair<string, object>(mc.Command, mc.Tag)
28 };
29 ContextMenuListBox.MouseLeftButtonUp += ContextMenuListBox_MouseLeftButtonUp;
30 ContextMenuPopup.Margin = new Thickness(e.OffsetX, e.OffsetY, 0, 0);
31 ContextMenuPopup.IsOpen = true;
32 _ContextMenuTimer.Start();
33 }
34 }
35 }
36
37 FrameworkElement GetContextMenuItemVisual(string command, Image image)
38 {
39 StackPanel ret = new StackPanel
40 {
41 Orientation = Orientation.Horizontal,
42 Margin = new Thickness(2),
43 };
44if (image != null)
45 {
46 image.Margin = new Thickness(0, 0, 4, 0);
47 ret.Children.Add(image);
48 }
49 ret.Children.Add(new TextBlock { Text = command });
50return ret;
51 }
52
53
54void ContextMenuListBox_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
55 {
56if (ContextMenuListBox.SelectedItem != null)
57 ((ISupportContextMenu)_RightClickIntrcpt.RightClickedControl).HandleContextMenuClick(this, new ContextMenuClickEventArgs
58 {
59 ClickedItem = (KeyValuePair<string, object>)((ListBoxItem)ContextMenuListBox.SelectedItem).Tag
60 });
61 ContextMenuListBox.MouseLeftButtonUp -= ContextMenuListBox_MouseLeftButtonUp;
62 ContextMenuPopup.IsOpen = false;
63 }
64
65void ContextMenu_MouseEnter(object sender, RoutedEventArgs e)
66 {
67 _ContextMenuTimer.Stop();
68 ContextMenuPopup.IsOpen = true;
69 }
70void ContextMenu_MouseLeave(object sender, RoutedEventArgs e)
71 {
72 ContextMenuPopup.IsOpen = false;
73 _ContextMenuTimer.Stop();
74 }
This ensures that a control has total say if it wants a context menu, and if so, what the content should be, and how the application should respond to user selection. I know that a lot of things are not elaborated here, but I am hoping it will make sense when you go through the code. If needed, I can augment this with some diagrams. Let me know.