Up next in my series on ways you’ve probably used design patterns in real-life and may not have even known it, the Observer and Command design patterns. This continues on from my post Composite and Memento Design Patterns – A Real World Example. Command pattern is meant to actually decouple the GUI and back-end code. It may seem like using an event and executing a command are the same things, and as we are using them here they are pretty close.

XAML for this post

<Window x:Class="CompositeMementoSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" MinWidth="150" ItemsSource="{Binding CompositeList}" />
        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <Button Click="AddFiles">Add Files</Button>
            <Button Command="{Binding SaveXmlCommand}">Save XML</Button>
            <Button Command="{Binding LoadXmlCommand}">Load XML</Button>
        </StackPanel>
    </Grid>
</Window>

Looking at the button where we add files you can see clearly the “Click” event. Again, what that does is register our code-behind as a listener such that when the click event is fired on the button the event is called. But how do we generate unit tests for this? Well, let’s look at the event.

Click event for AddFiles in the code-behind

private void AddFiles(object sender, RoutedEventArgs e)
{
	FileGroupViewModel vm = (FileGroupViewModel)DataContext;
	OpenFileDialog ofd = new OpenFileDialog();
	ofd.Filter = vm.Model.OpenFileDialogFilter;
	ofd.Multiselect = true;

	if (ofd.ShowDialog() != true)
		return;

	vm.AddFiles(ofd.FileNames);
}

We can see here that the AddFiles exists in the view model and provides a way for us to get from the view to the view model.  In standard GUI code-behind we create the button and add a “Click” event. This is part of the observer design pattern, where the code-behind is registered as a listener and gets notified any time the button is clicked.  Observer pattern is kind of like, “Tell me what to call (aka notify) when I’ve been clicked and I’ll do it.”

Okay, cool, but, well, this isn’t very MVVMy. MVVM likes to bind everything to properties. If you look at the “Save XML” button and the “Load XML” button, that is exactly what we are doing.  Command pattern works by saying, “hey, you, command there, when I do my click, you execute whatever it is you are supposed to do.”

Notice that this is different from observer.  I know the differences may seem pretty minor, and they really are, but they are also important. In the code-behind above there is still a dependency between the view and the view model. There still has to be this AddFiles event that calls the view models AddFiles. That means there is code that we can’t test against without instantiating the view. In the example here the code bit is fairly small but it still is code that isn’t being tested in a unit test. In command, because the command is looking for the action to happen rather then waiting to be told that it has happened the dependencies are opposite.

Now we have to get a bit off track here as we look at the SaveXmlCommand and the LoadXmlCommand.  We don’t want to create a SaveFileDialog and an OpenFileDialog in our view model. The view model is not a view, so we shouldn’t be creating elements that affect the UI there. So I have two separate classes that implement the below ISaveDialog and IOpenDialog interfaces that are a part of my views.  Then on my constructor I take the services that implement the interface and use them when the commands are called.

public interface ISaveDialog
{
	string Filter { get; set; }
	string SaveFileName { get; set; }
	bool? ShowSaveFileDialog();
}

public interface IOpenDialog
{
	string Filter { get; set; }
	string OpenFileName { get; set; }
	bool? ShowOpenFileDialog();
}

And finally we get to the guts of my FileGroupViewModel

IOpenDialog OpenDialogService;
ISaveDialog SaveDialogService;

public FileGroupViewModel(IOpenDialog OpenDialogService, ISaveDialog SaveDialogService)
{
	this.Model = new FileGroupModel();
	this.OpenDialogService = OpenDialogService;
	this.SaveDialogService = SaveDialogService;

	OpenDialogService.Filter = model.OpenFileDialogFilter;
	SaveDialogService.Filter = model.OpenFileDialogFilter;

	saveXmlCommand = new DelegateCommand(SaveXml, CanSaveXml);
	loadXmlCommand = new DelegateCommand(LoadXml);
}

public void AddFiles(string[] FileNames)
{
	foreach (string path in FileNames)
	{
		if (path.EndsWith(Model.SerializedExtension))
		{
			try
			{
				FileGroupModel fgm = FileGroupModel.ReadFromFile(path);
				if (fgm != null)
				{
					if (string.IsNullOrEmpty(fgm.Name))
					{
						fgm.Name = System.IO.Path.GetFileNameWithoutExtension(path) + " (" + path + ")";
					}
					Model.Groups.Add(fgm);
					continue;
				}
			}
			//if we get an exception assume it's not a FileGroupModel and add as a regular file
			catch { }
		}
		Model.Files.Add(new System.IO.FileInfo(path));
	}
}

DelegateCommand saveXmlCommand;
public ICommand SaveXmlCommand
{
	get { return saveXmlCommand; }
}

bool CanSaveXml()
{
	return Model.CompositeList.Count > 0;
}

void SaveXml()
{
	if (SaveDialogService.ShowSaveFileDialog() != true)
		return;

	Model.WriteToFile(SaveDialogService.SaveFileName);
}

DelegateCommand loadXmlCommand;
public ICommand LoadXmlCommand
{
	get { return loadXmlCommand; }
}

void LoadXml()
{
	if (OpenDialogService.ShowOpenFileDialog() != true)
		return;

	FileGroupModel newModel = FileGroupModel.ReadFromFile(OpenDialogService.OpenFileName);
	if (newModel == null)
		return;

	Model = newModel;
}

What we can do here is that when creating our unit tests we can fake an ISaveDialog and an IOpenDialog that could return true or false on their respective show methods depending on what we are testing.

Now, I have to wade a bit further into the weeds here. This isn’t really the way I would implement this in a production application. One of the problems with MVVM is that views and view models tend to be classes unto themselves that don’t deal with anything else, just themselves. Models will get referenced all over but not necessarily views and view models.

A lot of blogs, like mine, tend to sometimes do things a bit off in the interest of brevity. A lot of what is here in this post, if you’ve read my past series on MVVM, you’ve already seen. The purpose here was that I wanted to talk about the differences between Command and Observer design patterns. The differences are important and I hope I’ve gotten across to you why it is best to use Command in an MVVM development environment.

How to do it more right

So how would I implement something like this in a production application? Think about the Cut command (of Cut/Paste fame). Anywhere in a UI I should be able to add a cut command. I want to be able to have it in my Edit menu, I want to be able to have it in my context menu, I want to be able to put it as a button on my toolbar and finally I want to bind a key command to it. In standard code-behind each button click would have to implement some click event in which it has to then figure out what to do. There is a huge potential for a lot of duplication of code as well as a lot of code that may not be subjected to a unit test. The solution in MVVM is to use a library like Unity where you can register global commands to handle this exact situation. Unity is outside of the scope of this post but I encourage you to find out more about it. I may even do a series on unity.

I know at first MVVM seems like a massive headache (and it can be). It seems like there is so much extra crap you have to add on. But if you can hang on long enough to get through the crap I think you’ll find that you have better, sounder, more stable applications.

Thanks,
Brian

Image Credits:
Observer
Command

One thought on “Observer and Command Design Patterns – A Real World Example

Leave a Reply

Your email address will not be published. Required fields are marked *

FormatException

928 East Plymouth Drive Asbury Park, NJ 07712