Hi I need to create a custom context menu for a web browser control in wpf. Here is my xaml code which is not working:
<WebBrowser x:Name="EmailBox" ap:BrowserBehavior.HtmlString="{Binding Message, Mode=OneWay}">
<WebBrowser.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" Command="ApplicationCommands.Copy"/>
<MenuItem Header="Copy to Customer Reference ID"
Command="{Binding CopyID}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}},
Path=PlacementTarget.Selection.Text}">
<MenuItem.Icon>
<Image Source="{StaticResource CopyImageSource}" Width="16" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy to Comments"
Command="{Binding CopyToCommentsCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}},
Path=PlacementTarget.Selection.Text}">
<MenuItem.Icon>
<Image Source="{StaticResource NoteCopyI}" Width="16" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</WebBrowser.ContextMenu>
</WebBrowser>
I copied the context menu code from somewhere else. This works in other controls but not for the webbrowser control. Is it possible to make this work?
Hi You have to add reference to Microsoft HTML Object Library and than...
XAML
<Window x:Class="WPFCustomContextMenuInWebBrowser.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFCustomContextMenuInWebBrowser"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ContextMenu x:Key="MnuCustom" StaysOpen="True">
<MenuItem Header="Custom 1"></MenuItem>
<MenuItem Header="Custom 1"></MenuItem>
<MenuItem Header="Custom 3"></MenuItem>
</ContextMenu>
</Window.Resources>
<Grid>
<WebBrowser x:Name="Wb"></WebBrowser>
</Grid>
</Window>
C#
using System.Windows.Controls;
using MSHTML;
namespace WPFCustomContextMenuInWebBrowser {
public partial class MainWindow {
private HTMLDocumentEvents2_Event _docEvent;
public MainWindow() {
InitializeComponent();
Wb.Navigate("http://google.com");
Wb.LoadCompleted += delegate {
if (_docEvent != null) {
_docEvent.oncontextmenu -= _docEvent_oncontextmenu;
}
if (Wb.Document != null) {
_docEvent = (HTMLDocumentEvents2_Event)Wb.Document;
_docEvent.oncontextmenu += _docEvent_oncontextmenu;
}
};
}
bool _docEvent_oncontextmenu(IHTMLEventObj pEvtObj) {
WbShowContextMenu();
return false;
}
public void WbShowContextMenu() {
ContextMenu cm = FindResource("MnuCustom") as ContextMenu;
if (cm == null) return;
cm.PlacementTarget = Wb;
cm.IsOpen = true;
}
}
}
No, It's not possible to make this work.
WebBrowser control is a very thin wrapper around native WebBrowser ActiveX component, which is a part of Internet Explorer subsystem. It is hosted in it's own window host (WPF window and WebBrowser have different HWNDs), so WPF knows only about focus entering and leaving WebBrowser, but has no knowledge of any keyboard/mouse events. Also, there is so called 'airspace problem': WPF-rendered and native regions of screen area cannot overlap.
Therefore you cannot use WPF ContextMenu with WebBrowser, because:
WPF doesn't recieve Mouse Right Click event to open Context Menu
WPF cannot draw Context Menu above WebBrowser
Also, I don't think there is easy way to emulate ContextMenu with html/js in browser's content - as I recall, ActiveX component uses IE5 (quirk) rendering mode, and it is not possible to change that without changing registry files.
You can try to use ActiveX API with WebBrowser.Document object to disable native context menu and draw another one yourself through WinAPI, which is not an easy task.
So, I would recommend to look for other, pure-WPF browser controls or HTML renderers, such as awesomium
The XAML as follows:
<!--WebBrowser to Display Chat Messages-->
<WebBrowser Name="webBrowser"
Source="http://stakoverflow.com"
Navigated="webBrowser_Navigated"
Navigating="webBrowser_Navigating"
LoadCompleted="webBrowser_LoadCompleted">
<WebBrowser.ContextMenu>
<ContextMenu x:Name="wbContextMenu" >
<MenuItem x:Name="menuItem1" Header="Test Item" Click="menuItem1_Click" />
</ContextMenu>
</WebBrowser.ContextMenu
</WebBrowser>
Code as follows:
using mshtml;
private mshtml.HTMLDocumentEvents2_Event documentEvents;
in constructor or xaml set your LoadComplete event:
webBrowser.LoadCompleted += webBrowser_LoadCompleted;
then in that method create your new webbrowser document object and view the available properties and create new events as follows:
private void webBrowser_LoadCompleted(object sender, NavigationEventArgs e)
{
documentEvents = (HTMLDocumentEvents2_Event)webBrowserChat.Document; // this will access the events properties as needed
documentEvents.oncontextmenu += webBrowserChat_ContextMenuOpening;
}
private bool webBrowserChat_ContextMenuOpening(IHTMLEventObj pEvtObj)
{
wbContextMenu.PlacementTarget = pEvtObj as ContextMenu; // Creates target spot where contextmenu will appear
wbContextMenu.IsOpen = true; // Opens contextmneu
return false; // ContextMenu wont open
// return true; ContextMenu will open
// Here you can create your custom contextmenu or whatever you want
}
I've taken the solution provided by #MartinHoly and i've encountered the problem: to context menu can be popped up only once (if you right-click on the scrollbar, for example, or select a custom menu item - next time you right-click the WebBrowser brings the standard IE menu). I have made the following workaround:
The xaml:
<Window x:Class="WPFCustomContextMenuInWebBrowser.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFCustomContextMenuInWebBrowser"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ContextMenu x:Key="MnuCustom" StaysOpen="True">
<MenuItem Header="Custom 1"></MenuItem>
<MenuItem Header="Custom 2"></MenuItem>
<MenuItem Header="Custom 3"></MenuItem>
</ContextMenu>
</Window.Resources>
<Grid>
<WebBrowser x:Name="Browser"></WebBrowser>
</Grid>
The code behind:
using System.Windows.Controls;
using MSHTML;
namespace WPFCustomContextMenuInWebBrowser {
public partial class MainWindow {
public MainWindow()
{
InitializeComponent();
Browser.LoadCompleted += BrowserOnLoadCompleted;
}
void BrowserOnLoadCompleted(object sender, NavigationEventArgs navigationEventArgs)
{
var mshtmlDoc = Browser.Document as HTMLDocument;
if (mshtmlDoc == null) return;
var doc2event = mshtmlDoc as HTMLDocumentEvents2_Event;
if (doc2event != null)
{
doc2event.onfocusin += FocusInContextMenu;
}
}
bool OpenContextMenu(IHTMLEventObj pEvtObj)
{
WbShowContextMenu(pEvtObj as ContextMenu);
return false;
}
void FocusInContextMenu(IHTMLEventObj pevtobj)
{
var mshtmlDoc = Browser.Document as HTMLDocument;
var doc2event = mshtmlDoc as HTMLDocumentEvents2_Event;
if (doc2event != null)
{
doc2event.oncontextmenu -= OpenContextMenu;
doc2event.onfocusin -= FocusInContextMenu;
doc2event.oncontextmenu += OpenContextMenu;
doc2event.onfocusin += FocusInContextMenu;
}
}
public void WbShowContextMenu()
{
ContextMenu cm = FindResource("MnuCustom") as ContextMenu;
if (cm == null) return;
cm.PlacementTarget = Browser;
cm.IsOpen = true;
}
}
}
I have a indirect implementation, which involves calling each other in C# and javascript.
xaml:
<WebBrowser x:Name="webbrowser">
<WebBrowser.ContextMenu>
<ContextMenu>
<MenuItem Header="item1"/>
</ContextMenu>
</WebBrowser.ContextMenu>
</WebBrowser>
c#:
using System.Runtime.InteropServices;
public MainWindow()
{
InitializeComponent();
//webbrowser.Navigate(new Uri("https://www.google.com"));
webbrowser.ObjectForScripting = new ScriptManager(this);
webbrowser.LoadCompleted += Webbrowser_LoadCompleted;
}
private void Webbrowser_LoadCompleted(object sender, NavigationEventArgs e)
{
//call C# method and disable the default contextmenu here.
webbrowser.InvokeScript("eval", new object[] { "document.oncontextmenu = function() { window.external.ShowContextMenu(); return false; };" });
}
[ComVisible(true)]
public class ScriptManager
{
private MainWindow mainWindow;
public ScriptManager(MainWindow MainWindow)
{
mainWindow = MainWindow;
}
public void ShowContextMenu()
{
mainWindow.webbrowser.ContextMenu.IsOpen = true;
}
}
But it still has a native contextmenu when page is loading because LoadCompleted event hasn't triggered.
So if the html page is writed by self, you can add this line directly to the script section in html and don't need the LoadCompleted event:
document.oncontextmenu = function() { window.external.ShowContextMenu(); return false; };
Related
I'm trying to create a ContextMenu on a UserControl derived object. Note: This is NOT a WinForms App, it is pure WPF.
So I create the ContextMenu thus:
<UserControl.Resources>
<ContextMenu x:Key="cmLCD_CopyCutPaste">
<MenuItem Name="CutOption" Header="{x:Static p:Resources.Popup_Cut}" Command="Cut" Click="MenuItem_Cut" IsEnabled="True"/>
<MenuItem Name="CopyOption" Header="{x:Static p:Resources.Popup_Copy}" Command="Copy" Click="MenuItem_Copy" IsEnabled="True"/>
<MenuItem Name="PasteOption" Header="{x:Static p:Resources.Popup_Paste}" Command="Paste" Click="MenuItem_Paste" IsEnabled="True"/>
</ContextMenu>
</UserControl.Resources>
The mousebutton event is setup thus:
d:DesignHeight="66" d:DesignWidth="340" Focusable="True" KeyDown="Grid_KeyDown" MouseRightButtonDown="EMS_UI_LCDscreen_MouseRightButtonDown" >
In the code behind, the constructor starts like this:
public EMS_UI_LCDscreen()
{
InitializeComponent();
// Find the ContextMenu created on this object - it is called cmLCD_CopyCutPaste
ContextMenu cm = FindResource("cmLCD_CopyCutPaste") as ContextMenu;
// If we found the contextMenu, assign it the ContextMenu placeholder for this instance.
if (cm != null)
{
ContextMenu = cm;
}
...
The mouse click(right button) is handled like this:
private void EMS_UI_LCDscreen_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (this.ContextMenu is ContextMenu cm)
{
if (cm != null)
{
if (DataContext is DeviceEditorData ded)
{
cm.DataContext = ded.Device.GetStructuredLocation(0);
cm.IsEnabled = true;
cm.PlacementTarget = sender as Button;
cm.IsOpen = true;
}
}
}
}
There are the three event handlers to actually respond to the user selection on the ContextMenu:
private void MenuItem_Cut(object sender, RoutedEventArgs e)
{
}
private void MenuItem_Copy(object sender, RoutedEventArgs e)
{
}
private void MenuItem_Paste(object sender, RoutedEventArgs e)
{
}
View of the ContextMenu being shown on the LCD Image component
The component I have created appears in a few different contexts, but the one I'm mainly interested in is inside of a DataGrid that shows various items of data, each with one of these components. when right clicked, the component faithfully displays the popup context menu as expected..... BUT all the items on the menu are grayed out and basically not enabled.
So my question is, what is the missing piece of glue that effectively enables the menu items so that they can be clicked to do the required actions. Most of the answers already seen on the net go into detail of how to do it in a WinForms app, but despite hours of searching, I can find no clear solution to what should be a very simple task of enabling the menu items.
Can some kind soul please put me out of my anguish and in a few lines of code show me how to do it! Thank you
UPDATE: This is the XAML implementation for the LCD Component:
<UserControl x:Class="EMS_Config_Tool.UIComponents.WPF.EMS_UI_LCDscreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:p="clr-namespace:EMS_Config_Tool.Properties"
mc:Ignorable="d"
d:DesignHeight="66" d:DesignWidth="340" Focusable="True" KeyDown="Grid_KeyDown" MouseRightButtonDown="EMS_UI_LCDscreen_MouseRightButtonDown" >
<UserControl.Resources>
<ContextMenu x:Key="cmLCD_CopyCutPaste">
<MenuItem Name="CutOption" Header="{x:Static p:Resources.Popup_Cut}" Command="{Binding Cut}"/>
<MenuItem Name="CopyOption" Header="{x:Static p:Resources.Popup_Copy}" Command="{Binding Copy}"/>
<MenuItem Name="PasteOption" Header="{x:Static p:Resources.Popup_Paste}" Command="{Binding Paste}"/>
</ContextMenu>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="Cut"
CanExecute="CutCommand_CanExecute"
Executed="CutCommand_Executed" />
<CommandBinding Command="Copy"
CanExecute="CopyCommand_CanExecute"
Executed="CopyCommand_Executed" />
<CommandBinding Command="Paste"
CanExecute="PasteCommand_CanExecute"
Executed="PasteCommand_Executed" />
</UserControl.CommandBindings>
</UserControl>
As can be seen, it is very simple - there are no items added to the control - it only serves as a Canvas upon which the code behind draws all the needed items which are purely graphic "draw" items.
This is the implementations of the 6 methods referenced, in the corresponding .cs file for the xaml above:
private void CutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Verify relevant conditions and set CanExecuteRoutedEventArgs.CanExecute accordingly
e.CanExecute = true;
}
private void CutCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// Execute the command action
if (sender is EMS_UI_LCDscreen lcd)
{
if (TheDevice != null)
{
Clipboard.SetDataObject(TheDevice.GetStructuredLocation(0).ToString());
TheDevice.SetLocation(0, "");
lcd.StructuredTextToShow = TheDevice.GetStructuredLocation(0);
}
else
{
Clipboard.SetDataObject(lcd.StructuredTextToShow.ToString());
lcd.StructuredTextToShow.SetString("");
}
}
}
private void CopyCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Verify relevant conditions and set CanExecuteRoutedEventArgs.CanExecute accordingly
e.CanExecute = true;
}
private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// Execute the command action
if (sender is EMS_UI_LCDscreen lcd)
{
if (TheDevice!=null)
Clipboard.SetDataObject(TheDevice.GetStructuredLocation(0).ToString());
else
Clipboard.SetDataObject(lcd.StructuredTextToShow.ToString());
}
}
private void PasteCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Verify relevant conditions and set CanExecuteRoutedEventArgs.CanExecute accordingly
IDataObject iData = Clipboard.GetDataObject();
// Is the Data Text?
if (iData.GetDataPresent(DataFormats.Text))
e.CanExecute = true;
else
e.CanExecute = false;
}
private void PasteCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// Execute the command action
if (sender is EMS_UI_LCDscreen lcd)
{
// Retrieves data
IDataObject iData = Clipboard.GetDataObject();
// Is the Data Text?
if (iData.GetDataPresent(DataFormats.Text))
{
if (TheDevice != null)
{
TheDevice.SetLocation(0, (string)iData.GetData(DataFormats.Text));
lcd.StructuredTextToShow = TheDevice.GetStructuredLocation(0);
}
else
{
lcd.StructuredTextToShow.SetString((string)iData.GetData(DataFormats.Text));
}
}
}
}
Finally, the ContextMenu is assigned to the object in the constructor:
public EMS_UI_LCDscreen()
{
InitializeComponent();
// Find the ContextMenu created on this object - it is called cmLCD_CopyCutPaste
ContextMenu cm = FindResource("cmLCD_CopyCutPaste") as ContextMenu;
// If we found the contextMenu, assign it the ContextMenu placeholder for this instance.
if (cm != null)
{
ContextMenu = cm;
}
...
The menu is shown on the component in response to right-click, the cut/copy/paste methods respond to the key ops, but clicking on the menu items does not fire the corresponding methods.
I can't see why that should be so, it all looks correct to me, but perhaps there is something missing that is preventing it working. One suggestion was to use a "relay command" type of thingy, but what and how that may be, is unclear.
You should remove all code that sets IsEnabled to true. It's redundant as the value is true by default.
The buttons are disabled because you have (accidentally?) attached a command to the MenuItem.Command property but no corresponding command handler.
The framework will try to invoke a CanExecute handler. Since there is no one defined, a default handler is returned that sets CanExecuteRoutedEventArgs.CanExecute to false, which will disable the button (ICommandSource).
Not sure if your intention was to use a command here or you wrongfully guessed the Command property is like a name property (since you have also registered Click event handlers). See Commanding Overview to learn more.
Anyway, this is how you can register a command handler:
You have assigned predefined application commands (cut, copy and paste - the MenuItem.Command string values in your XAML implicitly references the static ApplicationCommands commands). These commands are routed commands (behavior is identical to routed events - in fact routed commands are routed events). Therefore, you must define the command bindings on a parent element of the command source (the element that invokes the command) as the command will bubble up the tree.
A UIElement.CommandBinding consists of the specified CommandBinding.Executed handler and the optional CommandBinding.CanExecute handler.
Use the CanExecute handler to control the disabled states of the command source e.g., a Button. If the command source should be always enabled, simply omit the CanExecute handler.
XAML
<Window>
<Window.CommandBindings>
<CommandBinding Command="Cut"
CanExecute="CutCommand_CanExecute"
Executed="CutCommand_Executed" />
</Window.CommandBindings>
</Window>
C#
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var cutCommandBinding =
new CommandBinding(ApplicationCommands.Cut, CutCommand_Executed, CutCommand_CanExecute)
this.CommandBindings.Add(cutCommandBinding);
}
}
Then create the corresponding command handlers in the code-behind.
private void CutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Verify relevant conditions and set CanExecuteRoutedEventArgs.CanExecute accordingly
e.CanExecute = true;
}
private void CutCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
// Execute the command action
}
Update
To force command handling on the UserControl, you can explicitly set the MenuItem.CommandTarget property to reference the UserControl.
You can also assign the UserControl.Contextmenu from XAML:
<UserControl>
<UserControl.Resources>
<Style TargetType="MenuItem">
<Setter Property="CommandTarget"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}, Path=PlacementTarget}" />
</Style>
</UserControl.Resources>
<UserControl.Contextmenu>
<ContextMenu>
<MenuItem Name="CutOption" Header="{x:Static p:Resources.Popup_Cut}" Command="{Binding Cut}"/>
<MenuItem Name="CopyOption" Header="{x:Static p:Resources.Popup_Copy}" Command="{Binding Copy}"/>
<MenuItem Name="PasteOption" Header="{x:Static p:Resources.Popup_Paste}" Command="{Binding Paste}"/>
</ContextMenu>
</UserControl.Contextmenu>
<UserControl.CommandBindings>
<CommandBinding Command="Cut"
CanExecute="CutCommand_CanExecute"
Executed="CutCommand_Executed" />
<CommandBinding Command="Copy"
CanExecute="CopyCommand_CanExecute"
Executed="CopyCommand_Executed" />
<CommandBinding Command="Paste"
CanExecute="PasteCommand_CanExecute"
Executed="PasteCommand_Executed" />
</UserControl.CommandBindings>
</UserControl>
This was the solution that finally worked for me. The elixir was assigning the commands, not with the "Binding" term, but directly:
<UserControl x:Class="EMS_Config_Tool.UIComponents.WPF.EMS_UI_LCDscreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:p="clr-namespace:EMS_Config_Tool.Properties"
mc:Ignorable="d"
d:DesignHeight="66" d:DesignWidth="340" Focusable="True" KeyDown="Grid_KeyDown" MouseRightButtonDown="EMS_UI_LCDscreen_MouseRightButtonDown" >
<UserControl.Resources>
<Style TargetType="MenuItem">
<Setter Property="CommandTarget"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}, Path=PlacementTarget}" />
</Style>
</UserControl.Resources>
<UserControl.ContextMenu>
<ContextMenu>
<MenuItem Name="CutOption" Header="{x:Static p:Resources.Popup_Cut}" Command="Cut" />
<MenuItem Name="CopyOption" Header="{x:Static p:Resources.Popup_Copy}" Command="Copy" />
<MenuItem Name="PasteOption" Header="{x:Static p:Resources.Popup_Paste}" Command="Paste" />
</ContextMenu>
</UserControl.ContextMenu>
<UserControl.CommandBindings>
<CommandBinding Command="Cut"
CanExecute="CutCommand_CanExecute"
Executed="CutCommand_Executed" />
<CommandBinding Command="Copy"
CanExecute="CopyCommand_CanExecute"
Executed="CopyCommand_Executed" />
<CommandBinding Command="Paste"
CanExecute="PasteCommand_CanExecute"
Executed="PasteCommand_Executed" />
</UserControl.CommandBindings>
</UserControl>
The constructor and assignment of images looks like this:
public EMS_UI_LCDscreen()
{
InitializeComponent();
Image ObjImage1 = new Image();
Image ObjImage2 = new Image();
Image ObjImage3 = new Image();
ObjImage1.Source = new BitmapImage(new Uri(#"pack://application:,,,/Graphics\Misc Icons\Cut.png"));
CutOption.Icon = ObjImage1;
ObjImage2.Source = new BitmapImage(new Uri(#"pack://application:,,,/Graphics\Misc Icons\Copy.png"));
CopyOption.Icon = ObjImage2;
ObjImage3.Source = new BitmapImage(new Uri(#"pack://application:,,,/Graphics\Misc Icons\Paste.png"));
PasteOption.Icon = ObjImage3;
...
... and displaying the context menu on the component in response to the mouse right click:
private void EMS_UI_LCDscreen_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (ContextMenu is ContextMenu cm)
{
if (DataContext is DeviceEditorData ded)
{
cm.DataContext = ded.Device.GetStructuredLocation((uint)LocationIndex);
cm.IsEnabled = true;
cm.PlacementTarget = sender as EMS_UI_LCDscreen;
cm.IsOpen = true;
}
if (DataContext is EMSBasicDevice dev)
{
cm.DataContext = dev.GetStructuredLocation((uint)LocationIndex);
cm.IsEnabled = true;
cm.PlacementTarget = sender as EMS_UI_LCDscreen;
cm.IsOpen = true;
}
}
}
For brevity, I have not shown the ..._CanExecute / ...Executed methods, they are as shown previously in the question.
Thanks to all who have helped in resolving this issue.
View of the finished context Menu in situ
So, if you are using DataContext and Binding's, you can do the following
remove the direct control with event handlers, i.e. Click, and direct setting of Enabled.
use Binding to bind menu action to the handler inside the view model: Command={Binding CutCommand} (or any other handler).
use ICommand.CanExecute of the command handler (CutCommand in the sample above). to manage Enabled state of menu item.
So, let's put all together.
In the XAML
<MenuItem Name="CutOption" Header="{x:Static p:Resources.Popup_Cut}"
Command="{Binding CutCommand}"/>
In the ViewModel
class ViewModel: INotifyPropertyChanged
{
...
public ICommand CutCommand { get; }
public ViewModel()
{
//you can just omit second arg if you always can call Cut.
CutCommand = new RelayCommand(CutHandler, ()=>CanCut()));
}
...
}
Here RelayCommand is a https://learn.microsoft.com/en-us/windows/communitytoolkit/mvvm/relaycommand or any other implementation of simple command.
If you need to update the Enabled state on-the-fly, you can call CommandManager.InvalidateRequerySuggested(); to reevaluate CanExecute of all commands.
I found one bug, and I don't understand, why it's happening.
I have DataGrid. On left mouse button click I want to open ContextMenu.
I made it. It works fine until I start clicking on DataGrid's Cells on random (every time ContextMenu is closing and reappearing at new place). But at one moment something is happening and ContextMenu newer showing (and Window don't response to any Mouse events like clicking on buttons and so on)... until I move cursor out of window and return it (or clicking Alt or F10).
Here some code:
ContextMenu (inside <DataGrid.Resources>)
<ContextMenu Style="{StaticResource DefaultContextMenuStyle}" x:Key="cm"
DataContext="{Binding Data, Source={StaticResource WindowViewModel}}">
</ContextMenu>
DataGrid column:
<DataGridTemplateColumn Header="TestHeader" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DockPanel ContextMenu="{StaticResource cm}" VerticalAlignment="Stretch" Background="Transparent">
<i:Interaction.Behaviors>
<local:OpenContextMenuLeftBehavior />
</i:Interaction.Behaviors>
<TextBlock Style="{StaticResource TextVCenteredCellStyle}" Text="{Binding LPU}" />
</DockPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
And behavior to open ContextMenu:
public class OpenContextMenuLeftBehavior : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
AssociatedObject.PreviewMouseUp += OpenContextMenu;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseUp -= OpenContextMenu;
}
void OpenContextMenu(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) {
FrameworkElement Sender = sender as FrameworkElement;
Sender.ContextMenu.PlacementTarget = Sender;
Sender.ContextMenu.IsOpen = true;
}
}
}
When I found that bug, I tried to find some info with Snoop WPF.
And here some info from it:
Before bad thing happened one click on cell was like:
before bad thing
After bad thing:
after bad thing
First event happened at Popup (ContextMenu part?), it doesn't belong to window VisualTree. This Popup seems to stretch all over the main Window and closing, when i move mouse out of it.
So, I lost 2 days on that bug and this is all, that I can find.
Please help me.
EDIT:
I created minimal example:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="dg1" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DockPanel>
<i:Interaction.Behaviors>
<local:OpenContextMenuLeftBehavior />
</i:Interaction.Behaviors>
<DockPanel.ContextMenu>
<ContextMenu>
<MenuItem Header="123456"></MenuItem>
</ContextMenu>
</DockPanel.ContextMenu>
<TextBlock Text="123" />
</DockPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dg1.ItemsSource = new List<int>()
{
1,2,3,4,5,6,7,8,9,0
};
}
}
public class OpenContextMenuLeftBehavior : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
AssociatedObject.PreviewMouseUp += OpenContextMenu;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseUp -= OpenContextMenu;
}
void OpenContextMenu(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) {
FrameworkElement Sender = sender as FrameworkElement;
if (Sender != null) {
Sender.ContextMenu.PlacementTarget = Sender;
Sender.ContextMenu.IsOpen = true;
Sender.ContextMenu.UpdateLayout();
}
}
}
}
To reproduce this bug just click rapidly on different cells
Revised Answer
After you published a full, simple example, I'd suggest the following:
after you create the ContextMenu
Sender.ContextMenu.IsOpen = true;
Sender.ContextMenu.PreviewMouseUp += ContextMenu_PreviewMouseUp;
Having defined
private void ContextMenu_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
var Sender = (sender as ContextMenu);
if (Sender != null)
{
Sender.IsOpen = true;
e.Handled = true;
}
}
(you still have to manage the grid line selection, but it's like off topic here)
I'm sure I'm missing something really silly and stupid here, and I'll probably kick myself when I see it, but I just have a simple question.
I've got some code in the constructor of the code-behind for a view with a grid, that does the following:
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
So my question is how can I do this from the ViewModel? The ViewModel doesn't know what "this" is, and doesn't have a reference to "this" either.
It is my understanding that the view model object is created in XAML by calling:
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
And setting the data context like so:
<Grid DataContext="{StaticResource TimersHostViewModel}" Style="{DynamicResource styleBackground}">
But this doesn't give the TimersHostViewModel any knowledge about "this.Content", and saying TimersHost.Content doesn't help, because TimersHost isn't an actual object, but a class, and I need an actual object to get the ".Content" from, and it should be the right object, the object that is from the code behind, but how can I get that into the view model?
After all following MVVM means that the ViewModel shouldn't have any knowledge about the View, and the View shouldn't have any knowledge about the ViewModel, and they just communicate back and forth with bindings and INotifyPropertyChanged and other such message passing techniques. I've done a fair bit of this stuff in another application, so I'm some-what familiar with the basics, but still some-what new, and still learning and even re-learning.
I've included the full source below. As you can see I'm in the process of trying to get the code out of the code behind and into the ViewModel, but I'm running into a compiler error when attempting to get this.Content from the main grid.
XAML:
<Window
x:Class="TimersXP.TimersHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:TimersXP"
Name="TimersHostView"
SizeToContent="Height"
Title="TimersXP"
WindowStartupLocation="CenterScreen"
WindowStyle="ToolWindow">
<Window.Resources>
<ObjectDataProvider x:Key="TimersHostViewModel" ObjectType="{x:Type local:TimersHostViewModel}"/>
</Window.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="21"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions>
<!--Main Menu-->
<Menu IsMainMenu="True" Style="{DynamicResource styleBanner}" Margin="0,0,0,1">
<MenuItem Header="_Help" Style="{DynamicResource styleBanner}">
<MenuItem Header="_About" Style="{DynamicResource styleBanner}"/>
</MenuItem>
</Menu>
<!--Top Most Check Box-->
<CheckBox Content="Top Most" Grid.Column="1" Height="16" HorizontalAlignment="Left" Margin="11,2,0,0" Name="checkBox1" VerticalAlignment="Top" />
<!--Stopwatch & Countdown Tab Defintions-->
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Style="{DynamicResource styleContentArea}">
<TabItem Header="Stopwatch"/>
<TabItem Header="Countdown"/>
</TabControl>
<!-- CONTEXT MENU -->
<Grid.ContextMenu>
<ContextMenu Style="{DynamicResource styleBanner}" MenuItem.Click="OnMenuItemClick">
<MenuItem Tag=".\Resources\Skins\BlackSkin.xaml" IsChecked="True">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Black" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\GreenSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Green" />
</MenuItem.Header>
</MenuItem>
<MenuItem Tag=".\Resources\Skins\BlueSkin.xaml">
<MenuItem.Header>
<Rectangle Width="120" Height="40" Fill="Blue" />
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
Code Behind:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public partial class TimersHost : Window
{
public TimersHost()
{
try
{
InitializeComponent();
}
catch (Exception ex)
{
Debug.WriteLine("CTOR Exception: " + ex.Message);
}
// Load the default skin.
Grid mainGrid = this.Content as Grid;
MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
this.ApplySkinFromMenuItem(item);
}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
//mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which
// contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace TimersXP
{
public class TimersHostViewModel
{
public TimersHostViewModel()
{
// Load the default skin.
Grid mainGrid = this.Content as Grid; <---- ERROR HERE
}
//public void TimersHostViewModel()
//{
// // Load the default skin.
// Grid mainGrid = TimersHost.Content as Grid;
// MenuItem item = mainGrid.ContextMenu.Items[0] as MenuItem;
// //this.ApplySkinFromMenuItem(item);
//}
public void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
//Grid mainGrid = this.Content as Grid;
//foreach (MenuItem mi in mainGrid.ContextMenu.Items)
// mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
App app = Application.Current as App;
app.ApplySkin(skinDictUri);
}
}
}
check out this link:
ContextMenu in MVVM
You need to bind your context menu items to a collection/property in your viewmodel. "This." will not work because that is the code behind and does not translate across to a view model.
Put this in view model:
class ContextItem : INotifyPropertyChanged
{
public string Name;
public ICommand Action;
public Brush Icon;
}
ObservableCollection<ContextItem> Items {get;set;}
then in your view's context menu:
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}/>
Anything you want to "pass" to the view needs to be a property/collection in your view model, you will never directly use a visual element object like a Gird/Context menu in you viewmodel. WPF handles the binding for you, which is the main benefit of WPF. Just make sure you implement INotifyPropertyChanged for the properties. I didn't to simplify the sample.
Now this does not mean there is never a case for code behind, but it should only involve visual elements, and not the data the visual elements bind to.
Hope this helps
Is it possible to attach context menu to a wpf control and have it opened on left-click (as opposed to more customary right-click)? I want to achieve that using xaml only (this should be a part of my control's view template).
Here is a way to show context menu on left-click:
Create a new left button handler on the Border element:
<Border x:Name="Win"
Width="40"
Height="40"
Background="Purple"
MouseLeftButtonUp="UIElement_OnMouseLeftButtonUp">
and then add this:
private void UIElement_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
var mouseDownEvent =
new MouseButtonEventArgs(Mouse.PrimaryDevice,
Environment.TickCount,
MouseButton.Right)
{
RoutedEvent = Mouse.MouseUpEvent,
Source = Win,
};
InputManager.Current.ProcessInput(mouseDownEvent);
}
What it does, it basically maps the left-click into right-click. For reusability, you can wrap this into an attached behavior.
Here is how I would do a simple example of what I am suggesting:
The XAML:
<Window x:Class="LeftClickMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow">
<Grid>
<Border Width="400" Height="300" Background="#ccc" BorderBrush="#333"
BorderThickness="1"
MouseLeftButtonDown="Border_MouseLeftButtonDown"
MouseRightButtonUp="Border_MouseRightButtonUp">
<Border.ContextMenu>
<ContextMenu x:Name="myContextMenu">
<MenuItem Header="Menu Item 1" />
<MenuItem Header="Menu Item 2" />
<MenuItem Header="Menu Item 3" />
<MenuItem Header="Menu Item 4" />
<MenuItem Header="Menu Item 5" />
</ContextMenu>
</Border.ContextMenu>
</Border>
</Grid>
</Window>
And the code-behind:
using System.Windows;
using System.Windows.Input;
namespace LeftClickMenu
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
myContextMenu.IsOpen = true;
}
private void Border_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
}
}
}
I also added the extra MouseRightButtonUp event to inhibit the right-click popup of the context menu.
Create a method to programmatically open a submenu as stated in this SO article:
Show menu programmatically in WPF
Create an event for LeftMouseButtonDown and call that event in XAML.
I have a menu item on my WPF form that runs an import routine, I have bound the command property to a ICommand property in my view model but for some reason the method won't fire.
This is the xaml:
<Menu Height="21"
Margin="0,-2,0,0"
VerticalAlignment="Top"
Grid.ColumnSpan="2">
<MenuItem Header="File" Command="{Binding ImportFileCommand}">Import</MenuItem>
</Menu>
And this is in my view model:
private ICommand importfilecommand;
public ICommand ImportFileCommand
{
get
{
if (this.importfilecommand == null)
{
this.importfilecommand = new RelayCommand(parm => ImportFile());
}
return this.importfilecommand;
}
}
private void ImportFile()
{
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "Tab Files (*.tab)|*.tab*";
if (dialog.ShowDialog() == true)
{
// MessageBox.Show(dialog.FileName);
}
}
This is the pattern that I have used for all my buttons on the form but the menu item just won't work. Am I missing something or do menu items have to be done differently?
Thanks.
Change your XAML to
<Menu Height="21" Margin="0,-2,0,0" VerticalAlignment="Top" Grid.ColumnSpan="2">
<MenuItem Header="File">
<MenuItem Header="Import" Command="{Binding ImportFileCommand}" />
</MenuItem>
</Menu>
In your example, the "Import" content of the MenuItem element implicitly creates a child MenuItem of the parent File MenuItem. This child MenuItem has no Command property defined and so is unable to be executed. Apparently the executability of the Command defined on the parent MenuItem is overridden by the sub-menu expansion functionality.