Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

Going from Model-View-Presenter to MV-VM with WPF Commands (Part #2: Commands with MVP)

by Ioannis Panagopoulos

In my previous post, I have presented the first two approaches for implementing the GUI and the logic of a toy application:

 

  • Approach #1: All the logic in the code behind file.
  • Approach #2: The Model-View-Presenter pattern.

 

In this post, we will pick up from where we stopped, in our route to migrate to the benefits of the MVVM model and the use of Commands. It is recommended to take a look at the previous post in order to get an idea of the application and the two previous approaches)

 

Approach #3: A hybrid MVP that uses Routed Commands (Seeing the first benefits)

In this approach we will keep the MVP pattern but we will try to substitute the MenuItems' event handlers with Routed Commands. For a Routed Command to work, we need a place to define it, a control that fires the command and a control that will implement the logic that needs to be performed. They are called Routed because in the visual tree, the command will bubble up from the control that fired it to the control that carries the binding that implement its logic.

In our small scale application, we will need 3 Commands which are defined in a separate Command.cs file as follows:

public class ClientCommands
{
    private static RoutedUICommand addClient;
    private static RoutedUICommand resetClient;
    private static RoutedUICommand deleteClient;

    static ClientCommands()
    {
        InputGestureCollection inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.N, ModifierKeys.Control, "Ctrl+N"));
        addClient = new RoutedUICommand("Add Client", "AddClient", typeof(ClientCommands), inputs);

        inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
        resetClient = new RoutedUICommand("Reset Client", "ResetClient", typeof(ClientCommands), inputs);

        inputs = new InputGestureCollection();
        inputs.Add(new KeyGesture(Key.D, ModifierKeys.Control, "Ctrl+D"));
        deleteClient = new RoutedUICommand("Delete Client", "DeleteClient", typeof(ClientCommands), inputs);
    }

    public static RoutedUICommand AddClient
    {
        get { return addClient; }
    }
    public static RoutedUICommand DeleteClient
    {
        get { return deleteClient; }
    }
    public static RoutedUICommand ResetClient
    {
        get { return resetClient; }
    }
}

Note that three commands are defined for the Add Client, Delete Client and Reset Client actions of the MenuItems. The Commands are bound to specific key shortcuts (this is global - a nice benefit out of the box with Commands) and in their constructors provide the text that will appear in the Buttons or MenuItems that will fire them. For example, the MenuItem that will fire the Command AddClient and all other future Buttons that will fire the same Command, will display with no extra effort the text "Add Client" (a second benefit with Commands where you have a centralized way of defining the language of the text of the controls- great for localization).

Having defined the commands we need to alter the xaml file for using them in the View. This is done as follows. First we define that the Commands are fired from the MenuItems (in the place where the event handler definitions used to be):

 

<MenuItem Header="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}" 
     Command="local:ClientCommands.AddClient" />
<MenuItem Header="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}" 
     Command="local:ClientCommands.DeleteClient"/>
<MenuItem Header="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}" 
     Command="local:ClientCommands.ResetClient"/>
 

Do not forget to reference the namespace the ClientCommands class resides at the beginning of the xaml file as "local". Then, we need to specify the place Routed Commands are caught to be handled. We choose that place to be the Window itself. Therefore the command bindings are defined in the Window as follows:

 

<Window.CommandBindings>
    <CommandBinding Command="local:ClientCommands.AddClient" 
	Executed="AddClientCommand_Executed" CanExecute="AddClientCommand_CanExecute"/>
    <CommandBinding Command="local:ClientCommands.DeleteClient" 
	Executed="DeleteClientCommand_Executed" CanExecute="DeleteClientCommand_CanExecute"/>
    <CommandBinding Command="local:ClientCommands.ResetClient" 
	Executed="ResetClientCommand_Executed"  CanExecute="ResetClientCommand_CanExecute"/>
</Window.CommandBindings>

 

For the Commands, we define two handlers. The one is the Executed method which holds the implementation of the logic of each command. The other is the CanExecute method which is called by the GUI in order to determine whether the control which fires the command should be enabled or not. Note that implementing this method will provide a disable/enable mechansim to all the Controls the fire the command with no additional effort.

The logic in the code-behind file, is similar to the previous case with the difference that now instead of event handles we have Routed Event handlers:

    public partial class Window3 : Window,IView
    {
        IPresenter myPresenter = new Presenter();
        public Window3()
        {
            myPresenter.SetView(this);
            InitializeComponent();
            myPresenter.InitGUI();
        }
        private void ListBoxClients_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            myPresenter.SetSelectedClient(ListBoxClients.SelectedItem as Client);
        }

        #region IView Members

        public void ToggleMenuItems(bool Enable)
        {
            this.MenuItemEditClient.IsEnabled =Enable;
            this.MenuItemDeleteClient.IsEnabled = Enable;
        }
        public void InitializeList(ObservableCollection<Client> Clients)
        {
            this.ListBoxClients.ItemsSource = Clients;
        }

        #endregion

        private void AddClientCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myPresenter.AddClient();
        }
        private void AddClientCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }
        private void DeleteClientCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myPresenter.DeleteClient();
        }
        private void DeleteClientCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = myPresenter.EditAddCanExecute();
        }
        private void ResetClientCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myPresenter.ResetClient();
        }
        private void ResetClientCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = myPresenter.EditAddCanExecute();
        }
    }

 We need to add in the Presenter class the method EdtAddCanExecute which determines when the commands need to be fired. Let's summarize what we have achieved s far:

 

  • We have enabled easy testing for our application with the MVP pattern (previous post).
  • We have enabled easy change of the View or the logic of the Presenter through the use of interfaces.
  • We have centralized the key shortcuts and the dispayed text of the commands in our application.
  • We have provided a specific method that defines for all controls that fire the command, whether they should be enabled or not

 

But we still have some problems. First there is still a lot of wiring up that needs to be done in the code behind file. Second, commands are implemented in the code-behind file (although they divert the execution to the presenter). Consider that it is problematic to implement the CanExecute wiring in every Window that fires the command.

In the next and final post on this series we will fully implement the MVVM model which also solves the aforementioned issues.

kick it on DotNetKicks.com Shout it
blog comments powered by Disqus
hire me