Workarounds for weaker RelativeSource binding in Silverlight 4

Tags: Silverlight 4, databinding, datagrid, mvvm

or How to design an MVVM-friendly Expand/Collapse button in a Silverlight Datagrid

Simple senario: you'd like to use the RowDetails feature of the Silverlight Datagrid, but you don't like its default behavior -- that the row details expand when the row is selected. Instead, you'd like a button to toggle the visibility of row details. The button's appearence should change (between + and -, lets say) to show the user that the row details can be expanded or hidden. I've seen some awful code to implement this, with code-behind setting a button's content to hard-coded strings.  But for every wrong way, there's a right way.  Conceptually, there is a pretty simple solution. Instead of using a regular Button, if we use a ToggleButton, we have a control that maintains state and (thanks to VisualStateManager) can have a different appearance for each state. All we then have to do is bind the ToggleButton's IsChecked property to the DataGridRow's visibility, using an IValueConverter to convert bool to the Visibility enum. The only problem is, in Silverlight 4, you can't create this binding in XAML. Since rows are automatically generated at runtime, how do you set the binding's target to the DataGridRow that the button is contained in? In WPF, the RelativeSource binding allows you to "search" up the visual tree by control type. Silverlight's RelativeSource binding is weaker. Silverlight 5 will be delivering this feature (Ancestor RelativeSource), but what do we do in the meantime? I have two suggested workarounds. Both use an extension method I end up putting in most of my Silverlight projects.

     /// <summary>
    /// Walk up the VisualTree, returning first parent object of the type supplied as type parameter
    /// </summary>
    public static T FindAncestor<T>(this DependencyObject obj) where T : DependencyObject
    {
        while (obj != null)
        {
            T o = obj as T;
            if (o != null)
                return o;

            obj = VisualTreeHelper.GetParent(obj);
        }
        return null;
    }

One method is to wire up the binding when the ToggleButtons load:

     private void ToggleButton_Loaded(object sender, RoutedEventArgs e)
    {
        ToggleButton button = sender as ToggleButton;
        DataGridRow row = button.FindAncestor<DataGridRow>();  //Custom Extension
        row.SetBinding(DataGridRow.DetailsVisibilityProperty, new Binding() 
        {   
            Source = button, 
            Path = new PropertyPath("IsChecked"), 
            Converter = new VisibilityConverter(), 
            Mode = BindingMode.TwoWay 
        });
    }

Even though this uses code-behind, it does not break the MVVM pattern since we are only coding display logic and not application logic into the View's code-behind.

Still, if you don't like any code in code-behind, another method is to add a Behavior (or more specifically, an Action) to the ToggleButton.

     public class ExpandRowAction : TriggerAction<ToggleButton>
    {
        protected override void Invoke(object o)
        {
            var row = this.AssociatedObject.FindAncestor<DataGridRow>();
            if (row != null)
            {
                if (this.AssociatedObject.IsChecked == true)
                    row.DetailsVisibility = Visibility.Visible;
                else
                    row.DetailsVisibility = Visibility.Collapsed;
            }
        }
    }

Then in XAML:

     <ToggleButton Style="{StaticResource PlusMinusToggleButtonStyle}" >
	    <i:Interaction.Triggers>
		    <i:EventTrigger EventName="Click">
			    <behaviors:ExpandRowAction/>
		    </i:EventTrigger>
	    </i:Interaction.Triggers>
    </ToggleButton>

Writing up this post, another solution occurred to me -- combine BOTH previous methods! Create an Action, automatically triggered on the ToggleButton.Loaded event that wires up the binding. Here are all 3 solutions in action:

Download the source code.

Add a Comment