24 Sep 2009
In the first post of the series we looked at using Binding expressions to display data from the ViewModel on our screen. The other major part of our interaction is having the view invoke functionality in the ViewModel. In the MVVM pattern this is done using the Command pattern, specifically the ICommand interface.
A Command object encapsulates an action the user can take and is exposed by the ViewModel. Unfortunately Silverlight unlike WPF doesn't have any built in support for Commands beyond the actual interface itself. All the major Silverlight frameworks such as Prism,Caliburn etc has their own variation on how they should be implemented (usually through Attached Behaviors).
Given that we want to keep our MVVM structure "Blendable" we'll be using the Behaviors that come with Blend. What'll we be doing is creating an ExecuteCommandAction that we can trigger using any Blend trigger, this action will be bound to a ICommand exposed by the ViewModel.
In the current version of Silverlight standard behaviors don't support Binding, there is a work around I discussed earlier on this blog by PeteBlois using BindingListeners and exposing Bindings rather than ICommand. You can view more on this at his blog. The code for our new action looks like this, it shouldn't have to be this complicated but thats another rant.
public class ExecuteCommandAction : TriggerAction<FrameworkElement>
{
private readonly BindingListener commandListener;
private readonly BindingListener commandParameterListener;
public ExecuteCommandAction()
{
commandListener = new BindingListener();
commandParameterListener = new BindingListener();
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(Binding), typeof(ExecuteCommandAction), new PropertyMetadata(null, OnCommandChanged));
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(Binding), typeof(ExecuteCommandAction), new PropertyMetadata(null, OnCommandParameterChanged));
public Binding Command
{
get
{
return (Binding)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
public Binding CommandParameter
{
get
{
return (Binding)GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ExecuteCommandAction)d).OnCommandBindingChanged(e);
}
private static void OnCommandParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ExecuteCommandAction)d).OnCommandParameterBindingChanged(e);
}
private void OnCommandBindingChanged(DependencyPropertyChangedEventArgs e)
{
commandListener.Binding = (Binding)e.NewValue;
}
private void OnCommandParameterBindingChanged(DependencyPropertyChangedEventArgs e)
{
commandParameterListener.Binding = (Binding)e.NewValue;
}
protected override void OnAttached()
{
base.OnAttached();
commandListener.Element = AssociatedObject;
commandParameterListener.Element = AssociatedObject;
}
protected override void OnDetaching()
{
base.OnDetaching();
commandListener.Element = null;
commandParameterListener.Element = null;
}
protected override void Invoke(object parameter)
{
var command = commandListener.Value as ICommand;
if(command == null)
return;
var commandParameter = commandParameterListener.Value;
if(command.CanExecute(commandParameter))
command.Execute(commandParameter);
}
}
The functionality we're going to provide is to list the available cocktails, then once a user selects one we show similar cocktails (similar being defined as sharing two or more ingredients). We've modified the Cocktails service to look like this.
public class CocktailService
{
private readonly List<Cocktail> cocktails = new List<Cocktail>
{
new Cocktail
{
Id = 1,
Name = "Black Russian",
Ingredients = new [] { "Vodka", "Kahlua" }
},
new Cocktail
{
Id = 2,
Name = "White Russian",
Ingredients = new[] { "Vodka", "Cream", "Kahlua" }
},
new Cocktail
{
Id = 3,
Name = "Gin and Tonic",
Ingredients = new[] { "Gin", "Tonic" }
}
};
public IEnumerable<Cocktail> GetCocktails()
{
return cocktails;
}
public IEnumerable<Cocktail> GetCocktailsSimilarTo(Cocktail cocktail)
{
return from c in cocktails
where c.Ingredients.Intersect(cocktail.Ingredients).Count() >= 2 && c.Id != cocktail.Id
select c;
}
}
We also need an implementation of ICommand, I've gone with a very simple DelegateCommand which simply has an executed command invoke a method on the ViewModel.
public class DelegateCommand<T> : ICommand
{
public event EventHandler CanExecuteChanged;
private readonly Action<T> action;
private readonly Func<T, bool> predicate;
public DelegateCommand(Action<T> action)
{
if(action == null)
throw new ArgumentNullException("action");
this.action = action;
predicate = t => true;
}
public DelegateCommand(Action<T> action, Func<T, bool> predicate)
{
if(action == null)
throw new ArgumentNullException("action");
if(predicate == null)
throw new ArgumentNullException("predicate");
this.action = action;
this.predicate = predicate;
}
protected virtual void OnCanExecuteChanged(EventArgs e)
{
var canExecuteChanged = CanExecuteChanged;
if(canExecuteChanged != null)
canExecuteChanged(this, e);
}
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged(EventArgs.Empty);
}
public bool CanExecute(object parameter)
{
return predicate((T)parameter);
}
public void Execute(object parameter)
{
action((T)parameter);
}
}
Now we have all the pieces to build our new ViewModel. As before we expose our ObservableCollection of AvailableCocktails which is populated on construction, we'll now elso expose a similar collection of SimilarCocktails. Now the new stuff, we'll create a private method that takes a cocktail and fills the SimilarCocktails with ones similar to the parameter. We then wrap this method with a DelegateCommand and expose it via an ICommand property.
public class CocktailsViewModel : ViewModelBase<CocktailsViewModel>
{
private readonly CocktailService cocktailService = new CocktailService();
public CocktailsViewModel()
{
AvailableCocktails = new ObservableCollection<Cocktail>();
SimilarCocktails = new ObservableCollection<Cocktail>();
GetSimilarCocktailsCommand = new DelegateCommand<Cocktail>(GetSimilarCocktails);
AvailableCocktails.AddRange(cocktailService.GetCocktails());
}
public ObservableCollection<Cocktail> AvailableCocktails
{
get;
set;
}
public ObservableCollection<Cocktail> SimilarCocktails
{
get; set;
}
public ICommand GetSimilarCocktailsCommand
{
get; private set;
}
protected void GetSimilarCocktails(Cocktail cocktail)
{
SimilarCocktails.Replace(cocktailService.GetCocktailsSimilarTo(cocktail));
}
}
Excellent, our ViewModel is complete, notice we've done all the functionality of the page without worrying about how the UI will look or feel. I'm going to assume that while I'm building this our designer is building a lovely interface for our application (hopefully using some of theSketchFlow and SampleData functionality in Blend). The screencast shows how to hook up the View to the ViewModel using Blend.
If you want to download the source of the project you can find it here, (it does contain some of the code for the next post).