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

April 1, 2011

one comment

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

Code Snippet
  1. public class MessageListViewModel : ViewModelBase
  2. {
  3.     private bool _messageDetailsAvailable;
  4.     private MessageViewModel _selectedMessage;
  5.  
  6.     public ObservableCollection<MessageViewModel> Messages { get; private set; }
  7.         
  8.     public MessageViewModel SelectedMessage
  9.     {
  10.         get { return _selectedMessage; }
  11.         set
  12.         {
  13.             if (_selectedMessage != value)
  14.             {
  15.                 _selectedMessage = value;
  16.                 NotifyPropertyChanged("SelectedMessage");
  17.             }
  18.         }
  19.     }        
  20.  
  21.     public MessageListViewModel()
  22.     {
  23.         Messages = new ObservableCollection<MessageViewModel>
  24.         {
  25.             new MessageViewModel
  26.             {
  27.                 From = "tomer.shamam@email.co.il",
  28.                 Subject = "MVVM Howto's",
  29.                 Size = 23,
  30.                 Received = DateTime.Now
  31.             },
  32.             new MessageViewModel
  33.             {
  34.                 From = "tomer.shamam@email.co.il",
  35.                 Subject = "Open window from view-model",
  36.                 Size = 15,
  37.                 Received = DateTime.Now
  38.             },
  39.             new MessageViewModel
  40.             {
  41.                 From = "tomer.shamam@email.co.il",
  42.                 Subject = "Custom action",
  43.                 Size = 3,
  44.                 Received = DateTime.Now
  45.             },
  46.         };            
  47.     }
  48.  
  49.     public bool MessageDetailsAvailable
  50.     {
  51.         get { return _messageDetailsAvailable; }
  52.         set
  53.         {
  54.             if (_messageDetailsAvailable != value)
  55.             {
  56.                 _messageDetailsAvailable = value;
  57.                 NotifyPropertyChanged("MessageDetailsAvailable");
  58.             }
  59.         }
  60.     }
  61.  
  62.     public ICommand MessageDetailsRequestCommand
  63.     {
  64.         get
  65.         {
  66.             return new RelayCommand<object>(
  67.                 result => MessageDetailsAvailable = true,
  68.                 result => SelectedMessage != null);
  69.         }
  70.     }
  71.  
  72.     public RelayCommand<bool?> MessageDetailsDismissCommand
  73.     {
  74.         get
  75.         {
  76.             return new RelayCommand<bool?>(
  77.                 result => MessageDetailsAvailable = false,
  78.                 result => true);
  79.         }
  80.     }
  81. }

The view-model exposes the messages and selected message to the view. In addition it provides:

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

Lets look at how the view bound with the view-model:

