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

WPF Command Pattern Applied

Rate me:
Please Sign up or sign in to vote.
4.97/5 (33 votes)
23 Apr 2012CPOL6 min read 142.1K   2.2K   124   21
Applying of the Command pattern in a WPF application.

WPF Command Pattern

Introduction 

WPF Commanding offers the base for integration of the GoF Command pattern into an application. This article describes an approach with the following goals:

Program Architecture

  • Creation of functional units in hierarchies
  • Reduction of complexity in large applications
  • Automated testing
  • Reusability of the pattern and the commands

Command Description

  • Description of a command should be neutral to the GUI in use
  • Commands should be easy to localize

Command Runtime Behaviour

  • Static parameter (per command instance)
  • Context sensitive runtime data
  • Source provided runtime parameters (ICommandSource.CommandParameter)

UI Integration

  • Images and ToolTips with Gestures for existing command sources (Button and MenuItem)
  • Visualization of a Disabled state for Button controls
  • Selectable binding via XAML or Code-Behind
  • Integration of existing WPF commands as for example the System.Windows.Documents.EditingCommands

Command Repository

  • Centralizing the management of the commands
  • Dynamic registration of commands
  • Access to all commands
  • Tracking of command execution

The attached example demonstrates how to use the Command pattern in an RTF editor:

WPF Command Pattern Editor

The test project also shows how to unit test a command. The control CommandComboBox shows how to implement a custom ICommandSource.

Command

The class Command, itself derived from RoutedUICommand, provides the foundation for all commands. It serves as a common ancestor for developing individual command structures. The following overview shows some command classes of the RTF editor:

Image 3

Note: This class hierarchy serves only for demonstrating command structures, and is not intended for production level usage of an RTF editor.

The implementation of a command class is compact and descriptive, and offers a well defined entry point for analysis in future extension steps:

C#
// ------------------------------------------------------------------------
public class EditUndo : EditorCommand
{

    // ----------------------------------------------------------------------
    public EditUndo( Type ownerType, CommandDescription description ) :
           base( "EditUndo", ownerType, description )
    {
    } // EditUndo

    // ----------------------------------------------------------------------
    protected override bool OnCanExecute( EditorCommandContext commandContext )
    {
      if ( !base.OnCanExecute( commandContext ) )
      {
        return false;
      }
      return commandContext.TextBox.CanUndo;
    } // OnCanExecute

    // ----------------------------------------------------------------------
    protected override void OnExecute( EditorCommandContext commandContext )
    {
      commandContext.TextBox.Undo();
    } // OnExecute

} // class EditUndo

The class WrapperCommand allows to integrate existing WPF commands. The following example shows how to reuse the system command EditingCommands.IncreaseIndentation:

C#
// ------------------------------------------------------------------------
public class IncreaseIndentation : WrapperCommand
{

    // ----------------------------------------------------------------------
    public IncreaseIndentation( Type ownerType, CommandDescription description ) :
      base( EditingCommands.IncreaseIndentation, ownerType, description )
    {
    } // IncreaseIndentation

    // ----------------------------------------------------------------------
    protected override bool OnCanExecute( CommandContext commandContext )
    {
      if ( !base.OnCanExecute( commandContext ) )
      {
        return false;
      }

      // add some additional enabling conditions

      return true;
    } // OnCanExecute

} // class IncreaseIndentation

Command Runtime Behaviour

During the execution of a command, a command context is created which encapsulates all the runtime parameters. The following diagram documents the runtime behavior:

WPF Command Pattern Runtime

Creation of a CommandContext instance is delegated to the Command class itself, to support individual context types. The RTF editor example uses the classes ApplicationCommandContext (error handling) and EditorCommandContext (access to the RichTextBox) for this purpose.

Note: Combining all the runtime values into the class CommandContext enhances flexibility for future modifications: CommandContext can be extended without having to update the method signatures of all existing commands.

Further down it will be shown how to use the CommandContext for Unit Testing.

The property Command.Target offers a way to set a parameter per command instance. Because the life cycle of a command is static, this Command.Target property corresponds to a static parameter.

UI Integration

Using the property Command.HasRequirements (Default=True) allows to specify whether Enabling/Disabling is relevant for the command. If there are no requirements, the command is always active and Command.CanExecute() will never be executed.

Button Command

The class ButtonCommandProvider can be used, to attach a Command to a Button control:

XAML

XML
<Button cmd:ButtonCommandProvider.Command="{x:Static rtf:RichTextEditorCommands.EditUndo}" />

Code-Behind

C#
Button undoButton = new Button();
ButtonCommandProvider.SetCommand( undoButton, RichTextEditorCommands.EditUndo );

ButtonCommandProvider offers various functionality aspects which can be activated/deactivated on an optional basis:

ButtonFunctionalityRoutedCommandCommand
ImageInsert a button image (preview in Visual Studio XAML Designer)Using WrapperCommandYes
EnablingVisualization of the Disabled stateButton-ContentButton-Content
ToolTipToolTip according to the command descriptionUsing WrapperCommandYes
ToolTip GestureToolTip gesture according to the command description YesYes

