Putting Silverlight Commands to Use

In the current release of Silverlight, we have the ICommand interface, which defines a command object, just like in WPF.  However, unlike WPF, it seems that Silverlight does not have any built-in support for actually using ICommand objects just yet.  This blog post shows how we can work around this shortcoming until the platform has proper support for command sources (such as the ICommandSource interface in WPF).  My implementation allows you to execute commands when any ButtonBase-derived control is clicked, pass the command a parameter, and honor the can-execute status of the command (when CanExecute returns false, the button automatically becomes disabled).

In case you are wondering why I bothered implementing this, bear in mind that I’m a huge fan of the Model-View-ViewModel (MVVM) pattern.  One of the fundamental principles in MVVM is that the View consumes functionality of the ViewModel by executing commands exposed by the ViewModel.  In order to execute a command, you need to have UI controls that are able to do so, such as when a Button executes a command upon being clicked.

I created a static class called ButtonBaseExtensions, which exposes two attached properties.  These properties can only be set on ButtonBase subclasses.  The Command attached property is of type ICommand, and it allows you to specify which command should execute when the button is clicked.  The CommandParameter attached property is of type Object, and is used to pass an optional parameter value to the CanExecute and Execute methods of the command.  You can see these two properties in use here:

<Button
  Content="Capitalize"
  cmd:ButtonBaseExtensions.Command="{Binding CapitalizeCommand}"
  />

<Button
  Content="Remove One Letter"
  Margin="6,0"
  cmd:ButtonBaseExtensions.Command="{Binding RemoveSubstringCommand}"
  >
  <cmd:ButtonBaseExtensions.CommandParameter>
    <sys:Int32>1</sys:Int32>
  </cmd:ButtonBaseExtensions.CommandParameter>
</Button>

<HyperlinkButton
  Content="Remove Two Letters"
  cmd:ButtonBaseExtensions.Command="{Binding RemoveSubstringCommand}"
  >
  <cmd:ButtonBaseExtensions.CommandParameter>
    <sys:Int32>2</sys:Int32>
  </cmd:ButtonBaseExtensions.CommandParameter>
</HyperlinkButton>

The main point of interest in the demo app is in the ButtonBaseExtensions class.  Here is most of the code involved with the Command attached property.  Note, all references to ICommands and ButtonBase objects are stored in WeakReferences, so this will not create a memory leak.

static readonly CommandToButtonsMap s_commandToButtonsMap = new CommandToButtonsMap();

public static ICommand GetCommand(ButtonBase btn)
{
    return (ICommand)btn.GetValue(CommandProperty);
}

public static void SetCommand(ButtonBase btn, ICommand value)
{
    btn.SetValue(CommandProperty, value);
}

public static readonly DependencyProperty CommandProperty =
    DependencyProperty.RegisterAttached(
    "Command",
    typeof(ICommand),
    typeof(ButtonBaseExtensions),
    new PropertyMetadata(null, OnCommandChanged));

static void OnCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    ButtonBase btn = obj as ButtonBase;
    if (btn == null)
        throw new ArgumentException("...");

    ICommand oldCommand = e.OldValue as ICommand;
    if (oldCommand != null)
    {
        s_commandToButtonsMap.RemoveButtonFromMap(btn, oldCommand);
        oldCommand.CanExecuteChanged -= OnCommandCanExecuteChanged;
        btn.Click -= OnButtonBaseClick;
    }

    ICommand newCommand = e.NewValue as ICommand;
    if (newCommand != null)
    {
        s_commandToButtonsMap.AddButtonToMap(btn, newCommand);
        newCommand.CanExecuteChanged += OnCommandCanExecuteChanged;
        btn.Click += OnButtonBaseClick;
    }
}

static void OnCommandCanExecuteChanged(object sender, EventArgs e)
{
    ICommand cmd = sender as ICommand;
    if (cmd != null && s_commandToButtonsMap.ContainsCommand(cmd))
        s_commandToButtonsMap.ForEachButton(cmd, btn =>
        {
            object parameter = ButtonBaseExtensions.GetCommandParameter(btn);
            btn.IsEnabled = cmd.CanExecute(parameter);
        });
}

static void OnButtonBaseClick(object sender, RoutedEventArgs e)
{
    ButtonBase btn = sender as ButtonBase;
    ICommand cmd = ButtonBaseExtensions.GetCommand(btn);
    object parameter = ButtonBaseExtensions.GetCommandParameter(btn);
    if (cmd != null && cmd.CanExecute(parameter))
        cmd.Execute(parameter);
}

Download the demo app here.  It was created against Silverlight 2, using Visual Studio 2008 SP1.


Comments  (1 )

inturbidus
on Thu, Sep 30 2010 5:15 PM

Thank you so much for posting this.  Although we moved to Silverlight 4, which has commanding for Button's, most controls still don't support commanding.

So we used your code, changed ButtonBase to our Custom Control, and routed the ButtonClick (in our case) to the Command instead of Click, and blam it worked!  Much appreciated!

Add a Comment

Please Login or Register to add a comment.