Click here to Skip to main content
15,885,365 members
Articles / Web Development / ASP.NET

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

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
3 Apr 2011CPOL3 min read 11.8K   57   4   1
How to open a Window, triggered by the View-Model.

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:

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

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

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect CodeValue
Israel Israel
Tomer Shamam is a Software Architect and a UI Expert at CodeValue, the home of software experts, based in Israel (http://codevalue.net). Tomer is a speaker in Microsoft conferences and user groups, and in-house courses. Tomer has years of experience in software development, he holds a B.A degree in computer science, and his writings appear regularly in the Israeli MSDN Pulse, and other popular developer web sites such as CodePlex and The Code Project. About his thoughts and ideas you can read in his blog (http://blogs.microsoft.co.il/blogs/tomershamam).

Comments and Discussions

 
QuestionInteresting Article Pin
Chilopoda30-Aug-11 14:29
Chilopoda30-Aug-11 14:29 

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.