Display of the image can be controlled using the class ButtonImage. The property ButtonCommandProvider.DisabledOpacity determines how the content of an inactive button will be displayed.

The source of an image is determined by the class CommandImageService. In addition to that, the property Command.ImageUri offers a way to set another source for images. By default, button images are taken as:

  • In PNG format
  • Embedded resource in the assembly of the command
  • Located in the 'Images' folder

The utility CommandToolTipService controls the format of the button ToolTip.

MenuItem Command

Using the class MenuItemCommandProvider can be used, to attach a Command to a MenuItem control:

XAML

XML
<Button cmd:MenuItemCommandProvider.Command="{x:Static rtf:RichTextEditorCommands.EditUndo}" />

Code-Behind 

C#
MenuItem undoMenuItem = new MenuItem();
MenuItemCommandProvider.SetCommand( undoMenuItem, RichTextEditorCommands.EditUndo );

MenuItemCommandProvider offers various functionality aspects which can be activated/deactivated on an optional basis:

MenuItemFunctionalityRoutedCommandCommand
ImageDisplay an image along with the menu entryUsing WrapperCommandYes
ToolTipToolTip according to the command descriptionUsing WrapperCommandYes
ToolTip GestureToolTip gesture according to the command descriptionYesYes

Display of the image can be controlled using the class MenuItemImage.

The format of the ToolTip is controlled by the class CommandToolTipService.

Command Repository

The CommandRepository offers access to all the commands and command bindings. The events CommandExecuting and CommandExecuted allow tracking of command executions. The command 'Command Statistics' in the 'Help' menu of the RTF editor is a sample implementation which simply lists all registered commands. The statusbar of the RTF editor displays information about the running command and the total number of executed commands.

Unit Testing

With the basis of the CommandContext, we now have a way to realize individual test cases. The following testing method of the attached test project shows how to automate testing of a command:

C#
// ----------------------------------------------------------------------
[TestMethod]
public void TestToggleBoldDefault()
{
    EditorCommandContext commandContext = CreateCommandContext();

    // setup selection
    TextRange startContent = new TextRange( commandContext.Document.ContentStart,
                                            commandContext.Document.ContentEnd );
    commandContext.Selection.Select( startContent.Start, startContent.End );
    commandContext.Selection.ApplyPropertyValue( FlowDocument.FontWeightProperty,
                                                 FontWeights.Normal );
    string startText = startContent.Text;

    // run the command
    FontWeight defaultFontWeigth = RichTextEditorCommands.ToggleBold.BoldFontWeight;
    RichTextEditorCommands.ToggleBold.Execute( commandContext );

    // tests
    TextRange endContent = new TextRange( commandContext.Document.ContentStart,
                                          commandContext.Document.ContentEnd );
    string endText = endContent.Text;
    Assert.AreEqual( startText, endText );

    object selectionFontWeight =
     commandContext.Selection.GetPropertyValue( FlowDocument.FontWeightProperty );
    Assert.AreEqual( defaultFontWeigth, selectionFontWeight );
} // TestToggleBoldDefault

// ----------------------------------------------------------------------
private EditorCommandContext CreateCommandContext()
{
    // document
    FlowDocument flowDocument = new FlowDocument();

    for ( int i = 0; i < 10; i++ )
    {
      Paragraph paragraph = new Paragraph();
      for ( int n = 0; n < 5; n++ )
      {
        paragraph.Inlines.Add( new Run( "Paragraph Text " +
                                        n.ToString() + " " ) );
      }
      flowDocument.Blocks.Add( paragraph );
    }

    // editor
    RichTextBox richTextBox = new RichTextBox();
    richTextBox.Document = flowDocument;

    // command context
    return new EditorCommandContext( richTextBox );
} // CreateCommandContext

Custom Command Control

According to the description in MSDN, the control CommandComboBox shows the creation of a control which serves as a source for commands. Selection of a list entry will execute the command. To recognize whether the entry has been selected by the user (as opposed to a programmed way), the class UIElementInput provides the relevant event information.

Usage

To use the command system, the following approach is suggested:

  1. Design and implement the command class hierarchy (optional: abstract classes and individual command contexts)
  2. Design and implement the command unit tests (optional)
  3. Create a CommandCollection: static binding of each command with a CommandDescription
  4. Embed button image resources (optional)
  5. Add Command references in XAML
  6. Configure the tooltip format upon application start (optional)
  7. Setup the CommandRepository at application start: CommandRepository.AddRange()
  8. Setup the CommandBinding in the application window: CommandBindings.AddRange()

Points of Interest

