Click here to Skip to main content
15,881,248 members
Articles / Desktop Programming / WPF
Article

WPF Quick Recipes - Dialogs

Rate me:
Please Sign up or sign in to vote.
4.50/5 (5 votes)
15 Mar 2010BSD8 min read 28.3K   289   25   3
A Blend behaviour that can be used for displaying a ViewModel in a separate window

Introduction

This article provides a simple solution for opening and closing a dialog that displays the implicit DataTemplate of a ViewModel. Opening dialogs when you have structured your solution using a Model-View-Presenter/Model-View-ViewModel pattern can be awkward. When you have button click handlers all over in the code-behind files you don't have this problem, but when you are striving to make your application driven only through commands and bindings, this becomes an issue. Because you want loose coupled layers, separation of concerns, etc. So code like this in the ViewModel would look really ugly:

C#
private ICommand displayInvoice;

public ICommand DisplayInvoice
{
    get
    {
        return displayInvoice ?? (displayInvoice = new DelegateCommand(
            a =>
            {
                var dialog = new Window();
                var invoiceViewModel = new InvoiceViewModel();
                
                dialog.DataContext = invoiceViewModel;
                dialog.Content = invoiceViewModel;
                dialog.ShowDialog();
                
                if (invoiceViewModel.IsApproved)
                {
                    // process invoice
                }
            }));
    }
}

The UtilityDialog

There are a lot of ways of doing things in WPF. Simplifying a little bit, there are two main approaches for the UI creation: you can build your entire visual tree using tight coupled controls, and after that, set DataContexts for all the nodes in the tree that require so; at the other end, you only set a DataContext for your root control (usually the main Window) and rely on implicit DataTemplates from there to work their magic. I tend to use the latter approach and this article is better suited for this. In this way, you can have a single window for all your 'dialog needs', as long as what you need to display is a View for a specific ViewModel. I use a UtilityDialog control, which is actually a Window with some properties set, so it is styled like a ToolWindow (but you could change this, or even have one for each type of dialog you need). When I want to display a dialog, I simply create a new instance of this window and set its DataContext to a ViewModel which has an implicit DataTemplate defined somewhere in the resource scope. The Content of the UtilityDialog binds to the DataContext so the template will be loaded and displayed. This eliminates having a Window for every ViewModel that needs one. The only thing left to figure out is how to open/close this dialog.

A First Approach

I used to handle this by raising a plain C# event, combined with an attached behaviour. All the ViewModels that have to open a dialog would need implement a simple interface:

C#
public interface IDialogOwner
{
    event EventHandler<RequestOpenDialogEventArgs> RequestOpenDialog;
}

public sealed class RequestOpenDialogEventArgs : EventArgs
{
    public RequestOpenDialogEventArgs(object dialogDataContext)
    {
        DialogDataContext = dialogDataContext;
    }
    
    public object DialogDataContext
    {
        get;
        private set;
    }
}

Suppose we want to display an invoice form to a customer and let him agree or disagree with the payment terms, and we want to do this using a modal dialog. We have a nice dedicated ViewModel for this, with commands for approve and reject:

C#
public sealed class InvoiceViewModel : IRemovable
{
    public bool IsApproved
    {
        get;
        private set;
    }
    
    private ICommand approve;
    
    public ICommand Approve
    {
        get
        {
            return approve ?? (approve = new DelegateCommand(
                a =>
                {
                    IsApproved = true;
                    RequestRemove(this, EventArgs.Empty);
                }));
        }
    }
    
    private ICommand reject;
    
    public ICommand Reject
    {
        get
        {
            return reject ?? (reject = new DelegateCommand(
                a =>
                {
                    IsApproved = false;
                    RequestRemove(this, EventArgs.Empty);
                }));
        }
    }
    
    public event EventHandler RequestRemove = delegate { };
}

The approving or rejecting is done through ICommands, so we would bind these to ICommandSource objects (like Buttons) in order to approve/reject the invoice. Once one of these commands is invoked, we also want to close the dialog. To avoid code-behind we have to introduce yet another interface, for ViewModels that are displayed using dialogs and which need to close:

C#
public interface IRemovable
{
    event EventHandler RequestRemove;
}

Displaying the invoice requires raising the RequestOpenDialog event:

C#
public sealed class MainViewModel : IDialogOwner
{
    private ICommand displayInvoice;
    
    public ICommand DisplayInvoice
    {
        get
        {
            return displayInvoice ?? (displayInvoice = new DelegateCommand(
                a =>
                {
                    var invoice = new InvoiceViewModel();
                    RequestOpenDialog(this, new RequestOpenDialogEventArgs(invoice));
                    
                    if (invoice.IsApproved)
                    {
                        // process invoice
                    }
                }));
        }
    }
    
    public event EventHandler<requestopendialogeventargs /> RequestOpenDialog = delegate { };
}

The two events used in our ViewModels - RequestRemove and RequestOpenDialog are monitored by two attached behaviours (since it is such a nice-to-have feature these days). They are implemented by taking advantage of the Blend's System.Windows.Interactivity assembly, but you can do it as well in the 'traditional' way as advertised by the original creator of the pattern, John Gossman. These behaviours must be set on the views associated with the ViewModels that raise the events (in this case, on the main window and on the UtilityDialog).

