MVVM Dialogs with Caliburn.Micro

Background

In every applications life there comes a time when you need to show some kind of message to the user. Be it a question whether he really wants to delete something or a simple message that says that some operation was successfull. The most simple way to do that is the good ol’ MessageBox.Show() with its zillion overloads.

MessageBox.Show("Foo", "Bar", MessageBoxButton.OKCancel);

But in the shiny MVVM World , poluting your ViewModels with MessageBoxes is usually frowned upon since it breaks a lot of stuff, especially automated unit testing and theming.

You can find quite a lot solutions about how the ‘MVVMize’ MesasgeBoxes and dialog screens in general. Most of them involve wrapping the MessageBox.Show() in some kind of IService, setting up some kind of event infrastructure and other funky stuff. Surprisingly, all of those solutions completely ignore the first M in MVVM, namely the Model, and none really tackles the problem at its heart.

One Model to rule them all

Well, let’s forget about all the View and ViewModel stuff for now. We will start by specifying what we actually want to achieve with a dialog.

We want to display some message concerning some topic and a list of possible Responses from which the user can choose one.

So, let’s create a model with conforms to those specifications

public class Dialog<TResponse>
{
	public DialogType DialogType { get; set; }
	public string Subject { get; set; }
	public string Message { get; set; }

	public IEnumerable<TResponse> PossibleResponses { get; protected set; }
	public TResponse GivenResponse {get; set; }
	public bool IsResponseGiven { get; private set; }
}

public enum DialogType
{
	None,
	Question,
	Warning,
	Information,
	Error
}

The DialogType in conjunction with the subject defines the topic and the rest is pretty much straightforward. We also need a IsResponseGiven Property so that we can distinguish between default and unset values because TResponse may or may not be a value type (and hence not nullable).

One ViewModel to bind them

The ViewModel is responsible for bringing the Responses in a bindable format and setting the response on the dialog when the user selects one. The ViewModel also handles the case where the user closes the window without giving any response at all.

For supporting default (the user presses ‘Enter’) and cancel (the user presses ‘Escape’) responses,  I will use a convention based approach, namely defining the first response in the list as the default response and the last response as the cancel response.

public class BindableResponse<TResponse>
{
	public TResponse Response { get; set; }
	public bool IsDefault { get; set; }
	public bool IsCancel { get; set; }
}
public interface IDialogViewModel<TResponse>
{
	bool IsClosed { get; set; }
	Dialog<TResponse> Dialog { get; set; }
	IObservableCollection<BindableResponse<TResponse>> Responses { get; }
	void Respond(BindableResponse<TResponse> bindableResponse);
}

The implementation is pretty straightforward and omitted for brevity but can be found here.

One View to show them all

I will present the WPF version of the view here because the SL version requires a workaround for the nonexisting IsDefault/IsCancel Properties of the Button. For those interested in the SL version, the source is here. I will also omit all irrelevant (styling) properties.

<Window x:Class="Caliburn.Micro.Contrib.Interaction.DialogView"
        Title="{Binding Dialog.Subject}"
        Contrib:DialogCloser.DialogResult="{Binding CanClose}">
    <Window.Icon>
        <Binding Path="Dialog.DialogType">
            <Binding.Converter>
                <Converter:DialogTypeToSystemIconConverter />
            </Binding.Converter>
        </Binding>
    </Window.Icon>
    <DockPanel Focusable="False" LastChildFill="True">
        <ItemsControl x:Name="Responses">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding Response}"
                            IsCancel="{Binding IsCancel}"
                            IsDefault="{Binding IsDefault}"
                            Micro:Message.Attach="Respond($dataContext)" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <TextBlock Text="{Binding Dialog.Message}" />
    </DockPanel>
</Window>

The most important part is where we bind the Responses to an ItemsControl (by using Caliburn.Micros Convention Binding Feature) and create a Button for each Response which will call the Respond() Method on the ViewModel with the bound Response as a parameter. The Subject of the Dialog is bound to the Title of the Window and the DialogType is converted to an Icon.

And in the IResult wrap them

No Caliburn.Micro Extension with the corresponding IResult to use them !

