Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

WPF Routed Commands Simplified

Rate me:
Please Sign up or sign in to vote.
4.39/5 (28 votes)
25 Jun 2015CPOL12 min read 112.2K   767   26   54
In this article I will demonstrate methods to simplify the use of routed commands in WPF.

WPF is far more powerful than WinForms and introduces many new concepts. Unfortunately as with many things in .NET, evolution sometimes brings along unwanted side effects. Routed commands in WPF bring a lot of features, unfortunately they also bring a lot of unnecessary complexity which causes many users to either create far more code than is necessary for a simple task, or avoid them completely.

The Problem

Task: Create a simple WPF application with a menu which supports keyboard shortcuts (ie Ctrl-F etc).

This is a very basic and common task required by many applications. In WinForms this problem is so easily solved in a few seconds that one could not honestly define it as a problem. In WPF the solution is far from simple.

.NET is very powerful and in most areas very well designed. However from time to time areas of .NET suffer from abstraction without pragmatism. I have often described it as "It makes the hard stuff easy, but the easy stuff hard". Databinding in .NET 1.0 was the first notable example of this, and now we see it on a lesser degree with routed commands. Databinding was improved in later .NET versions, but routed commands in WPF are basically the same since introduction. Attached property usage in code is another example, but fortunately this has long since been addressed.

Demo - WinForms

First I will demonstrate a simple application using WinForms which will serve as our demo for comparison to WPF. The application is a single form application which has a main menu containing two items: Hello and Bye. The two items are exclusive. You cannot select Bye before saying Hello, and vice versa except on startup where Hello can be used immediately. Selecting either of these items displays a message box.

On start, Hello is available, but Bye is disabled.

Image 1

Clicking Hello causes this dialog to appear.

 Image 2

Now Bye is enabled but Hello is disabled.

Image 3

Image 4

The hot keys (Ctrl+H and Ctrl+B) for the menu items were specified by selecting the menu item in the form designer, and entering the shortcut into the property ShortcutKeys.

Image 5

The code for this application is shown below.

C#
public partial class Form1 : Form {
  public Form1() {
    InitializeComponent();
  }

  private void helloToolStripMenuItem_Click(object sender, EventArgs e) {
    byeToolStripMenuItem.Enabled = true;
    helloToolStripMenuItem.Enabled = false;
    MessageBox.Show("Hello");
  }

  private void byeToolStripMenuItem_Click(object sender, EventArgs e) {
    byeToolStripMenuItem.Enabled = false;
    helloToolStripMenuItem.Enabled = true;
    MessageBox.Show("Bye");
  }
}

Logical Separation

The WinForms demo makes no attempt at being an MVVM, MVP, MVC or any other type of logically segregated application. One of the driving design goals behind routed commands is MVVM. The problem is that they were done in a way which makes their implementation clumsy at best for the developer requiring code scattered in several places and because of how they are used, this side effect often causes more problems than it is intended to solve. Please note that I am not speaking of MVVM itself, only that of the consequences of the current implementation of routed commands.

In the initial version of this article many of the points were not made well. Because of this I have integrated stock based examples from MSDN and other sources to better illustrate the points of the article.

In this article I take no position on MVVM. But I do take issue that MVVM is not suited for all scenarios and that the overhead and being forced into such a decision as a "one size fits all" approach is inappropriate. In fact I quote from Wikipedia:

"A criticism of the pattern comes from MVVM creator John Gossman himself, who points out that the overhead in implementing MVVM is "overkill" for simple UI operations. He also states that for larger applications, generalizing the View layer becomes more difficult. Moreover, he illustrates that data binding in very large applications can result in considerable memory consumption."

This pretty much sums up the issue with routed commands in non MVVM applications. The problem is not MVVM, but being coerced into such a situation for all cases when using certain WPF features. Gossman mentions that it becomes difficult in large applications. This is true, but in fact it is the large applications that benefit the most from MVVM. In smaller applications it usually just "gets in the way". And yes, I realize that sometimes small applications become big ones, but most don't and those that do can easily be reengineered before they become too large.

Quite simply - the need to add Ctrl-H handling into an application should not require significant code or a change to MVVM simply for this feature.

Using WPF

WPF is the successor to WinForms. WinForms is in maintenance mode. WPF is mature.

Given these factors, porting the previous demo to WPF should be straightforward. Well actually it is, unless you want hot key support (Ctrl-B, Ctrl-H). That little detail creates a complication in WPF applications.

