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); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(
"IsOpen",
typeof(bool),
typeof(OpenWindowBehavior),
new FrameworkPropertyMetadata(
default(bool),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
IsOpenChanged));
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); }
}
public static readonly DependencyProperty IsModalProperty =
DependencyProperty.Register(
"IsModal",
typeof(bool),
typeof(OpenWindowBehavior),
new FrameworkPropertyMetadata(default(bool), IsModalChanged));
private static void IsModalChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
}
#endregion
#region Owner Property
public Window Owner
{
get { return (Window)GetValue(OwnerProperty); }
set { SetValue(OwnerProperty, value); }
}
public static readonly DependencyProperty OwnerProperty =
DependencyProperty.Register(
"Owner",
typeof(Window),
typeof(OpenWindowBehavior),
new FrameworkPropertyMetadata(default(Window), OwnerChanged));
private static void OwnerChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
}
#endregion
#region CloseCommand Property
public ICommand CloseCommand
{
get { return (ICommand)GetValue(CloseCommandProperty); }
set { SetValue(CloseCommandProperty, 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); }
}
public static readonly DependencyProperty WindowUriProperty =
DependencyProperty.Register(
"WindowUri",
typeof(Uri),
typeof(OpenWindowBehavior),
new FrameworkPropertyMetadata(default(Uri), WindowUriChanged));
private static void WindowUriChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
}
#endregion
#region DataContext Property
public object DataContext
{
get { return (object)GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
}
public static readonly DependencyProperty DataContextProperty =
DependencyProperty.Register(
"DataContext",
typeof(object),
typeof(OpenWindowBehavior),
new FrameworkPropertyMetadata(default(object),
DataContextChanged));
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.
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).