To actually show the dialog to the user, we would have to

  1. Create the dialog
  2. Import the IWindowManager in the ViewModel
  3. Create the ViewModel and pass it the dialog
  4. Invoke ShowDialog() on the IWindowManager with the ViewModel as a parameter

Well, the first step cannot be encapsulated in an IResult, but 2-4 rest can easily be encapsulated.

public class DialogResult<TResponse> : IResult
{
	private Func<IDialogViewModel<TResponse>> _locateVM = () => new DialogViewModel<TResponse>();

	public DialogResult(Dialog<TResponse> dialog)
	{
		Dialog = dialog;
	}

	public Dialog<TResponse> Dialog { get; private set; }

	public void Execute(ActionExecutionContext context)
	{
		IDialogViewModel<TResponse> vm = _locateVM();
		vm.Dialog = Dialog;
		Micro.Execute.OnUIThread(() => IoC.Get<IWindowManager>().ShowDialog(vm));
	}

	public DialogResult<TResponse> In(IDialogViewModel<TResponse> dialogViewModel)
	{
		_locateVM = () => dialogViewModel;
		return this;
	}

	public DialogResult<TResponse> In<TDialogViewModel>()
		where TDialogViewModel : IDialogViewModel<TResponse>
	{
		_locateVM = () => IoC.Get<TDialogViewModel>();
		return this;
	}
}

We do not only get reusable code, but also a nice way to change the implementation of IDialogViewModel<> for specific dialogs if we want to.

Last but not least we can write a small Extension Method to get even more readable code !

public static DialogResult<TResponse> AsResult<TResponse>(this Dialog<TResponse> dialog)
        {
            return new DialogResult<TResponse>(dialog);
        }

And use it in the coroutine