Code Snippet
  1. <UserControl x:Class="WPFOutlook.PresentationLayer.Views.MessageListView"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:viewmodels="http://schemas.sela.co.il/advancedwpf"
  5.              xmlns:views="clr-namespace:WPFOutlook.PresentationLayer.Views"
  6.              xmlns:behaviors="clr-namespace:WPFOutlook.PresentationLayer.Behaviors"
  7.              xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"             
  8.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  9.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  10.              mc:Ignorable="d"
  11.              d:DesignHeight="300" d:DesignWidth="300">
  12.  
  13.     <UserControl.DataContext>
  14.         <viewmodels:MessageListViewModel />
  15.     </UserControl.DataContext>
  16.  
  17.     <i:Interaction.Behaviors>
  18.         <behaviors:OpenWindowBehavior WindowUri="/Dialogs/MessageDetailsDialog.xaml"
  19.                                       IsModal="True"
  20.                                       Owner="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
  21.                                       DataContext="{Binding SelectedMessage}"
  22.                                       IsOpen="{Binding MessageDetailsAvailable}"
  23.                                       CloseCommand="{Binding MessageDetailsDismissCommand}" />
  24.     </i:Interaction.Behaviors>
  25.     
  26.     <Grid>
  27.         <Grid.RowDefinitions>
  28.             <RowDefinition Height="*" />
  29.             <RowDefinition Height="Auto" />
  30.         </Grid.RowDefinitions>
  31.         
  32.         <DataGrid ItemsSource="{Binding Messages}"
  33.                   SelectedItem="{Binding SelectedMessage}"
  34.                   AutoGenerateColumns="False"
  35.                   CanUserAddRows="False"
  36.                   CanUserDeleteRows="False" Grid.RowSpan="2">
  37.             
  38.             <DataGrid.Columns>
  39.                 <DataGridTextColumn Header="From" Binding="{Binding From}" IsReadOnly="True" />
  40.                 <DataGridTextColumn Header="Subject" Binding="{Binding Subject}" IsReadOnly="True" />
  41.                 <DataGridTextColumn Header="Received" Binding="{Binding Received}" IsReadOnly="True" />
  42.                 <DataGridTextColumn Header="Size" Binding="{Binding Size}" IsReadOnly="True" />
  43.             </DataGrid.Columns>
  44.             
  45.             <i:Interaction.Triggers>
  46.                 <i:EventTrigger EventName="MouseDoubleClick">
  47.                     <behaviors:InvokeCommandAction Command="{Binding MessageDetailsRequestCommand}" />                    
  48.                 </i:EventTrigger>
  49.             </i:Interaction.Triggers>
  50.  
  51.         </DataGrid>
  52.         
  53.         <CheckBox Content="Force Details"
  54.                   Margin="16"
  55.                   IsChecked="{Binding MessageDetailsAvailable}"                  
  56.                   HorizontalAlignment="Left" Grid.Row="1" />
  57.  
  58.         <Button Content="Show Details"
  59.                 Margin="16"
  60.                 Command="{Binding MessageDetailsRequestCommand}"
  61.                 HorizontalAlignment="Right" Grid.Row="1" />
  62.  
  63.     </Grid>
  64. </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 details is required.
  • Having a custom OpenWindowBehavior, the view is triggered by the view-model that a message details should be displayed. This is done by using the IsOpen property. The OpenWindowBehavior behavior opens the window and notifies the view-model when user clicks on the close button by invoking the view-model MessageDetailsDismissCommand.

Here is the code for the OpenWindowBehavior:

Code Snippet
  1. public class OpenWindowBehavior : Behavior<FrameworkElement>
  2. {
  3.     #region Fields
  4.  
  5.     private Window _host;
  6.  
  7.     #endregion
  8.  
  9.     #region IsOpen Property
  10.  
  11.     public bool IsOpen
  12.     {
  13.         get { return (bool)GetValue(IsOpenProperty); }
  14.         set { SetValue(IsOpenProperty, value); }
  15.     }
  16.  
  17.     /// <value>Identifies the IsOpen dependency property</value>
  18.     public static readonly DependencyProperty IsOpenProperty =
  19.         DependencyProperty.Register(
  20.         "IsOpen",
  21.         typeof(bool),
  22.         typeof(OpenWindowBehavior),
  23.             new FrameworkPropertyMetadata(
  24.                 default(bool),
  25.                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
  26.                 IsOpenChanged));
  27.  
  28.     /// <summary>
  29.     /// Invoked on IsOpen change.
  30.     /// </summary>
  31.     /// <param name="d">The object that was changed</param>
  32.     /// <param name="e">Dependency property changed event arguments</param>
  33.     private static void IsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  34.     {
  35.         var behavior = d as OpenWindowBehavior;
  36.         if (DesignerProperties.GetIsInDesignMode(behavior))
  37.         {
  38.             return;
  39.         }
  40.  
  41.         behavior.OnOpenChanged((bool)e.NewValue);            
  42.     }
  43.  
  44.     private void OnOpenChanged(bool opening)
  45.     {
  46.         if (AssociatedObject == null)
  47.         {
  48.             Dispatcher.BeginInvoke(() => OnOpenChanged(opening), DispatcherPriority.Loaded);
  49.             return;
  50.         }
  51.  
  52.         if (opening)
  53.         {
  54.             OpenWindow();
  55.         }
  56.         else
  57.         {
  58.             CloseWindow();
  59.         }
  60.     }        
  61.  
  62.     private void window_Closing(object sender, CancelEventArgs e)
  63.     {
  64.         var window = sender as Window;
  65.         e.Cancel = true;            
  66.         if (CloseCommand.CanExecute(window.DialogResult))
  67.         {
  68.             Dispatcher.BeginInvoke(() => CloseCommand.Execute(window.DialogResult), DispatcherPriority.Loaded);
  69.         }
  70.     }
  71.  
  72.     #endregion
  73.  
  74.     #region IsModal Property
  75.  
  76.     public bool IsModal
  77.     {
  78.         get { return (bool)GetValue(IsModalProperty); }
  79.         set { SetValue(IsModalProperty, value); }
  80.     }
  81.  
  82.     /// <value>Identifies the IsModal dependency property</value>
  83.     public static readonly DependencyProperty IsModalProperty =
  84.         DependencyProperty.Register(
  85.         "IsModal",
  86.         typeof(bool),
  87.         typeof(OpenWindowBehavior),
  88.             new FrameworkPropertyMetadata(default(bool), IsModalChanged));
  89.  
  90.     /// <summary>
  91.     /// Invoked on IsModal change.
  92.     /// </summary>
  93.     /// <param name="d">The object that was changed</param>
  94.     /// <param name="e">Dependency property changed event arguments</param>
  95.     private static void IsModalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  96.     {
  97.     }
  98.  
  99.     #endregion
  100.  
  101.     #region Owner Property
  102.  
  103.     public Window Owner
  104.     {
  105.         get { return (Window)GetValue(OwnerProperty); }
  106.         set { SetValue(OwnerProperty, value); }
  107.     }
  108.  
  109.     /// <value>Identifies the Owner dependency property</value>
  110.     public static readonly DependencyProperty OwnerProperty =
  111.         DependencyProperty.Register(
  112.         "Owner",
  113.         typeof(Window),
  114.         typeof(OpenWindowBehavior),
  115.             new FrameworkPropertyMetadata(default(Window), OwnerChanged));
  116.  
  117.     /// <summary>
  118.     /// Invoked on Owner change.
  119.     /// </summary>
  120.     /// <param name="d">The object that was changed</param>
  121.     /// <param name="e">Dependency property changed event arguments</param>
  122.     private static void OwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  123.     {
  124.     }
  125.  
  126.     #endregion
  127.  
  128.     #region CloseCommand Property
  129.  
  130.     public ICommand CloseCommand
  131.     {
  132.         get { return (ICommand)GetValue(CloseCommandProperty); }
  133.         set { SetValue(CloseCommandProperty, value); }
  134.     }
  135.  
  136.     /// <value>Identifies the CloseCommand dependency property</value>
  137.     public static readonly DependencyProperty CloseCommandProperty =
  138.         DependencyProperty.Register(
  139.         "CloseCommand",
  140.         typeof(ICommand),
  141.         typeof(OpenWindowBehavior),
  142.             new FrameworkPropertyMetadata(NullCommand.Instance, null, CoerceCloseCommand));
  143.         
  144.     private static object CoerceCloseCommand(DependencyObject d, object baseValue)
  145.     {
  146.         if (baseValue == null)
  147.         {
  148.             return NullCommand.Instance;
  149.         }
  150.  
  151.         return baseValue;
  152.     }
  153.  
  154.     #endregion
  155.  
  156.     #region WindowUri Property
  157.  
  158.     public Uri WindowUri
  159.     {
  160.         get { return (Uri)GetValue(WindowUriProperty); }
  161.         set { SetValue(WindowUriProperty, value); }
  162.     }
  163.  
  164.     /// <value>Identifies the WindowUri dependency property</value>
  165.     public static readonly DependencyProperty WindowUriProperty =
  166.         DependencyProperty.Register(
  167.         "WindowUri",
  168.         typeof(Uri),
  169.         typeof(OpenWindowBehavior),
  170.             new FrameworkPropertyMetadata(default(Uri), WindowUriChanged));
  171.  
  172.     /// <summary>
  173.     /// Invoked on WindowUri change.
  174.     /// </summary>
  175.     /// <param name="d">The object that was changed</param>
  176.     /// <param name="e">Dependency property changed event arguments</param>
  177.     private static void WindowUriChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  178.     {
  179.     }
  180.  
  181.     #endregion        
  182.  
  183.     #region DataContext Property
  184.  
  185.     public object DataContext
  186.     {
  187.         get { return (object)GetValue(DataContextProperty); }
  188.         set { SetValue(DataContextProperty, value); }
  189.     }
  190.  
  191.     /// <value>Identifies the DataContext dependency property</value>
  192.     public static readonly DependencyProperty DataContextProperty =
  193.         DependencyProperty.Register(
  194.         "DataContext",
  195.         typeof(object),
  196.         typeof(OpenWindowBehavior),
  197.             new FrameworkPropertyMetadata(default(object), DataContextChanged));
  198.  
  199.     /// <summary>
  200.     /// Invoked on DataContext change.
  201.     /// </summary>
  202.     /// <param name="d">The object that was changed</param>
  203.     /// <param name="e">Dependency property changed event arguments</param>
  204.     private static void DataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  205.     {
  206.     }
  207.  
  208.     #endregion        
  209.  
  210.     #region Privates
  211.  
  212.     private void CloseWindow()
  213.     {
  214.         if (_host != null)
  215.         {
  216.             _host.Closing -= window_Closing;
  217.             _host.Close();
  218.             _host = null;
  219.         }
  220.     }
  221.  
  222.     private void OpenWindow()
  223.     {
  224.         var window = (Window)Application.LoadComponent(WindowUri);
  225.         window.Owner = Owner;
  226.         window.DataContext = DataContext;
  227.         window.Closing += window_Closing;
  228.  
  229.         _host = window;
  230.         if (IsModal)
  231.         {
  232.             _host.Show();
  233.         }
  234.         else
  235.         {
  236.             _host.ShowDialog();
  237.         }
  238.     }
  239.         
  240.     #endregion
  241.  
  242.     #region Null Command
  243.  
  244.     private class NullCommand : ICommand
  245.     {
  246.         #region ICommand Members
  247.  
  248.         public bool CanExecute(object parameter)
  249.         {
  250.             return true;
  251.         }            
  252.  
  253.         public void Execute(object parameter)
  254.         {
  255.         }
  256.  
  257.         public event EventHandler CanExecuteChanged = delegate { };
  258.  
  259.         #endregion
  260.  
  261.         #region Singleton Pattern
  262.         private NullCommand() { }
  263.         private static NullCommand _instance = new NullCommand();
  264.         public static NullCommand Instance
  265.         {
  266.             get { return _instance; }
  267.         }
  268.         #endregion
  269.     }        
  270.         
  271.     #endregion
  272. }

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.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

one comment

  1. LeonJanuary 10, 2012 ב 12:24

    Tomer hi,

    Great post.

    I did love the Dispatcher.BeginInvoke(() => OnOpenChanged(opening), DispatcherPriority.Loaded) in one line you resolve the issue of ‘rasing’ condition.

    However in some cases this could cause the Dispatcher to stack. For example, if there is considerable time till this contol is loaded. Since the priority is Loaded it is placed in the message queue and would be called again and again, those preventing other messages with lower priority being processed.

    And if Loaded of this control depends on some other thread with lower priority doing something it would never ever arrive here.

    Of cause the other threads priority could be changed, but what if you don’t know who the users of your control and how it would be used.

    Thank you,

    Leon

    Reply