Index
This article documents a WPF dialog service implementation that can show message boxes as ContentDialogs and supports a wide range of other dialogs (Progress, Login ...) that can be implemented as ContentDialog. A ContentDialog is a view that looks like a dialog but is part of the content of a window:
It was back in 2013 when had written [1] a replacement for the standard .Net MessageBox. My replacement used WPF, MVVM and a service oriented architecture. The world of computing has changed towards the Internet of Things (smart phones, tablets and so forth) and these little gadget-apps start to influence the design of modern desktop applications.
This is why I wanted to re-implement my message box service implementation [1] and cast it into a ContentDialog driven implementation using the same or a very similar API as before. Towards this end, I have found only the MahApps.Metro project to contain useful hints on this subject. So, I have taken this project as reference and cut out what I needed, refactored it into a service oriented architecture and ended up with a set of libraries I now refer to as MLib framework.
One large part of the MLib framework is the ContentDialog part which has some functionalities that are very similar to MahApps.Metro but also contains functionality that was added later on and cannot be found anywhere else.
The MDemo component is the main executable in this sample application. The MLib library contains mostly theming definitions, such as, control definitions and so forth, while the 3 MWindow components:
- MWindowLib, MWindowInterfaceLib, MWindowDialogLib,
lead us to the components that define what a MetroWindow is (MWindowLib) and how ContentDialogs can be displayed in it (see ContentDialogService
in MWindowDialogLib).
The deployment diagram is completed with the Settings [2] and the ServiceLocator [3] component which are described elsewhere. The IContentDialogService drives the ContentDialogs in this project, so lets detail these next.
The ContentDialogService
class in the MWindowDialogLib creates an instance that implements the IContentDialogService
interface:
public interface IContentDialogService</code>
The first 3 properties in the IContentDialogService
interface expose service components that implement specific services while the last property is a kind of helper property that ensure that the application behaves consistent when showing multiple ContentDialogs during its live time.
The IMessageBoxService
is the service that I had implemented 4 year ago [1] and got re-implemented here in MWindowDialogLib.Internal.MessageBoxServiceImpl
. The IDialogManager
and the IDialogCoordinator
interfaces represent services that are also implemented in the MWindowDialogLib.Internal namespace. They support the IMessageBoxService
in its async and non-async implementation and also support a CustomDialog
implementation, as we will see below.
The IMetroWindowService
interface describes a service that can create external modal MetroWindow dialogs. The corresponding instance is initialized and injected in the ServiceInjector
class in the MDemo project.
We detail these items in the form of samples next. This should help us to complete the picture of more than 100 samples in this project.
The attached MLib.zip code requires Visual Studio 2015 Community Edition or better. Set MDemo as Start Up Project and compile. This will download components via Nuget. So, you might have to enable Nuget in order to compile the code after downloading the zip file.
The code presented here implements a dialog service that can display dialogs in an async context or in a normal blocking context. It shows how the same service interface can be used to support more than one type of dialog (Message, Progress, Login ...) as a ContentDialog or a standard modal dialog.
The demo includes more than 100 samples so take your time and come back to the article when you feel like it.
The MDemo application shows off many samples and different dialogs that are supported with the MLib framework. It contains 2 parts, a menu driven list of samples under Dialogs > and the list of 17 buttons x 2 in the content of the MainWindow:
The demos under the Dialogs > menu entry are equivalent to the demo dialogs that can be found in the MahApps.Metro project, although, the technical implementation is quit different. The double list of buttons below Async Test and Sync Test is equivalent to the Message Box demo application that I've published earlier on Codeplex [1] but this time we find support for many more use cases, such as , ContentDialog, async and non-async, modal and so forth.
The dialog samples that originate from the MahApps.Metro project can structured in 5 types of dialogs:
- Custom Dialog
- Message Dialog
- Input Dialog
- Login Dialog
- Progress Dialog
each of these dialogs pretty much supports what the name says. But they are all based on one dialog within the MLib framework. The dialog the are based on is the CustomDialog
in the MWindowDialogLib.Dialogs
namespace. You might wonder how on earth this could be possible and I am going to tell you next. Each of the above demos is obvisously initiated through the MainWindow but the backend functions are located in their respective demo viewmodels:
- MDemo.Demos.CustomDialogDemos
- MDemo.Demos.MessageDialogDemos
- MDemo.Demos.InputDialogDemos
- MDemo.Demos.LoginDialogDemos
- MDemo.Demos.ProgressDialogDemos
As far as I know, every demo initiated through the menu is routed through these classes - so locating the correct code for each sample should be a piece of cake :-) Now, lets have a look at how these samples work by scrutenizing the InputDialogDemo (to start of with a simple sample) and continue with the Progress dialog (to finish with a rather involved and complex sample).
The easiest method to explain in the Input Dialog Demo code is the async void ShowDialogFromVM(object context)
method:
internal async void ShowDialogFromVM(object context)
{
var viewModel = new Demos.ViewModels.InputDialogViewModel()
{
Title = "From a VM"
, Message = "This dialog was shown from a VM, without knowledge of Window"
, AffirmativeButtonText = "OK"
, DefaultResult = DialogIntResults.OK
};
var customDialog = new MWindowDialogLib.Dialogs.CustomDialog(new Demos.Views.InputView(), viewModel);
var coord = GetService<IContentDialogService>().Coordinator;
var result = await coord.ShowMetroDialogAsync(context, customDialog);
}
This method creates an InputDialogViewModel
and hands it over to the class constructor of the CustomDialog
class along with an instance of an InputView
object. The constructor of the CustomDialog
class assigns the view to its content and the ViewModel to its DataContext
property:
public CustomDialog(object contentView
, object viewModel = null
, IMetroDialogFrameSettings settings = null)
: base(null, settings)
{
InitializeComponent();
this.PART_Msg_Content.ChromeContent = contentView;
this.DialogThumb = this.PART_Msg_Content.PART_DialogTitleThumb;
this.DataContext = viewModel;
this.Loaded += MsgBoxDialog_Loaded;
}
The DataContext
and viewmodel part is straightforward provided that you do have some experience with WPF. But what about the ChromeContent
stuff, whats that exactly? Well, it turns out that a CustomDialog
can visually be decomposed into 3 main visual items:
- MWinodwLib.Dialogs.
DialogFrame
- MWindowLib.Dialogs.
DialogChrome
- and the
View
The above schematic view gives you an idea on how the 3 layers on the right are composed over each other to make up one single item as shown on the left. It turns out, the statement PART_Msg_Content.ChromeContent
refers to a DialogChrome
object and its content which is a bound ContentControl
inside a ScrollViewer
. So, the ChromeContent
really is equivalent to the blue area in the above schematic. I have chosen this design because having a title and a close button often seems handy and having the default dialog behavior (Cancel with Escape and Accept with Enter) also seems to be useful in many if not all cases.
The default dialog behavior is implemented in the DialogFrame
class. All dialogs, be it a CustomDialog
or an MsgBoxDialog
, are based on the DialogFrame
class to inherit its behavior. The DialogFrame
gives it a natural and consistent look and feel, although, the tile and close button may not always be needed either they are usually welcome to be there. The view (e.g. InputView) can be any UserControl that could be inserted here.
This design gives us the freedom to insert any view we see fit and connect it with any viewmodel to be displayed as ContentDialog inside the MainWindow of the application. The construction code handles view and viewmodel as object
so as to require no special properties or methods on these items.
Note however, that every view being displayed in the CustomDialog
must either have:
- a timed live span that can be await with an async ShowDialog call,
See CustomDialogDemo.ShowCustomDialog
- a custom event that closes the dialog, or
See CustomDialogDemo.ShowAwaitCustomDialog
- a viewmodel that implements the close mechanism that is supported by
DialogFrame
control:
See InputDialogDemos.ShowDialogFromVM
If none of the above methods match your requirements you might have to invent your own or your dialog may never be closed - which does not seem to be useful either. The base viewmodel that implements the standard dialog behavior consistently with the DialogFrame
control is the MsgDemoViewModel
. So, coming back to the code sample at the beginning of this section we are now ready to note that it really constructs a view and viewmodel and injects them into the CustomDialog
. The last lines of code in the above sample:
var coord = GetService<IContentDialogService>().Coordinator;
var result = await coord.ShowMetroDialogAsync(context, customDialog);
access the ContentDialogService via its registered interface in the MDemo.ServiceInjector
class. The Coordinator.ShowMetroDialogAsync
method translate the context object parameter into a reference to a IMetroWindow
via the binding registry in the MWindowDialogLib.Dialogs.DialogParticipation
class. This reference is then used to call the equivalent DialogManager.ShowMetroDialogAsync
method which inserts the ContentDialog into the MetroActiveDialogContainer, wait for its load to complete, and waits for the WaitForButtonPressAsync()
method to complete, - in order to unload the dialog again.
The other dialog demo classes I`ve noted above (message, input, login etc.) are very similar in the way their methods are named and their code functions. So, understanding them should be possible based on the above explanation, if you take a minute and let Visual Studio´s CodeLense walk you through the project. But there are 2 other items that are similar but different: The progress dialog demo and the refactored IMessageBoxService
dialog service, which we will explain next.
The progress dialog is different from the other ´normal´ dialogs, because it does seem useful to have:
- progress dialogs that close automatically when a progress has finished succesfully.
A progress dialog may require more than 1 button click interaction, because we might want to be able to:
- cancel a progress,
- wait for result to display that ending status,
- and close the dialog.
In addition, it might be useful to:
- start a processing task with an in infinite progress display,
so as to say, gathering information for a finite processing and progress display, and
- continue with the finite progress display, so as to say, processing step 1 of n, please wait.
The demo in the ProgressDialogDemos
class demonstrates the above use cases. We detail the last use case next since it is the most complex one and partially covers the other demonstrations, as well. So, lets look at the Show2CancelProgressAsync
method now:
async Task<int> Show2CancelProgressAsync(IMetroWindow parentWindow
, bool closeDialogOnProgressFinished = false)
This demo is called directly from the MainWindow's code, but we could also abstract the concrete view away, if we used the registration and context object approach via the DialogParticipation
class as explained above.
So, lets look at the method itself:
progressColl[0] = new ProgressSettings(0, 1, 0, true
, progressText
, false
, isVisible
, closeDialogOnProgressFinished)
{
Title = "Please wait...",
Message = "We are baking some cupcakes!",
ExecAction = GenCancelableSampleProcess()
};
progressColl[1] = new ProgressSettings(0, 1, 0, false
, progressText
, true
, isVisible
, closeDialogOnProgressFinished)
{
Title = "Please wait... some more",
Message = "We are baking some cupcakes!",
ExecAction = GenCancelableSampleProcess()
};
The above code initializes an array of 2 progress configuration objects, the first being an infinite non-cancelable progress while the second is finite and can be canceled by the user. This use case may work if the first stage of determining the processing scope is quick while the second stage may take long but can be observed in detail (e.g. step 1- 10) by the user.
The next 3 lines below initialize the viewmodel and view with the viewmodel being assigned to the DataContext
property via the constructor. The array of setting objects that we created above is handed over to the StartProcess
method that starts a processing task in a shoot and forget fashion. That is, control returns immediately to the GetService<IContentDialogService>()
line, which looks up the IContentDialogService
to show and await the progress dialog result.
var viewModel = new Demos.ViewModels.ProgressDialogViewModel();
var customDialog = CreateProgressDialog(viewModel);
viewModel.StartProcess(progressColl);
var dlg = GetService<IContentDialogService>();
var manager = dlg.Manager;
var result = await manager.ShowMetroDialogAsync(parentWindow, customDialog);
Console.WriteLine("Process Result: '{0}'", viewModel.Progress.ProcessResult);
return result;
The progress dialog can be closed in many ways but most of them are routed through
- the
CloseCommand
which is invoked when the user clicks the Close button or the (X) button in the upper right corner of the dialog, or
- if the code invokes the viewmodel´s
OnExecuteCloseDialog()
method on end of progress.
What is going on depends on the configuration array mentioned above. But how does that work in the fire and forget fashion of the StartProcess
method? Lets have a look at that as well:
The heart of the array of the progress setting objects is this property:
public Action<CancellationToken, IProgress> ExecAction { get; set; }
Its an abstract representation of a void
method that accepts 2 parameters:
- a
CancellationToken
and
- an object that implements the
IProgress
interface.
The actual void
method that is called must not necessarily be known at compile time, but can be assigned at run-time and be invoked through the above property. So, the .Net framework invokes the assigned method (more exactly Action
), in our case generated either in:
- private Action<CancellationToken, IProgress> GenSampleNonCancelableProocess() or
- private Action<CancellationToken, IProgress> GenCancelableSampleProcess()
The invocation is basically taking place in the foreach
loop of the below code sample. Here, the ProgressViewModel resets itself according to the current settings, check if there was any request to cancel:
- _CancelToken.ThrowIfCancellationRequested(); // throws an
Exception
if yes)
and starts the assigned void method:
- item.ExecAction(_CancelToken, Progress);
giving it access to the cancellation token and IProgress
interface to react on a request for cancellation or display the current state of the progress in the dialog.
internal void StartProcess(ProgressSettings[] settings)
{
_CancelTokenSource = new CancellationTokenSource();
_CancelToken = _CancelTokenSource.Token;
Progress.Aborted(false, false);
IsEnabledClose = false;
SetProgressing(true);
Task taskToProcess = Task.Factory.StartNew(stateObj =>
{
try
{
foreach (var item in settings)
{
this.ResetSettings(item);
_CancelToken.ThrowIfCancellationRequested();
item.ExecAction(_CancelToken, Progress);
}
}
catch (OperationCanceledException)
{
Progress.Aborted(true, true);
}
catch (Exception)
{
}
finally
{
SetProgressing(false);
IsEnabledClose = true;
if (CloseDialogOnProgressFinished == true &&
Progress.AbortedWithCancel == false && Progress.AbortedWithError == false)
{
OnExecuteCloseDialog();
}
}
});
}
It should be obvious that the exception handlers should be more complete, for example, with a throw statement to give users a chance to understand why something may not work when it does not. The least one should do here is to show a message box with the exception and/or log the exception with something like Log4Net. The OperationCanceledException
is handled with the Progress.Aborted()
method call. In more complex scenarios, this could also involve a clean-up/dispose method that could also be defined via another Action()
property parameter definition.
The last if block in the above code performs the call to the close dialog method, which will close the dialog automatically, if the progress was configured so, and if there was no Cancel or Errror involved.
The diagram below summarizes the items that we have seen in the last sections. The IBaseMetroDialigFrameViewModel<TResult>
interface in the MWindowInterfaceLIb indicates the basic items that should be implemented in a dialog's viewmodel to succesfully implement another kind of CustomDialog. This interface is implemented in the MsgDemoViewModel
which is the base of all dialogs in the MDemo assembly.
The DialogIntResult and DialogStatChangedEventArgs classes are useful base classes if you decide to build a dialog based on IBaseMetroDialigFrameViewModel<int>
type of interface as I did here.
It should be mentioned that the resulting dialog value is not limited to any number of buttons or a particular datatype since the base is flexibly defined with IBaseMetroDialigFrameViewModel<TResult>
.
Phew, I guess thats all I can think of the progress dialog demo right now.
We should realize that all the code in the Demos namespace of the MDemo application should normally be hidden away in a separate assembly. So, lets have a look at the IMessageBoxService
implementation to understand exactly how this could be done in the next section below.
The built in Message Box service is based on the interface that I designed and implemented a few years ago. The enumeration that configures a MsgBoxDialog in the MWindowInterfacesLib.MsgBox.Enums namespace are the same, except for the StaticMsgBoxModes
which is detailled further below.
The MsgBoxResult
enum configures the results that can be optained while the MsgButtons
and MsgBoxImage
enumerations configure the buttons and image shown in the dialog (see also IMessageBoxService.cs on CodePlex). You should be able to re-use the ContentDialog version without much pain since I made sure that the old API is mostly still available and extended with new settings to take advantage of new features.
Backward compatibility is granted because I was able to re-use most of the code with only minor changes and I also implemented the test page with the 17 predefined tests implemented years ago. Here is a sample for a message box display:
var msg = GetService<IContentDialogService>().MsgBox;
var result = await msg.ShowAsync("Displays a message box", "WPF MessageBox");
This service gives you the same options in terms of viewing message dialogs but this time you can choose, whether the message box should support:
- The async - await scenario (as ContentDialog only) or
- The normal modal blocking scenario with 3 options for display:
- A ContentDialog, or
- A modal fixed dialog displayed over the main window, or
- A modal dragable dialog displayed over the main window.
The first async scenario is covered with the IMessageBoxService
method calls that are named ShowAsync
while the second scenario is covered with the Show
methods calls as in the previous implementation.
A ContentDialog is a UserControl
that is displayed as part of the MainWi
scenaro
ndow´s content. A modal fixed dialog is a modal MetroWindow
dialog in the traditional sense, but it is displayed over the MainWindow as if it was a ContentDialog. So, a modal fixed dialog is a good approximation to a ContentDialog if the actual ContentDialog is not possible.
A modal movable or dragable dialog is a modal dialog that is displayed over the main window but can be dragged away with the title bar:
The 3 options for the second scenario above can be configured with the:
IMetroDialogFrameSettings
DialogSettings { get; }
property in the IContentDialogService
interface. This DialogSetting is evaluated through the:
protected StaticMsgBoxModes
MsgBoxModes { get; }
property of the IMessageBoxService
service to determine whether a UserControl
or a MetroWindow
should be constructed. The MetroWindow
is made dragable by attaching a MetroThump
control in the View (UserControl based on IMsgBoxDialogFrame<MsgBoxResult>
) to the corresponding event handlers in the window (see DialogManager.ShowModalDialogExternal
() method):
...
if (settings.MsgBoxMode == StaticMsgBoxModes.ExternalMoveable)
{
if (dlgControl.DialogThumb != null && dlgWindow is IMetroWindow)
((IMetroWindow)dlgWindow).SetWindowEvents(dlgControl.DialogThumb);
}
...
This last tweak was necessary because the MsgBoxView
(UserControl
) completely overlays the original dialog, making the thumb in its DialogChrome
inaccessible for the mouse cursor. So, the modal dialog is actually a stack of 4 main layer items: The MetroWindow
(at the bottom) with the 3 layers (DialogFrame, DialogChrome, and View as shown in the above schematic view) on top of the window.
The diagram below shows another way of implementing a ContentDialog view based on the IBaseMetroDialogFrame
. I did not implement this interface for the CustumDialog above because I wanted the CustomDialog to be more flexible and less complicated ,therefore, I ommited things like SetZIndex because I did not feel that it would be necesary there.
The resulting interface IMsgDialogFrame<TResult>
is also flexible in terms of the results it can report back to the caller. I have obviously implemented what I ued before but you are free to roll your own implementation using a completely different TResult
enumeration or datatype (int etc...).
The saying 'async all the way' refers to the recommendation that you should call async code with ascync code and so forth. That is, if you start using async statements, you should use it up to the root of a call to ensure that your code behaves consistently since you will otherwise encounter strange behaviours, which are difficult to find or fix.
I've learned a better understanding of async and await in this project. And while many people told me that I should not artificially block on async code [4], it might be necessary at times. In this project it was necessary to block the async code in order to support the old API while delivering the new UI. I was lucky to uncover the WPF tip from Stephen Toub and it never failed for me as I implemented it in the MessageBoxServiceImpl
class of the MWindowDialogLib
project:
public static void WaitWithPumping(this Task task)
{
if (task == null) throw new ArgumentNullException(“task”);
var nestedFrame = new DispatcherFrame();
task.ContinueWith(_ => nestedFrame.Continue = false);
Dispatcher.PushFrame(nestedFrame);
task.Wait();
}
The above code is hard to find, because many sources say, correctly, that you should not block on your async code [4]. But the decision should be left to those who decide and not to those that post answers in a forum...
Designing an XAML in WPF is not always clear because a button may be useful in some use case but not necessary or wanted in others. I used to use a Visibility
property on the viewmodel to hide an element in these cases. An equivalent but more elegant solution is to hide an element when its binding is not available. I call such bindings optional, because the binding is not required and there will be no error or warning, if it is not there. Here is a sample code from the DialogFrame
control discussed earlier:
<TextBlock Text="{Binding Title}"
TextWrapping="Wrap" >
<TextBlock.Visibility>
<PriorityBinding>
<Binding Path="Title" Converter="{StaticResource nullToVisConv}" />
<Binding Source="{x:Static Visibility.Collapsed}" Mode="OneWay" />
</PriorityBinding>
This above listing shows the TextBlock that displays the Title of a dialog inside the DialogFrame
control. But there are dialogs that do not need a Title. So, the PriorityBinding
figures out, if the Title can be bound or not (via the nullToVisConv converter that returns a visibility recommendation). The PriorityBinding
will evaluate the second option, if the first one cannot be bound. The second option cannot be missed and will always evaluate to Visibility.Collapsed
- hidding the TextBlock and making the airspace available for other elements.
The next listing shows a similar but more advanced approach for the Close button of the DialogFrame
control. Here the IsEnabledClose
property could also be optional if a dialog should be closeable in all situation (e.g. MessageBoxDialog) but the property should be present and useful if a dialog has a state where closing it can lead to disaster (e.g. ProgressDialog).
<Button Command="{Binding CloseCommand}"
ToolTip="close"
Style="{DynamicResource {x:Static reskeys:ResourceKeys.WindowButtonStyleKey}}">
<Button.Visibility>
<PriorityBinding>
<Binding Path="CloseWindowButtonVisibility" Converter="{StaticResource BoolToVisConverter}" Mode="OneWay" UpdateSourceTrigger="PropertyChanged"/>
<Binding Source="{x:Static Visibility.Collapsed}" Mode="OneWay" />
</PriorityBinding>
The effect of optional binding is that the view can gracefully handle situations in which a binding is unavailable. The same view can be more flexible because it can handle more situations without extra support from a viewmodel.
Making a ContentDialog modal is a bit of a nightmare [5]. I had problems to ensure that a user cannot activate a control that is outside the ContentDialog. This is particularly difficult, because WPF seems to have a few ways of letting users activate other controls (e.g.: cursor keys, tab, etc). The best solution I found here was to completely disable the MainWindow and set the focus into a dialog area that is surrounded by a non-focusable area.
So, the first part of the mission - disabling everything in the MainWindow- is achieved by adding a new bool
IsContentDialogVisible dependency propery into the MetroWindow control. This property is set/unset in every method that shows or hides a dialog. The IsContentDialogVisible property is true, if one or more dialog(s) are currently shown, and it is otherwise false. There is a triggers in the XAML of MetroWindow.xaml that makes the window buttons non-focusable when a ContentDialog is shown:
<Trigger Property="IsContentDialogVisible" Value="true">
<Setter TargetName="Restore" Property="Focusable" Value="false" />
<Setter TargetName="Maximize" Property="Focusable" Value="false" />
<Setter TargetName="Minimize" Property="Focusable" Value="false" />
<Setter TargetName="Close" Property="Focusable" Value="false" />
</Trigger>
...and there is an entry in MainWindow.xaml that disables the main menu on the same condition via converter:
<Menu IsEnabled="{Binding Path=IsContentDialogVisible, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type MWindow:MetroWindow}}, Converter={StaticResource InverseBooleanConverter}}">
The second part of the mission - setting focus and giving no chance to escape the ContentDialog - is achieved with a design decision where I made the inner Border of the dialog non-focusable:
<ControlTemplate TargetType="{x:Type Dialogs:DialogFrame}">
<ControlTemplate.Resources>
<Storyboard x:Key="DialogShownStoryboard">
<DoubleAnimation AccelerationRatio=".9"
BeginTime="0:0:0"
Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity"
To="1" />
</Storyboard>
</ControlTemplate.Resources>
<Grid Background="{TemplateBinding Background}">
<Border FocusVisualStyle="{x:Null}"
Focusable="False"
BorderBrush="{DynamicResource {x:Static reskeys:ResourceKeys.DialogFrameBrushKey}}"
BorderThickness="1"
>
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Loaded">
<EventTrigger.Actions>
<BeginStoryboard Storyboard="{StaticResource DialogShownStoryboard}" />
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
...and gave the DialogChrome
, that is placed inside the above dialog (in-place of the ContentPresenter) the ability to focus and keep the focus via the Keyboard navigation cycle setting:
<UserControl x:Class="MWindowDialogLib.Dialogs.DialogChrome"
...
Focusable="True"
FocusVisualStyle="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
KeyboardNavigation.ControlTabNavigation="Cycle"
> ...
The above XAML on focusability is the pre-requisite for the code that executes in the CustomDialogs load event;
private void MsgBoxDialog_Loaded(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
bool bForceFocus = true;
var vm = this.DataContext as IBaseMetroDialogFrameViewModel<int>;
if (vm != null)
{
if ((int)vm.DefaultCloseResult > 1)
{
bForceFocus = false;
}
}
if (bForceFocus == true)
{
this.Focus();
if (this.PART_Msg_Content != null)
this.PART_Msg_Content.Focus();
}
}));
}
This code attempts to either set a focus on the DialogChrome we discussed above or lets a button acquire the focus if the ViewModel indicates that we should have default button. The XAML can use the SetKeyboardFocusWhenIsDefault
behavior to set a focus on a default button at load time, if it was marked as default (via binding or static IsDefault property).
I am by no means an expert for focusing issues, but I tried different settings and situations through research and combinational theory. And this was the best solution I was able to come up with. Any comments on this are particularly welcome.
The ContentDialogService presented in this article shows how flexible a WPF controls library can be, because I took quit a bit of tested and working source code - that was about 4 years old [1] - and had not much trouble to make it work in a context that is similar but quit different to the original implementation. Therefore, I am convinced that WPF with MVVM really is a milestone towards software re-usability and user oriented UI design.
We can verify with this project that classic software architecture patterns like service oriented interfaces are still a great base for building software. And the WPF binding techniques make the resulting UI even more flexible.
Software engineering is not just about interfaces, algorithms and structures. WPF also requires visual design and decomposition of UI elements (aka frame, chrome, view) to deliver the best and most flixible solution there is. We can verify the flexibility of MVVM through the 4 different dialogs (Progress, MessageBox, Input, and Login) that are all based on only 1 CustomDialog (view) implementation.