The Silverlight Data Trigger May Be The Answer To All Your View Model MVVM Issues
Live Example: http://silverlight.adefwebserver.com/ViewModelTreeControl2
(Note: If you are new to View Model (MVVM), see: Silverlight View Model Style: An (Overly) Simplified Explanation)
I have found that you either love View Model (MVVM) or you hate it. if you hate it, I know why... USING CODE BEHIND IS EASIER. :) The primary thing that code behind allows you to do is, A) Do one thing, then B) Do another. The problem with View Model style programming is that while bindings are great for most situations, many have found it frustrating and sometimes seemingly impossible to implement certain functionality with View Model that would normally be easy to do using code behind.
In this article, I hope to show you that the key tool that should make most of your View Model (MVVM) issues go away is the Silverlight DataTrigger.
The Problem
In the article, Programmatic Silverlight Tree View Control Node Expansion using View Model (MVVM), I showed how to create a Behavior that will expand a selected Tree Node without using code behind. The problem is that it only works if the property that indicates that the Tree Node is selected is set BEFORE the Behavior runs.
With code behind, this would not be a problem. When the user clicks the Button, the code selects the Categories, then the code expands the Tree Nodes. The problem with View Model is that even if the Button were set to trigger setting the Categories, and triggering the Behavior to select the Tree Nodes, the selected Categories may not set before the Behavior runs, so the Behavior will not expand the Tree Nodes. Grrrrrr!
The Solution
The solution is to execute the Behavior ONLY AFTER the Categories have been selected.
This is done by setting a Boolean property in the View Model AFTER the Categories have been selected. A Silverlight Expression Blend DataTrigger (If you don't have Expression Blend, you can download the SDK at this link), is used to bind the Behavior to the property in the View Model, and will trigger the Behavior and expand the Tree Nodes.
The key place that this solution addresses your "normal code behind way of doing things", is that when the SetCategoryCommand
is triggered by the Button, you can "run all the code you want" before you set the ExpandSelectedTreeNodes
property.
In normal code behind, this is all you can do anyway; run code and then set (or not set) a value on the UI (the View in this case). The thing that normally drives people crazy about View Model (MVVM) is that they feel they lose the:
- Click a Button
- Run code
- Change the UI
- The end
Now you can:
- Click a Button
- Run code
- Change property in View Model
- Behavior (bound to the property in the View Model) changes the UI
- The end
The Example Application
In the example application, we have two Behaviors. One that expands the Tree Control (TreeExpandBehavior
) and one that collapses it (TreeCollapseBehavior
).
Below are the Behaviors:
TreeExpandBehavior
[System.ComponentModel.Description("Expands Tree Node")]
public class TreeExpandBehavior : TargetedTriggerAction<TreeView>, INotifyPropertyChanged
{
private TreeView objTreeView;
protected override void OnAttached()
{
base.OnAttached();
objTreeView = (TreeView)(this.AssociatedObject);
}
protected override void Invoke(object parameter)
{
SelectNode();
}
void objTreeView_Loaded(object sender, RoutedEventArgs e)
{
SelectNode();
}
#region SelectNode
private void SelectNode()
{
objTreeView.UpdateLayout();
MainPageViewModel objMainPageViewModel =
(MainPageViewModel)objTreeView.DataContext;
ObservableCollection<Category> colCategories =
(ObservableCollection<Category>)objMainPageViewModel.colCategory;
if (colCategories != null)
{
foreach (var Cat in colCategories)
{
var result = (from objCategory in Cat.AllChildren()
where objCategory.IsSelected == true
select objCategory).FirstOrDefault();
if (result != null)
{
TreeViewItem objTreeViewItem = (TreeViewItem)
objTreeView.ItemContainerGenerator.ContainerFromItem(Cat);
objTreeViewItem.IsExpanded = true;
objTreeView.UpdateLayout();
ExpandChildNode(objTreeViewItem, Cat);
}
}
}
}
private void ExpandChildNode(TreeViewItem objTreeViewItem, Category Cat)
{
foreach (var item in Cat.Categories)
{
var result = (from objCategory in item.AllChildren()
where objCategory.IsSelected == true
select objCategory).FirstOrDefault();
if (result != null)
{
TreeViewItem SubTreeViewItem = (TreeViewItem)
objTreeViewItem.ItemContainerGenerator.ContainerFromItem(item);
SubTreeViewItem.IsExpanded = true;
objTreeView.UpdateLayout();
ExpandChildNode(SubTreeViewItem, item);
}
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
TreeCollapseBehavior
[System.ComponentModel.Description("Collapse Tree Nodes")]
public class TreeCollapseBehavior :
TargetedTriggerAction<TreeView>, INotifyPropertyChanged
{
private TreeView objTreeView;
protected override void OnAttached()
{
base.OnAttached();
objTreeView = (TreeView)(this.AssociatedObject);
}
protected override void Invoke(object parameter)
{
CollapseTree();
}
void objTreeView_Loaded(object sender, RoutedEventArgs e)
{
}
#region CollapseTree
private void CollapseTree()
{
objTreeView.UpdateLayout();
MainPageViewModel objMainPageViewModel =
(MainPageViewModel)objTreeView.DataContext;
ObservableCollection<Category> colCategories =
(ObservableCollection<Category>)objMainPageViewModel.colCategory;
if (colCategories != null)
{
foreach (var Cat in colCategories)
{
var result = (from objCategory in Cat.AllChildren()
select objCategory).FirstOrDefault();
TreeViewItem objTreeViewItem = (TreeViewItem)
objTreeView.ItemContainerGenerator.ContainerFromItem(Cat);
if (objTreeViewItem != null)
{
objTreeViewItem.IsExpanded = false;
objTreeView.UpdateLayout();
CollapseChildNode(objTreeViewItem, Cat);
}
}
}
}
private void CollapseChildNode(TreeViewItem objTreeViewItem, Category Cat)
{
foreach (var item in Cat.Categories)
{
var result = (from objCategory in item.AllChildren()
select objCategory).FirstOrDefault();
TreeViewItem SubTreeViewItem = (TreeViewItem)
objTreeViewItem.ItemContainerGenerator.ContainerFromItem(item);
if (SubTreeViewItem != null)
{
SubTreeViewItem.IsExpanded = false;
objTreeView.UpdateLayout();
CollapseChildNode(SubTreeViewItem, item);
}
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
In the properties for the Behaviors, we click the New Button to change the TriggerType
to DataTrigger
, and we set the Binding
to the ExpandSelectedTreeNodes
property in the View Model. For the TreeExpandBehavior
, we set the Comparison
to True, and for the TreeCollapseBehavior
, we set the Comparison
to False.
Note: If the window does not pop up to allow you to change the TriggerType, you can drop a ChangePropertyAction Behavior on the page and then remove it. This appears to add references needed to the project to make Expression Blend pop up the TriggerType selection box when you click the New Button.
The Code
When the Set Nodes Button is clicked, it calls the SetCategory
method in the View Model:
public ICommand SetCategoryCommand { get; set; }
public void SetCategory(object param)
{
foreach (var Cat in colCategory)
{
var result = from objCategory in Cat.AllChildren()
where
(objCategory.CategoryName == "Category Sub2-1" ||
objCategory.CategoryName == "Category Two Sub2")
select objCategory;
foreach (var item in result)
{
item.IsSelected = true;
}
ExpandSelectedTreeNodes = true;
this.NotifyPropertyChanged("ExpandSelectedTreeNodes");
}
}
The ExpandSelectedTreeNodes
property is not set until AFTER the method has set the Categories
.
Yes that's it. :)
DataTrigger + Behaviors = No Code Behind
If you are told of the benefits of using View Model and bindings, you expect that you should be able to use them for everything. If bindings are 'good', then you should be able to use them 100% of the time.
Hopefully we have demonstrated that the Silverlight DataTrigger allows you to "raise an event" that a Behavior can "Detect" on the UI (the View). The View Model is still fully decoupled from the View (it doesn't even know if 'anyone is listening').
Behaviors are easy to write, in many cases you will find you have less code than if you used code behind. Furthermore Behaviors are easily reused and can easily be consumed by non-programmers, for example Designers.
Kunal Chowdhury's Article
To really understand Triggers and to also see how they work with methods in your View Model (not just ICommands
), see Kunal Chowdhury's article Using EventTrigger in XAML for MVVM – No Code Behind.
Further Reading