Open Window, Dialog, or Message Box from a ViewModel – Part 2

Introduction

In my previous post, I have shown how to open a Window bound to a View-Model triggered by the View, using a simple Action. In this post, I’ll show how to open a Window, triggered by the View-Model.

Opening a window directly by the View where the View decides when a Window should be opened, is an incorrect approach since the View shouldn’t make that decision. This decision belongs to the Application layer and not the Presentation layer.

What if the View shouldn’t be opened because of application state, user permissions, or any other application decision?

In that case, the View-Model should decide and then trigger the Window or View creation.

Revisiting the problem again, we’ve got a MessageListViewModel, MessageListView for the email messages view and MessageDetailsViewModel, MessageDetailsView for the email details view that should be presented inside a MessageDetailsDialog.

Now, instead of attaching a simple Action to the View, we should delegate the request to the View-Model by saying: “Hey, I’m the View, someone double-clicked an item, FYI!”, using the same trigger, but now invoking a command on the View-Model, this would be Phase 1. Next, the View-Model should decide how to continue on by changing a property for example, and this would be Phase 2. Finally, an Action bound with the View-Model decision property will popup the Window, and this would be the final phase. Of course, the View-Model should be asked and be notified again whenever the user closes the Window.

For phase 1, I’ll use a simple trigger with an invoke command action.

For phase 2, I’ll have a bool property on the View-Model, notifying that a Window should be opened.

For the final phase, I’ll have an Action attached with the View which creates the Window on property change.

Here is the View-Model:

public class MessageListViewModel : ViewModelBase
{
    private bool _messageDetailsAvailable;
    private MessageViewModel _selectedMessage;

    public ObservableCollection<MessageViewModel> Messages { get; private set; }

    public MessageViewModel SelectedMessage
    {
        get { return _selectedMessage; }
        set
        {
            if (_selectedMessage != value)
            {
                _selectedMessage = value;
                NotifyPropertyChanged("SelectedMessage");
            }
        }
    }        

    public MessageListViewModel()
    {
        Messages = new ObservableCollection<MessageViewModel>
        {
            new MessageViewModel
            {
                From = "tomer.shamam@email.co.il",
                Subject = "MVVM Howto's",
                Size = 23,
                Received = DateTime.Now
            },
            new MessageViewModel
            {
                From = "tomer.shamam@email.co.il",
                Subject = "Open window from view-model",
                Size = 15,
                Received = DateTime.Now
            },
            new MessageViewModel
            {
                From = "tomer.shamam@email.co.il",
                Subject = "Custom action",
                Size = 3,
                Received = DateTime.Now
            },
        };            
    }

    public bool MessageDetailsAvailable
    {
        get { return _messageDetailsAvailable; }
        set
        {
            if (_messageDetailsAvailable != value)
            {
                _messageDetailsAvailable = value;
                NotifyPropertyChanged("MessageDetailsAvailable");
            }
        }
    }

    public ICommand MessageDetailsRequestCommand
    {
        get
        {
            return new RelayCommand<object>(
                result => MessageDetailsAvailable = true,
                result => SelectedMessage != null);
        }
    }

    public RelayCommand<bool?> MessageDetailsDismissCommand
    {
        get
        {
            return new RelayCommand<bool?>(
                result => MessageDetailsAvailable = false,
                result => true);
        }
    }
}

The View-Model exposes the messages and the selected message to the View. In addition, it provides:

  • MessageDetailsRequestCommand command – should be executed whenever a message detail is required.
  • MessageDetailsAvailable property – indicating that a message detail is available and should be displayed.
  • MessageDetailsDismissCommand – should be executed whenever a message detail should be dismissed.

Let’s look at how the View is bound with the View-Model:

<UserControl x:Class="WPFOutlook.PresentationLayer.Views.MessageListView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:viewmodels="http://schemas.sela.co.il/advancedwpf"
         xmlns:views="clr-namespace:WPFOutlook.PresentationLayer.Views"
         xmlns:behaviors="clr-namespace:WPFOutlook.PresentationLayer.Behaviors"
         xmlns:i="clr-namespace:System.Windows.Interactivity;
                  assembly=System.Windows.Interactivity"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         mc:Ignorable="d"
         d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <viewmodels:MessageListViewModel />
    </UserControl.DataContext>

    <i:Interaction.Behaviors>
        <behaviors:OpenWindowBehavior WindowUri="/Dialogs/MessageDetailsDialog.xaml"
              IsModal="True"
              Owner="{Binding RelativeSource={RelativeSource 
                     Mode=FindAncestor, AncestorType={x:Type Window}}}"
              DataContext="{Binding SelectedMessage}"
              IsOpen="{Binding MessageDetailsAvailable}"
              CloseCommand="{Binding MessageDetailsDismissCommand}" />
    </i:Interaction.Behaviors>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <DataGrid ItemsSource="{Binding Messages}"
                  SelectedItem="{Binding SelectedMessage}"
                  AutoGenerateColumns="False"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False" Grid.RowSpan="2">

            <DataGrid.Columns>
                <DataGridTextColumn Header="From" 
                  Binding="{Binding From}" IsReadOnly="True" />
                <DataGridTextColumn Header="Subject" 
                  Binding="{Binding Subject}" IsReadOnly="True" />
                <DataGridTextColumn Header="Received" 
                  Binding="{Binding Received}" IsReadOnly="True" />
                <DataGridTextColumn Header="Size" 
                  Binding="{Binding Size}" IsReadOnly="True" />
            </DataGrid.Columns>

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseDoubleClick">
                    <behaviors:InvokeCommandAction 
                      Command="{Binding MessageDetailsRequestCommand}" />                    
                </i:EventTrigger>
            </i:Interaction.Triggers>

        </DataGrid>

        <CheckBox Content="Force Details"
                  Margin="16"
                  IsChecked="{Binding MessageDetailsAvailable}"                  
                  HorizontalAlignment="Left" Grid.Row="1" />

        <Button Content="Show Details"
                Margin="16"
                Command="{Binding MessageDetailsRequestCommand}"
                HorizontalAlignment="Right" Grid.Row="1" />

    </Grid>
</UserControl>

The View is bound with the View-Model properties and commands as follows:

  • Having a double-click trigger, the View invokes the MessageDetailsRequestCommand on the View-Model saying that a message detail is required.
  • Having a custom OpenWindowBehavior, the View is triggered by the View-Model that a message detail should be displayed. This is done by using the IsOpen property. The OpenWindowBehavior behavior opens the Window and notifies the View-Model when the user clicks on the Close button by invoking the View-Model MessageDetailsDismissCommand.

Here is the code for the OpenWindowBehavior:

public class OpenWindowBehavior : Behavior<FrameworkElement>
{
    #region Fields

    private Window _host;

    #endregion

    #region IsOpen Property

    public bool IsOpen
    {
        get { return (bool)GetValue(IsOpenProperty); }
        set { SetValue(IsOpenProperty, value); }
    }

    /// <value>Identifies the IsOpen dependency property</value>
    public static readonly DependencyProperty IsOpenProperty =
        DependencyProperty.Register(
        "IsOpen",
        typeof(bool),
        typeof(OpenWindowBehavior),
            new FrameworkPropertyMetadata(
                default(bool),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                IsOpenChanged));

    /// <summary>
    /// Invoked on IsOpen change.
    /// </summary>
    /// <param name="d">The object that was changed</param>
    /// <param name="e">Dependency property changed event arguments</param>
    private static void IsOpenChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as OpenWindowBehavior;
        if (DesignerProperties.GetIsInDesignMode(behavior))
        {
            return;
        }