public IEnumerable<IResult> Foo()
{
	var question = new Dialog<Answer>(DialogType.Question,
									  "Isn't this a nice way to create a Dialog Window?",
									  Answer.Yes,
									  Answer.No);

	yield return question.AsResult();

	if (question.GivenResponse == Answer.Yes)
		Console.WriteLine(" :] ");
	else
		Console.WriteLine(" :[ ");
}
Category(s): C#, Caliburn.Micro, CMContrib, Silverlight, WPF
  • Hans Pickelmann

    Hallo Kevin,
    vorerstmal thnx für die interessante Idee zum Implementieren von Dialogen. Leider habe ich in Silverlight Probleme im bzw. in den Dialogen die entsprechenden Buttons, Subject und Message zu binden. Bei mir tu sich da leider gar nichts., sehe nur den Dialog aber ohne Buttons und Message, Title usw.
    Mach ich was falsch, habe mir mal dein Repository geholt und mit der CMContrib.SL.Demo “gespielt”.

    Vielleicht könntest du mir einen Tipp geben…
    wäre dir dankbar, so long und Gruß
    Hans

  • http://www.codesomnia.de Kevin

    Hallo Hans,

    da war in der Tat ein “Bug” im ViewModel. Ich hatte die Klasse ‘internal’ gemacht, allerdings unterstuetzt SL keine Bindings an ‘internal’ Klassen. Hab das ViewModel wieder ‘public’ gemacht und jetzt funktionierts wieder. Zieh dir einfach die neuste version ausm Repository.

    Gruss und danke fuer den Hinweis

    kev

  • Hans Pickelmann

    Hi Kevin
    Danke für deine prompte Antwort, werde ich morgen gleich mal ausprobieren. und in meine erste SL & CM Anwendung einbauen…

    thnx und Gruß
    Hans

  • http://www.codesomnia.de Kevin

    kein problem. Falls es noch Probleme mit SL gibt, einfach melden. Ich hab mich momentan primaer auf die WPF Version konzentriert, wie man an der SL Demo erkennen kann ;)

  • rekna

    Can CMContrib release be downloaded somewhere? Or do we have compile source ourselves?

  • http://www.codesomnia.de Kevin

    you have to compile it yourself for now. i will create a nuget package eventually though

  • rekna

    It’s a pity there is a dependency on the silverlight toolkit… is this really necessary or could you make it optional somehow (DialogView.xaml).

  • rekna

    In Dynamics.cs, there are 3 warnings in the method ParseIdentifier : possible unintended reference comparison on if-statements…

  • http://www.jsrsoft.co.uk JohnXl

    Hi,
    Very useful.
    Is there (will there be) a WPF version?

    thanks
    John

  • http://www.codesomnia.de Kevin

    there is already a version for SL and WPF

  • LowTide76

    Hello,

    I downloaded the code and compiled and tested it out using the CMContrib.SL.Demo project and that compiles with no errors. When I run the sample in I never get an error, but the dialog is never shown either. I added the source project and debugged it, basically it is all good until the line “yield return warning.AsResult();” then it just exits Foo with this showing in the output windows “A first chance exception of type ‘System.NullReferenceException’ occurred in Caliburn.Micro.Contrib”

    I can’t seem to track down what the issue is. I have tried it with CM 1.0 and CM 1.1. Any ideas?

    var warning = new Warning("abc", Answer.Retry, Answer.Ignore);

    yield return warning.AsResult();

    Response = warning.GivenResponse;

  • http://www.codesomnia.de Kevin

    the dynamic.cs is from Microsoft, so as long as it works i don’t intend to change anything in it. The dependency is because of the busy indicator which isn’t part of the standard lib (sadly). I could write my own, but I sadly can’t spare time for it at the moment.

  • http://www.codesomnia.de Kevin

    Hey,
    I’m sorry but i can’t reproduce the error myself. As far as i can see, the only point where the exception can come from is Line31 is in the DialogResult.cs. Maybe set a breakpoint there and see if it throws. If not, what system are you running?

    Cheers

  • LowTide76

    Yes, you are right – on the line: IDialogViewModel vm = _locateVM();

    I am getting Object reference not set to an instance of an object.
    at Caliburn.Micro.Contrib.Interaction.DialogViewModel`1.CreateResponses()
    at Caliburn.Micro.Contrib.Interaction.DialogViewModel`1.set_Dialog(Dialog`1 value)
    at Caliburn.Micro.Contrib.Interaction.DialogViewModel`1..ctor(Dialog`1 dialog)
    at Caliburn.Micro.Contrib.Interaction.DialogViewModel`1..ctor()
    at Caliburn.Micro.Contrib.Results.DialogResult`1.b__0()
    at Caliburn.Micro.Contrib.Results.DialogResult`1.Execute(ActionExecutionContext context)

    I am on Windows 7 64-bit running VS 2010 with SP1 and Silverlight 4 application. What else can I provide you?

    TIA

  • http://www.codesomnia.de Kevin

    so i just pushed a fix to the repository although i still didn’t get the error ?! give it a try and tell me if it actually fixed the bug.
    Thanks for the feedback !

  • LowTide76

    Yes, that fixed the display issue. The problem now, which may or may not be a problem, is the dialog doesn’t close when clicking an option. Using the code below, when I click ignore I get ” :[ " in the console, but the dialog doesn't go away and on subsequent clicks it doesn't do anything.

    Thank you for the prompt responses.

    var question = new Dialog(DialogType.Question,
    "Isn't this a nice way to create a Dialog Window?",
    Answer.Yes,
    Answer.No);

    yield return question.AsResult();

    if (question.GivenResponse == Answer.Yes)
    System.Diagnostics.Debug.WriteLine(" :] “);
    else
    System.Diagnostics.Debug.WriteLine(” :[ “);

  • LowTide76

    BTW, private void CreateResponses() inside of DialogViewModel.cs is getting called twice, the first time Dialog is null so your fix checking for null allows it to pass through and then the next time Dialog has a value.

  • http://www.codesomnia.de Kevin

    I duuno how but i messed up something in the Repository. There is some stuff missing and other stuff was deleted and i don’t know exactly why. I should really stop moving/renaming files with while changing them :/ It also says that some code files are binary which is very strange…I’ll give an update when I got it up and running again

  • LowTide76

    Sounds good. I really like what you have done and am looking forward to using it! If I can do anything on my side to help troubleshoot or test, just let me know.