vrijdag 4 december 2009

A Generic Dialog ViewModel

Introduction

A few months ago I worked on a wpf viewmodel for dialogs, because it seemed strange to me that you needed for instance windows forms dialogs to open or save files in wpf. So I set about creating my own wpf dialogs. After creating the dialog viewmodels there still remained a small issue with the library and also I didn't have much time to write an article about it. I however published the library containing the viewmodels for dialogs with another article on my blog. Someone saw the viewmodel in this library and asked if I could write about it. I had a good look at the code again and solved the small problem and so here's the article. I remembered looking for a nice viewmodel for dialogs a few months ago but couldn't find any. In the mean time since I wrote the viewmodel Sacha Barber published his Cinch framework. Part 3 of his series on the codeproject site describes a dialog service. My solution looks quite different so I hope you find this article still interesting.


The DialogViewModel Definition

The dialog viewmodel is a generic viewmodel. The DialogViewModel expects another viewmodel as a parameter. This parameter is the viewmodel for the content of the dialog window. Below you see the interface of the DialogViewModel and a short description of every property:

  • As can be seen in the definition of the DialogViewModel class the DialogContent property of the interface must be another viewmodel that uses the ICloneable<V> interface. This property binds to a ContentPresenter class in the dialog window.
  • The DialogResult property indicates if an Ok button or Cancel button is pressed. Initially it is null. When it becomes true or false the dialog closes.
  • When you assign an action to the FillDialogContent property, the action is called before the dialog is shown. You can do some stuff before like filling the dialog with content before the dialog appears. When you assign an action to the ProcessNewContent property this action gets called after the Ok button of the dialog is pressed.  An example of it's use can be found in my sample project that you can download. In it I created a progressbar dialog that raises it's value every second. In the FillDialogContent action the timer is started.
    public interface IDialogViewModel<V>
    {
        #region Data Members (4) 
 
        V DialogContent { get; set; }
 
        bool? DialogResult { get; set; }
 
        Action<V> FillDialogContent { get; set; }
 
        Action<V> ProcessNewContent{ get; set; }
 
        #endregion Data Members 
    }
 
    /// <summary>
    /// The ViewModel for the application's main window.
    /// </summary>
    public class DialogViewModel<V> : WorkspaceViewModel, IDialogViewModel<V>, IEditableObject
        where V : ViewModelBase, ICloneable<V>, new()
 
Why did I define my viewmodel like this? Suppose for instance you have a Silverlight and a wpf project with some kind of form you want to fill in. In the wpf project you want to fill in the form through a dialog form. In the Silverlight project you maybe want to have the form on some kind of tab page. For both projects you can write one form viewmodel. All that is needed for creating a form viewmodel with extra dialog functionality is defining a dialog viewmodel with the form viewmodel as a parameter like this: DialogViewModel<MyFormViewModel>.

Inner workings of the DialogViewModel

The DialogViewModel is derived from the WorkspaceViewModel. This viewmodel already has Show and Close commands. I added an extra Help Command for dialogs that have a Help button and an Ok Command for when the Ok button is pressed. Below you see the constructor of the viewmodel. The RequestShow and RequestClose events are defined in the WorkspaceViewModel and are called when the Show and Close commands of this viewmodel are called.

        public DialogViewModel(V content)
        {
            DialogContent = content;
            _dialogService = new DialogService<V>();
            this.RequestShow += new EventHandler(DialogViewModel_RequestShow);
            this.RequestClose += new EventHandler(DialogViewModel_RequestClose);
            BeginEdit();
        }
 
        void DialogViewModel_RequestShow(object sender, EventArgs e)
        {
            Wpf.MVVM.Views.DialogBehavior.ShowDialog(this);
        }
 
        void DialogViewModel_RequestClose(object sender, EventArgs e)
        {
            CancelEdit();
        }
 
The ShowDialog method resides in a static class called DialogBehavior and is needed to keep the viewmodel separated from the view. The method looks like this and is self explanatory I think:

/// <summary>
        /// Shows the dialog.
        /// First it creates a dialog window. Then it connects the viewmodel
        /// to the DataContext of the dialog. The FillDialogContent action is called
        /// so the program can perform some actions before the dialog is shown.
        /// The ProcessNewContent action is called so the program can perform some
        /// actions after the dialog is shown and the Ok button is pressed.
        /// </summary>
        /// <param name="viewModel">The viewmodel contained in the dialog viewmodel.</param>
        /// <returns></returns>
        static public bool ShowDialog<T>(IDialogViewModel<T> viewModel) where T : ViewModelBase, ICloneable<T>, new()
        {
            bool dialogResult = false;
            DialogWindow window = new DialogWindow();
            window.DataContext = viewModel;
            if (viewModel.FillDialogContent != null)
                viewModel.FillDialogContent(viewModel.DialogContent);
            bool? result = window.ShowDialog();
            if (result.HasValue && result.Value)
            {
                if (viewModel.ProcessNewContent != null)
                    viewModel.ProcessNewContent(viewModel.DialogContent);
                dialogResult = true;
            }
            window = null;
            return dialogResult;
        }