Further insights resulting from this article:

  • Disabling a ComboBox depending on the state of an instance of the class CommandComboBox
  • Content driven enabling: As soon as the font size reaches 25pt, the command IncreaseFontSize gets disabled
  • Recognizing differing formats in the RTF selection: EditFontSizeCommand.GetFontSize()
  • Localization of a CommandDescription in RichTextEditorCommands
  • Access to class values from XAML through the DependencyProperty of CommandWindow.CurrentCommandInfo
  • Updating the statusbar during selection of a menu entry using a XAML Style EventSetter with a call to CommandWindow.OnStateUpdate() and CommandWindow.OnStatusReset()
  • XAML Styling through class inheritance as in the examples ButtonImage and MenuItemImage

History

  • 23th April, 2012 - v1.1.0.0
    • Renamed ButtonCommand to ButtonCommandProvider
    • Renamed MenuItemCommand to MenuItemCommandProvider
    • ReSharped source code
    • Refreshed article and fixed formatting
  • 23th April, 2008
    • Image for a MenuItemCommand
    • New classes ImageButton and MenuItemButton
    • New class WrapperCommand
    • New section: Points of Interest
  • 21th April, 2008
    • Initial public release

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)
Switzerland Switzerland
👨 Senior .NET Software Engineer

🚀 My Open Source Projects
- Time Period Library 👉 GitHub
- Payroll Engine 👉 GitHub

Feedback and contributions are welcome.



Comments and Discussions

 
GeneralMy vote of 5 Pin
Kanasz Robert20-Sep-12 2:21
professionalKanasz Robert20-Sep-12 2:21 
AnswerRe: My vote of 5 Pin
Jani Giannoudis20-Sep-12 2:35
Jani Giannoudis20-Sep-12 2:35 
GeneralExcellent Article! Pin
Your Display Name Here14-May-12 6:44
Your Display Name Here14-May-12 6:44 
AnswerRe: Excellent Article! Pin
Jani Giannoudis14-May-12 9:08
Jani Giannoudis14-May-12 9:08 
GeneralMy vote of 5 Pin
Assil15-Nov-11 8:23
professionalAssil15-Nov-11 8:23 
QuestionWhat about adding an XAML CommandBinding? Pin
ca0v26-Sep-09 17:24
ca0v26-Sep-09 17:24 
GeneralProblem with WindowsFormsHost Pin
agro_jupp29-Jun-09 21:38
agro_jupp29-Jun-09 21:38 
QuestionRe: Problem with WindowsFormsHost Pin
Jani Giannoudis30-Jun-09 18:44
Jani Giannoudis30-Jun-09 18:44 
AnswerRe: Problem with WindowsFormsHost Pin
agro_jupp30-Jun-09 23:44
agro_jupp30-Jun-09 23:44 
AnswerRe: Problem with WindowsFormsHost Pin
Jani Giannoudis1-Jul-09 19:46
Jani Giannoudis1-Jul-09 19:46 
Hi Jupp

Do you receive the event after calling System.Windows.Input.CommandManager.InvalidateRequerySuggested()?

Cheers,
Jani [My Articles]

GeneralRe: Problem with WindowsFormsHost Pin
agro_jupp1-Jul-09 20:49
agro_jupp1-Jul-09 20:49 
GeneralError 3 Metadata file 'CSharp3rdPCodes=-\WPF\WpfCmdPattern\Commands\bin\Debug\Itenso.Solutions.Community.Commands.dll' could not be found C:\-=CSharp3rdPCodes=-\WPF\WpfCmdPattern\CommandDemo\CSC Pin
Sazuke-kun19-May-09 13:58
Sazuke-kun19-May-09 13:58 
AnswerRe: Error 3 Metadata file 'CSharp3rdPCodes=-\WPF\WpfCmdPattern\Commands\bin\Debug\Itenso.Solutions.Community.Commands.dll' could not be found C:\-=CSharp3rdPCodes=-\WPF\WpfCmdPattern\CommandDemo\CSC Pin
Jani Giannoudis19-May-09 20:46
Jani Giannoudis19-May-09 20:46 
QuestionVery Good Work Pin
agro_jupp2-Apr-09 3:29
agro_jupp2-Apr-09 3:29 
AnswerRe: Very Good Work Pin
Jani Giannoudis3-Apr-09 0:38
Jani Giannoudis3-Apr-09 0:38 
GeneralThanks for the code Pin
Ellis Whitehead5-Jul-08 4:49
Ellis Whitehead5-Jul-08 4:49 
AnswerRe: Thanks for the code Pin
Jani Giannoudis5-Jul-08 12:18
Jani Giannoudis5-Jul-08 12:18 
GeneralRe: Thanks for the code Pin
Ellis Whitehead7-Jul-08 4:55
Ellis Whitehead7-Jul-08 4:55 
AnswerRe: Thanks for the code Pin
Jani Giannoudis7-Jul-08 5:44
Jani Giannoudis7-Jul-08 5:44 
GeneralThe Lib style code Pin
Wei Xu30-May-08 10:35
Wei Xu30-May-08 10:35 
AnswerRe: The Lib style code Pin
Jani Giannoudis30-May-08 19:20
Jani Giannoudis30-May-08 19:20 

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.