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.
Figure 1 : Event Routing of Event to
ViewModel
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>
<!---->
<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; private StudentInfoViewModel[] m_strStudentDetails; #endregion Data Member
#region Constructor
public MainWindowViewModel()
{
m_strStudentDetails = new StudentInfoViewModel[3];
InitializeStudent(); NotifyOfPropertyChange(() => Student);
}
#endregion Constructor
#region Private Methods
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
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
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:
Figure 2 : One-to-One mapping between View and ViewModel
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>
<!---->
<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; private StudentInfoViewModel[] m_strStudentDetails; private Visibility[] m_IsSelected;
#endregion Data Member
#region Constructor
public MainWindowViewModel()
{
m_strStudentDetails = new StudentInfoViewModel[3]; m_IsSelected = new Visibility[3];
InitializeStudent(); vUpdateVisibility();
}
#endregion Constructor
#region Private Methods
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
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
public Visibility[] IsVisible
{
get
{
return m_IsSelected;
}
}
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;
}
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
.
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) =>
{
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.
To overcome this problem I have
suggested four solutions among them I would prefer
Solution 4, as it solves the
problem using Caliburn’s property itself and that too with minimal change. By replacing the DataContext/Content proptery of WPF with Solution 4 our datacontext is now turned into the databound to a DependencyProperty (
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://blog.martindoms.com/2013/01/15/caliburn-micro-datatemplates-actionmessages-and-virtualized-itemscontrols/
http://msdn.microsoft.com/en-us/library/ms752914(v=vs.110).aspx