Using a Service Locator to Work with MessageBoxes in an MVVM Application

http://www.codeproject.com/KB/WPF/MessageBoxInMVVM.aspx

 

Why might calling MessageBox.Show() from a ViewModel object not work out so well?  The two most common issues are custom message boxes and unit testing.  I explained the former issue in great detail in my book ‘Advanced MVVM’,

 

。。。。MVVM为何不推荐直接使用MessageBox.Show()。。。。

 

Introduction

This article explains a simple and testable technique for working with message boxes from the ViewModel layer of a WPF or Silverlight application based on the Model-View-ViewModel design pattern.  The demo application was created in Visual Studio 2008 with Service Pack 1.

Background

One of the most common questions I see people asking about MVVM application design is how to show a message box from their ViewModel objects.  At first glance the question might seem absurd.  Just call MessageBox.Show(), right?  In some scenarios, that answer is absolutely correct.  That answer falls flat on its face in other scenarios.

Why might calling MessageBox.Show() from a ViewModel object not work out so well?  The two most common issues are custom message boxes and unit testing.  I explained the former issue in great detail in my book ‘Advanced MVVM’, so I won’t cover that topic in this article.  In case you’re wondering what I mean by a custom message box, the following screenshot from the book’s demo application, BubbleBurst, shows an example:


 
The latter issue, unit testing, is a more common requirement for many developers.  The problem with showing a message box from a ViewModel object is that it might be shown while running your unit tests.  This prevents the tests from completing until someone walks over and closes the message box.  If your unit tests are running on a build server, which is not monitored by anyone or perhaps does not even have a monitor attached, this can be a serious problem.

The problem we face is twofold.  First, how can a ViewModel prompt the user for a decision, such as whether to save their work before the application closes, without jumping through a bunch of hoops?  Second, how can we test those ViewModel objects without causing the test suite to be unable to complete due to an open message box?

Service Locator to the Rescue

The solution to the problem is simple: do not call MessageBox.Show() from a ViewModel object.  Instead, provide the ViewModel with a “message box service” object that invokes MessageBox.Show().  It might not seem like this layer of indirection solves the problem of having message boxes open up while running unit tests, but it can.

The key is that the ViewModel object does not know, or care, about what a message box service does.  When running unit tests you can provide the ViewModel with a fake message box service, that does not call MessageBox.Show(), but simply returns whatever MessageBoxResult value you specified in advance.  The ViewModel object never knows the difference; it just gets a message box service, calls the Show method, and gets a return value to work with.

What I’m describing here is an example of a technique known as Service Locator.  Entire books have been written, frameworks created, wars waged, egos battered, and philosophies based on what Service Locator is and what it is not.  So, I’m not going to bother explaining it in depth here.  Instead I present you with the following image, which I use as the basis of an analogy.


 
In this analogy the arm, and the rest of the person’s body attached to it, is your application.  Each organ in the body, such as the heart and brain, is analogous to a module or subsystem in the application.  For whatever reason, this person’s body needs some radioactive albumin in it.  Organs in the body depend on radioactive albumin, which makes that substance a dependency of the organs.

In order to get radioactive albumin to the body’s organs, we put it into the person’s bloodstream, which spreads it around the body.  The blood can be thought of as locating whatever dependencies the organs have, or as storing them like a container.  Once the blood contains the radioactive albumin, the organs can extract it from the blood as necessary.  In order to inject the radioactive albumin into the bloodstream, we rely on something to load/put/inject it into the container from which it can be located, in this case a medical syringe that introduces the substance.

So far the analogy has held together quite well, but the last part I want to mention is a bit of a stretch.  One of the important concerns with using Service Locator has to do with when the container is filled with service dependencies.  In our medical injection analogy a tourniquet represents the time at which dependencies are placed into the container.  Quite often, they are injected into a container before the application’s modules load and try to locate them.  In this sense, the fact that the injection occurs on application start-up is like a tourniquet that prevents the rest of the body from causing problems during the injection.

Now let’s see how this applies to working with message boxes from ViewModel objects.

Example Scenario

