MVVM : Event Routing to the Child ViewModel
When there is a collection of Child ViewModel then how to route the event to the ChildViewModel
Introduction
Currently, MVVM pattern is the most recommended pattern for designing the architecture of the WPF application.
MVVM principle is based on the following component:
Generally, we design our application in the same way where there is one ViewModel for a View. ViewModel is responsible to provide/update the data and event handling for its view. It’s clear from the above diagram that for controls such as Button, ContextMenu etc there is an event handler e.g. OnButtonClickEvent defined inside the ViewModel. So on the occurrence of event such as click, registered/specified Event handler will get called.
Note: View and ViewModel Binding is done by using DataContext or Content [if Element used in XAML is ContentControl]. To develop this Sample program I have chosen the Caliburn.Micro framework. Download Caliburn.micro from here
Background/Objective
We can have a requirement, where we need one view which can be attached to the ViewModel instance from the collection of ViewModel [Multiple instances of the same type of ViewModel].
Will DataBinding and Event routing work in the expected way?
Here is the ideal pictorial representation of the scenario.
So this is how we expect the application to work i.e. on the selection of child view index, Child View will get bind to the respective childviewmodel. Displayed data and the Event Routing of the Child View will be handled by the current selected ChildViewModel.
To verify the above scenario I developed the WPF application using Caliburn framework, which represents the Student Data. Based on the selected Student, application will make a call to the registered number. After doing this POC [Proof of Concept] I got the following Observation:
- On selecting the desired Student, Student View displays the correct information like Student Name and Student Number.
Note : By default, first selected index is for Student A, later user can change it to any say B, C or A
Student A : On Clicking the Contact Button it pop up the message box saying Contacting to Student A on : XXXXXXXXX.
Student B / C : On clicking the Contact, it shows the same Message as Contacting to Student A on: XXXXXXXXX. Same observation is noticed for Student C.
Note: If default student is changed from A to B or C in code, then on clicking the Contact Button of any Student Message Box will display the information of the default Student.
Using the Code
Here is MainWindow View
which hosts the StudentInfoView
<Window x:Class="ContactStudent.Views.MainWindowView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro" xmlns:nsVm="clr-namespace:ContactStudent.ViewModels" xmlns:nsVi="clr-namespace:ContactStudent.Views" Title="Student Details" Height="400" Width="488" ResizeMode="NoResize" Background="DarkGray"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="90"/> <RowDefinition Height="302"/> <RowDefinition Height="3*" /> </Grid.RowDefinitions> <Grid Grid.Row="0" Background="DarkGray"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--<Three Students i.e. A B C>--> <Button Name="Student_A" Grid.Column="0" Content="A" Width="110" Margin="22,22,22,28" ToolTip="Student A Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('A')]"/> <Button Name="Student_B" Grid.Column="1" Content="B" Width="110" Margin="22,22,22,28" ToolTip="Student B Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('B')]"/> <Button Name="Student_C" Grid.Column="3" Content="C" Width="110" Margin="22,22,22,28" ToolTip="Student C Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('C')]"/> </Grid> <nsVi:StudentInfoView Grid.Row="1" DataContext="{Binding Student}" Background="DarkGray" HorizontalAlignment="Left"/> </Grid> </Window>
Highlighted [Bold marked ]section in above code snippet shows the StudentInfoView
and its DataContext
Binding. It could be in the following way as well:
<ContentControl Grid.Row="1" Content="{Binding Student}" Background="DarkGray" HorizontalAlignment="Left"/>
StudentInfoView
: Button
Message.Attach
property is highlighted using bold format.
<UserControl x:Class="ContactStudent.Views.StudentInfoView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro" xmlns:local="clr-namespace:ContactStudent" mc:Ignorable="d" Height="185" Width="302" Background="DarkGray"> <Grid> <TextBlock Text="STUDENT'S DETAIL" TextAlignment="Center" FontWeight="Bold" Margin="64,0,96,165" /> <TextBlock Name="Student_Name" Text="Student Name" FontWeight="Bold" Margin="-1,37,206,128" TextAlignment="Center"/> <TextBlock Name="Student" Text="{Binding StudentName}" FontWeight="Bold" Background="LightGray" Margin="123,35,33,128" TextAlignment="Left"/> <TextBlock Name="Contact_Number" Text="Phone Number" FontWeight="Bold" Margin="0,69,206,97" TextAlignment="Center"/> <TextBlock Name="Number" Text="{Binding PhoneNumber}" FontWeight="Bold" Background="LightGray" Margin="123,69,33,97" /> <Button Name="Contact" Content="Contact" Margin="149,108,90,51" BorderBrush="Black" cal:Message.Attach="[Event Click]=[Action OnContactClick($source, $eventArgs)]"/> </Grid> </UserControl>
MainWindowViewModel
: It depicts that how StudentViewModel index selection
is done under the DisplayStudentDetails();
please refer the following code.
using Caliburn.Micro; public class MainWindowViewModel : PropertyChangedBase { #region Data Member private static int m_nCurrentStudentIndex = 0; //Current selected student index private StudentInfoViewModel[] m_strStudentDetails; //Collection of students #endregion Data Member #region Constructor public MainWindowViewModel() { //allocate the memory for the Student A,B and C m_strStudentDetails = new StudentInfoViewModel[3]; InitializeStudent();//Initialize StudentInfoViewModel instances to hold the information NotifyOfPropertyChange(() => Student); } #endregion Constructor #region Private Methods /// <summary> /// Initialization /// </summary> private void InitializeStudent() { int nIndex = 0; m_strStudentDetails[nIndex] = new StudentInfoViewModel("A", 1122334456); nIndex++; m_strStudentDetails[nIndex] = new StudentInfoViewModel("B", 1111111111); nIndex++; m_strStudentDetails[nIndex] = new StudentInfoViewModel("C", 1111111122); } #endregion Private Methods #region Event Handler /// <summary> /// Click Event Handler for the Button A,B and C /// </summary> /// <param name="strStudent">A/B/C</param> public void DisplayStudentDetails(String strStudent) { switch (strStudent) { case "A": m_nCurrentStudentIndex = 0; break; case "B": m_nCurrentStudentIndex = 1; break; case "C": m_nCurrentStudentIndex = 2; break; } NotifyOfPropertyChange(() => Student); } #endregion Event Handler #region Properties /// <summary> /// Get Current selected Student /// </summary> public StudentInfoViewModel Student { get { return m_strStudentDetails[m_nCurrentStudentIndex]; } } #endregion Properties }
DisplayStudentDetails()
is the Event Handler to handle the click event of the buttons used
in the MainWindowView. This handler decides the index, based on the button
clicked, for the StudentInfoViewModel.
One point which is to be noted i.e. in student View displays the correct information like Name and Phone Number; it means attached viewmodel is as per the selected Student. In other words DataContxet is updating.
Then, Why Click Event of
Contact Button is not routing to the attached ViewModel? Why is it always
routing to the first attached ViewModel only? Is Message.Attach
property
updating on the change of DataContext
?
Anyone or the Beginner who is trying to use the single View attached to any of the instance from the collection of ViewModel, may face the problem related to the routing the Event to correct ChildViewModel.
Problem
When a Child View is placed inside a main View and child view is binded to its ViewModel using DataContext or Content.
Main View has the option to
update the Child view by selecting the index, which in turns updates the
childviewodel instance [using DataContext/Content] at the run time; in this
scenario change in the DataContext/Content property of Child View doesn’t
update the Message.Attach
property.
In another words target of the action handler of child view doesn’t get updated as per the selection gets update inside the Main view
Solutions
I am suggesting several/multiple approach to fix this issue. Each approach has its pros and cons..
Solution 1: Maintain One-To-One Mapping between View and ViewModel
This solution talks about the maintaining one-to-one mapping between View and ViewModel as depicted in below given diagram:
Placing multiple ContentControl
and binding them to their ViewModel Instances is defined inside
the attached document.
This approach is very simple and easy to implement for small size application. Basically it’s about to maintain the views for each view model.
Based on the current selected index, View will be visible to the user and other view instances will be invisible; also viewmodel related to the selected view will be in action.
For my application where I have targeted only three students, I need to update the MainWindow View to hold the three ContentControl Instances for three StudentView. Based on the current selected index respective ContentControl will be visible.
MainWindowView
<Grid Grid.Row="0" Background="DarkGray"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--<Three Students i.e. A B C>--> <Button Name="Student_A" Grid.Column="0" Content="A" Width="110" Margin="22,22,22,28" ToolTip="Student A Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('A')]"/> <Button Name="Student_B" Grid.Column="1" Content="B" Width="110" Margin="22,22,22,28" ToolTip="Student B Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('B')]"/> <Button Name="Student_C" Grid.Column="3" Content="C" Width="110" Margin="22,22,22,28" ToolTip="Student C Information" cal:Message.Attach="[Event Click]=[Action DisplayStudentDetails('C')]"/> </Grid> <ContentControl Grid.Row="1" Content="{Binding StudentArr[0]}" Background="DarkGray" Visibility="{Binding IsVisible[0]}"/> <ContentControl Grid.Row="1" Content="{Binding StudentArr[1]}" Background="DarkGray" Visibility="{Binding IsVisible[1]}"/> <ContentControl Grid.Row="1" Content="{Binding StudentArr[2]}" Background="DarkGray" Visibility="{Binding IsVisible[2]}"/>
Highlighted [Bold marked] lines are the
modifications in the MainWindowView to host the three StudentInfoView. Each ContentControl
is having its own Content and Visibility property.
Note: Content property is an Object which contains the Control’s content. Because the Content property is of type Object, there are no restrictions on what you can put in a ContentControl.
MainWindowViewModel
ViewModel does also need
modification to provide the Content and Visibility for each ContentControl to
put the respective StudentInfoViewModel
.
public class MainWindowViewModel : PropertyChangedBase
{
#region Data Member
private static int m_nCurrentStudentIndex = 0; //Current selected student index
private StudentInfoViewModel[] m_strStudentDetails; //Collection of students
private Visibility[] m_IsSelected; //Collection of Visibility Property for ContentControl element
#endregion Data Member
#region Constructor
public MainWindowViewModel()
{
m_strStudentDetails = new StudentInfoViewModel[3]; // A,B and C
m_IsSelected = new Visibility[3];
InitializeStudent();//Initialize StudentInfoViewModel instances to hold the information
vUpdateVisibility(); //Initialize the visibilty property
}
#endregion Constructor
#region Private Methods
/// <summary>
/// Initialization
/// </summary>
private void InitializeStudent()
{
int nIndex = 0;
m_strStudentDetails[nIndex] = new StudentInfoViewModel("A", 1122334456);
nIndex++;
m_strStudentDetails[nIndex] = new StudentInfoViewModel("B", 1111111111);
nIndex++;
m_strStudentDetails[nIndex] = new StudentInfoViewModel("C", 1111111122);
}
#endregion Private Methods
private void vUpdateVisibility()
{
switch (m_nCurrentStudentIndex)
{
case 0:
m_IsSelected[0] = Visibility.Visible;
m_IsSelected[1] = Visibility.Hidden;
m_IsSelected[2] = Visibility.Hidden;
break;
case 1:
m_IsSelected[1] = Visibility.Visible;
m_IsSelected[0] = Visibility.Hidden;
m_IsSelected[2] = Visibility.Hidden;
break;
case 2:
m_IsSelected[2] = Visibility.Visible;
m_IsSelected[0] = Visibility.Hidden;
m_IsSelected[1] = Visibility.Hidden;
break;
}
NotifyOfPropertyChange(() => IsVisible);
}
#region Event Handler
/// <summary>
/// Click Event Handler for the Button A,B and C
/// </summary>
/// <param name="strStudent">A/B/C</param>
public void DisplayStudentDetails(String strStudent)
{
switch (strStudent)
{
case "A":
m_nCurrentStudentIndex = 0;
break;
case "B":
m_nCurrentStudentIndex = 1;
break;
case "C":
m_nCurrentStudentIndex = 2;
break;
}
vUpdateVisibility();
NotifyOfPropertyChange(() => StudentArr);
}
#endregion Event Handler
/// <summary>
/// Visibility of the ContentControl
/// </summary>
public Visibility[] IsVisible
{
get
{
return m_IsSelected;
}
}
/// <summary>
/// Content property value for ContentControl
/// </summary>
public StudentInfoViewModel[] StudentArr
{
get
{
return m_strStudentDetails;
}
}
}
}
It’s clearly understood from
the ViewModel implementation that I have maintained the array of Visibility and StudentInfoViewModel
. DisplayStudentDetails()
method decides the currently selected ContenControl. Based
on the m_nCurrentStudentIndex
vUpdateVisibility ()
set the visibility as visible for the current selection and hidden
for others.
Conclusion
Placing multiple ContentControl Instances and changing their visibility has the performance advantage of only updating the control layout by forcing view to be rebuilt. In another words the ContentControl will be available in the GUI at all times and easy to be referenced in the implementation when binding properties are updated.
Using One-To-One mapping in this scenario is OK as we are dealing with 3 students only, imagine the scenario where instances are required in 1000s or more than that. What would be size of the MainWindowView.xaml?
Secondly, we are using ContentControl Framework Element. ContentControl is suitable when Content is not known till runtime. Content can be string or any Object. But In this scenario we know about the View and Object so we should avoid the usage of ContentControl when it can be replaced with other Framework element.
Thirdly, unload and load time will get increase for the child view having more controls like buttons or more UI elements.
Also it will impact the size of the application as well.
Solution 2: Access the UI Element inside the ViewModel and update the Event Handler
This Solution is about to access the UI element inside the ViewModel and based on the selected index assign the related event handler e.g. we can get the Button handle inside the ViewModel and we can change the event handler.
Following section explains how to subscribe the event handler inside the view model for a UI element. Event handler can be updated based on the current selected viewmodel. By following this approach UI Control event can be routed to the target viewmodel among the available collection of viewmodel.
Steps to Implement
a. Get the UI Element inside the ViewModel
To get the UI element inside
the viewmodel one has to handle the "Loaded
” event for the UserControl
i.e. StudentInfoView
.
<UserControl x:Class="ContactStudent.Views.StudentInfoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
xmlns:local="clr-namespace:ContactStudent"
mc:Ignorable="d"
Height="185" Width="302" Background="DarkGray" cal:Message.Attach="[Event Loaded] = [Action vOnLoaded($Source)]" >
Note: Event Handler is defined
using Caliburn’s Message.Attach
property. Also remove the click event attached
for the Contact Button in StudentInforView.xaml
StudentInfoViewModel
public static Button ContactBtn
{
get;
set;
}
//Handles the Loaded event for the view and assign the handler for the Click Event
public void vOnLoaded(object sender)
{
ContactBtn = ((StudentInfoView)sender).Contact;
ContactBtn.Click += OnContactClick; }
Highlighted part is the value for the Name property of the control i.e. Button is the control in this example and Contact is the value for the Name property.
b.Update the Event Handler for the Control’s Event e.g. Click
Considering the Student example, Contact is the button and its
Click event needs update whenever there is a selection among the student. So
that Click event can be routed to the targeted viewmodel. This Switching is
defined inside the MainWindowViewModel
as this plays the controller role for
StudentInfoViewModel
.
/// <summary>
/// Click Event Handler for the Button A,B and C
/// <param name="strStudent">A/B/C</param>
public void DisplayStudentDetails(String strStudent)
{
StudentInfoViewModel.ContactBtn.Click -= Student.OnContactClick;
switch (strStudent)
{
case "A":
m_nCurrentStudentIndex = 0;
break;
case "B":
m_nCurrentStudentIndex = 1;
break;
case "C":
m_nCurrentStudentIndex = 2;
break;
}
StudentInfoViewModel.ContactBtn.Click += Student.OnContactClick;
NotifyOfPropertyChange(() => Student);
}
Highlighted section is the
addition to the DisplayStudentDetails()
method, as I have already
mentioned this is the method which identify the current selected StudentInfoViewModel
based on the selection is made
on GUI. In general terms this routing can be defined in the eventhandler which
is responsible to handle the change in selection event.
Conclusion
This implementation is about accessing the UI element inside the viewmodel, but this force the tight coupling between View and ViewModel.
For any change in the Control will force to implement the changes inside the ViewModel. This implementation is OK for small size application, means when there are not so many controls inside the UI to be handled in the same way.
Along with the increase in number of controls what if viewmodel instances are also large in number then it will require more focus on the handler registration and deregistration. This may cause crash in your application if this scenario won’t be handled properly.
You need to be very careful about the number of controls those required to be accessed inside the viewmodel and if you are ready to go against the MVVM principle.
Solution 3: Use of DependencyProperty
Defined problem can also be
solved using DependencyProperty
i.e. a property that can be set through methods such as, styling,
data binding, animation, and inheritance.
Dependency properties are the
properties of classes that derive from DependencyObject
, and they're special in
that rather than simply using a backing field to store their value, they use
some helper methods on DependencyObject
.
The dependency property is a public static read-only field that must be registered first. After it has been registered, this static property is used to get and set the value in the internal storage system.
Following section explains the steps to solve the problem
using DependencyProperty
.
Steps to Implement
a. Define the DependencyProperty inside the CustomAttachedProperties class
public static class CustomAttachedProperties
{
public static readonly DependencyProperty AttachExProperty =
DependencyProperty.RegisterAttached(
"AttachEx",
typeof(string),
typeof(CustomAttachedProperties),
new PropertyMetadata(null, OnAttachExChanged)
);
public static void SetAttachEx(DependencyObject d, string attachText)
{
d.SetValue(AttachExProperty, attachText);
}
public static string GetAttachEx(DependencyObject d)
{
return d.GetValue(AttachExProperty) as string;
}
private static void OnAttachExChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as FrameworkElement;
if (control == null)
{
return;
}
control.DataContextChanged += (sender, args) =>
{
//Need to clear AttachProperty value as Caliburn forces the MessageTrigger only when AttachProperty
//has different value then previous one
control.ClearValue(Message.AttachProperty);
control.SetValue(Message.AttachProperty, e.NewValue);
};
}
}
In the above given code snippet we have used RegisterAttached
to
register the property i.e. AttachEx
and the PropertyMetaData
with its PropertyChangedCallback
i.e. OnAttachExChanged
implementation reference.
b. Use the dependency property
inside the StudentInfoView
for its control to define their
event.
<Button Name="Conatct" Content="Contact"local:CustomAttachedProperties.AttachEx="[Event Click]=[Action OnContactClick($source,$eventargs)]" Margin="149,108,90,51" />
Highlighted section shows the way to use the DependencyProperty inside
the View i.e. StudentInfoView. Instead of using the Caliburn’s Message.Attach property, DependencyProperty
i.e. AttachEx
is used subscribe theDataContextChanged event. So whenever there is a change in the PropertyMetadata
, it
invokes the OnAttachExChanged()
.
This method internally handles the DataContextChanged
event, where it cleans up the Control’s Message.AttachProperty
and sets the new value.
Conclusion
A DependencyProperty
is set to enable declarative code to alter the properties of an
object which reduces the data requirements by providing a more powerful
notification system regarding the change of data in a very specific way. The DependencyProperty
is useful when application
requires some kind of animation or you need to allow the property to be set in
Style setters.
Cons:
- DependencyProperties are meant to be used by WPF's binding system, which is what ties the UI layer to the Data layer. They should be kept in the UI layer, and not in the data layer (ViewModels).
- DependencyProperties will be applicable mainly at the
VisualElements level so it won't be good idea if we create lot of DPs for each
of our business requirements. Also there is a greater cost for DP than an
INotifyPropertyChanged
.
Solution 4: Using Caliburn’s View.Model property
This solution is related to Caliburn’s property only and most of us who just started dealing with C# and various framework available for MVVM implementation won’t be aware of this.
View.Model property of Caliburn is the finding for me. After going through all the approaches I came to know about this property.
In
more related to the WPF language View.Model is the DependencyProperty
provided
by Caliburn.Micro. This provide us a way to host content inside
a ContentControl, bind it to model provided by View.Model and
update the view based on the selected model
Steps to Implement
First thing I would like to mention is this is very simple and one liner change inside the UserControl which controls selection of the viewmodel.
Just update the MainWindowView as given below and highlighted property is the key to fix the issue. J
<nsVi:StudentInfoView Grid.Row="1" cal:View.Model="{Binding Student}" Background="DarkGray"/>
or
<ContentControl Grid.Row="1" cal:View.Model="{Binding Student}" Background="DarkGray"/>
View.Model
This property binds the view to
the specified model. As in this example view or ContentControl is binded to the
"Student” i.e. current datacontext. Where it’s not only taking care of
change in DataContext but also considers the updates in child view’s Message.Attach
property.
As Message.Attach property is similar to define the delegate for some events, and view.model takes care of delegates as well by attaching the view to the current datacontext or telling view that this is the viewmodel/datalayer for data as well as for Event handling.
So we have discussed the four solution along with their pros and cons, I would like to conclude/summarize the approaches.
Summary/Conclusion
Inside the StudentInfo View’s
event handler is defined using Caliburn micro’s Message.Attach
property [No binding happens on the use of Message.Attach
.]
All this does is set up an EventTrigger
on the defined event like click for button with an Action of type Caliburn.Micro.ActionMessage
. This happens only once for
each view that is created! So when a DataTemplate instance is recycled by the
host view, although the DataContext/Content and all relevant bindings are
updated (StudentInfoView displays the correct information like Student Name and
Number), the ActionMessage associated with this view is never set to target
i.e. the new ViewModel.
Caliburn.Micro.View.Model
),
when the template’s DataContext is changed this DependencyProperty is updated.
This causes Caliburn.Micro to instantiate a new View (the View.Model
DepedencyProperty change handler calls the ViewLocator, which instantiates a
new view if one doesn’t exist), where our ActionMessage is correctly bound.Though other approaches like
use of user defined DependencyProperty
, fetching the UI element inside the ViewModel helped to resolve the issue but they have their
pros and cons. All these approaches can be implemented for limited number of
controls, as number of controls will get increase it will impact the
maintenance. Secondly these will be against of MVVM principle.
References
http://maonet.wordpress.com/2010/09/24/nested-viewusercontrol-binding-in-caliburn/
http://msdn.microsoft.com/en-us/library/ms752914(v=vs.110).aspx