To support hot keys, one can no longer use the main menu component solely but is required to create routed commands and bindings or provide key support via keypress events.

Demo - WPF (Routed1)

Becuase both XAML and C# are extremely versatile, there are numerous ways to implement routed commands. In this demo I have used one of the commonly accepted methods of using routed commands. I have used a demo from Understanding Routed commands. (Note: I found no license, but I am providing attributing by linking to the original article)

I have made only minimal changes to this demo. Because this article concerns use of the menu, I added a menu and also introduced two commands instead of one. I also changed the logic to perform Hello / Bye as shown in the WinForms application. Otherwise I have left the demo alone. The routed commands and all other structures have been preserved as I found them.

Window

Image 6

XML
<Window x:Class="RoutedCommandDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:RoutedCommandDemo"
    Title="Window1" Height="300" Width="300">
  <Window.InputBindings>
    <KeyBinding Command="{x:Static local:Commands.Hello}" Modifiers="Control" Key="H"/>
    <KeyBinding Command="{x:Static local:Commands.Bye}" Modifiers="Control" Key="B"/>
  </Window.InputBindings>
  <Window.CommandBindings>
    <CommandBinding
      Command="{x:Static local:Commands.Hello}"
      CanExecute="Hello_CanExecute"
      Executed="Hello_Executed"
      />
    <CommandBinding
      Command="{x:Static local:Commands.Bye}"
      CanExecute="Bye_CanExecute"
      Executed="Bye_Executed"
      />
  </Window.CommandBindings>

  <Grid>
    <Menu HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Name="menuMain">
      <MenuItem Header="_File">
        <MenuItem Header="_Hello" Name="mitmFile_Hello" InputGestureText="Ctrl+H" Command="{x:Static local:Commands.Hello}"/>
        <MenuItem Header="_Bye" Name="mitmFile_Bye" InputGestureText="Ctrl+B" Command="{x:Static local:Commands.Bye}"/>
      </MenuItem>
    </Menu>
    <StackPanel Margin="0,48,0,0">
    <Button 
      Command="{x:Static local:Commands.Hello}" 
      Content="Hello"
      />
    <Button 
      Command="{x:Static local:Commands.Bye}" 
      Content="Bye"
      />
    </StackPanel>
  </Grid>
</Window>

Commands.cs

C#
public static class Commands {
  public static readonly RoutedCommand Hello = new RoutedCommand();
  public static readonly RoutedCommand Bye = new RoutedCommand();
}

Code behind window

C#
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
  }

  bool mHelloSaid = false;

  void Hello_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
    e.CanExecute = !mHelloSaid;
  }

  void Hello_Executed(object sender, ExecutedRoutedEventArgs e) {
    mHelloSaid = true;
    MessageBox.Show("Hello");
  }

  void Bye_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
    e.CanExecute = mHelloSaid;
  }

  void Bye_Executed(object sender, ExecutedRoutedEventArgs e) {
    mHelloSaid = false;
    MessageBox.Show("Bye!");
  }
}

The first thing most developers will notice is that the size and complexity of the code versus the WinForms demo has increased significantly. I have followed the pattern that is most prevalent among samples from the resources section at the end of this article as well as MSDN. They do vary on aspects of XAML and some move some code to C#, but I have strictly adhered to the original demo architecturally.

To better illustrate the issues let us examine what is required to add a new user option. First, we must add a new menu item, a new button, and code that will execute. Using WinForms we would be finished. But with routed commands we need to perform quite a few additional steps.

Step 1 - Command

C#
  public static class Commands {
    public static readonly RoutedCommand Hello = new RoutedCommand();
    public static readonly RoutedCommand Bye = new RoutedCommand();
    public static readonly RoutedCommand Exit = new RoutedCommand();
  }

The command represents the description of the action, and if it is a custom routed command it might contain an implementation as well.

Notice that they are declared as static. While this may save a small amount of memory when multiple window instances exist, the reason for declaring them as static is other. Non static commands may be used as well, but to provide design time binding support in XAML, a static is necessary.

The use of the readonly keyword is not necessary and is simply a preference of mine to ensure no accidental changes occur which would lead to unexpected behavior.

Step 2 - InputBinding

XML
<Window.InputBindings>
  <KeyBinding Command="{x:Static local:Commands.Hello}" Modifiers="Control" Key="H"/>
  <KeyBinding Command="{x:Static local:Commands.Bye}" Modifiers="Control" Key="B"/>
</Window.InputBindings>