The demo application discussed in this article is available for download at the top of this page.  It contains a very simple application that allows the user to type the name of a person.  If the user types in a valid name (i.e. the first and last name are entered) the Save button becomes enabled so that they can save the data.  Once the data has been saved, the Save button is disabled again until the name is changed.  When the user has entered a valid name but not yet clicked the Save button, if they try to close the window the application shows a message box, asking if they want to save before closing.


 
The Person data model class represents a person with a name.  It keeps track of whether it has been edited, and it can also validate itself.  The following class diagram shows its important members:


 
An instance of the Person class is wrapped by PersonViewModel.  That class is responsible for presenting Person data and allowing it to be saved.  It also implements my IScreen interface, which is used to give the PersonViewModel a chance to ask the user if unsaved changes should be saved before closing.


 
One important thing to take note of is that PersonViewModel derives from ViewModelBase.  That class has two points of interest.  It inherits from the ObservableObject class in my MVVM Foundation library so that it can get support for property change notifications for free.  In this demo app there is no need to send property change notifications, but all self-respecting ViewModel base classes must support this feature because most ViewModel objects require it.  The other thing that ViewModelBase does is expose a GetService method, which we will look at later.

As noted above, PersonViewModel implements the IScreen interface so that it will be asked if it can close.  By “close” I mean that the View displaying it can be removed from the user interface.  When PersonViewModel is asked if it can close, it checks to see if its Person data object can be saved.  If so, it asks the user what to do, as seen below:

Collapse Copy Code
bool IScreen.TryToClose()
{
    if (this.CanSave)
    {
        var msgBox = base.GetService<IMessageBoxService>();
        if (msgBox != null)
        {
            var result = msgBox.Show(
                "Do you want to save your work before leaving?",
                "Unsaved Changes",
                MessageBoxButton.YesNoCancel,
                MessageBoxImage.Question);

            if (result == MessageBoxResult.Cancel)
                return false;

            if (result == MessageBoxResult.Yes)
                this.Save();
        }
    }
    return true;
}

The method seen above uses the GetService method of ViewModelBase in order to get a reference to a message box service.  This is where PersonViewModel’s dependency on IMessageBoxService is located.  The next section of this article explains how the application implements a lightweight service locator to resolve this dependency.

Exploring the Service Container

At this point we have seen how the demo application relies on service location to provide its ViewModel objects with the ability to show a message box.  Now let’s turn our attention to how I implemented support for this. 

In a large production application I would strongly consider using a pre-existing class or framework for my service locating needs.  But for a simple demonstration of a simple application, that would be overkill.  So I wrote my own simple locator.  It weighs in at just under fifty lines of lightning fast code.  The ServiceContainer class is displayed below:

Collapse Copy Code
public class ServiceContainer
{
    public static readonly ServiceContainer Instance = new ServiceContainer();

    private ServiceContainer()
    {
        _serviceMap = new Dictionary<Type, object>();
        _serviceMapLock = new object();
    }

    public void AddService<TServiceContract>(TServiceContract implementation)
        where TServiceContract : class
    {
        lock (_serviceMapLock)
        {
            _serviceMap[typeof(TServiceContract)] = implementation;
        }
    }

    public TServiceContract GetService<TServiceContract>()
        where TServiceContract : class
    {
        object service;
        lock (_serviceMapLock)
        {
            _serviceMap.TryGetValue(typeof(TServiceContract), out service);
        }
        return service as TServiceContract;
    }

    readonly Dictionary<Type, object> _serviceMap;
    readonly object _serviceMapLock;
}

For the purposes of the demo application, the locks placed around usage of the service map are unnecessary since a service implementation is never replaced after application start-up.  I included the locks to help prevent threading issues for people who use this class in a more dynamic system but might forget to add them.

Previously I mentioned that ViewModelBase has a method called GetService which resolves service dependencies for its child classes.  That method exists so that all ViewModel objects rely on the same strategy for resolving service dependencies.  If later on you decide to change the way that those dependencies are located, you only need to update that one base class method.  That method is seen here:

Collapse Copy Code
public TServiceContract GetService<TServiceContract>()
    where TServiceContract : class
{
    return ServiceContainer.Instance.GetService<TServiceContract>();
}

So far we have seen how PersonViewModel relies on a service interface, IMessageBoxService, to show a message box.  We saw that its IScreen.TryToClose method calls into its base class’s GetService method, which simply delegates to the GetService method of ServiceContainer.  Next up, we examine how the service container is populated and how this design can be leveraged to create good unit tests.

Test-time 

