WPF - Data-binding a lesson in framework design

|

In WPF we tend to take data-binding for granted. It is just one of those features that just works. In this post I want to briefly bring your attention to how the data-binding system works with regards to non-FrameworkElement derived classes, not because you need to know to be able to use it, but because I think it has all the hallmarks of a good API design. [...snip...]

Consider this XAML:

  xmlns="..."
  xmlns:x="..."
  xmlns:sys="clr-namespace:System;assembly=mscorlib">

 
   
      Black
      Red
      Green
      Blue
   

            Text="{Binding SelectedItem, ElementName=colors}"
        FontSize="36"
        Margin="10">
     
                  Color="{Binding SelectedItem, ElementName=colors}" />
     

   
 

If you put this XAML straight into something like XamlCruncher or Kaxaml (XamPad doesn't quite cut it because of the external reference to MSCoreLib) you end up with something like this:

DataBindingToBrushName

When you click the items in the ListBox, the TextBlock foreground colour changes and so does the text.

Great, data-binding in action - a somewhat contrived example but it works as you would expect. However, things get a little more interesting when you try and achieve the same effect in procedural code.

Setting the TextBlock text is simple enough as TextBlock is derived from FrameworkElement that has a SetBinding method that we can use - we would write code similar to the following:

// Create the binding
Binding colorsBinding = new Binding();
colorsBinding.Source = this.colors;
colorsBinding.Path = new PropertyPath("SelectedItem");

// Create the TextBlock
TextBlock tb = new TextBlock();
tb.FontSize = 36;
tb.Margin = new Thickness(10);

// Bind the TextProperty to the ListBox.SelectedValue
tb.SetBinding(TextBlock.TextProperty, colorsBinding);

This code is considerably more verbose than using the lovely data-binding XAML syntax, but it should be clear what's going on. Here's the equivalent XAML, as we saw above:

    Text="{Binding SelectedItem, ElementName=colors}"
    FontSize="36"
    Margin="10">

This would render as:

DataBindingToBrushName2

Note the lack of colour, let's do that next. We can use the same Binding class instance as before and apply it to the ColorProperty dependency property on the SolidColorBrush class, then apply that brush to the TextBlock instance, as shown here:

SolidColorBrush brush = new SolidColorBrush();
brush.SetBinding(SolidColorBrush.ColorProperty, colorsBinding);
tb.Foreground = brush;

However, if you try and compile this code your compiler will complain that SolidColorBrush does not have a SetBinding method. This is because SolidColorBrush derives from Brush, which derives from Animatable, and then from Freezable, before we finally hit DependenyObject:

  • DependencyObject
    • Freezable
      • Animatable
        • Brush
          • SolidColorBrush

But TextBlock has a somewhat different lineage:

  • DependencyObject
    • Visual 
      • Animatable
        • UIElement 
          • FrameworkElement
            • TextBlock

So what's the story here, these two objects only have DependencyObject in their common ancestry, and that class does not have the necessary SetBinding method, how can the binding possibly work at all?

If you were to look at the implementation code for the SetBinding method in FrameworkElement the answer would immediately present itself:

public BindingExpressionBase SetBinding(
    DependencyProperty dp,
    BindingBase binding)
{
    return BindingOperations.SetBinding(this, dp, binding);
}

BindingOperations.SetBinding, this static method asks for a target dependency object, a dependency property and a binding instance. So we should be able to re-write our previous code to use this class directly, like this:

SolidColorBrush brush = new SolidColorBrush();
BindingOperations.SetBinding(
    brush, SolidColorBrush.ColorProperty, colorsBinding);
tb.Foreground = brush;

Which works as expected, therefore the visual outcome is the same as above, but this time the effect is achieved by using procedural code:

DataBindingToBrushName

Now we understand what is going on a little better how does this helps us? "So what?" you might ask. You can still write the same XAML there's nothing new there; learning this has not changed a single thing for you when it comes to data-binding, it just works. It is exactly the it just works aspect of the data-binding design I think is soooo important in this implementation.

The designers of the data-binding API put the convenience method (SetBinding) in exactly the right place to make sense for the object model (high cohesion); but they implemented the feature in such a way that it's not tied to that specific scenario, meaning that have enabled many scenarios with only the absolutely minimum requirements or contract being placed on the API, therefore loosely coupled. Brilliant!

Every single day I learn more about WPF and every day I appreciate the quality of the framework design. Enjoy and learn from a great design, I know I will.

WPF - Selecting TreeViewItems in code

|

This appears to be a common problem for people with various solutions, most involving some walking of the visual tree, which to my mind is a little overkill. Also, you've got to wonder what the WPF team were thinking when they left out such a core (and apparently much demanded) feature, so there has to be an easier way, right? [...snip...]

As with most things like this you rarely find a complete solution in a single location, but that is what this post (and possibly a one or two more) will give you - which will not involve walking the visual tree, but will actually leverage the WPF data binding engine.

Let us start with the application scenario, to give us something to work with. Consider the following XML:



   
       
       
       
       
   

   
       
       
       
   

   
       
       
       
   

A very simple schema for a library of books. Our first step is to get this data into a TreeView. We'll use CLR objects, rather than XML directly (the reasoning for this will be apparent later) - so lets assume that there is an appropriate object model of CLR objects to support this data schema, something like:

LibraryDataModel

To wire a TreeView and this object model together is pretty straight forward: we provide the TreeView with a HierarchicalDataTemplate, something like this:


 
      
       DataType="{x:Type ld:Author}"
       ItemsSource="{Binding Path=Books}">
      
    
    
      
    

 
  

The only significant difference between a DataTemplate and a HierarchicalDataTemplate is the ItemsSource property; this simply tells the TreeView where to get the children for the templated item from, the Books property in this example.

Next we must provide the TreeView with its ItemsSource, which is the list of Authors; some like this:

private void OnLoaded(object sender, RoutedEventArgs e)
{
   this.tree.ItemsSource = DataProvider.GetAuthors();
}

This will produce an output similar to the following:

LibraryTreeViewOutput 

Right, so that's the basic application, lets start trying to tackle the actual problem.

Before we go too much further we need to talk a little bit about what makes up a TreeView. The important thing to understand for this solution is that each tree node is actually a type called TreeViewItem, and it is this item that displays the data template we defined earlier for our custom types Author and Book.

So you should be able to see from the image above (click on it to see a lager version) is that the TreeViewItem is the container for our content. The TreeView kindly provides a simple way for us to work with the TreeViewItem indirectly by using the TreeView.ItemsContainerStyle. This property enables us to modify the style associated with the TreeViewItem template - remember that a Style tweaks the properties on UI elements that the Template loaded in the visual tree to render our view. Simply put: a Style work with objects already in the visual tree by changing the values of (dependency) properties.

What this means for us is that the TreeViewItem has two properties that we can use: IsSelected and IsExpanded, if we bind our business objects to these two properties via the ItemsContainerStyle like so:


   
       
   

We can effect the behaviour of the tree buy manipulating our business objects! To make this all work we need to add these two properties to all the business objects that can appear in the tree control, Author and Book in this case. Then we must add change notification for the binding system to notice when we update these properties, which will then in turn update the tree control. Here's the new data model:

LibraryDataModelWithChangeNotification

With all this in place, we're all set to manipulate the tree by using our data model. For example, when the application loads, the tree nodes are all collapsed (IsExpanded defaults to false), therefore to expand all the Author nodes and to select the Book with the title "Beautiful Evidence" we would write the following code:

foreach (Author a in this.tree.ItemsSource)
{
    a.IsExpanded = true;

    if (a.Name == "Edward Tufte")
    {
        foreach (Book b in a.Books)
        {
            if (b.Title == "Beautiful Evidence")
            {
                b.IsSelected = true;
                break;
            }
        }
    }
}

Running this code should produce something that looks like this:

LibraryTreeViewRunning

Here's the complete code for this simple example, note that this code is just the bare bones of what we've been talking about in this post, it's far from production code:

PaulJ.TreeViewManipulation.zip

This is not the end of the story, however. This solution is still a little sucky, due to the tight coupling between the data model and the requirements of the UI by way of the TreeViewItem; the current data model would not be great for reuse in a WCF service or a Silverlight application for example.

Therefore in future post we'll look at tackling the separation of concerns with a couple of design patterns, probably the Decorator pattern (regular readers would have heard that story before) and the most likely sound solution, in my opinion, will probably be a Model View Controller approach, but we'll see - we can figure it out together.

Until next time, enjoy!

Newer Posts Older Posts Home