The input bindings map key gestures (aka hotkeys, shortcuts) to actions. This step can be eliminated by adding the key gestures to the action directly, but this practice is not common in most samples.

In this example, these key gestures are specific to this window only, but so are the actions. Commands can be shared across windows and even applications. Examples of common commands are copy, cut, paste, page up, page down, etc. .NET also includes many predefined commands that can be used.

Step 3 - CommandBinding

XML
  <Window.CommandBindings>
    <CommandBinding
      Command="{x:Static local:Commands.Hello}"
      CanExecute="Hello_CanExecute"
      Executed="Hello_Executed"
      />
    <CommandBinding
      Command="{x:Static local:Commands.Bye}"
      CanExecute="Bye_CanExecute"
      Executed="Bye_Executed"
      />
    <CommandBinding
      Command="{x:Static local:Commands.Exit}"
      CanExecute="Exit_CanExecute"
      Executed="Exit_Executed"
      />
  </Window.CommandBindings>

Command bindings bind the command to the implementation as methods in the Window.

Review

Just to add a single new action, we had to edit three additional sections of code in separate areas. As the options increase, your Window code often gets swamped by these simple structures. I regularly encounter UI code with more routed command scaffolding code than actual code.

This code is also quite brittle and can be prone to mistakes. Intellisense is not available in many cases:

Image 7

Visual Studio often offers descriptive error messages, but in XAML it is less common. When using XAML, there are many more cases that Visual Studio will not catch the errors until run time. In the binding method that is used here, it is generally reliable. The XAML designer does not parse the code as much as Visual Studio does in C#, but instead relies on the last compiled output. So you need rebuild your project more often.

Refactoring or renaming commands will not propagate into the XAML and also must be done by hand. Finally, if you are a coder like me, you may simply prefer to use XAML as declaratively as possible and rely on C# for UI and logic.

Solution

The idea of separating commands from the UI is a great one. It allows easier code consolidation for example when you have a menu item and a tool bar button which execute the same action. It is the implementation that feels immature though and showing no signs of improvement.

15 years ago Delphi introduced something similar with action lists. Delphi got the RAD part right, but unfortunately Delphi's actions were too limited and suffered from other design issues. These design issues caused most developers to abandon them quickly. A similar issue exists with routed commands causing many developers to abandon them and instead use KeyDown and other methods to implement keyboard support.

Ultimately it would be nice to have a visual editor for commands, but extending Visual Studio is far more complex than it should be. And before anyone jumps on me saying "oh its easy, you just don't know how", consider two things. First, while Delphi is more limited in its extensibility, one can easily write a IDE plugin within a few minutes. Something that is not generally possible in Visual Studio. Secondly, I am one of the founders of the Cosmos project which is one of the largest Visual Studio extensions available including a custom debugger which is a virtually undocumented feature of Visual Studio. Back to our discussion of routed commands....

Demo - WinForms Hack (Routed2)

Because of the complexity involved in creating a full design time visual solution,  I wanted to create something in the middle. I didn't have time to create a full Visual Studio component set or editor, but I certainly could improve the situation with some minimal effort. This solution also requires the use of a menu, which your application may not use. The idea however can be expanded and one could even create an invisible menu bar to use. The idea behind the menu is that its a "hacked" visual editor for the routed commands.

This approach will not work in all cases, and is not even a "good" approach. However at the time I was helping some developers port thousands of simple screens for customers coming out of a Linux console and mainframe type environment. The customers simply did not want a modern UI as it would change their workflow too much. Simple form based screens with menus and common Windows components however were acceptable to them.

We could have easily used WinForms, however WPF offered a lot of other advantages and allowed for better future expansion. The developers were also new to .NET and introducing too many concepts at once would have simply sunk the project.

For a better solution, read on to the Routed4 example.

Window

Image 8

C#
<Window x:Class="Routed2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
  <Grid>
    <Menu HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Name="menuMain">
      <MenuItem Header="_File">
        <MenuItem Header="_Hello" Name="mitmFile_Hello" InputGestureText="Ctrl+H" Click="mitmFile_Hello_Click" />
        <MenuItem Header="_Bye" Name="mitmFile_Bye" IsEnabled="False" InputGestureText="Ctrl+B" Click="mitmFile_Bye_Click" />
      </MenuItem>
    </Menu>
    <StackPanel Margin="0,48,0,0">
      <Button Name="butnHello" Content="Hello" />
      <Button Name="butnBye" Content="Bye"/>
    </StackPanel>
  </Grid>
</Window>

