How to simulate modal windows inside a single WPF window using anonymous methods ?

Introduction

In our windows applications we are commonly using modal windows. Let's remind the idea. Using windows forms, once a window is created, we can choose to show it in a modal manner (form.ShowDialog()). The window then becomes THE front window between all the other windows of our application. Moreover, all the other windows seem to be disabled. To be exact, the other windows are not responding to any input event anymore (keyboard, mouse, etc), but the are still able to respond to other events like paint.
The user must close the modal window by validating or canceling to come back to the previous state. You can repeat this model and then have a stack of modal windows.

This mechanism also exists using Windows Presentation Foundation. Let's remind that WPF windows are "real" windows even if they are hosting a DirectX surface.

Therefore, WPF brings a bunch of new functionalities that are mainly taking advantage of the control tree (events, datacontext, datatemplates, styles, resources, commandbindings, etc). So it's quite interesting to stay in the same window having an unique control tree.

Moreover, the natural vectorial capabilities of WPF let us imagine a complete nested world inside a single window, recreating a workspace with its own internal windows logic, like we know in games interfaces.

Obviously, you cannot define a child control as being modal. In this article, I will try to offer a solution to simulate this behavior.

How to block controls ?

The first step is to disable all the child controls of a same container, excepted the front one.

Disabling the input interaction will be easily done using:

 

 control.IsEnabled = false;

Let's imagine a method that would add a UserControl on the top of a child control collection of a Grid control, disabling existing children:

 

 void NavigateTo(UserControl uc) 
{
    foreach (UIElement item in modalGrid.Children)
    item.IsEnabled = false;
    modalGrid.Children.Add(uc);
}

To "close" the UserControl as we could close a modal window, re-enabling the previous child control we could write:

 

 void GoBackward() 
{
    modalGrid.Children.RemoveAt(modalGrid.Children.Count - 1);
    UIElement element = modalGrid.Children[modalGrid.Children.Count - 1];
    element.IsEnabled = true;
}

This part is done. Those two methods allow to simulate a stack of graphic controls with a modal behavior. This solution supports pushing multiple controls.

That was the easy part. The next step is more complex.

How to block the calling code ?

Using Windows Forms, calling form.ShowDialog() is blocking.

This means that the following instructions will only be executed when the modal windows will close and return its modal value.

 

 if (f.ShowDialog() == DialogResult.Ok)
{
    //Action OK
}
else
{
    //Action not OK
}

The following actions will only be executed when the modal window is closed, creating a sequential execution, simple and comfortable for the developer.

Creating such a behavior in a single window using WPF is really too complex, almost impossible. Though we will try to simulate it.

We want to run an action at the moment when the modal control is closed. We will use a delegate to represent this action. This delegate will be invoked by the one that is closing the modal control. We will offer him a boolean to represent the modal result.

 

 public delegate void BackNavigationEventHandler(bool dialogReturn);

Thanks to anonymous methods, we will keep a very comparable syntax that we had with windows forms:

 

 NavigateTo(new UserControl1(), delegate(bool returnValue) {
    if (returnValue)
        MessageBox.Show("Return value == true");
    else
        MessageBox.Show("Return value == false");
});

The NavigateTo() method now accepts a second parameter we will have to store somewhere to call it later when closing the control.

As this method will have to support successive calls, an unique value will not be enough to store this delegate. We will use a stack to keep all these delegates:

 

 private Stack<BackNavigationEventHandler> _backFunctions
    = new Stack<BackNavigationEventHandler>();

The NavigateTo() implementation becomes:

 void NavigateTo(UserControl uc, BackNavigationEventHandler backFromDialog)
{
    foreach (UIElement item in modalGrid.Children)
        item.IsEnabled = false;
    modalGrid.Children.Add(uc);
    _backFunctions.Push(backFromDialog);
}

We now need to get the delegate back from the stack (Pop) when calling GoBackward().

 void GoBackward(bool dialogReturnValue)
{
    modalGrid.Children.RemoveAt(modalGrid.Children.Count - 1);
    UIElement element = modalGrid.Children[modalGrid.Children.Count - 1];
    element.IsEnabled = true;

    BackNavigationEventHandler handler = _backFunctions.Pop();
    if (handler != null)
        handler(dialogReturnValue);
}

The one that is closing the control just need to call GoBackward(true); or GoBackward(false);

Make the access global

Last step, it would be useful to provide a global access to these two methods across the application. Doing such, any UserControl could easily call NavigateTo() to push a control and GoBackward() to close it, without knowing the modal context.

Let's group these functionnalities into an interface:

 

 public interface IModalService
{
    void NavigateTo(UserControl uc, BackNavigationEventHandler backFromDialog);
    void GoBackward(bool dialogReturnValue);
}

In our sample, we will simply implement this interface in our main window "Window1". It's quite a natural choice since "modalGrid" is contained in Window1.

A public static scope will provide a global access to the interface:

 

 public class GlobalServices
{
    public static IModalService ModalService
    { 
        get
        {
            return (IModalService) Application.Current.MainWindow;
        } 
    } 
}

Here we are !

We can now call anywhere in our code:

 

 GlobalServices.ModalService.NavigateTo(new UserControl1(), delegate(bool returnValue) 
{ 
    if (returnValue) 
        MessageBox.Show("Return value == true"); 
    else 
        MessageBox.Show("Return value == false"); 
});

and 

 

 GlobalServices.ModalService.GoBackward(true);

Conclusion

Windows Presentation Foundation graphic possibilities are incredible. We now have to create smart ergonomic solutions to take advantage of this engine.
Anonymous methods are really surprising. Strange at first use, they are bringing new and incredible possibilities.

C# 3.0 is coming really soon…

WPFModal.zip