Figure 1: Settings and Greeting! are disabled in the menu
Figure 2: All menu items are enabled
Introduction
The majority of MFC apps have always had an About... menu entry in the
main window's system menu, and this was primarily because the App Wizard
generated code for that by default. I wanted to do something similar in a WPF
application I've been working on, and I wanted to do it in an MVVM friendly
manner. In this article I'll explain a neat way of doing it so that you can
easily add menu items and attach command handlers to them while retaining the
basic MVVM paradigm. The code supports command parameters as well as an UI
enabling/disabling mechanism. The basic idea is to make it so that it should be
very easy to add system menu-items and then bind commands to them without having
to make major changes to code.
Using the code
There are only two steps to using the class.
- Derive your main window class from
SystemMenuWindow
instead
of from Window
. If you had your own DerivedWindow
class then you need to change that class to derive from
SystemMenuWindow
. You will also have to change the window's Xaml to
reflect this change.
- Add system menu command handlers within the
MenuItems
tag.
That's it. You are ready to go!
The demo app
The demo app has three buttons.
- An About button that is always enabled and brings up an About dialog (a
messagebox in the demo).
- A Settings button that can be enabled or disabled based on a checkbox on
the main window. When enabled, it brings up the settings dialog (again, a
messagebox).
- A Greeting! button that is enabled only if the name text box has at
least one character. When clicked it brings up a greeting messagebox where
the text in the text box is passed as a command parameter.
All the three buttons have corresponding entries in the main window's system
menu.
The View Model class
Here's the rather simple view model class that shows the various command
handlers.
internal class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private ICommand aboutCommand;
public ICommand AboutCommand
{
get
{
return aboutCommand ?? (aboutCommand = new DelegateCommand(
() =>
MessageBox.Show(
"Copyright (c) Nish Sivakumar. All rights reserved.",
"About...")
));
}
}
private ICommand settingsCommand;
public ICommand SettingsCommand
{
get
{
return settingsCommand ?? (settingsCommand =
new DelegateCommand(
() => MessageBox.Show(
"Settings dialog placeholder.",
"Settings"),
() => SettingsEnabled
));
}
}
private ICommand greetingCommand;
public ICommand GreetingCommand
{
get
{
return greetingCommand ??
(greetingCommand = new DelegateCommand<string>(
(s) => MessageBox.Show(
String.Concat("Hello ", s, ". How are you?"),
"Greeting"),
(s) => !String.IsNullOrEmpty(s)
));
}
}
private bool settingsEnabled;
public bool SettingsEnabled
{
get
{
return settingsEnabled;
}
set
{
if (settingsEnabled != value)
{
settingsEnabled = value;
this.FirePropertyChanged("SettingsEnabled");
}
}
}
private string enteredName;
public string EnteredName
{
get
{
return enteredName;
}
set
{
if (enteredName != value)
{
enteredName = value;
this.FirePropertyChanged("EnteredName");
}
}
}
}
The View (Xaml)
Here's the Xaml code for the main window.
<nsmvvm:SystemMenuWindow x:Class="SystemMenuWindowDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nsmvvm="clr-namespace:NS.MVVM"
Title="SystemMenu Window Demo Application"
Height="210" Width="330" ResizeMode="NoResize"
WindowStartupLocation="CenterScreen">
<nsmvvm:SystemMenuWindow.MenuItems>
<nsmvvm:SystemMenuItem Command="{Binding AboutCommand}"
Header="About" Id="100" />
<nsmvvm:SystemMenuItem Command="{Binding SettingsCommand}"
Header="Settings" Id="101" />
<nsmvvm:SystemMenuItem Command="{Binding GreetingCommand}"
CommandParameter="{Binding EnteredName}"
Header="Greeting!" Id="102" />
</nsmvvm:SystemMenuWindow.MenuItems>
<Grid>
<StackPanel Height="50" HorizontalAlignment="Right"
Name="stackPanelButtons" VerticalAlignment="Bottom"
Width="270" Orientation="Horizontal">
<Button Content="About" Command="{Binding AboutCommand}"
Height="23" Name="buttonAbout" Width="75" Margin="5,0, 5, 0" />
<Button Content="Settings" Command="{Binding SettingsCommand}"
Height="23" Name="buttonSettings" Width="75" Margin="5, 0, 5, 0" />
<Button Content="Greeting!" Command="{Binding GreetingCommand}"
CommandParameter="{Binding EnteredName}"
Height="23" Name="buttonGreeting" Width="75" Margin="5, 0, 5, 0" />
</StackPanel>
<CheckBox Content="Enable Settings" Height="16"
HorizontalAlignment="Left" Margin="45,24,0,0"
Name="checkBoxSettingsEnabled" VerticalAlignment="Top"
IsChecked="{Binding SettingsEnabled}" />
<Label Content="Your name:" Height="28" HorizontalAlignment="Left"
Margin="47,62,0,0" Name="labelName" VerticalAlignment="Top" />
<TextBox Height="23"
Text="{Binding EnteredName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
Margin="144,64,0,0" Name="textBoxName"
VerticalAlignment="Top" Width="152" />
</Grid>
</nsmvvm:SystemMenuWindow>
The same command bindings are used by the buttons as well as the system menu
entries.
Implementation Details
A system menu entry is represented by the SystemMenuItem
class
which is derived from Freezable
for data context inheritance. It
has bindable properties for Command
, CommandParameter
,
Id
(for the menu item), and Header
(for the menu
text). While similar to a WPF MenuItem
object, this is not the same
class at all. The system menu is also very different to a WPF menu since it's a
native Windows HWND
(or rather HMENU
) based menu.
public class SystemMenuItem : Freezable
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command", typeof(ICommand), typeof(SystemMenuItem),
new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter", typeof(object), typeof(SystemMenuItem));
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register(
"Header", typeof(string), typeof(SystemMenuItem));
public static readonly DependencyProperty IdProperty =
DependencyProperty.Register(
"Id", typeof(int), typeof(SystemMenuItem));
public ICommand Command
{
get
{
return (ICommand)this.GetValue(CommandProperty);
}
set
{
this.SetValue(CommandProperty, value);
}
}
public object CommandParameter
{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
public string Header
{
get
{
return (string)GetValue(HeaderProperty);
}
set
{
SetValue(HeaderProperty, value);
}
}
public int Id
{
get
{
return (int)GetValue(IdProperty);
}
set
{
SetValue(IdProperty, value);
}
}
protected override Freezable CreateInstanceCore()
{
return new SystemMenuItem();
}
private static void OnCommandChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SystemMenuItem systemMenuItem = d as SystemMenuItem;
if (systemMenuItem != null)
{
if (e.NewValue != null)
{
systemMenuItem.Command = e.NewValue as ICommand;
}
}
}
}
The SystemMenuWindow
class is a Window-derived class that
implements the native system menu handling. It exposes a
FreezableCollection<SystemMenuItem>
property MenuItems
which
is used to specify the custom entries that need to be added. The reason I use a
FreezableCollection<>
is for data context inheritance. I wasted
some time initially writing my own IList
(yeah the non-generic one
is what the Xaml parser looks for by default) derived Freezable
collection class before I found this class.
The class implementation is rather straightforward. I handle the Loaded
event and insert the menu items into the system menu using the
InsertMenu
API function. There is a WndProc
hook added using
HwndSource
and both WM_SYSCOMMAND
and
WM_INITMENUPOPUP
are handled appropriately. The code is shown below.
public class SystemMenuWindow : Window
{
private const uint WM_SYSCOMMAND = 0x112;
private const uint WM_INITMENUPOPUP = 0x0117;
private const uint MF_SEPARATOR = 0x800;
private const uint MF_BYCOMMAND = 0x0;
private const uint MF_BYPOSITION = 0x400;
private const uint MF_STRING = 0x0;
private const uint MF_ENABLED = 0x0;
private const uint MF_DISABLED = 0x2;
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool InsertMenu(IntPtr hmenu, int position,
uint flags, uint item_id,
[MarshalAs(UnmanagedType.LPTStr)]string item_text);
[DllImport("user32.dll")]
private static extern bool EnableMenuItem(IntPtr hMenu,
uint uIDEnableItem, uint uEnable);
public static readonly DependencyProperty MenuItemsProperty =
DependencyProperty.Register(
"MenuItems", typeof(FreezableCollection<SystemMenuItem>),
typeof(SystemMenuWindow),
new PropertyMetadata(new PropertyChangedCallback(OnMenuItemsChanged)));
private IntPtr systemMenu;
public FreezableCollection<SystemMenuItem> MenuItems
{
get
{
return (FreezableCollection<SystemMenuItem>)
this.GetValue(MenuItemsProperty);
}
set
{
this.SetValue(MenuItemsProperty, value);
}
}
public SystemMenuWindow()
{
this.Loaded += this.SystemMenuWindow_Loaded;
this.MenuItems = new FreezableCollection<SystemMenuItem>();
}
private static void OnMenuItemsChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
SystemMenuWindow obj = d as SystemMenuWindow;
if (obj != null)
{
if (e.NewValue != null)
{
obj.MenuItems = e.NewValue
as FreezableCollection<SystemMenuItem>;
}
}
}
private void SystemMenuWindow_Loaded(object sender, RoutedEventArgs e)
{
WindowInteropHelper interopHelper = new WindowInteropHelper(this);
this.systemMenu = GetSystemMenu(interopHelper.Handle, false);
if (this.MenuItems.Count > 0)
{
InsertMenu(this.systemMenu, -1,
MF_BYPOSITION | MF_SEPARATOR, 0, String.Empty);
}
foreach (SystemMenuItem item in this.MenuItems)
{
InsertMenu(this.systemMenu, (int)item.Id,
MF_BYCOMMAND | MF_STRING, (uint)item.Id, item.Header);
}
HwndSource hwndSource = HwndSource.FromHwnd(interopHelper.Handle);
hwndSource.AddHook(this.WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg,
IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch ((uint)msg)
{
case WM_SYSCOMMAND:
var menuItem = this.MenuItems.Where(
mi => mi.Id == wParam.ToInt32()).FirstOrDefault();
if (menuItem != null)
{
menuItem.Command.Execute(menuItem.CommandParameter);
handled = true;
}
break;
case WM_INITMENUPOPUP:
if (this.systemMenu == wParam)
{
foreach (SystemMenuItem item in this.MenuItems)
{
EnableMenuItem(this.systemMenu, (uint)item.Id,
item.Command.CanExecute(
item.CommandParameter) ?
MF_ENABLED : MF_DISABLED);
}
handled = true;
}
break;
}
return IntPtr.Zero;
}
}
The WM_SYSCOMMAND
handler is used for Command.Execute
while the WM_INITPOPUP
handler is used for
Command.CanExecute
. This is very similar to MFC's command/UI handler
mechanism. Some things stay the same I guess :-)
Conclusion
As usual, all kinds of feedback, criticism, and suggestions are welcome and
wholly appreciated. Thank you.
History
- April 4, 2010 - Article first published.
- April 6, 2010 - Superfluous
Cast<>()
removed from source-code and article body.
- April 9, 2010 - Removed the unnecessary attached behavior I added
for live text binding and replaced it with
UpdateSourceTrigger=PropertyChanged
. Thank you
Richard Deeming.