Commands.cs (No longer needed)

C#
 

Code behind window

C#
public partial class MainWindow : Window {

  public MainWindow() {
    InitializeComponent();

    SimpleCommand.ScanMenu(this, menuMain.Items);
    butnHello.Command = mitmFile_Hello.Command;
    butnBye.Command = mitmFile_Bye.Command;
  }

  private void mitmFile_Hello_Click(object sender, RoutedEventArgs e) {
    mitmFile_Hello.IsEnabled = false;
    mitmFile_Bye.IsEnabled = true;
    MessageBox.Show("Hello");
  }

  private void mitmFile_Bye_Click(object sender, RoutedEventArgs e) {
    mitmFile_Bye.IsEnabled = false;
    mitmFile_Hello.IsEnabled = true;
    MessageBox.Show("Bye");
  }

}

It should be apparent that this code is far simpler. One simply needs to design the menu and define click events. This was a concept that was very familiar to the developers coming from VB6, Delphi, and WinForms and allowed screens to be constructed quickly.

CanExecute is hooked by default to an event which checks to see if the menu item is enabled or not, and the hotkey is taken from InputGestureText. The "magic" occurs in the call in the ctor to .ScanMenu.

The source for this is available in SimpleCommand.cs and is only a few dozen lines of code.

To add a new item such as Exit, one only needs to add a new menu item. The rest is assembled for you, and the command and bindings are still accessible should you wish to further modify their behavior.

This is not a catch all solution and of course has limitations. Use with caution and only in narrow circumstances.

Demo (Routed4)

Routed4 is a demo which demonstrates a pragmatic way to simplify routed command usage. It does this by automating and consolidating much of the scaffolding without limiting flexibility.

Window1.xaml.cs

C#
namespace Routed4 {
  public partial class Window1 : Window {
    public static readonly Command CmdHello = Command.Create("Hello", ModifierKeys.Control, Key.H);
    public static readonly Command CmdBye = Command.Create("Bye", ModifierKeys.Control, Key.B);

    public Window1() {
      InitializeComponent();
      Command.RegisterBindings(this);
    }

    bool mHelloSaid = false;

    void CmdHello_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
      e.CanExecute = !mHelloSaid;
    }

    void CmdHello_Executed(object sender, ExecutedRoutedEventArgs e) {
      mHelloSaid = true;
      MessageBox.Show("Hello");
    }

    void CmdBye_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
      e.CanExecute = mHelloSaid;
    }

    void CmdBye_Executed(object sender, ExecutedRoutedEventArgs e) {
      mHelloSaid = false;
      MessageBox.Show("Bye!");
    }
  }
}

The code uses a new class called Command. Command descends from RoutedUICommand and is only about a screen worth of code. It allows a simpler declaration of the commands, and also adds an important feature which is discussed below: Command.RegisterBindings(this).

Compared to Routed2 the C# code is a little simpler. But the XAML is a lot simpler and the overall excessive distributed nature of the scaffolding has nearly disappeared as we shall see next.

The reason that a static .Create method is used instead of a constructor is due to one of the several design oversights in RoutedCommands.

Window1.xaml

XML
<Window x:Class="Routed4.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:w="clr-namespace:Routed4"
    Title="Window1" Height="300" Width="300">
  <Grid>
    <Menu HorizontalAlignment="Left" VerticalAlignment="Top" Width="100">
      <MenuItem Header="_File">
        <MenuItem Command="{x:Static w:Window1.CmdHello}"/>
        <MenuItem Command="{x:Static w:Window1.CmdBye}"/>
      </MenuItem>
    </Menu>
    <StackPanel Margin="0,48,0,0">
      <Button Command="{x:Static w:Window1.CmdHello}" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
      <Button Command="{x:Static w:Window1.CmdBye}" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" />
    </StackPanel>
  </Grid>
</Window>

XAML is where the real difference appears. There is no need to explicitly add InputBindings (although this can be done away with by specifying them in the Command), and no wordy CommandBindings.

Command.RegisterBindings(this)

RegisterBindings is what provides most of the simplicity gains. Register bindings uses reflection to enumerate all Commands that exist as non public fields of the Window.

For each command that it finds, it then looks for a <FieldName>_Executed and <FieldName>_CanExecute non public instance method on the Window class. Using these, it creates command bindings automatically.

Speling!

I have tried to catch spelling errors, but if you happen to find any please report them. I have also lived my life between countries which use at least 4 different dialects of English and sometimes you may find a British spelling, other times an American one. I have done my best to standardize them to American English.