        behavior.OnOpenChanged((bool)e.NewValue);            
    }

    private void OnOpenChanged(bool opening)
    {
        if (AssociatedObject == null)
        {
            Dispatcher.BeginInvoke(() => 
              OnOpenChanged(opening), DispatcherPriority.Loaded);
            return;
        }

        if (opening)
        {
            OpenWindow();
        }
        else
        {
            CloseWindow();
        }
    }        

    private void window_Closing(object sender, CancelEventArgs e)
    {
        var window = sender as Window;
        e.Cancel = true;            
        if (CloseCommand.CanExecute(window.DialogResult))
        {
            Dispatcher.BeginInvoke(() => 
              CloseCommand.Execute(window.DialogResult), 
              DispatcherPriority.Loaded);
        }
    }

    #endregion

    #region IsModal Property

    public bool IsModal
    {
        get { return (bool)GetValue(IsModalProperty); }
        set { SetValue(IsModalProperty, value); }
    }

    /// <value>Identifies the IsModal dependency property</value>
    public static readonly DependencyProperty IsModalProperty =
        DependencyProperty.Register(
        "IsModal",
        typeof(bool),
        typeof(OpenWindowBehavior),
            new FrameworkPropertyMetadata(default(bool), IsModalChanged));

    /// <summary>
    /// Invoked on IsModal change.
    /// </summary>
    /// <param name="d">The object that was changed</param>
    /// <param name="e">Dependency property changed event arguments</param>
    private static void IsModalChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
    }

    #endregion

    #region Owner Property

    public Window Owner
    {
        get { return (Window)GetValue(OwnerProperty); }
        set { SetValue(OwnerProperty, value); }
    }

    /// <value>Identifies the Owner dependency property</value>
    public static readonly DependencyProperty OwnerProperty =
        DependencyProperty.Register(
        "Owner",
        typeof(Window),
        typeof(OpenWindowBehavior),
            new FrameworkPropertyMetadata(default(Window), OwnerChanged));

    /// <summary>
    /// Invoked on Owner change.
    /// </summary>
    /// <param name="d">The object that was changed</param>
    /// <param name="e">Dependency property changed event arguments</param>
    private static void OwnerChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
    }

    #endregion

    #region CloseCommand Property

    public ICommand CloseCommand
    {
        get { return (ICommand)GetValue(CloseCommandProperty); }
        set { SetValue(CloseCommandProperty, value); }
    }

    /// <value>Identifies the CloseCommand dependency property</value>
    public static readonly DependencyProperty CloseCommandProperty =
        DependencyProperty.Register(
        "CloseCommand",
        typeof(ICommand),
        typeof(OpenWindowBehavior),
        new FrameworkPropertyMetadata(NullCommand.Instance, 
                                      null, CoerceCloseCommand));

    private static object CoerceCloseCommand(DependencyObject d, object baseValue)
    {
        if (baseValue == null)
        {
            return NullCommand.Instance;
        }

        return baseValue;
    }

    #endregion

    #region WindowUri Property

    public Uri WindowUri
    {
        get { return (Uri)GetValue(WindowUriProperty); }
        set { SetValue(WindowUriProperty, value); }
    }

    /// <value>Identifies the WindowUri dependency property</value>
    public static readonly DependencyProperty WindowUriProperty =
        DependencyProperty.Register(
        "WindowUri",
        typeof(Uri),
        typeof(OpenWindowBehavior),
            new FrameworkPropertyMetadata(default(Uri), WindowUriChanged));

    /// <summary>
    /// Invoked on WindowUri change.
    /// </summary>
    /// <param name="d">The object that was changed</param>
    /// <param name="e">Dependency property changed event arguments</param>
    private static void WindowUriChanged(DependencyObject d, 
                   DependencyPropertyChangedEventArgs e)
    {
    }

    #endregion        

    #region DataContext Property

    public object DataContext
    {
        get { return (object)GetValue(DataContextProperty); }
        set { SetValue(DataContextProperty, value); }
    }

    /// <value>Identifies the DataContext dependency property</value>
    public static readonly DependencyProperty DataContextProperty =
        DependencyProperty.Register(
        "DataContext",
        typeof(object),
        typeof(OpenWindowBehavior),
            new FrameworkPropertyMetadata(default(object), 
                DataContextChanged));

    /// <summary>
    /// Invoked on DataContext change.
    /// </summary>
    /// <param name="d">The object that was changed</param>
    /// <param name="e">Dependency property changed event arguments</param>
    private static void DataContextChanged(DependencyObject d, 
                        DependencyPropertyChangedEventArgs e)
    {
    }

    #endregion        

    #region Privates

    private void CloseWindow()
    {
        if (_host != null)
        {
            _host.Closing -= window_Closing;
            _host.Close();
            _host = null;
        }
    }

    private void OpenWindow()
    {
        var window = (Window)Application.LoadComponent(WindowUri);
        window.Owner = Owner;
        window.DataContext = DataContext;
        window.Closing += window_Closing;

        _host = window;
        if (IsModal)
        {
            _host.Show();
        }
        else
        {
            _host.ShowDialog();
        }
    }

    #endregion

    #region Null Command

    private class NullCommand : ICommand
    {
        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            return true;
        }            

        public void Execute(object parameter)
        {
        }

        public event EventHandler CanExecuteChanged = delegate { };

        #endregion

        #region Singleton Pattern
        private NullCommand() { }
        private static NullCommand _instance = new NullCommand();
        public static NullCommand Instance
        {
            get { return _instance; }
        }
        #endregion
    }        

    #endregion
}

The behavior above displays a Window when the IsOpen property changes to true, and closes the Window otherwise. This property is controlled by the View-Model using a simple property binding.

When the user triggers the Close by clicking the Window’s X button for example, the close request is always ignored, letting the View-Model to decide. In that case, the View-Model may (or may not) change the MessageDetailsAvailable property to false.

You can download the full code from here.

This entry was posted in MVC. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s