.NET Framework | .NET Core | .NET |
---|---|---|
✔️ 4.5 | ✔️ 3.1 | ✔️ 5.0 |
This project helps showing dialogs in WPF applications directly from the view model. Dialogs are displayed using adorners.
[toc]
Showing dialogs to the user of an application is necessary in almost all projects. This DialogProvider aims at helping in this matter when using the View Model First approach for WPF applications. It simply lets you show dialogs only knowing about view models. Being aware about the views that are shown to the user is unnecessary, as an IViewProvider
from the Nuget package Phoenix.UI.Wpf.Architecture.VMFirst.ViewProvider is used to resolve the views that will be bound to the view models. More about what view providing is, can be found in the documentation of that package.
The dialogs will be shown using a System.Windows.Documents.Adorner.
An adorner is a custom FrameworkElement that is bound to a UIElement. Adorners are rendered in an adorner layer, which is a rendering surface that is always on top of the adorned element or a collection of adorned elements; rendering of an adorner is independent from rendering of the UIElement that the adorner is bound to. An adorner is typically positioned relative to the element to which it is bound, using the standard 2-D coordinate origin located at the upper-left of the adorned element.
So basically the dialog sits on top of a FrameworkElement. For most cases this is simply the MainWindow of a WPF application (see DefaultDialogManager). But dialogs can also be shown in separate parts of the UI, overlapping only their adorned / linked view (see DialogManager).
Externally only instances of IDialogManager
must be used to show dialogs within an application. The following types are available.
The DialogManager
always needs to be initialized with a view, which will be adorned and used to display dialogs. Therefore the view model using the DialogManager
needs to be at least a little bit aware of the view it is bound to.
One possible initialization strategy would be to bind the views Loaded event to the view model and there initialize the DialogManager
.
- XAML code for the view that binds the Loaded event to its own code behind
<UserControl
Loaded="ViewLoaded"
>
- The views code behind uses the event handler ViewLoaded to forward itself to its bound view model:
private void ViewLoaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is ViewModel viewModel)
{
viewModel.OnViewLoaded(this);
}
}
- The view model then uses the view instance passed to its OnViewLoaded method to initialize the
DialogManager
:
private IDialogManager DialogManager { get; }
internal void OnViewLoaded(FrameworkElement view)
{
// Initialize the dialog manager.
this.DialogManager.Initialize(view);
}
Alternatively when using the DefaultViewProvider
from the separate NuGet package Phoenix.UI.Wpf.ViewProvider this view provider will automatically create a DialogManager
that can be used by the view model. To make use of this simply:
- let the view model implement
IDialogManagerViewModel
. This is the recommended way. - provide a property of type
IDialogManager
namedDialogManager
. This uses reflection and is therefore not recommended.
This special dialog manager does not need to be initialized with a view, as it automatically uses the current applications MainWindow to show dialogs. The reference to the MainWindow is obtained via the Activated
event of the application. Therefore the DefaultDialogManager
may not be initialized in the constructor of the first view model, as the Activated
event hasn't been raised by then.
There are different types of dialogs that all IDialogManager
s can show and they are represented by methods matching those types.
Messages | Simple or complex message with optional title and content. |
Warnings | The same as messages. Depending on the used Dialog Views they can be styled differently. |
Exceptions | All types of errors including stack trace view. |
Custom Content | Container for a custom view model that should be shown. |
All dialogs are displayed asynchronous and return an awaitable DialogTask
.
In addition all methods for showing dialogs provide cancellation support via a System.Threading.CancellationToken.
In general there is really not much difference between showing a Message or a Warning dialog. The used Dialog View may choose to implement different UI for those two types, but that's it. So in the following description Warning dialogs are not further mentioned, as everything that applies to messages also applies to them.
To show a simple Message Dialog use the following syntax.
this.DialogManager.ShowMessage
(
title: "Message",
message: "Message"
);
When displaying Message dialogs it is possible instead of just showing one message at a time, to display multiple, preferably selectable messages at once within a single dialog. To achieve this, use the ShowMessage
method that accepts a collection of MessageDialogModel
s as one of its parameters.
messageDialogModels = new[]
{
new MessageDialogModel(identifier: "Rule #01", title: @"Don't re-invent the wheel", message: @"Too often, our better judgment…"),
new MessageDialogModel(identifier: "Rule #02", title: @"Keep things as simple as possible but not simpler", message: @"It's very easy to sit down…"),
// …
new MessageDialogModel(identifier: "Rule #10", title: @"Scalability is next to godliness.", message: @"No sooner than you design…"),
};
return this.DialogManager.ShowMessage(messageDialogModels);
❕ Note that the way multiple message are displayed is based on the used Dialog View.
The following screenshot is taken from the Metro Styled Dialogs:
Multiple Messages
Even though most of the time exceptions are gracefully handled by all applications, there may sometimes arise the need to display a totally unforeseeable exception to the user instead of letting the application crash. Fear not, the DialogManager
has a proper solution.
this.DialogManager.ShowException
(
title: "Exception",
exception: new ApplicationException()
);
Sometimes it may be necessary to show more than those simple messages or exceptions. To address this requirement, the DialogManager
supports showing any view model as so called custom content.
this.DialogManager.ShowContent
(
viewModel: new SomeViewModel()
);
If the custom dialog needs to be aware of certain DialogOptions
, then have a look here.
If you also want total control about how and when this custom dialog can be closed, then have a look here.
This is the awaitable Task all dialogs return and whose result is a DialogResult
which specifies how or why the dialog was closed.
var dialogTask = this.DialogManager.ShowMessage(message: "Some message");
await dialogTask;
It also has a single CloseDialog
method that can be used to close the dialog with a specific DialogResult
.
var dialogTask = this.DialogManager.ShowMessage(message: "Some message");
dialogTask.CloseDialog(DialogResult.Yes);
An enumeration of dialog results returned by an awaited DialogTask
. The values are:
None | This is the initial value of any dialog. |
Yes | The dialog answer was Yes or anything meaning approval. |
No | The dialog answer was No or anything meaning disapproval. |
Killed | The dialog was somehow killed. This can happen when cancellation has been requested. |
❕ Note that if DialogResult.None
is returned by any ButtonConfiguration
, then the close callback of the IDialogHandler
won't be called thus preventing the dialog from closing.
When showing dialogs some options are available to configure the dialog.
Some options that directly change the behavior or layout of dialogs.
None | No special options. |
HideTransparencyToggle | Hides the transparency toggle in the dialog view. |
HideStacktrace | Hides the call stack trace of error dialogs. |
AutoExpandStacktrace | Automatically expands the call stack trace of error dialogs. |
❕ Note that not all those options may have any effect since they have to be respected by the different Dialog Views.
If the view or the view model of custom dialog content needs to be aware the DialogOptions
, then the view models of such custom dialogs need to only implement IOptionsAwareDialogContentViewModel
or inherit from the abstract base class DialogContentViewModel
. They can then access those options via the DialogOptions
property of the interface / base class.
If neither implementing nor inheriting is an option, then directly specifying the property in the view model is fine too, as reflection is used to set it.
DialogOptions DialogOptions { get; set; }
❕ Note that the property does not need an setter, but defining one makes reflection a little bit faster, as otherwise the backing field of the property would be manipulated.
All IDialogManager
s have two options that specify where dialogs can be shown. The DisplayLocation
enumeration allows to specify this location when showing a dialog.
Window | This is the default and will show the dialog in the MainWindow of the current application. |
Self | This option will show the dialog in the FrameworkElemet that the manager is bound to. |
❕ Note that both options are the same for the DefaultDialogManager
as its bound FrameworkElemet is the MainWindow.
The following screen shots are taken from the Metro Styled Dialogs:
- Dialog shown in the applications main window
- Dialog shown in the adorned FrameworkElement
To display buttons within a dialog the following options exist.
When showing a dialog, a collection of ButtonConfiguration
can be specified. If this is omitted, then the default will just be a simple Ok button.
Creating a ButtonConfiguration
can be done with one of the many overloaded constructors. Basically all boil down to those two flavors:
- Below will create buttons that execute the specified callback and then close the dialog with the specified
DialogResult
. This is useful when the result of the dialog is not depending on external data.
public ButtonConfiguration(string caption, DialogResult dialogResult, DialogButtonBehavior buttonBehavior, Action callback)
- Below will create buttons that execute the specified callback and then close the dialog with the
DialogResult
returned by the callback. This can be used if custom logic needs to be executed that changes the result depending on external data.
public ButtonConfiguration(string caption, DialogButtonBehavior buttonBehavior, Func<Task<DialogResult>> callback)
❕ Note that if DialogResult.None
is returned by any ButtonConfiguration
, then the close callback of the IDialogHandler
won't be called thus preventing the dialog from closing.
If a button should be the default or cancel button of a dialog, this can be specified optionally with the DialogButtonBehavior
.
None | No special behavior. |
Enter | Button command can be executed via System.Windows.Input.Key.Enter. |
Cancel | Button command can be executed via System.Windows.Input.Key.Escape. |
Example
// Create the button configuration for the dialog.
var buttonConfigurations = new[]
{
new ButtonConfiguration
(
caption: "Accept",
buttonBehavior: DialogButtonBehavior.Enter,
dialogResult: DialogResult.Yes
),
new ButtonConfiguration
(
caption: "Cancel",
buttonBehavior: DialogButtonBehavior.Cancel,
callback: () => new Random().Next(0, 2) == 0 ? DialogResult.Yes : DialogResult.None // 50:50 percent chance that the dialog closes.
),
};
// Show the dialog.
return this.DialogManager.ShowMessage
(
messageModels: new List<MessageDialogModel>()
{
new MessageDialogModel(identifier: "", title: "Licence", message: "Please read the following license and accept it to proceed.")
},
buttonConfigurations: buttonConfigurations
);
For the most common buttons predefined configurations are available. Those can be used directly. Alternatively some overloads of the methods to show dialogs also accept an DialogButtons
enumeration.
Button configuration | Enumeration value |
---|---|
OkButtonConfiguration | DialogButtons.Ok |
CancelButtonConfiguration | DialogButtons.Cancel |
YesButtonConfiguration | DialogButtons.Yes |
NoButtonConfiguration | DialogButtons.No |
SaveButtonConfiguration | DialogButtons.Save |
CloseButtonConfiguration | DialogButtons.Close |
Example
this.DialogManager.ShowMessage
(
title: "Quiz",
message: "Is this usefull?",
DialogButtons.Yes | DialogButtons.No
);
Of course implementing custom buttons in the content view is also a valid option. The view models of such custom views need to only implement ICloseableDialogContentViewModel
or inherit from the abstract base class DialogContentViewModel
. They can then request to close the dialog with the RequestClose
callback of the interface or base class.
If neither implementing nor inheriting is an option, then directly specifying one of the following properties in a view model is fine too, as reflection is used to set the close callback.
Action<DialogResult> RequestClose { get; set; }
Action<bool> RequestClose { get; set; }
❕ Note that the property does not need an setter, but defining one makes reflection a little bit faster, as otherwise the backing field of the property would be manipulated.
The views used for displaying dialogs are provided in separate assemblies. This way they can be styled to anyone's liking. Currently the following such assemblies are available.
Due to breaking changes in MahApps separate packages are available for metro styled dialog views.
Phoenix.UI.Wpf.Architecture.VMFirst.DialogProvider.Metro → MahApps v2.x
.NET Framework | .NET Core | .NET |
---|---|---|
➖ | ✔️ 3.1 | ✔️ 5.0 |
Phoenix.UI.Wpf.Architecture.VMFirst.DialogProvider.MetroLegacy → MahApps v1.6.5
.NET Framework | .NET Core | .NET |
---|---|---|
✔️ 4.5 | ➖ | ➖ |
This NuGet package contains dialog views styled in accordance with Microsoft's Metro design language and is based on MahApps Metro.
Since all the views completely overlap their parent views, they provide a TransparencyToggle in the top right corner that can be used to make the dialog transparent so the underlying view can be seen. The toggle can be disabled via DialogOptions.HideTransparencyToggle
.
Message without title
Message with overflowing content
Multiple Messages
Warning
Error
Error with inner exceptions and stack information
Aggregated error
- Felix Leistner: v1.x - v2.x