When you look again at the definition of the dialog viewmodel you see that it has an IEditableObject interface. This interface provides functionality to commit or rollback changes to an object that is used as a data source. Below you see the implemented methods. The BeginEdit method is called when the show command is executed. The content of the dialog is saved in the _oldContent variable. And a copy of the original content is created. This copy will be edited in the dialog. This is why the DialogContent property needs a viewmodel that implements the ICloneable<V> interface. This interface has the Clone and MakeCopyTo methods. The dialog viewmodel uses these two methods to make a temporary copy of the original dialog content. When the Cancel button is pressed the CancelEdit method is called and the original content becomes the DialogContent again. When the Ok button is pressed the EndEdit method is called and the content that is changed in the dialog becomes the DialogContent.

        public void BeginEdit()
        {
            if (!Editing)
            {
                _oldContent = _dialogContent; // The content to be saved.
                DialogContent = _dialogContent.Clone(); // The content to be edited.
            }
            Editing = true;
        }
 
        public void CancelEdit()
        {
            Debug.Assert(true, "CancelEdit");
            // The Dialogcontent should become the old class.
            if (Editing)
                DialogContent = _oldContent;
            Editing = false;
            DialogResult = false;
        }
 
        public void EndEdit()
        {
            // The edited content is copied to the old content and that becomes the new DialogContent.
            DialogContent = DialogContent.MakeCopyTo(_oldContent);
            Editing = false;
            DialogResult = true;
        }
 
        #endregion
    }

The View

The dialog window has three buttons and a ContentPresenter control. The xaml code you see below:

<Window x:Class="Wpf.MVVM.Views.DialogWindow"
   <!—the libraries used. Removed here for clarity -->
   Title="{Binding Path=DisplayName}"
       vw:DialogBehavior.CloseWithResultWhenDone="{Binding Path=DialogResult}"
       WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight"
       Closing="Window_Closing">
    <!—some styles removed here for clarity-->
    <StackPanel>
        <ContentPresenter Content="{Binding Path=DialogContent}"></ContentPresenter>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
            <Button x:Name="HelpButton" Width="72" Command="{Binding Path=HelpCommand}" Visibility="{Binding Path=HelpVisible}">Help</Button>
            <Button x:Name="OkButton" IsDefault="True" Width="72" Command="{Binding Path=OkCommand}" Visibility="{Binding Path=OkVisible}">Ok</Button>
            <Button x:Name="CancelButton" Width="72" IsCancel="True" Command="{Binding Path=CloseCommand}" Visibility="{Binding Path=CancelVisible}">Cancel</Button>
        </StackPanel>
    </StackPanel>
</Window>
The DialogResult property of the Window class is not a dependency property. In the static DialogBehavior class there's a dependency property called CloseWithResultWhenDone. When the DialogResult property of the viewmodel changes the dependency property changes and the method OnCloseWithResultWhenDone is called. In this method the DialogResult of the window class is set. When the DialogResult of the window is set the dialog closes. The ContentPresenter binds to the DialogContent of the dialog viewmodel. The way the dialog looks depends on the type of viewmodel of the generic DialogContent property. Every viewmodel needs a datatemplate to connect the viewmodel with a view.

Some Examples

The simplest example I can give you of a real world example is that of a message dialog. The viewmodel of this dialog is defined below:

public class MessageViewModel : ViewModelBase, ICloneable<MessageViewModel>
    {
        string _message;
        public string Message
        {
            get { return _message; }
            set
            {
                _message = value;
                RaisePropertyChanged(() => Message);
            }
        }
        #region ICloneable<MessageViewModel> Members
 
        public MessageViewModel Clone()
        {
            return MakeCopyTo(new MessageViewModel());
        }
 
        public MessageViewModel MakeCopyTo(MessageViewModel copyTo)
        {
            copyTo.DisplayName = this.DisplayName;
            copyTo.Message = this.Message;
            return copyTo;
        }
 
        #endregion
    }
There's a message property and the ICloneable interface is implemented. The Clone method of this interface creates a new viewmodel and fills it with the current property values of the message viewmodel. The MakeCopyTo method does this for an already existing viewmodel. See the BeginEdit, CancelEdit and EndEdit methods described in the "Inner workings of the DialogViewModel" chapter to see how the methods of the ICloneable interface are used. The DataTemplate of the message viewmodel contains only a label with the Content bound to the Message property of the viewmodel.

    <DataTemplate DataType="{x:Type vm:MessageViewModel}">
        <Label Content="{Binding Path=Message}" Margin="10" Padding="2,2"/>
    </DataTemplate>
 
You can create a message dialog viewmodel by calling these methods:
DialogViewModel<MessageViewModel> messageViewModel = new DialogViewModel<MessageViewModel>(dvm.MessageViewModel); messageViewModel.CancelVisible = Visibility.Hidden; 
You can also create a MessageDialogViewModel like this:

    public class MessageDialogViewModel : DialogViewModel<MessageViewModel>
    {
        public MessageDialogViewModel()
            : base()
        {
            CancelVisible = Visibility.Hidden;
        }
 
        public MessageDialogViewModel(MessageViewModel vm) : base(vm) { }
    }
For the more complicated viewmodels this is the recommended method. I also created a dialog viewmodel and form for selecting folders and one for a dialog with a progress bar. Most of the code for selecting a folder I "borrowed" from the MEF examples. It's included in an example called XFileExplorer.

Remarks

If you have suggestions or any other comments please let me know. I would really like to hear from you. The source code for Windows Visual Studio 2010 (solution file DialogsMVVM4) and visual studio 2008 (solution file DialogsMVVM) you can download here.
Shout it kick it on DotNetKicks.com

2 opmerkingen:

iPad Application Development zei

I have read your blog by translate language.I got some useful information from this blog so would say thanks to you for this article.

Robert zei

Thanks!
I wrote the article some years ago I hope it was still usefull.