Windows 8 Dev Tip: Nullable<T> Dependency Properties and Binding

posted on 24 Jul 2012 | .NET | Windows Store Apps

Author's Note: If you're here just for a solution and are not interested in the extraneous bits, feel free to jump to the workaround details.

First, some background...

Let's say you're building a user control and one of your control's properties needs to be a three state boolean (true, false, null).

No sweat, we'll just create a nullable boolean (bool?) dependency property:

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value",
        typeof(bool?),
        typeof(TestControl),
        new PropertyMetadata(null));

Ok, now that we have that taken care of, we want the user control to display different colors based on the value of the Value property.

So let's add logic to handle the onchange callback of the dependency property:

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value",
        typeof(bool?),
        typeof(TestControl),
        new PropertyMetadata(null, ValueChanged));

private static void ValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
    ((TestControl)sender).ValueChanged();
}

private void ValueChanged() {
    bool? value = this.Value;
    if (!value.HasValue) {
        this.grid.Background = new SolidColorBrush(Colors.Blue);
    }
    else if (value.Value) {
        this.grid.Background = new SolidColorBrush(Colors.Green);
    }
    else {
        this.grid.Background = new SolidColorBrush(Colors.Red);
    }
}

Doing good, now lets piece it all together by adding a main page, binding our new user control to the page's datacontext, and creating a few radio buttons to change the value of the datacontext:

TestControl.xaml:
<UserControl x:Class="NullableBoolBinding.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid Name="grid" Background="Blue"/>
</UserControl>
TestControl.xaml.cs:
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace NullableBoolBinding
{
    public sealed partial class TestControl : UserControl
    {
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value",
                typeof(bool?),
                typeof(TestControl),
                new PropertyMetadata(null, ValueChanged));

        public bool? Value {
            get { return (bool?)this.GetValue(ValueProperty); }
            set { this.SetValue(ValueProperty, value); }
        }

        public TestControl() {
            this.InitializeComponent();
        }

        private static void ValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
            ((TestControl)sender).ValueChanged();
        }

        private void ValueChanged() {
            bool? value = this.Value;
            if (!value.HasValue) {
                this.grid.Background = new SolidColorBrush(Colors.Blue);
            }
            else if (value.Value) {
                this.grid.Background = new SolidColorBrush(Colors.Green);
            }
            else {
                this.grid.Background = new SolidColorBrush(Colors.Red);
            }
        }
    }
}
MainPage.xaml:
<Page x:Class="NullableBoolBinding.MainPage"
      IsTabStop="false"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:NullableBoolBinding">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <local:TestControl Value="{Binding}"/>

        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    Grid.Row="1">
            <RadioButton Content="True"
                         Checked="TrueRadioButtonChecked"
                         Margin="10"/>
            <RadioButton Content="False"
                         Checked="FalseRadioButtonChecked"
                         Margin="10"/>
            <RadioButton Content="Null"
                         Checked="NullRadioButtonChecked"
                         Margin="10"/>
        </StackPanel>
    </Grid>
</Page>
MainPage.xaml.cs:
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace NullableBoolBinding
{
    public sealed partial class MainPage : Page
    {
        public MainPage() {
            this.InitializeComponent();
        }

        protected override void OnNavigatedTo(NavigationEventArgs e) {
            this.DataContext = null;
        }

        private void TrueRadioButtonChecked(object sender, RoutedEventArgs e) {
            this.DataContext = true;
        }

        private void FalseRadioButtonChecked(object sender, RoutedEventArgs e) {
            this.DataContext = false;
        }

        private void NullRadioButtonChecked(object sender, RoutedEventArgs e) {
            this.DataContext = null;
        }
    }
}

Ok, we're done, so let's fire up our new app and test it out!

Binding Failure

Wait a second... the null value works, but when you select true or false nothing changes. What gives?

Glancing at our output window while we have the debugger attached gives us this piece of information:

Error: Converter failed to convert value of type 'Windows.Foundation.IReference`1<Boolean>' to type 'IReference`1<Boolean>'; BindingExpression: Path='' DataItem='Windows.Foundation.IReference`1<Boolean>'; target element is 'NullableBoolBinding.TestControl' (Name='null'); target property is 'Value' (type 'IReference`1<Boolean>').

Ok then, what the heck is a IReference`1 and why are we trying to convert it?

Turns out that under the covers most of the .NET primitive types are converted to equivalent Windows Runtime types. IReference happens to be the Windows Runtime equivalent of Nullable in .NET. In fact, Jeremy Likness has a good blog post WinRT/.NET type conversion that I'd recommend taking a look at.

So right about now you're thinking, "Great, thanks for the technological archaeology lesson, but that doesn't explain why it's not working or what I need to do to fix it..."

So, despite some considerable effort in researching the issue (and finding a WinRT dev forum post saying that nullable dependency properties in custom user controls are not supported), I couldn't find any explanation as to why this fails, but it turns out there is a workaround...

And the answer is...

Change the dependency property type to object. So, in our example, we change:

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value",
        typeof(bool?),
        typeof(TestControl),
        new PropertyMetadata(null, ValueChanged));

to:

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value",
        typeof(object),
        typeof(TestControl),
        new PropertyMetadata(null, ValueChanged));

That's it. At this point everything just starts working as expected. You don't even have to change your instance property type.

Binding Success

It's a shame reflection doesn't work on the WinRT XAML controls since they're written in native code, because I'd love to see how the ToggleButton.IsChecked property is implemented...