Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

A ContentDialog in a WPF Desktop Application

Rate me:
Please Sign up or sign in to vote.
4.91/5 (15 votes)
24 Feb 2017CPOL21 min read 45.1K   2K   34   4
In a dialog with WPF

Index

Introduction

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:

Image 1

Image 2

Background

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 Architecture

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).

Image 3

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

The ContentDialogService class in the MWindowDialogLib creates an instance that implements the IContentDialogService interface:

C#
public interface IContentDialogService</code>
{
  IMessageBoxService MsgBox { get; }

  IDialogManager Manager { get; }
  IDialogCoordinator Coordinator { get; }

  IMetroDialogFrameSettings DialogSettings { get; }
}

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.

Image 4

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.

Using the Code

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.

Understanding the MDemo Application

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:

Image 5

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 MahApps.Metro Samples below the Dialogs > Menu

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 Input Dialog Demo

The easiest method to explain in the Input Dialog Demo code is the async void ShowDialogFromVM(object context) method:

C#
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  // Return Key => OK Clicked
    };

    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:

C#
public CustomDialog(object contentView
                    , object viewModel = null
                    , IMetroDialogFrameSettings settings = null)
    : base(null, settings)
{
    InitializeComponent();

    // Set the display view here ...
    this.PART_Msg_Content.ChromeContent = contentView;
    this.DialogThumb = this.PART_Msg_Content.PART_DialogTitleThumb;

    // Get a view and bind datacontext to it
    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

Image 6

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:

C#
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 Demo

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:

C#
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:

C#
// Configure 1 progress display with its basic settings
progressColl[0] = new ProgressSettings(0, 1, 0, true // IsInfinite
                                        , progressText
                                        , false        // isCancelable
                                        , isVisible
                                        , closeDialogOnProgressFinished)
{
    Title = "Please wait...",
    Message = "We are baking some cupcakes!",
    ExecAction = GenCancelableSampleProcess()
};

// Configure 2nd progress display with its basic settings
progressColl[1] = new ProgressSettings(0, 1, 0, false // IsInfinite
                                        , progressText
                                        , true        // isCancelable
                                        , 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.

C#
var viewModel = new Demos.ViewModels.ProgressDialogViewModel();
var customDialog = CreateProgressDialog(viewModel);

// Start Task in ProgressViewModel and wait for result in Dialog below
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.

C#
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.

MDemo Summary

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.

Image 7

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 IMessageBoxService Dialog Service

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.

Image 8

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:

  1. The async - await scenario (as ContentDialog only) or
  2. 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:

Image 9

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):

C#
...

            if (settings.MsgBoxMode == StaticMsgBoxModes.ExternalMoveable)
            {
                // Relay drag event from thumb to outer window to let user drag the dialog
                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.

IMessageBoxService Summary

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.

Image 10

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...).

Points of Interest

Async All the Way

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:

C#
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...

Optional Binding

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:

C#
<TextBlock Text="{Binding Title}"
           TextWrapping="Wrap" >
    <TextBlock.Visibility>
        <PriorityBinding>
            <Binding Path="Title" Converter="{StaticResource nullToVisConv}" />
            <Binding Source="{x:Static Visibility.Collapsed}" Mode="OneWay" />
        </PriorityBinding>
    </TextBlock.Visibility>
</TextBlock>

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).

C#
<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>
</Button.Visibility>
<Button.IsEnabled>
    <PriorityBinding>
        <Binding Path="IsEnabledClose" Mode="OneWay" UpdateSourceTrigger="PropertyChanged"/>
        <Binding>
            <Binding.Source>
                <sys:Boolean>True</sys:Boolean>
            </Binding.Source>
        </Binding>
    </PriorityBinding>
</Button.IsEnabled>
<Button.Content>
    <Grid>
        <TextBlock Text="r" FontFamily="Marlett" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="0,0,0,1" />
    </Grid>
</Button.Content>
</Button>

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.

Focus On

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:

XML
<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:

XML
<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:

XML
<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;

C#
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)
        {
            // Lets set a focus only if there is no default button, otherwise
            // the button will be focused via binding and behaviour in xaml...
            // But the focus should be gotten for sure since users can otherwise
            // tab or cursor navigate the focus outside of the content dialog :-(
            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.

Conclusions

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.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
The Windows Presentation Foundation (WPF) and C# are among my favorites and so I developed Edi

and a few other projects on GitHub. I am normally an algorithms and structure type but WPF has such interesting UI sides that I cannot help myself but get into it.

https://de.linkedin.com/in/dirkbahle

Comments and Discussions

 
QuestionVery nice Pin
DotNetDev201623-Feb-17 23:22
DotNetDev201623-Feb-17 23:22 
AnswerRe: Very nice Pin
Dirk Bahle24-Feb-17 5:06
Dirk Bahle24-Feb-17 5:06 
GeneralRe: Very nice Pin
DotNetDev201625-Feb-17 0:57
DotNetDev201625-Feb-17 0:57 
GeneralRe: Very nice Pin
Dirk Bahle25-Feb-17 22:46
Dirk Bahle25-Feb-17 22:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.