DialogUtils

A small library that provides a simple yet complete way to use DialogHost from Material Design in XAML in a WPF MVVM app.


Keywords
csharp, materialdesigninxaml, wpf
License
MIT
Install
Install-Package DialogUtils -Version 1.0.0-rc.1

Documentation

DialogUtils

Poster

Dialogs are a little bit tricky in the world of MVVM because it can be easy to break the basic rule of MVVM by introducing dialogs inside view models. For example, you want to show a dialog when a user clicks a button which is typically data bound to a command of a view model, and the code to show the dialog is within the implementation of that command. If you create an instance of the dialog within that code, you're making your view model aware of a view, and hence the break of the basic rule of MVVM.

There's no simple way to deal with dialogs in MVVM. ReactiveUI's solution is to abstract the dialogs as interactions. Material Design In XAML Toolkit's solution is DialogHost. I'm using the latter in Aclass for Windows but extend it a little bit to make it easy to work with Microsoft MVVM Toolkit and Microsoft Dependency Injection/.NET Generic Host.

Super Quick Start

If you're going to start a new project, it is highly recommended to use the VSIX template installer to install a clean project template for your Visual Studio.

  1. Find and install "dialogutils" in Visual Studio's Manage Extensions dialog box.
  2. Search "dialogutils" and create a new project with the "Material Design WPF App with DialogUtils" project template in Visual Studio.
  3. Hit F5 to run the app.

Note that the project template further leverages Material Design Extensions for MaterialWindow and AppBar.

Getting Started

If you're going to use DialogUtils in an existing project that already has Material Design In XAML Toolkit installed and configured, below is the guide.

  1. Search and install DialogUtils in NuGet Package Manager.
  2. In MainWindow.xaml, insert a <DialogHost/> between <Window/> and it's original child.
<Window>
    <md:DialogHost Identifier="MainHost">
        <!-- original child -->
    </md:DialogHost>
</Window>
  1. Register DialogHostService using the AddDialogHostService extension method and MainViewModel in App.xaml.cs. If you're using .NET Generic Host, you can use the UseDialogHostService extension method instead on the IHostBuilder object.
public partial class App : Application
{
    public App()
    {
        Ioc.Default.ConfigureServices(ConfigureServices());
    }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddDialogHostService();
        services.AddTransient<MainViewModel>();

        return services.BuildServiceProvider();
    }
}
  1. Setup MainViewModel as MainWindow's DataContext. If you're using .NET Generic Host, you might already have both MainViewModel and MainWindow registered in the container. In this case you can inject MainViewModel in MainWindow's constructor.
public MainWindow()
{
    InitializeComponent();

    DataContext = Ioc.Default.GetService<MainViewModel>();
}
  1. Inject IDialogHostService in MainViewModel's constructor, save it as a private member for showing dialogs within any command implementation.
public class MainViewModel : ObservableObject
{
    private IDialogHostService _dialogHostService;

    private ICommand _showMessageCommand;
    public ICommand ShowMessageCommand => _showMessageCommand ?? (_showMessageCommand = new RelayCommand(ShowMessageImpl));
    private async void ShowMessageImpl()
    {
        await _dialogHostService.ShowMessageAsync(
            dialogIdentifier: "MainHost",
            message: "Hello, World");
    }

    public MainViewModel(IDialogHostService dialogHostService)
    {
        _dialogHostService = dialogHostService;
    }
}

Usage

  1. How to show a message?

The simplest way to do so is call the ShowMessageAsync on an instance of IDialogHostService usually injected via constructor as below. The dialogIdentifier and message parameters are required; the others such as header are optional.

await _dialogHostService.ShowMessageAsync(DialogHostIdentifier, Message);
  1. How to show a message with OK and Cancel buttons and return whether the user clicks the OK button?

Passing true to the isNegativeButtonVisible parameter of ShowMessageAsync will show both OK and Cancel buttons. The return value will be true if the user clicks OK button; otherwise false. The text of both buttons can also be customized by using the affirmativeButtonText and negativeButtonText parameters of ShowMessageAsync respectively.

var result = await _dialogHostService.ShowMessageAsync(DialogHostIdentifier, Message, isNegativeButtonVisible: true);
  1. How to get input from user?

Call ShowInputAsync as below which will show a dialog with a TextBox inside. If the user types some text within the TextBox and clicks OK button, the method will return that as a string; otherwise, null. If you want also to show an existing value in the TextBox, you can use the third parameter which is currently null in the below code.

var result = await _dialogHostService.ShowInputAsync(DialogHostIdentifier, Message, null, Header);
  1. How to show a progress dialog?

Call ShowProgressAsync as below which will show a small dialog with an indeterminate progress ring inside. You'll need to close the dialog manually by calling CloseDialog on an instance of IDialogHostService with the same DialogHostIdentifier.

_dialogHostService.ShowProgressAsync(DialogHostIdentifier);

If you want also a cancel button on the dialog, you can pass true to the cancellable parameter. In this case, clicking that cancel button will close the current progress dialog.

ShowProgressAsync returns the view model used by the current progress dialog. If you want to change the value of the progress ring, you can pass false to the isIndeterminate parameter and increase the value of the Value property as below. Note that you can also use the Close method of the view model to close the current progress dialog.

var vm = _dialogHostService.ShowProgressAsync(
    DialogHostIdentifier,
    isIndeterminate: false);

for (double d = 0; d < 100; d += .5)
{
    vm.Value = d;
    await Task.Delay(10);
}

vm.Close();
  1. How to show a custom dialog?

Let's say you have a view named EditContactView and a view model named EditContactViewModel registered as transient in App's ConfigureServices method as below. Note that views are required to inherit from UserControl.

services.AddTransient<EditContactViewModel>();
services.AddTransient<EditContactView>();

And tell DialogHostService that they relate to each other via Configure method as below. Note that if you create a project with the VSIX template, the ConfigureDialogHostService method is already created for you.

private static IDialogHostService ConfigureDialogHostService(IServiceProvider services)
{
    var dialogHostService = new DialogHostService(services);
    dialogHostService.Configure<EditContactViewModel, EditContactView>();
    return dialogHostService;
}

Now you can show it via ShowDialogAsync method as below. As you can see, you tell it which view model to use, it'll find the corresponding view to show. You can also initialize the view model by passing a lambda.

await _dialogHostService.ShowDialogAsync<EditContactViewModel>(
    DialogHostIdentifier,
    vm => vm.Init(Contact));

Usually we inject a view model into a view via constructor as below. In this case DialogHostService will use the view model you injected. Otherwise, it will get one from the container and associate it to the view for you.

public EditContactView(EditContactViewModel viewModel)
{
    InitializeComponent();
    DataContext = viewModel;
}
  1. How to know if a dialog is opened or being closed?

You can register DialogHostMessage with WeakReferenceMessenger's Register method, and then check the DialogIdentifier property or the ViewModelType property. These are actually two dimensions because one dialog host can be used for multiple dialogs and one dialog can be hosted in multiple dialog hosts. To know whether the dialog host or a dialog view model is opened or being closed, you can check the DialogHostEvent property to see if it's DialogHostEvent.Opened or DialogHostEvent.Closing respectively.

Thanks