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.
1 private void OnMouseDown(object sender, HtmlEventArgs e) 2 { 3 if (e.MouseButton == MouseButtons.Right) 4 { 5 foreach (FrameworkElement ei in _ElementsToIntercept) 6 { 7 if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei)) 8 { 9 e.PreventDefault(); 10 if (MouseRightButtonDown != null) 11 MouseRightButtonDown(this, e); 12 RightClickedControl = ei; 13 _IsButtonDown = true; 14 break; 15 } 16 } 17 } 18 } 19 private void OnMouseUp(object sender, HtmlEventArgs e) 20 { 21 if (e.MouseButton == MouseButtons.Right && _IsButtonDown) 22 { 23 e.PreventDefault(); 24 _IsButtonDown = false; 25 if (MouseRightButtonUp != null) 26 MouseRightButtonUp(this, e); 27 } 28 } 29 30 private void OnContextMenu(object sender, HtmlEventArgs e) 31 { 32 foreach (FrameworkElement ei in _ElementsToIntercept) 33 { 34 if (IsPointWithinBounds(new Point(e.OffsetX, e.OffsetY), ei)) 35 { 36 e.PreventDefault(); 37 break; 38 } 39 } 40 } 41
The second component is the "ISupportCOntextMenu" interface.
1 public interface ISupportContextMenu 2 { 3 void 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).
1 public static bool IsPointWithinBounds(Point p, FrameworkElement element) 2 { 3 try 4 { 5 GeneralTransform gt = element.TransformToVisual(Application.Current.RootVisual as FrameworkElement); 6 Point cp = gt.Transform(new Point(0, 0)); 7 8 if (p.X <= cp.X + element.ActualWidth && p.X >= cp.X && p.Y <= cp.Y + element.ActualHeight && p.Y >= cp.Y) 9 return true; 10 else 11 return false; 12 } 13 //if the control has been cleared from parent 'transformtovisual' will throw an error. 14 //In that case return false 15 catch { return false; } 16 }
If it did, then it asks the control to provide data for the context menu (GetContextMenuContent()).
1 //From instantiating control: 2 3 public Home() 4 { 5 InitializeComponent(); 6 ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx1); 7 ((MainPage)App.Current.RootVisual).RegisterControlWithContextMenu(Ctx2); 8 } 9 10 //From RootVisual: 11 12 public void RegisterControlWithContextMenu(ISupportContextMenu control) 13 { 14 _RightClickIntrcpt.AddElementToIntercept(control); 15 } 16 17 //From Interceptor: 18 19 public void AddElementToIntercept(ISupportContextMenu fe) 20 { 21 if (!_ElementsToIntercept.Contains(fe)) 22 _ElementsToIntercept.Add(fe); 23 }
This brings us to the third component: ContextMenuItemInfo.
1 public class ContextMenuItemInfo 2 { 3 public Image Image { get; set; } 4 public string Command { get; set; } 5 public Object Tag { get; set; } 6 }You 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.
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:
1 private void InitializeContextMenuIntercept() 2 { 3 InitializeContextMenuTimer(); 4 _RightClickIntrcpt = new ContextMenuInterceptor(); 5 _RightClickIntrcpt.MouseRightButtonUp += new EventHandler<System.Windows.Browser.HtmlEventArgs>(_RightClickIntrcpt_MouseRightButtonUp); 6 } 7 8 private void InitializeContextMenuTimer() 9 { 10 _ContextMenuTimer = new DispatcherTimer(); 11 _ContextMenuTimer.Interval = TimeSpan.FromSeconds(2); 12 _ContextMenuTimer.Tick += (s, e) => ContextMenu_MouseLeave(null, null); 13 } 14 15 void _RightClickIntrcpt_MouseRightButtonUp(object sender, System.Windows.Browser.HtmlEventArgs e) 16 { 17 ISupportContextMenu cntrl = _RightClickIntrcpt.RightClickedControl as ISupportContextMenu; 18 if (cntrl != null) 19 { 20 List<ContextMenuItemInfo> mnuContent = cntrl.GetContextMenuContent(e); 21 if (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 }; 44 if (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 }); 50 return ret; 51 } 52 53 54 void ContextMenuListBox_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) 55 { 56 if (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 65 void ContextMenu_MouseEnter(object sender, RoutedEventArgs e) 66 { 67 _ContextMenuTimer.Stop(); 68 ContextMenuPopup.IsOpen = true; 69 } 70 void 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.
Love it,
ReplyDeleteDoesn't work in firefox though :(
Regards
I know, the demo doesn't work in Firefox when served from the site. It does work in FF when I compile the project locally, and when I run it on a regular IIS server. The same thing with the timeline visualizer. Have no idea why, and I can't see the configuration on the silverlight live site that hosts the demos.
ReplyDelete-Jose
It does work in Chrome :)
ReplyDelete