Since PersonViewModel relies on a service dependency to show a message box, we can write unit tests that avoid causing message boxes to be shown.  This can be achieved by following three simple steps.

First we create a class that implements the IMessageBoxService interface.  This class is only used for testing purposes, which is why I put it in the UnitTests project.  The MockMessageBoxService class is shown below:

Collapse Copy Code
class MockMessageBoxService : IMessageBoxService
{
    public MessageBoxResult ShowReturnValue;

    public int ShowCallCount;

    public MessageBoxResult Show(
        string message, 
        string title, 
        MessageBoxButton buttons, 
        MessageBoxImage image)
    {
        ++ShowCallCount;
        return this.ShowReturnValue;
    }
}

Next we need to put an instance of MockMessageBoxService into the same ServiceContainer that a PersonViewModel uses to locate its service dependencies.  To ensure that the tourniquet is tied nice and tight, we can perform this step in a method decorated with AssemblyInitializeAttribute.  That attribute is part of the Visual Studio unit testing framework.  It marks a method that should be executed once before any tests in the assembly are run.

Collapse Copy Code
[TestClass]
static class MockServiceInjector
{
    // This method is called once before any test executes.
    [AssemblyInitialize]
    public static void InjectServices(TestContext context)
    {
        ServiceContainer.Instance.AddService<IMessageBoxService>(
            new MockMessageBoxService());
    }
}

The last step is to write unit tests that exercise PersonViewModel.  These tests can verify that the TryToClose method of PersonViewModel is behaving itself.

Collapse Copy Code
[TestMethod]
public void ShowsMessageBoxWhenClosedAndCanSave()
{
    var personVM = new PersonViewModel(new Person
    {
        FirstName = "Josh",
        LastName = "Smith"
    });

    var personScreen = personVM as IScreen;

    var msgBox = 
        personVM.GetService<IMessageBoxService>() 
        as MockMessageBoxService;
    
    // User clicks the Cancel button -- should not close or save
    msgBox.ShowReturnValue = MessageBoxResult.Cancel;
    msgBox.ShowCallCount = 0;
    Assert.IsTrue(personVM.CanSave);
    Assert.IsFalse(personScreen.TryToClose());
    Assert.IsTrue(personVM.CanSave);
    Assert.AreEqual(1, msgBox.ShowCallCount);

    // User clicks the No button -- should close but not save
    msgBox.ShowReturnValue = MessageBoxResult.No;
    msgBox.ShowCallCount = 0;
    Assert.IsTrue(personVM.CanSave);
    Assert.IsTrue(personScreen.TryToClose());
    Assert.IsTrue(personVM.CanSave);
    Assert.AreEqual(1, msgBox.ShowCallCount);

    // User clicks the Yes button -- should close and save
    msgBox.ShowReturnValue = MessageBoxResult.Yes;
    msgBox.ShowCallCount = 0;
    Assert.IsTrue(personVM.CanSave);
    Assert.IsTrue(personScreen.TryToClose());
    Assert.IsFalse(personVM.CanSave);
    Assert.AreEqual(1, msgBox.ShowCallCount);
}

Now let’s turn our focus to how the service container is configured when the user runs the application.

Run-time 

When the application is running we use a different implementation of IMessageBoxService.  This version of the service actually shows a message box and returns the result selected by the user.  That class is seen below:

Collapse Copy Code
internal class MessageBoxService : IMessageBoxService
{
    MessageBoxResult IMessageBoxService.Show(
        string text,
        string caption,
        MessageBoxButton buttons,
        MessageBoxImage image)
    {
        return MessageBox.Show(text, caption, buttons, image);
    }
}

An instance of the MessageBoxService class is placed into the service container when the application is first created:

Collapse Copy Code
public partial class App : Application
{
    public App()
    {
        ServiceInjector.InjectServices();
    }
}

// In the Demo.Services project
public static class ServiceInjector
{
    // Loads service objects into the ServiceContainer on startup.
    public static void InjectServices()
    {
        ServiceContainer.Instance.AddService<IMessageBoxService>(
            new MessageBoxService());
    }
}

In the demo application all of the code related to services, except for the mock service, is in the Demo.Services class library project.  This allows the MessageBoxService class to be marked internal so that the executable cannot directly reference its type.  Instead, the executable must reference IMessageBoxService, which helps to reduce coupling and allows for Inversion of Control to work its magic.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值