Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight DataTrigger is the Answer to View Model / MVVM Issues

0.00/5 (No votes)
6 Nov 2010 2  
How using the Silverlight DataTrigger allows you to run a process and then raise another when using View Model (MVVM)

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:

  1. Click a Button
  2. Run code
  3. Change the UI
  4. The end

Now you can:

  1. Click a Button
  2. Run code
  3. Change property in View Model
  4. Behavior (bound to the property in the View Model) changes the UI
  5. 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()
    {
        // Refresh Tree Control
        objTreeView.UpdateLayout();
        // Get the DataContext of the Tree Control
        MainPageViewModel objMainPageViewModel = 
		(MainPageViewModel)objTreeView.DataContext;

        // Get collection of items bound to Tree Control
        ObservableCollection<Category> colCategories = 
            (ObservableCollection<Category>)objMainPageViewModel.colCategory;

        if (colCategories != null)
        {
            // Loop through the top levels items
            foreach (var Cat in colCategories)
            {
                // Find the Node
                var result = (from objCategory in Cat.AllChildren()
                                where objCategory.IsSelected == true
                                select objCategory).FirstOrDefault();

                if (result != null)
                {
                    // Get the Tree Control node container for the item
                    TreeViewItem objTreeViewItem = (TreeViewItem)
			objTreeView.ItemContainerGenerator.ContainerFromItem(Cat);
                    // Expand the Node
                    objTreeViewItem.IsExpanded = true;

                    // Refresh Tree Control
                    objTreeView.UpdateLayout();

                    ExpandChildNode(objTreeViewItem, Cat);
                }
            }
        }
    }

    // This method expands child nodes
    private void ExpandChildNode(TreeViewItem objTreeViewItem, Category Cat)
    {
        // Loop through all the sub Categories
        foreach (var item in Cat.Categories)
        {
            // Find the Node
            var result = (from objCategory in item.AllChildren()
                            where objCategory.IsSelected == true
                            select objCategory).FirstOrDefault();

            if (result != null)
            {
                // Get the Tree Control node container for the item
                TreeViewItem SubTreeViewItem = (TreeViewItem)
		objTreeViewItem.ItemContainerGenerator.ContainerFromItem(item);
                // Expand the Node
                SubTreeViewItem.IsExpanded = true;

                // Refresh Tree Control
                objTreeView.UpdateLayout();

                // Recursively call the function                
                ExpandChildNode(SubTreeViewItem, item);
            }
        }
    }
    #endregion

    // Utility

    #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()
    {
        // Refresh Tree Control
        objTreeView.UpdateLayout();
        // Get the DataContext of the Tree Control
        MainPageViewModel objMainPageViewModel = 
            (MainPageViewModel)objTreeView.DataContext;

        // Get collection of items bound to Tree Control
        ObservableCollection<Category> colCategories = 
            (ObservableCollection<Category>)objMainPageViewModel.colCategory;

        if (colCategories != null)
        {
            // Loop through the top levels items
            foreach (var Cat in colCategories)
            {
                // Get Parent Node
                var result = (from objCategory in Cat.AllChildren()
                                select objCategory).FirstOrDefault();

                // Get the Tree Control node container for the item
                TreeViewItem objTreeViewItem = (TreeViewItem)
		objTreeView.ItemContainerGenerator.ContainerFromItem(Cat);

                if (objTreeViewItem != null)
                {
                    // Collapse the Node
                    objTreeViewItem.IsExpanded = false;

                    // Refresh Tree Control
                    objTreeView.UpdateLayout();

                    CollapseChildNode(objTreeViewItem, Cat);
                }
            }
        }
    }

    // This method collapses child nodes
    private void CollapseChildNode(TreeViewItem objTreeViewItem, Category Cat)
    {
        // Loop through all the sub Categories
        foreach (var item in Cat.Categories)
        {
            // Find the Node
            var result = (from objCategory in item.AllChildren()
                            select objCategory).FirstOrDefault();

            // Get the Tree Control node container for the item
            TreeViewItem SubTreeViewItem = (TreeViewItem)
		objTreeViewItem.ItemContainerGenerator.ContainerFromItem(item);

            if (SubTreeViewItem != null)
            {
                // Collapse the Node
                SubTreeViewItem.IsExpanded = false;

                // Refresh Tree Control
                objTreeView.UpdateLayout();

                // Recursively call the function                
                CollapseChildNode(SubTreeViewItem, item);
            }
        }
    }
    #endregion

    // Utility

    #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)
    {
        // Set Nodes as Selected
        foreach (var Cat in colCategory)
        {
            // Find the Node
            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)
            {
                // Set the IsSelected property so the checkbox
                // will be checked
                item.IsSelected = true;
            }

            // Change the value of ExpandSelectedTreeNodes to True
            ExpandSelectedTreeNodes = true;

            // This forces anything bound to is, to be notified
            // That the value has been updated so that
            // Any Behavior bound to it is fired
            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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here