C#
public sealed class CloseWindowBehavior : Behavior<Window>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        
        EventHandler closeWindow = (a, b) => AssociatedObject.Close();
        
        Action<object> hookRequestRemove = (a) =>
        {
            var removable = a as IRemovable;
            
            if (removable != null)
            {
                removable.RequestRemove += closeWindow;
            }
        };
        
        Action<object> unhookRequestRemove = (a) =>
        {
            var removable = a as IRemovable;
            
            if (removable != null)
            {
                removable.RequestRemove -= closeWindow;
            }
        };
        
        hookRequestRemove(AssociatedObject.DataContext);
        
        AssociatedObject.DataContextChanged += (a, b) =>
        {
            unhookRequestRemove(b.OldValue);
            hookRequestRemove(b.NewValue);
        };
    }
}

CloseWindowBehavior handles the DataContextChanged event for the associated window. When this is raised, if the new DataContext is an IRemovable, a handler is added for the RequestRemove event. This handler closes the associated window.

C#
public sealed class OpenDialogBehavior : Behavior<Window>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        
        EventHandler<RequestOpenDialogEventArgs> openDialog = (a, b) =>
        {
            var dialog = new UtilityDialog();
            dialog.DataContext = b.DialogDataContext;
            dialog.Owner = AssociatedObject;
            dialog.ShowDialog();
        };
        
        Action<object> hookRequestOpenDialog = (a) =>
        {
            var dialogOwner = a as IDialogOwner;
            
            if (dialogOwner != null)
            {
                dialogOwner.RequestOpenDialog += openDialog;
            }
        };
        
        Action<object> unhookRequestOpenDialog = (a) =>
        {
            var dialogOwner = a as IDialogOwner;
            
            if (dialogOwner != null)
            {
                dialogOwner.RequestOpenDialog -= openDialog;
            }
        };
        
        hookRequestOpenDialog(AssociatedObject.DataContext);
        
        AssociatedObject.DataContextChanged += (a, b) =>
        {
            unhookRequestOpenDialog(b.OldValue);
            hookRequestOpenDialog(b.NewValue);
        };
    }
}

OpenDialogBehavior also monitors the DataContextChanged event of its associated window. When this is raised, a handler is added for the RequestOpenDialog event (provided that the DataContext is a valid IDialogOwner entity). This handler creates a new instance of the UtilityDialog control, sets its DataContext to be the ViewModel received through the event arguments (an InvoiceViewModel object in this case) and displays the dialog.

Have you spotted the problems already? We're going through all this pain and create interfaces and attached behaviors and raise events in order to decouple the ViewModel from the View. But we haven't achieved this. We simply moved the code from one place to another (and added additional code along the way) but the ViewModel is still 100% coupled to the View. It may not instantiate a Window object inside the command's execute delegate, but:

  1. It knows that it opens a dialog, which is a View detail and shouldn't concern the ViewModel: RequestOpenDialog - the name says it all.
  2. In this particular case, it abuses the fact that it knows that the dialog will be opened as modal (and this is a mistake I made throughout an entire project, because I never needed non-modal dialogs) so the code to handle the state of the invoice object is written immediately after the open request, because since the dialog will be displayed as modal, the method will halt execution and wait.
  3. What if in another View for the same ViewModel (maybe another skin), we don't want to open a dialog? Maybe we want a custom control to come into view bouncing around in the same window. This means we will need to modify the ViewModel to handle that additional logic.

The Hidden Gem

At first sight, the FrameworkElement does not make very much sense in a XAML file. We use it in code for polymorphism mainly, but in XAML it doesn't seem much helpful since it does not have a visual appearance. Still, some of the most interesting tricks in XAML-only scenarios I know of are based on the FrameworkElement, and the fact that it does not actually have a visual template is exactly what makes it valuable.

  • Take a look at these examples by Charles Petzold: AllXamlClock.xaml (example, article), AnimatedSpiral1.xaml (example, article) and WindDownPendulum.xaml (example, article). These require no code and you can load them right away in an XAML-ready app (like IE or Kaxaml) to see them in action. In the first example, he uses a FrameworkElement in order to store in its Tag property the current time (the system time at the point when the XAML is loaded). Then he applies some clever transforms on this initial value in order to make the clock spin. In the other two examples, the FrameworkElement hosts some compound transforms which are used to define a spiral-like movement of an object and, respectively, to mimic a pendulum's friction with air, which slows it down gradually. Pretty slick!
  • Josh Smith's VirtualBranch takes advantage of the FrameworkElement in order to provide data to objects that are not part of the visual tree (and therefore cannot use bindings because they do not inherit the DataContext).
  • Dr. WPF couldn't miss this list. Among the plenitude of WPF advices the good doctor gives to his patients there is the 'attached dependency property theft'... sorry I meant borrowing scheme, when you use an attached dependency property from a built-in framework type in an XAML-only scenario, for testing a proof of concept of some sort and you need one or two extra properties (ok, so there are not that many useful FrameworkElement attached properties you can borrow, but the technique is worth being remembered).

