Tuesday, May 10, 2011

Databinding to POCOs with PDX

The first in a series of posts about PDX. You should probably check out the Getting Started page first, if you haven't already.

As I have been working with and learning more about WPF and MVVM, one thing that has always annoyed me is implementing INotifyPropertyChanged to support data binding. There are certainly ways to improve upon the most naive approach such as:
  • Using lambdas instead of strings
  • Using dynamic proxies
  • Using weaving
  • Using code generation
(Here is a quick overview of these approaches) None of these approaches appeal to me too much because they all place the concern on the Viewmodel in one way or another. Once the code is built and running, the Viewmodel still has to notify the UI some way, and I would rather have my Viewmodel not concerned with that.

Even worse is when I have a Viewmodel that closely mirrors a POCO - it appears I'm not the only one this happens to. Now I have to create a Viewmodel when what I really want to do is bind to a DTO or something. The DTO may not be under my control, but even if it is, I would rather not pollute it with UI notification concerns.

So while I was in the process of creating PDX, I was pleased to realize that I could make it support "Plain Old ViewModels".

A Simple Example

Ah, the ubiquitous Customer, fodder for so many software examples. And since I am a fan of defaults...

The Viewmodel

public class CustomerViewModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName
    {
        get
        {
            return String.Format("{0}, {1}", LastName, FirstName);
        }
    }
}

The View

<StackPanel>
    <UniformGrid Columns="2">

        <TextBlock Text="First Name:" />
        <pdx:Context PropertyPath="FirstName">
            <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </pdx:Context>
            
        <TextBlock Text="Last Name:" />
        <pdx:Context PropertyPath="LastName">
            <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </pdx:Context>
            
        <TextBlock Text="Full Name:" />
        <pdx:Context PropertyPath="FullName">
            <TextBlock Text="{Binding Value}" />
        </pdx:Context>
            
    </UniformGrid>
</StackPanel>

The result, with vanilla PDX, is that the Full Name is updated while the user is typing in First Name or Last Name.

How It Works

You probably noticed we're databinding the TextBoxes to "Value". This may have tipped you off that we're not binding directly to a POCO after all. Yes, I lied. The "pdx:Context" is the key here - it is a ContentControl that binds the DataContext of its Content to the "PropertyModel". The PropertyModel is a small object that sits between the View and one of the Viewmodel's properties, providing a standard way to access various aspects of the property. The PropertyModel is a larger topic, but what is relevant here is that the PropertyModel exposes the property value via the "Value" property. Since property changes are being routed through the PropertyModel, we can implement our own notification mechanism. Every time a PropertyModel's Value is set, all the other PropertyModels for the given Viewmodel check if they have changed. If they have, they will raise a notification.

More XAML?

"Oh great, XAML wasn't verbose enough, now I get to surround all my bindings with pdx:Contexts!" It's true that the XAML above is a little more verbose than just specifying the property in the TextBox's Binding. But with the power of Conventional DataTemplates, the above XAML could be written as:

<pdx:Context PropertyPath="FirstName">
    <pdx:Editor />
</pdx:Context>

With out-of-the-box PDX, this is equivalent to the more verbose XAML example. Conventional DataTemplates are another topic (they were actually my initial reason for writing PDX), but rest assured that your XAML when using PDX will be less verbose than a traditional approach.

Binding to a Child Object

I mentioned earlier the problem of duplicating properties in a Viewmodel when you really want to bind to a DTO or some other POCO. Consider this scenario:

public class CustomerPOCO
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}

public class CustomerViewModel
{
    // This tells the DefaultPropertyResolver to look here for properties.
    // So if "FirstName" isn't found on this (the CustomerViewModel), the
    // resolver will try to find it on the DataModel.
    [PDX.PropertySource]
    public CustomerPOCO DataModel { get; private set; }
        
    public CustomerViewModel()
    {
        DataModel = new CustomerPOCO();
    }

    public string FullName
    {
        get
        {
            return String.Format("{0}, {1}", DataModel.LastName, DataModel.FirstName);
        }
    }
}

Here the FirstName and LastName properties are on a POCO and the Viewmodel augments the POCO with the FullName property. The exact same XAML from the previous example works for this scenario too. The reason is the [PDX.PropertySource] attribute on DataModel - as the comments say, the property resolution mechanism will not find FirstName or LastName on the Viewmodel, so it will try DataModel.FirstName and DataModel.LastName. It will resolve them and create PropertyModels, and we still don't have to implement any notifications manually because all value changes are going through a PropertyModel.

If the CustomerPOCO is a WCF DTO, when the time comes, we can just send the DTO straight to the server; the values are already in it. No property mapping, no property wrapping - and only the PDX library is concerned with raising notifications.

When it Won't Work

Anytime a property value modification doesn't go through PDX in some way, PDX won't know about it. So if, for example, a Viewmodel has a Timer that changes a property value every second, PDX won't know about it unless the Viewmodel implements INotifyPropertyChanged. However, the Viewmodel doesn't have to actually implement it "correctly" - it can raise a dummy PropertyChanged event with the name of a non-existent property. All the PropertyModels will simply check if the value has changed since the last known value, and if so, they will notify the UI. This can be useful, for example, for a Command that may change properties. If you don't want to implement full INPC, just raise a dummy notification anytime properties might have changed.

Now consider something like this:

<Image>
    <Image.RenderTransform>
        <TranslateTransform X="{Binding OffsetX}" Y="{Binding OffsetY}" />
    </Image.RenderTransform>
</Image>

This is a problem for PDX because the Image can only reside in one pdx:Context - and the PropertyModel can only be for one property. Meaning we can do OffsetX or OffsetY but not both.

One approach I have been considering, but I don't recommend (at least not yet) is something like this:

<pdx:Context PropertyPath="OffsetX">
    <pdx:Forwarder x:Name="OffsetX" />
</pdx:Context>
<pdx:Context PropertyPath="OffsetY">
    <pdx:Forwarder x:Name="OffsetY" />
</pdx:Context>
<Image>
    <Image.RenderTransform>
        <TranslateTransform
            X="{Binding Value, ElementName=OffsetX}"
            Y="{Binding Value, ElementName=OffsetY}" />
    </Image.RenderTransform>
</Image>

The pdx:Forwarder doesn't exist (yet), it is a hypothetical invisible element that binds its "Value" DependencyProperty to PropertyModel.Value. This could allow us to "pipe" notifications from a PropertyModel to a Forwarder to any DependencyProperty in the name scope. But this seems sketch. It's probably cleaner to just implement them as traditional INPC properties.

Conclusion

One useful role of PDX is an alternate binding mechanism. In many cases, we don't need to implement INotifyPropertyChanged, allowing us to bind to POCOs.

Another benefit of using the PDX binding mechanism is that binding errors become much easier to catch - we can write tests easily and even generate build errors if we're using a design-time DataContext. I plan to write a post on this soon. Stay tuned...

No comments:

Post a Comment