Saturday, April 23, 2011

How to show a dialog in MVVM

In Mvvm you do not want direct references from your controllers (aka View-Models, I find it easier to call them controllers for simplicity).  This means you don't want code that specifically calls Window.Show or MessageBox.Show.  The reason is basically two-fold:

  1. First you will be unable to unit test the controller.  Message-boxes or Dialogs popping open will halt the test.
  2. If you decide to share some code with another project that uses a different UI technology, then not following Mvvm explicitly will prevent this. Also common is stakeholders changing their minds.
The solution is to make use of an Inversion of Control container (aka factory).



Message Box Usage

Message boxes pose a problem for unit testing because when open they block the thread from completing, meaning user intervention is required to continue a test. This is not acceptable. To circumvent this, MessageBox use can be accessed via an interface.

private IMessageBoxService backingMessageBoxService;
public IMessageBoxService MessageBox {
    get {
        return this.backingMessageBoxService ?? (this.backingMessageBoxService = new WpfMessageBoxService());
        }

        private set {
            // Provided for testing
            this.backingMessageBoxService = value;
        }
    }
}



The above code works in production, but during testing a mock will need to be injected into the MessageBox property.

        [Test(Description = "The save action should trigger a message box")]
        [Timeout(500)]
        public void SuccessfulSaveMessage() {
            var controller = new FormController(new ContactDataServiceStub());
            var accessor = new FormController_Accessor(controller);
            var messageBoxMock = new MessageBoxServiceMock();
            accessor.MessageBox = messageBoxMock;
 
            // Click the save button in the same way the View xaml would in production.
            controller.SaveCommand.Execute(controller.CurrentContact);
 
            Assert.IsTrue(messageBoxMock.WasShowCalled);
        }


public interface IGlassDemoDialog {
    event EventHandler Closed;
    void Show();
}

And from within your controller you can get a reference to the dialog through the use of an object factory (or using an interfaced property shown previously in this document).

var glassDialog = ObjectFactory.Container.GetInstance<IGlassDemoDialog>();
glassDialog.Show();

This of course assumes you have a registration with the object factory either in the app.config or in startup.

ObjectFactory.Initialize(init => {
    // Other config lines here
    init.For<IGlassDemoDialog>().Use<WpfGlassWindowDemo>();
});

Finally, your custom interface must be implemented by your View.
public partial class WpfGlassWindowDemo : Window, IGlassDemoDialog { }


[Test]
public void ShowGlassWindowTest() {
    var controller = new RighthandController();
    var mockRepository = new MockRepository();  // Rhino Mock
    var dialogMock = mockRepository.StrictMock<IGlassDemoDialog>();
    dialogMock.Expect(dialog => dialog.Show());
    mockRepository.ReplayAll();  // Prepare mocks.
    ObjectFactory.Initialize(init => {
        init.For<IGlassDemoDialog>().Use(dialogMock);
    });
    Assert.IsTrue(controller.GlassCommand.CanExecute(null));
    controller.GlassCommand.Execute(null);
    mockRepository.VerifyAll(); // Guarantee all mocks were used appropriately.

No comments:

Post a Comment