Sunday, February 14, 2016

WPF Binding Recommendations

I just audited the binding code in a reasonably large WPF application I wrote in July 2015 and was embarrassed to find lots of subtle bugs. Some controls were not bound to the specific properties they depended upon, some controls were bound to irrelevant or incorrect properties, and some binding properties were orphaned and unused.

The Visual Studio designer and the compiler can't fully bridge the gap between XAML and code, so human error is bound to creep in. You may accidentally cut-and-paste the same binding property into two different places, you may misspell a property, and so on. Many errors like this will not show up in the designer and you may only detect them by looking for binding error messages in the Visual Studio output window. Despite the danger of coding errors, I have set the following vague rules for myself to help binding work more reliably:
  1. Bind whenever it's possible.
  2. Put all binding logic in the converters.
  3. Don't forget to use the powerful MultiBinding.

When Possible

Although the syntax of binding is verbose and it's implementation is clumsy and incomplete, it's a great advance in the separation of business logic from UI behaviour. In the 1990s you could use MFC/C++ Data Exchange macros and classes in an attempt to move data in and out of the UI, but it was tedious and unnatural. In 2005 I participated in writing a huge Windows Forms app where we simulated WPF style binding in code. It generally worked, but the amount of code (and hacks) was staggering. A year later, Framework 3.0 and WPF were released, which made most of our WinForms code redundant. So modern binding is a great facility and you should use whenever possible.

Sometimes you'll find properties that can't be bound, a good example is the DataGrid's SelectedItems property. Some people use clever tricks like custom attached properties and behaviors to bind the unbindable, but I think this is overkill and I prefer to use a little bit of code-behind to fill the gap. With the DataGrid example I would do this:

private void Grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var selects = ((DataGrid)sender).SelectedItems.Cast<Foo>().ToArray();
  controller.SelectedFoos = selects;
}

Purists don't like any code-behind at all, but I think this is quite acceptable and easy to read. The SelectedFoos property on the controller participates in INotifyPropertyChanged so controls can bind to the property.

Use Converters

Always put the binding logic code inside value converter classes, don't be lazy and leave it in controllers. I used to do the following, if for example I wanted a control to be visible if Foo is not null:

public Foo SelectedFoo
{
  get { return _selfoo; }
  set
  {
    _selfoo = value;
    SendChange("SelectedFoo");
    SendChange("IsThingVisible");
  }
}

public Visibility IsThingVisible
{
  get { return _selfoo != null ? Visibility.Visible : Visibility.Collapsed; }
}

What I've done is make a fake binding property that converts null to a Visibility enum. This works and looks easy, but as the number of these fake properties grows, the relationships between them all gets confusing and errors can creep in. You must put this logic in a value converter where it's reusable and isolated, like this:

if (convertArg == "SomeToVisible")
{
  return value != null ? Visibility.Visible : Visibility.Collapsed;
}

Then the XAML to use this is something like:

Visibility="{Binding SelectedFoo,
    Converter={StaticResource MyConverter},
    ConverterParameter=SomeToVisible}"

You will probably find that a lot of binding logic simply converts between different types, so it can be packaged into neat groups in the type converter and reused throughout the XAML. The important thing is that all binding logic and conversion takes place inside your value converter classes.

Use MultiConverter

Like the sample above where I was lazy and put binding logic in the controller, I also fell into the bad habit of using fake properties to allow binding to multiple conditions. If for example I wanted a control to be visible if something is not null and a flag is false, I used to do this in the controller.

public List Foos
{
  ...
  set
  {
    _foos = value;
    SendChange("Foos");
    SendChange("IsThingVisible");
  }
}
public bool IsFoosLoading
{
  ...
  set
  {
    _foosloading = value;
    SendChange("IsFoosLoading");
    SendChange("IsThingVisible");
  }
}
public Visibility IsThingVisible
{
  get { return _foos != null && !_isFoosLoading ? Visibility.Visible : Visibility.Collapsed; }
}

This code manually duplicates exactly what MultiBinding was invented for. In this case, I should use MultiBinding with XAML like this example:

<Button ...>
<Button.Visibility>
  <MulitBinding Converter="{StaticResource MyMultiConverter}"
         ConverterParameter="SomeAndFalse">
    <Binding Path="Foos"/>
    <Binding Path="IsFoosLoading"/>
  </MultiBinding>
</Button>

The logic is now moved into the multi value converter class where it belongs.

if (convertArg == "SomeAndFalse")
{
  bool? flag = values[1] as bool?
  return values[0] != null && flag != true ? Visibility.Visible : Visibility.Collapsed;
}

Note that I didn't just cast values[1] directly to bool, as the value is unpredictably the value UnsetValue. I haven't determined exactly when or why this "try cast" necessary, so I have fallen into the habit of doing it all non-trivial cases to avoid runtime cast crashes.

Summary

Basically everything I've said in this post is a simple restatement of how binding works, but it's a reminder of how important it is to do everything "by the book" and use every binding feature exactly the way it was intended to be used.

No comments:

Post a Comment