Skip to content

How To: Use the new dialog system

brather1ng edited this page Jun 11, 2016 · 3 revisions

How To: Use the new dialog system

We now use the dialog system from MahApps.Metro for all kinds of dialogs. This article should give a you a quick introduction to opening and writing dialogs. At the bottom you'll also find a few general tips on using the MVVM pattern in our code base.

This is how they look:

The main difference from an implementation view point is that these are not real dialogs but just overlays that are opened using async/await. That means, if you want your code to only resume once the dialog is closed, you have to await the call. This will lead to async/await propagating all the way up your code (generally up to an event handler), but that isn't a bad thing at all.

One thing to note: Files mentioned here that are in POESKillTree.Controls.Dialogs have to be imported from that namespace. Their equivalents in MahApps.Metro.Controls.Dialogs must not be used.

View model and view refer to the terms from the MVVM pattern, which I highly recommend you to use as it makes a clear distinction between the logic (the view model) and the UI (the view) of a dialog.

Using the Popup/MessageBox replacement

From a view / if you have access to the window object

There are extension methods on MetroWindow that open message dialogs. These are defined in MahApps.Metro.Controls.Dialogs.ExtendedDialogManager.

ShowQuestionAsync, ShowErrorAsync, ShowWarningAsync, ShowInfoAsync are nearly identical to the methods from Popup from before.

ShowInputAsync can be used to let the user input a string into a text box.

ShowProgressAsync opens a dialog that has a progress bar and can be updated. It returns a ProgressDialogController that enables updating the progress. This replaces the LoadingWindow we had before.

ShowDialogAsync is used to display custom dialogs. More on that below.

Example usage:

        private async void Menu_UntagAllNodes(object sender, RoutedEventArgs e)
        {
            var response = await this.ShowQuestionAsync(L10n.Message("Are you sure?"),
                L10n.Message("Untag All Skill Nodes"), MessageBoxImage.None);
            if (response == MessageBoxResult.Yes)
                Tree.UntagAllNodes();
        }

From a view model

The IDialogCoordinator interface is used as a service for opening dialogs that can be injected into view models without them directly interacting with UI code. An instance of this interface should be passed to view models in their constructor. You can use DialogCoordinator.Instance for that.

The methods are the same as the extension methods described above, except that they require a context object instead of a MetroWindow. This context generally is the view model itself. The reference from the context to an UI object has to be registered beforehand. If you opened the view model through ExtendedDialogManager.ShowDialogAsync, this is done for you. If not, you need to call DialogParticipation.SetRegister(yourView, yourViewModel).

ShowDialogAsync is missing from IDialogCoordinator on purpose as you should not open views directly from view models. If you need to display custom dialogs, you should extend the interface with a new method for that (see POESKillTree.TreeGenerator.ViewModels.SettingsDialogCoordinator for an example).

Example usage:

        public async Task SkillAllTaggedNodesAsync()
        {
            if (!GetCheckedNodes().Except(SkilledNodes).Any())
            {
                await _dialogCoordinator.ShowInfoAsync(this,
                    L10n.Message("Please tag non-skilled nodes by right-clicking them."));
                return;
            }

            // Do stuff
        }

Writing custom dialogs

There is not much that is different in writing a dialog the new way. Mostly, you have to change the base class in your Xaml file from MetroWindow to CloseableBaseDialog. The errors that show up in the Xaml file itself will mostly be properties that no longer exist and can be safely removed as they represent the default behaviour.

If you only have a close button, you should remove it. CloseableBaseDialog has a button built in that already closes the dialog. If you have more than one close behaviour (e.g. OK vs. Cancel), you need to hide the built in close button with CloseButtonVisibility=Collapsed.

You should not give your dialog a fixed width through the normal properties as it will not show properly with that. However, you can use MaxContentWidth if the dialog looks bad if stretched.

