Share via


Exercise 2: Commands and the ViewModel

One of the goals of a ViewModel is to minimize the amount of code in the view, in order to facilitate unit testing. The first part of this lab illustrated how data binding makes it possible for a ViewModel to update properties of elements in the view. But what about when we need things to flow in the other direction? How does the ViewModel discover user activity such as button clicks?

One solution is to have ordinary event handlers in the view’s code behind, and have those call into methods on the ViewModel. For example, the ViewModel could define an OnRegisterButtonClicked method to be called by the view. While this doesn’t quite meet the ideal of avoiding all code in the view, that’s not a huge problem, because the only code required in the view is a single method call through to the ViewModel. However, Silverlight 4 introduces a feature that was previously only available on the desktop with WPF, which can help us do better: commands.

Commands are an abstraction representing an operation that can be invoked through some user action such as a button click. You can data bind a Button’s Command property to a command object exposed by a ViewModel, which avoids the need for a Click handler in the code behind. The button is able to invoke the command directly. But it adds an additional benefit: the command abstraction (represented by the ICommand interface) also supports the idea that a command may be enabled or disabled from time to time. This means the ViewModel also has a way of controlling whether the button associated with a command is enabled or disabled. (Of course, we could have done that by offering a bool property on the ViewModel and binding a button’s IsEnabled property to that. But commands let us kill two birds with one stone: by binding just the one property: Button.Command, we can have the ViewModel both handle button clicks and set the enabled state of the button.

In this exercise, we’ll use this technique for the Register and Unregister buttons you added in the previous part.

Commanding

  1. Add a new class to the SlEventManager project called RelayCommand, with the following implementation:
    Note:
    While Silverlight defines the ICommand interface, it doesn’t provide any implementations. So the first thing we need to do is provide our own. The most flexible approach is to write one class that simply relays the command invocation onto a delegate. That way we can use one command implementation to implement any number of commands.

    C#

    public class RelayCommand : ICommand { private Action _handler; public RelayCommand(Action handler) { _handler = handler; } private bool _isEnabled; public bool IsEnabled { get { return _isEnabled; } set { if (value != _isEnabled) { _isEnabled = value; if (CanExecuteChanged != null) { CanExecuteChanged(this, EventArgs.Empty); } } } } public bool CanExecute(object parameter) { return IsEnabled; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { _handler(); } }

    Visual Basic

    Public Class RelayCommand Implements ICommand Private _handler As Action Public Sub New(ByVal handler As Action) _handler = handler End Sub Private _isEnabled As Boolean Public Property IsEnabled() As Boolean Get Return _isEnabled End Get Set(ByVal value As Boolean) If value <> _isEnabled Then _isEnabled = value RaiseEvent CanExecuteChanged(Me, EventArgs.Empty) End If End Set End Property Public Function CanExecute(ByVal parameter As Object) As Boolean Implements System.Windows.Input.ICommand.CanExecute Return IsEnabled End Function Public Event CanExecuteChanged As EventHandler Implements System.Windows.Input.ICommand.CanExecuteChanged Public Sub Execute(ByVal parameter As Object) Implements System.Windows.Input.ICommand.Execute _handler() End Sub End Class
    Note:
    There are other common implementations of this general idea you can find on the Internet that are also called RelayCommand. The variations are in details such as whether they use the command parameter. This example doesn’t need it a command parameter, so we just ignore it. Another variation is whether the CanExecute status is handled with a property or a callback. But the basic principle is the same.
  2. Add two methods OnRegister and OnUnregister to the HomeViewModel.cs(C#) or HomeViewModel.vb(VB) ViewModel class. These will be used as handlers for the commands:

    C#

    private void OnRegister() { } private void OnUnregister() { }

    Visual Basic

    Private Sub OnRegister() End Sub Private Sub OnUnregister() End Sub
  3. Add the following property and field definitions to the HomeViewModel.cs(C#) or HomeViewModel.vb(VB) ViewModel class:

    C#

    private readonly RelayCommand _registerCommand; public ICommand RegisterCommand { get { return _registerCommand; } } private readonly RelayCommand _unregisterCommand; public ICommand UnregisterCommand { get { return _unregisterCommand; } }

    Visual Basic

    Private ReadOnly _registerCommand As RelayCommand Public ReadOnly Property RegisterCommand() As ICommand Get Return _registerCommand End Get End Property Private ReadOnly _unregisterCommand As RelayCommand Public ReadOnly Property UnregisterCommand() As ICommand Get Return _unregisterCommand End Get End Property
    Note:
    While these properties will be used for data binding, they never change, so there’s no need to for set accessors nor any need to raise change notifications.
  4. In the constructor, add the following code to initialize these command fields with RelayCommand objects referring to the two handlers. Put these at the start of the constructor before the call to UpdateUserForRole.

    C#

    _registerCommand = new RelayCommand(OnRegister); _unregisterCommand = new RelayCommand(OnUnregister);

    Visual Basic

    _registerCommand = New RelayCommand(AddressOf OnRegister) _unregisterCommand = New RelayCommand(AddressOf OnUnregister)
  5. Add a property to track the current selection.
    Note:
    We need to write the logic that will determine whether these commands should be enabled. This requires two things: we need to know events for which the user is already registered, and we need to know which event has currently been selected in the grid.

    C#

    private Event _selectedEvent; public Event SelectedEvent { get { return _selectedEvent; } set { _selectedEvent = value; UpdateRegistrationButtons(); } } private void UpdateRegistrationButtons() { }

    Visual Basic

    Private _selectedEvent As [Event] Public Property SelectedEvent() As [Event] Get Return _selectedEvent End Get Set(ByVal value As [Event]) _selectedEvent = value UpdateRegistrationButtons() End Set End Property Private Sub UpdateRegistrationButtons() End Sub
  6. Add the following using directive:

    C#

    using SlEventManager.Web;

    Visual Basic

    Imports SlEventManager.Web

Add Authentication Based Custom Domain Service Methods

  1. Add the following using directive in the EventManagerDomainService class in the SlEventManager.Web project:

    C#

    using System.Web.Security;

    Visual Basic

    Imports System.Web.Security
  2. Add this method, which will retrieve the IDs of the events for which the current user is registered.
    Note:
    This uses the ASP.NET Membership class to discover the current user, so there’s no need for any parameters.

    C#

    [Invoke] public IEnumerable<int> FetchEventsForWhichCurrentUserIsRegistered() { MembershipUser mu = Membership.GetUser(); if (mu == null) { return new int[0]; } var q = from attendeeEvent in this.ObjectContext.AttendeeEvents where attendeeEvent.Attendee.AspNetUserId == (Guid) mu.ProviderUserKey select attendeeEvent.EventID; return q; }

    Visual Basic

    <Invoke()> Public Function FetchEventsForWhichCurrentUserIsRegistered() As IEnumerable(Of Integer) Dim mu As MembershipUser = Membership.GetUser() If mu Is Nothing Then Return New Integer(){} End If Dim q = From attendeeEvent In Me.ObjectContext.AttendeeEvents Where attendeeEvent.Attendee.AspNetUserId = CType(mu.ProviderUserKey, Guid) Select attendeeEvent.EventID Return q End Function
    Note:
    The [Invoke] (C#) or <Invoke()>(VB)attribute tells WCF RIA Services that this method does not attempt to return any entities. By default, when a domain service method returns an IEnumerable<T>, RIA Services presumes that the method intends to act as a query over some domain entities. That would cause it to report an error here, because this method enumerates ints, which are not valid entities. Declaring that this is simply an invocation-style operation avoids the error.
  3. A the following methods to handle registering and unregistering the current user for events:

    C#

    [Invoke] public void RegisterCurrentUserForEvent(int eventId) { Attendee attendee = GetOrCreateAttendeeForCurrentUser(); if (!attendee.AttendeeEvents.Any(ev => ev.EventID == eventId)) { attendee.AttendeeEvents.Add(new AttendeeEvent { EventID = eventId }); } this.ObjectContext.SaveChanges(); } [Invoke] public void UnregisterCurrentUserForEvent(int eventId) { Attendee attendee = GetOrCreateAttendeeForCurrentUser(); AttendeeEvent av = attendee.AttendeeEvents.SingleOrDefault( x => x.EventID == eventId); if (av != null) { attendee.AttendeeEvents.Remove(av); this.ObjectContext.AttendeeEvents.DeleteObject(av); } this.ObjectContext.SaveChanges(); } private Attendee GetOrCreateAttendeeForCurrentUser() { MembershipUser mu = Membership.GetUser(); if (mu == null) { throw new InvalidOperationException("User not logged in"); } Attendee at = this.ObjectContext.Attendees.FirstOrDefault( x => x.AspNetUserId == (Guid) mu.ProviderUserKey); if (at == null) { at = new Attendee { AspNetUserId = (Guid) mu.ProviderUserKey }; this.ObjectContext.AddToAttendees(at); } return at; }

    Visual Basic

    <Invoke()> Public Sub RegisterCurrentUserForEvent(ByVal eventId As Integer) Dim attendee As Attendee = GetOrCreateAttendeeForCurrentUser() If Not attendee.AttendeeEvents.Any(Function(ev) ev.EventID = eventId) Then attendee.AttendeeEvents.Add(New AttendeeEvent With {.EventID = eventId}) End If Me.ObjectContext.SaveChanges() End Sub <Invoke()> Public Sub UnregisterCurrentUserForEvent(ByVal eventId As Integer) Dim attendee As Attendee = GetOrCreateAttendeeForCurrentUser() Dim av As AttendeeEvent = attendee.AttendeeEvents.SingleOrDefault(Function(x) x.EventID = eventId) If av IsNot Nothing Then attendee.AttendeeEvents.Remove(av) Me.ObjectContext.AttendeeEvents.DeleteObject(av) End If Me.ObjectContext.SaveChanges() End Sub Private Function GetOrCreateAttendeeForCurrentUser() As Attendee Dim mu As MembershipUser = Membership.GetUser() If mu Is Nothing Then Throw New InvalidOperationException("User not logged in") End If Dim at As Attendee = Me.ObjectContext.Attendees.FirstOrDefault(Function(x) x.AspNetUserId = CType(mu.ProviderUserKey, Guid)) If at Is Nothing Then at = New Attendee With {.AspNetUserId = CType(mu.ProviderUserKey, Guid)} Me.ObjectContext.AddToAttendees(at) End If Return at End Function

Implement the Bindings

  1. Add the following using declarations to HomeViewModel.cs(C#) or HomeViewModel.vb(VB) so the ViewModel can use these methods and remember which events the current user belongs to.

    C#

    using System.Collections.Generic; using SlEventManager.Web.Services;

    Visual Basic

    Imports System.Collections.Generic Imports SlEventManager.Web.Services
  2. Add the following field to the ViewModel:

    C#

    private HashSet<int> _currentUserRegisteredEventIds;

    Visual Basic

    Private _currentUserRegisteredEventIds As HashSet(Of Integer)
  3. Implement the UpdateRegistrationButton method that we added earlier.
    Note:
    This updates the button command status based on the current set of event ids:

    C#

    private void UpdateRegistrationButtons() { _registerCommand.IsEnabled = _currentUserRegisteredEventIds != null && SelectedEvent != null && !_currentUserRegisteredEventIds.Contains(SelectedEvent.EventID); _unregisterCommand.IsEnabled = _currentUserRegisteredEventIds != null && SelectedEvent != null && _currentUserRegisteredEventIds.Contains(SelectedEvent.EventID); }

    Visual Basic

    Private Sub UpdateRegistrationButtons() _registerCommand.IsEnabled = _currentUserRegisteredEventIds IsNot Nothing AndAlso SelectedEvent IsNot Nothing AndAlso Not _currentUserRegisteredEventIds.Contains(SelectedEvent.EventID) _unregisterCommand.IsEnabled = _currentUserRegisteredEventIds IsNot Nothing AndAlso SelectedEvent IsNot Nothing AndAlso _currentUserRegisteredEventIds.Contains(SelectedEvent.EventID) End Sub
  4. Add the following code at the end of the UpdateForUserRole method to populate the set of registered events:

    C#

    if (isLoggedIn) { var ctx = new EventManagerDomainContext(); ctx.FetchEventsForWhichCurrentUserIsRegistered((op) => { if (!op.HasError) { var items = op.Value; _currentUserRegisteredEventIds = new HashSet<int>(items); UpdateRegistrationButtons(); } }, null); } else { _currentUserRegisteredEventIds = null; UpdateRegistrationButtons(); }

    Visual Basic

    If isLoggedIn Then Dim ctx = New EventManagerDomainContext() ctx.FetchEventsForWhichCurrentUserIsRegistered(Sub(op) If Not op.HasError Then Dim items = op.Value _currentUserRegisteredEventIds = New HashSet(Of Integer)(items) UpdateRegistrationButtons() End If End Sub, Nothing) Else _currentUserRegisteredEventIds = Nothing UpdateRegistrationButtons() End If
  5. Implement the button command handlers we previously added empty methods for:

    C#

    private void OnRegister() { if (SelectedEvent != null) { var ctx = new EventManagerDomainContext(); ctx.RegisterCurrentUserForEvent(SelectedEvent.EventID, (op) => { UpdateForUserRole(); }, null); } } private void OnUnregister() { if (SelectedEvent != null) { var ctx = new EventManagerDomainContext(); ctx.UnregisterCurrentUserForEvent(SelectedEvent.EventID, (op) => { UpdateForUserRole(); }, null); } }

    Visual Basic

    Private Sub OnRegister() If SelectedEvent IsNot Nothing Then Dim ctx = New EventManagerDomainContext() ctx.RegisterCurrentUserForEvent(SelectedEvent.EventID, Sub(op) UpdateForUserRole(), Nothing) End If End Sub Private Sub OnUnregister() If SelectedEvent IsNot Nothing Then Dim ctx = New EventManagerDomainContext() ctx.UnregisterCurrentUserForEvent(SelectedEvent.EventID, Sub(op) UpdateForUserRole(), Nothing) End If End Sub
  6. Wire the view up to the commands. By binding the Command properties of the two buttons to the command objects exposed by the ViewModel:

    XAML

    <Button x:Name="registerForEventButton" Content="Register" Command="{Binding Path=RegisterCommand}" /> <Button x:Name="unregisterForEventButton" Content="Unregister" Command="{Binding Path=UnregisterCommand}" />
  7. Bind the SelectedItem property of the data grid by adding the following attribute to the data grid:

    XAML

    SelectedItem="{Binding Path=SelectedEvent, Mode=TwoWay}"
  8. Run the application.
  9. Log in as a non-admin user (e.g., ian, P@ssw0rd).
    Note:
    In the example database, the ian user is registered for the third event, so when you select that event the Register button should become disabled and the Unregister button should be enabled. For all other events, it should be the other way around. You should be able to us the buttons to register and unregister the user for events in the list.