With all these in mind, the solution to our dialog problem becomes stupid simple. Take a look at this piece of XAML:

XML
<FrameworkElement DataContext="{Binding Path=CurrentInvoice}">
    <i:Interaction.Behaviors>
        <w:DialogBehavior DisplayModal="True"/>
    </i:Interaction.Behaviors>
</FrameworkElement>

Instead of using an attached behaviour that monitors events raised by specific interface implementors, we could monitor instead the DataContextChanged event of the invisible FrameworkElement. When this is set to a non-null object, we display a dialog with the content set to this object. When the DataContext becomes null, we close the dialog.

C#
public sealed class DialogBehavior : Behavior<FrameworkElement>
{
    private static Dictionary<object, UtilityDialog> mapping = 
			new Dictionary<object, UtilityDialog>();
    
    protected override void OnAttached()
    {
        base.OnAttached();
        
        AssociatedObject.DataContextChanged += (a, b) =>
        {
            if (b.NewValue != null)
            {
                var dialog = new UtilityDialog();
                mapping.Add(b.NewValue, dialog);
                
                dialog.DataContext = b.NewValue;
                dialog.Owner = AssociatedObject as Window ?? 
				Application.Current.MainWindow;
                
                if (DisplayModal)
                {
                    dialog.ShowDialog();
                }
                else
                {
                    dialog.Show();
                }
            }
            else if (mapping.ContainsKey(b.OldValue))
            {
                var dialog = mapping[b.OldValue];
                mapping.Remove(b.OldValue);
                
                dialog.Close();
                dialog.DataContext = null;
                dialog.Owner = null;
            }
        };
    }
}

The ViewModel need not implement any interface, or care that it opens a dialog. Its only responsibility in this case is to manage the lifetime of its logically subordinated ViewModels - once brought into the ecosystem, it is the UI's concern what to do with the associated View of a specific ViewModel and in what way (if any) to display it.

C#
public sealed class MainViewModel : INotifyPropertyChanged
{
    private InvoiceViewModel currentInvoice;
    
    public InvoiceViewModel CurrentInvoice
    {
        get
        {
            return currentInvoice;
        }
        set
        {
            if (currentInvoice != value)
            {
                currentInvoice = value;
                PropertyChanged(this, new PropertyChangedEventArgs("CurrentInvoice"));
            }
        }
    }
    
    private ICommand displayInvoice;
    
    public ICommand DisplayInvoice
    {
        get
        {
            return displayInvoice ?? (displayInvoice = new DelegateCommand(
                a => CurrentInvoice = new InvoiceViewModel()));
        }
    }
}

So the DisplayInvoice command simply instantiates the CurrentInvoice property (the property that our invisible FrameworkElement binds to). And it stops there. We don't know how the view will be displayed, as a dialog or not, modal or modeless. This enforces the processing logic to be placed somewhere else, which is a good thing. Now, we need a mechanism for the ViewModels to communicate - the invoice has to tell the main ViewModel that the user has finished with it. You can rely on plain old CLR events again, but as your system will grow and every ViewModel needs to know about a lot of other ViewModels, it becomes very hard to maintain the entire relationship structure. So you might consider some sort of Mediator (like Josh Smith's from the MVVM Foundation, or Prism's EventAggregator). The sample uses a very simple custom made EventAggregator, that you should never use in production code.

C#
 public sealed class MainViewModel : INotifyPropertyChanged
{
    public MainViewModel()
    {
        EventAggregator<InvoiceReviewedEvent>.Subscribe(ProcessInvoice);
    }
    
    public void ProcessInvoice()
    {
        if (CurrentInvoice.IsApproved)
        {
            // process invoice
        }
        
        CurrentInvoice = null;
    }
}

public sealed class InvoiceViewModel
{
    public ICommand Approve
    {
        get
        {
            return approve ?? (approve = new DelegateCommand(
                a =>
                {
                    IsApproved = true;
                    EventAggregator<InvoiceReviewedEvent>.Broadcast();
                }));
        }
    }
}

When the invoice ViewModel is done, it signals this by broadcasting on a specific channel. The main ViewModel subscribes to such messages, and handles them in the ProcessInvoice method. If the invoice has been accepted, it does something significant with it. At the end, it sets CurrentInvoice to null - this object did its job, it is no longer useful. The attached behavior captures this change and closes the dialog. With this setup in place, if in another skin we don't want a dialog but the invoice to be displayed on the same window, docked to the left, we simply do:

XML
<ContentControl Content="{Binding Path=CurrentInvoice}" DockPanel.Dock="Left"/>
without having to modify anything in the ViewModel. That's pretty much it. Let the View decide!

History

  • 15th March, 2010 - Original article

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionBug? Pin
dougbtx18-Jul-10 23:04
dougbtx18-Jul-10 23:04 
AnswerRe: Bug? Pin
Teofil Cobzaru18-Jul-10 23:48
Teofil Cobzaru18-Jul-10 23:48 
Yes, I think you are right about that. Thanks.
GeneralGreat Article... Pin
Diving Flo15-Apr-10 0:45
Diving Flo15-Apr-10 0:45 

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.