Opening these dialogs is different. It is done via ShowDialogAsync. If you don't have a view model, you need to supply a CloseableViewModel that handles closing the dialog:

        private async void Menu_OpenHotkeys(object sender, RoutedEventArgs e)
        {
            await this.ShowDialogAsync(new CloseableViewModel(), new HotkeysWindow());
        }

Without view model

Not recommended, but I can't force you all to learn a new pattern.

If you followed the steps above, you'll still have compile errors in your code-behind file. If you now use the built in close button, you can just delete the handler of the old one. Generally, you can close the dialog with CloseCommand.Execute(null);.

If your dialog set DialogResult, you need to add that property yourself. It can then be used like this:

var w = new CraftWindow(PersistentData.EquipmentData);
await this.ShowDialogAsync(new CraftViewModel(), w);
if (!w.DialogResult) return;
// Do something

With view model

Since no one did dialogs with view models yet, I'll just point you to two simple dialogs I refactored to show how propert MVVM looks like: SettingsMenuWindow and DownloadItemsWindow (the views are located in POESKillTree.Views and their view models in POESKillTree.ViewModels).

These dialogs are called using ShowDialogAsync like above, just that they have a custom view model now:

await this.ShowDialogAsync(
    new SettingsMenuViewModel(_persistentData, DialogCoordinator.Instance),
    new SettingsMenuWindow());

Some notes and tips (examples for most of these can be found in the two mentioned dialogs):

  • Your view model is set as the DataContext of your view. That means, you can access properties from the view model in your xaml code with bindings.
  • If the properties of your view model can change (are not effectively readonly), you need to raise property changed events for the view to pick up the changes. This is done with the SetProperty() method:
        private string _itemsLink;
        public string ItemsLink
        {
            get { return _itemsLink; }
            private set { SetProperty(ref _itemsLink, value);}
        }
  • The class that defines SetProperty() -- POESKillTree.Utils.Notifier -- can also be used outside of view models, e.g. in classes like PoEBuild or Options that only hold data. DownloadItemsViewModel, for example, utilizes the INotifyPropertyChanged implementation of PoEBuild to adjust the inventory link every time the user changes account or character name through the view.
  • To get code completion in bindings in the xaml editor, you can specify the type of the data context with the following code (if you have ReSharper, you can do that over the light bulb menu that appears when you place the cursor on a Binding):
<dialogs:CloseableBaseDialog
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
    xmlns:viewModels="clr-namespace:POESKillTree.ViewModels"
    d:DataContext="{d:DesignInstance viewModels:SettingsMenuViewModel}">
  • You can specify the title of the dialog by setting DisplayName in your view model.
  • Button clicks and the like should generally not be handled with event handlers but with commands, if possible, so the code-behind of your view optimally contains no custom code at all. Commands are properties that implement ICommand. One implementation that uses a function passed as constructor parameter is RelayCommand. They are used in xaml with a Binding on your Button: <Button Command="{Binding OpenInBrowserCommand}">. You can optionally pass a parameter to the command and provide a function that returns whether the command can currently be executed. Example:
        private RelayCommand _openInBrowserCommand;
        public ICommand OpenInBrowserCommand
        {
            get
            {
                return _openInBrowserCommand ??
                       (_openInBrowserCommand = new RelayCommand(param => Process.Start(ItemsLink)));
            }
        }
  • If your dialog needs to do something when it's closed, add that code to the RequestsClose event. For example, handlers on persistent data objects (like PoEBuild) should be removed once the dialog is closed:
        public DownloadItemsViewModel(PoEBuild build)
        {
            // [...]
            Build.PropertyChanged += BuildOnPropertyChanged;
            RequestsClose += () => Build.PropertyChanged -= BuildOnPropertyChanged;
        }
  • If you provide you own buttons in the bottom row (e.g. "OK" and "Cancel"), you can change the base type from CloseableBaseDialog to BaseDialog and call Close() from the commands that handle those button clicks.