Other Resources

License

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


Written By
Cyprus Cyprus
Chad Z. Hower, a.k.a. Kudzu
"Programming is an art form that fights back"

I am a former Microsoft Regional DPE (MEA) covering 85 countries, former Microsoft Regional Director, and 10 Year Microsoft MVP.

I have lived in Bulgaria, Canada, Cyprus, Switzerland, France, Jordan, Russia, Turkey, The Caribbean, and USA.

Creator of Indy, IntraWeb, COSMOS, X#, CrossTalk, and more.

Comments and Discussions

 
QuestionMy vote of 5 Pin
ISanti26-Jun-15 5:48
ISanti26-Jun-15 5:48 
AnswerRe: My vote of 5 Pin
Chad Z. Hower aka Kudzu26-Jun-15 9:56
Chad Z. Hower aka Kudzu26-Jun-15 9:56 
GeneralMy vote => UP Pin
Ravi Shankar K26-Jun-15 3:41
Ravi Shankar K26-Jun-15 3:41 
GeneralRe: My vote => UP Pin
Chad Z. Hower aka Kudzu26-Jun-15 3:48
Chad Z. Hower aka Kudzu26-Jun-15 3:48 
QuestionWPF is far more powerful than WinForms? Pin
mag1326-Jun-15 0:34
mag1326-Jun-15 0:34 
AnswerRe: WPF is far more powerful than WinForms? Pin
Chad Z. Hower aka Kudzu26-Jun-15 2:30
Chad Z. Hower aka Kudzu26-Jun-15 2:30 
GeneralRe: WPF is far more powerful than WinForms? Pin
mag1326-Jun-15 2:50
mag1326-Jun-15 2:50 
Suggestion[My vote of 1] My vote of 1. Take this article down. Pin
Alexander Sokolov24-Apr-15 2:50
professionalAlexander Sokolov24-Apr-15 2:50 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Member 302386524-Apr-15 16:51
Member 302386524-Apr-15 16:51 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Alexander Sokolov27-Apr-15 9:48
professionalAlexander Sokolov27-Apr-15 9:48 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Chad Z. Hower aka Kudzu24-Jun-15 9:46
Chad Z. Hower aka Kudzu24-Jun-15 9:46 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Mark F.30-Mar-18 9:25
Mark F.30-Mar-18 9:25 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Chad Z. Hower aka Kudzu30-Mar-18 10:11
Chad Z. Hower aka Kudzu30-Mar-18 10:11 
GeneralRe: [My vote of 1] My vote of 1. Take this article down. Pin
Chad Z. Hower aka Kudzu12-Jul-18 6:18
Chad Z. Hower aka Kudzu12-Jul-18 6:18 
GeneralMy vote of 2 Pin
MahBulgaria22-Aug-14 3:11
MahBulgaria22-Aug-14 3:11 
GeneralWell done Chad Pin
Espen Harlinn22-Aug-14 2:05
professionalEspen Harlinn22-Aug-14 2:05 
GeneralRe: Well done Chad Pin
Chad Z. Hower aka Kudzu22-Aug-14 2:10
Chad Z. Hower aka Kudzu22-Aug-14 2:10 
GeneralRe: Well done Chad Pin
Espen Harlinn22-Aug-14 2:14
professionalEspen Harlinn22-Aug-14 2:14 
Questionsorry Pin
Sacha Barber21-Aug-14 19:13
Sacha Barber21-Aug-14 19:13 
AnswerRe: sorry Pin
Chad Z. Hower aka Kudzu22-Aug-14 6:39
Chad Z. Hower aka Kudzu22-Aug-14 6:39 
GeneralMy vote of 2 Pin
Ian Shlasko21-Aug-14 8:33
Ian Shlasko21-Aug-14 8:33 
GeneralRe: My vote of 2 Pin
Chad Z. Hower aka Kudzu21-Aug-14 11:08
Chad Z. Hower aka Kudzu21-Aug-14 11:08 
GeneralRe: My vote of 2 Pin
Ian Shlasko21-Aug-14 11:29
Ian Shlasko21-Aug-14 11:29 
GeneralRe: My vote of 2 Pin
Sacha Barber21-Aug-14 19:10
Sacha Barber21-Aug-14 19:10 
GeneralRe: My vote of 2 Pin
Chad Z. Hower aka Kudzu22-Aug-14 2:31
Chad Z. Hower aka Kudzu22-Aug-14 2:31 

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

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