Click here to Skip to main content
11,644,136 members (73,474 online)
Click here to Skip to main content

An MVVM friendly approach to adding system menu entries in a WPF application

, 9 Apr 2010 CPOL 56.1K 581 58
Rate this:
Please Sign up or sign in to vote.
This article shows you how to add menu items to the system menu and attach command handlers in an MVVM compatible fashion

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.

  1. 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.

  2. 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);
        }
    }

    /// <summary>
    /// Initializes a new instance of the SystemMenuWindow class.
    /// </summary>
    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 Smile | :)

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nish Nishant
United States United States
Nish Nishant is a Software Architect/Consultant based out of Columbus, Ohio. He has over 15 years of software industry experience in various roles including Lead Software Architect, Principal Software Engineer, and Product Manager. Nish is a recipient of the annual Microsoft Visual C++ MVP Award since 2002 (13 consecutive awards as of 2014).

Nish is an industry acknowledged expert in the Microsoft technology stack. He authored
C++/CLI in Action for Manning Publications in 2005, and had previously co-authored
Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his
WordPress blog. Nish is vastly experienced in team management, mentoring teams, and directing all stages of software development.

Contact Nish : You can reach Nish on his google email id voidnish.

Website and Blog

You may also be interested in...

Comments and Discussions

 
QuestionGreat - clean and tight - one issue My Titel Bar no longer drags and drops Pin
jerryboston5-Jul-13 9:19
memberjerryboston5-Jul-13 9:19 
AnswerRe: Great - clean and tight - one issue My Titel Bar no longer drags and drops Pin
Nish Sivakumar5-Jul-13 9:23
sitebuilderNish Sivakumar5-Jul-13 9:23 
Did you remove the Move menu-item? That can prevent the window from being dragged around using the title bar.
Regards,
Nish
Blog: voidnish.wordpress.com
The life of a Malayalee American - by Nish
An article I recently wrote for an event souvenir

GeneralMy vote of 5 Pin
jerryboston5-Jul-13 9:15
memberjerryboston5-Jul-13 9:15 
GeneralMy vote of 5 Pin
Tom Delany9-Mar-12 5:20
memberTom Delany9-Mar-12 5:20 
BugTargetInvocationException when setting DataContext in XAML Pin
Tolga Balci28-Dec-11 0:49
memberTolga Balci28-Dec-11 0:49 
GeneralRe: TargetInvocationException when setting DataContext in XAML Pin
Nishant Sivakumar28-Dec-11 2:53
mvpNishant Sivakumar28-Dec-11 2:53 
GeneralMy vote of 5 Pin
Doug Schott4-May-11 8:51
memberDoug Schott4-May-11 8:51 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar4-May-11 8:57
mvpNishant Sivakumar4-May-11 8:57 
GeneralThere is an easier way Pin
Sacha Barber27-Apr-10 20:05
mvpSacha Barber27-Apr-10 20:05 
GeneralRe: There is an easier way Pin
Nishant Sivakumar6-May-10 6:27
mvpNishant Sivakumar6-May-10 6:27 
GeneralRe: There is an easier way Pin
Sacha Barber6-May-10 6:30
mvpSacha Barber6-May-10 6:30 
GeneralRe: There is an easier way Pin
Nishant Sivakumar6-May-10 6:31
mvpNishant Sivakumar6-May-10 6:31 
GeneralRe: There is an easier way Pin
Sacha Barber6-May-10 9:39
mvpSacha Barber6-May-10 9:39 
GeneralThere is an easier way Pin
Sacha Barber27-Apr-10 20:05
mvpSacha Barber27-Apr-10 20:05 
GeneralRe: There is an easier way Pin
Nishant Sivakumar6-May-10 6:28
mvpNishant Sivakumar6-May-10 6:28 
GeneralCool again Pin
Dr.Luiji15-Apr-10 21:53
memberDr.Luiji15-Apr-10 21:53 
GeneralRe: Cool again Pin
Nishant Sivakumar16-Apr-10 1:21
mvpNishant Sivakumar16-Apr-10 1:21 
GeneralOne small suggestion Pin
Richard Deeming9-Apr-10 9:08
memberRichard Deeming9-Apr-10 9:08 
GeneralRe: One small suggestion Pin
Nishant Sivakumar9-Apr-10 13:00
mvpNishant Sivakumar9-Apr-10 13:00 
GeneralRe: One small suggestion Pin
Nishant Sivakumar9-Apr-10 13:46
mvpNishant Sivakumar9-Apr-10 13:46 
GeneralI'm sure it's great,but I can't code! Pin
Alan Beasley9-Apr-10 4:37
memberAlan Beasley9-Apr-10 4:37 
GeneralRe: I'm sure it's great,but I can't code! Pin
Nishant Sivakumar9-Apr-10 12:58
mvpNishant Sivakumar9-Apr-10 12:58 
GeneralRe: I'm sure it's great,but I can't code! Pin
Alan Beasley9-Apr-10 15:38
memberAlan Beasley9-Apr-10 15:38 
GeneralNice article Pin
NinethSense3-Apr-10 22:55
memberNinethSense3-Apr-10 22:55 
GeneralRe: Nice article Pin
Nishant Sivakumar4-Apr-10 0:48
mvpNishant Sivakumar4-Apr-10 0:48 
GeneralCool Pin
Abhinav S3-Apr-10 20:54
memberAbhinav S3-Apr-10 20:54 
GeneralRe: Cool Pin
Nishant Sivakumar4-Apr-10 0:48
mvpNishant Sivakumar4-Apr-10 0:48 
GeneralVery cool Pin
Daniel Vaughan3-Apr-10 13:15
mvpDaniel Vaughan3-Apr-10 13:15 
GeneralRe: Very cool Pin
Nishant Sivakumar3-Apr-10 13:44
mvpNishant Sivakumar3-Apr-10 13:44 
GeneralGood job Nish Pin
Pete O'Hanlon3-Apr-10 11:11
mvpPete O'Hanlon3-Apr-10 11:11 
GeneralRe: Good job Nish Pin
Nishant Sivakumar3-Apr-10 13:43
mvpNishant Sivakumar3-Apr-10 13:43 
GeneralRe: Good job Nish Pin
Nishant Sivakumar4-Jun-10 7:21
mvpNishant Sivakumar4-Jun-10 7:21 
GeneralRe: Good job Nish Pin
Pete O'Hanlon25-Jun-10 13:29
mvpPete O'Hanlon25-Jun-10 13:29 
GeneralRe: Good job Nish Pin
Nishant Sivakumar25-Jun-10 13:33
mvpNishant Sivakumar25-Jun-10 13:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150731.1 | Last Updated 9 Apr 2010
Article Copyright 2010 by Nish Nishant
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid