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

Smart Routed Commands in WPF

Rate me:
Please Sign up or sign in to vote.
4.67/5 (25 votes)
3 Nov 2007CPOL5 min read 72.9K   406   50   9
Giving routed commands default execution logic

Introduction

This article explains how to embed some intelligence into a routed command. That intelligence can be used as a fallback plan, in case no element in your UI knows what to do when the command is executed. It also allows you to encapsulate the most common command execution logic into the command itself, which makes it easier to reuse.

Background

The command system in WPF is based on the ICommand interface. Any object which implements that interface can be treated as a command. You can create a type which implements the interface and contains logic to determine if the command can be executed, and what to do when it is executed. This provides WPF developers with a convenient mechanism for encapsulating an action, and allowing it to be used by WPF.

Often the meaning of executing a command can change based on runtime context; such as whether some property on a domain object returns a specific value, or if a CheckBox is checked, etc. In situations like this it does not always make sense to encapsulate your logic in a command object, because it might need to have intimate knowledge of the UI in which the command was executed.

That is why WPF has the routed command system. It allows for the actual execution logic of a command to be delegated to an arbitrary node in the element tree. By bubbling a certain routed event up the element tree when a routed command is executed, it allows any element in the tree to say, "Hey, I know what to do when this command is executed. I'll take care of it." Paradoxically, routed commands are very useful because they don't do anything. All that they do is send a notification through the element tree when they are executed, letting others have a chance to perform their own version of the command's execution logic (as well as letting others decide if the command can execute at all).

To learn more about the command system in WPF, read this page in the SDK.

The Problem

Suppose that I am creating a command which displays a Web page. Most of the time, I would expect the Web page to be displayed in the user's default Web browser. I do not want to require that the page is displayed in any particular way, so the command should be a routed command. That will allow the UI in which the command is used to determine how to display a Web page, perhaps in a Frame element in the application. Since our command might be used in many UIs, and even many applications, it is important that we offer this flexibility.

Now we have arrived at an interesting problem. We want the command to be routed, so that it can be used in a flexible manner, but we also want to provide standard execution logic which opens a Web page in the user's default browser. The standard execution logic should be used as a fallback plan if the routed command was not handled by any element in the tree.

Unfortunately the RoutedCommand class does not expose any virtual methods that we can override to specify our default execution logic. We cannot simply subclass RoutedCommand, override a couple of methods, and be done with it.

How can we create a routed command which has "default" behavior?

The Solution

I created a subclass of RoutedCommand called SmartRoutedCommand. That class exposes an attached Boolean property called IsCommandSink. When you are using a SmartRoutedCommand in your UI, you must set IsCommandSink to true on the root element in the element tree. Doing so enables your SmartRoutedCommand subclasses to perform their default execution logic when routed command execution notifications are unhandled by the element tree.

The architects behind WPF's routed event system must have eaten "hot peppers that were grown deep in the jungle primeval by the inmates of a Guatemalan insane asylum". They made routed events so insanely flexible that it allows us to solve difficult problems like this with very few lines of code. Here is the entire SmartRoutedCommand class. Pay close attention to the OnIsCommandSinkChanged method.

C#
/// <summary>
/// This abstract class is a RoutedCommand which allows its
/// subclasses to provide default logic for determining if 
/// they can execute and how to execute.  To enable the default
/// logic to be used, set the IsCommandSink attached property
/// to true on the root element of the element tree which uses 
/// one or more SmartRoutedCommand subclasses.
/// </summary>
public abstract class SmartRoutedCommand : RoutedCommand
{
 #region IsCommandSink
 public static bool GetIsCommandSink(DependencyObject obj)
 {
  return (bool)obj.GetValue(IsCommandSinkProperty);
 }

 public static void SetIsCommandSink(DependencyObject obj, bool value)
 {
  obj.SetValue(IsCommandSinkProperty, value);
 }

 /// <summary>
 /// Represents the IsCommandSink attached property.  This is readonly.
 /// </summary>
 public static readonly DependencyProperty IsCommandSinkProperty =
  DependencyProperty.RegisterAttached(
  "IsCommandSink",
  typeof(bool),
  typeof(SmartRoutedCommand),
  new UIPropertyMetadata(false, OnIsCommandSinkChanged));

 /// <summary>
 /// Invoked when the IsCommandSink attached property is set on an element.
 /// </summary>
 /// <param name="depObj">The element on which the property was set.</param>
 /// <param name="e">Information about the property setting.</param>
static void OnIsCommandSinkChanged(
  DependencyObject depObj, DependencyPropertyChangedEventArgs e)
 {
  bool isCommandSink = (bool)e.NewValue;

  UIElement sinkElem = depObj as UIElement;
  if (sinkElem == null)
   throw new ArgumentException("Target object must be a UIElement.");

  if (isCommandSink)
  {
   CommandManager.AddCanExecuteHandler(sinkElem, OnCanExecute);
   CommandManager.AddExecutedHandler(sinkElem, OnExecuted);
  }
  else
  {
   CommandManager.RemoveCanExecuteHandler(sinkElem, OnCanExecute);
   CommandManager.RemoveExecutedHandler(sinkElem, OnExecuted);
  }
 }
 #endregion // IsCommandSink

 #region Static Callbacks
 static void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
 {
  SmartRoutedCommand cmd = e.Command as SmartRoutedCommand;
  if (cmd != null)
  {
   e.CanExecute = cmd.CanExecuteCore(e.Parameter);
  }
 }

 static void OnExecuted(object sender, ExecutedRoutedEventArgs e)
 {
  SmartRoutedCommand cmd = e.Command as SmartRoutedCommand;
  if (cmd != null)
  {
   cmd.ExecuteCore(e.Parameter);
   e.Handled = true;
  }
 }
 #endregion // Static Callbacks

 #region Abstract Methods
 /// <summary>
 /// Child classes override this method to provide logic which
 /// determines if the command can execute.  This method will 
 /// only be invoked if no element in the tree indicated that
 /// it can execute the command.
 /// </summary>
 /// <param name="parameter">The command parameter (optional).</param>
 /// <returns>True if the command can be executed, else false.</returns>
 protected abstract bool CanExecuteCore(object parameter);

 /// <summary>
 /// Child classes override this method to provide default 
 /// execution logic.  This method will only be invoked if
 /// CanExecuteCore returns true.
 /// </summary>
 /// <param name="parameter">The command parameter (optional).</param>
 protected abstract void ExecuteCore(object parameter);
 #endregion // Abstract Methods
}

The fundamental idea here is that when the IsCommandSink attached property is set on an element, we add a handler to the CommandManager's CanExecute and Executed attached events. Those events are bubbled up the element tree when a routed command is being queried to see if it can execute and when it has been executed, respectively.

When those events are raised on that element the event handling methods in SmartRoutedCommand are invoked, allowing us to then call the abstract methods which subclasses override to provide their default execution logic. This technique is like having the root of the element tree forward our command a notification that it is being used but nobody knows what to do, so that we can then use the subclass's default logic.

The Demo App

This article is accompanied by a demo application which shows how to use SmartRoutedCommand. The demo app lets you type in some keywords and then click a button to search Google with those words. It contains a command called OpenWebPageCommand, which derives from SmartRoutedCommand. The UI allows you to either open a custom Web browser (which is created by the demo app's Window), or to use your default Web browser (which is opened by the command itself).

Below is a screenshot of the demo app:

Screenshot of demo application

Here is OpenWebPageCommand:

C#
/// <summary>
/// A routed command which knows how to open a Web page in a browser.
/// </summary>
public class OpenWebPageCommand : SmartRoutedCommand
{
 /// <summary>
 /// Singleton instance of this class.
 /// </summary>
 public static readonly ICommand Instance = new OpenWebPageCommand();

 private OpenWebPageCommand() { }

 protected override bool CanExecuteCore(object parameter)
 {
  string uri = parameter as string;
  if (uri == null)
   return false;

  bool isUriValid = Uri.IsWellFormedUriString(uri, UriKind.Absolute);
  bool haveConnection = NetworkInterface.GetIsNetworkAvailable();

  return isUriValid && haveConnection;
 }

 protected override void ExecuteCore(object parameter)
 {
  string uri = parameter as string;
  Process.Start(uri);
 }
}

The XAML for the Window which uses that command is seen below. Notice how the IsCommandSink property is set on the Window, making it possible for the OpenWebPageCommand's default execution logic to be used.

XML
<Window 
  x:Class="SmartRoutedCommandDemo.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:SmartRoutedCommandDemo" 
  Title="SmartRoutedCommand Demo" 
  FontSize="12"
  Width="300" Height="140" 
  WindowStartupLocation="CenterScreen"
  local:SmartRoutedCommand.IsCommandSink="True"
  >
  <StackPanel Margin="2">
    <StackPanel.Resources>
      <local:KeywordsToGoogleSearchConverter x:Key="googleSearchConv" />
    </StackPanel.Resources>

    <StackPanel.CommandBindings>
      <CommandBinding 
        Command="{x:Static local:OpenWebPageCommand.Instance}" 
        CanExecute="OnCanCmdExecute" 
        Executed="OnCmdExecuted" 
        />
    </StackPanel.CommandBindings>

    <TextBlock Text="Enter Keywords:" />
    <TextBox x:Name="txtKeywords" Margin="0,4" />

    <Button 
      Command="{x:Static local:OpenWebPageCommand.Instance}" 
      CommandParameter="{Binding 
      Converter={StaticResource googleSearchConv}, 
      ElementName=txtKeywords, 
      Mode=OneWay,
      Path=Text}"
      HorizontalAlignment="Right"
      IsDefault="True"
      >
      Google It!
    </Button>

    <CheckBox 
      x:Name="chkUseCustomBrowser" 
      IsChecked="False"
      Margin="0,20,0,0"
      >
      Use Custom Web Browser
    </CheckBox>
  </StackPanel>
</Window>

Notice that the StackPanel has a CommandBinding for the OpenWebPageCommand. To determine if the StackPanel should handle the command execution, the following method is invoked by the WPF commanding system at various times:

C#
void OnCanCmdExecute(object sender, CanExecuteRoutedEventArgs e)
{
 // Only execute this Window's custom command logic if the
 // CheckBox is checked.  Otherwise let the default logic
 // of OpenWebPageCommand execute.
 bool useCustomBrowser =
  this.chkUseCustomBrowser.IsChecked.GetValueOrDefault();

 if(useCustomBrowser)
 {
  // Assume we have an internet connection, 
  // just to keep this demo simple.  By marking
  // CanExecute as true, this element will be
  // asked to execute the command.
  e.CanExecute = true;
 }
}

If the CanExecute property of the event argument is not set to true, then the CanExecute routed event keeps bubbling up the element tree until eventually the Window forwards the notification over to the OpenWebPageCommand itself. At that point the command's built-in logic will be used.

Conclusion

By using the SmartRoutedCommand class, you can have the best of both worlds; routed commands with default execution logic. This technique is not always appropriate, since some commands do not have a reasonable "default action". But if you find yourself implementing the same command execution logic many times for the same routed command, consider using SmartRoutedCommand to consolidate that logic into one convenient place.

License

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


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions

 
QuestionHow about a hot pepper? Pin
ca0v26-Sep-09 18:36
ca0v26-Sep-09 18:36 
GeneralThis really helps, right now! Pin
Kavan Shaban15-Apr-08 23:49
Kavan Shaban15-Apr-08 23:49 
GeneralRe: This really helps, right now! Pin
Josh Smith16-Apr-08 6:05
Josh Smith16-Apr-08 6:05 
QuestionCommandBinding Pin
Martin Lercher6-Nov-07 3:11
Martin Lercher6-Nov-07 3:11 
AnswerRe: CommandBinding Pin
Josh Smith6-Nov-07 3:17
Josh Smith6-Nov-07 3:17 
GeneralExcellent Pin
Sacha Barber4-Nov-07 4:05
Sacha Barber4-Nov-07 4:05 
GeneralRe: Excellent Pin
Josh Smith4-Nov-07 4:07
Josh Smith4-Nov-07 4:07 
Thanks a lot, Sacha.


:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

GeneralGreat material Pin
User 2710094-Nov-07 2:22
User 2710094-Nov-07 2:22 
GeneralRe: Great material Pin
Josh Smith4-Nov-07 3:52
Josh Smith4-Nov-07 3:52 

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.