Friday 12 June 2015

Compiled binding. Yeah, but converters don't seem to work.

Compiled binding is a nice idea, but dequehead has up to now avoided looking at it because of the fear of nasty behind-the-scenes stuff ('magic') messing with what is already a somewhat mystical subject. But now it is now.

It turns out there is no magic, and nothing to worry about. Except that it doesn't work properly, yet (more on that in a sec).

When you replace a {Binding} with an {x:Bind}, what happens is that the BlahPage.g.cs file gets even more generated code, this time to handle all the operations that are required for the page to be able to perform all the data binding operations that the XAML declares. That point is a bit subtle. Suppose you have a two-way bound TextBox called Fullname on a page. That needs to update is backing store, so an Update_Fullname method is generated for it, like so:

private void Update_Fullname(global::System.String obj, int phase)
{
    if (phase == NOT_PHASED || phase == DATA_CHANGED || phase == 0)
    {
        XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBox_Text(this.obj2, (global::System.String)((global::Windows.UI.Xaml.Data.IValueConverter)global::Windows.UI.Xaml.Application.Current.Resources["camelConv"]).Convert(obj, typeof(global::System.String), null, null), null);
    }
}

This nasty looking thing does the same job that an old-style binding would do, except that everything (nearly..) about it is known to the compiler, so there is no need for run-time reflection, hence it is apparantly more efficient. I'd guess much more efficient, but I've not seen any numbers.

Clearly there must be an issue or I would have gone home by now. Yes, there seems to be. As far as I can see it all works fine until you try to use a converter (as in the example above). As you can see the code is looking for an IValueConverter instance called "camelConv" in the app resource dictionary. Well, I can tell you it aint there. I have looked. If you run code like this you get a "WinRT information: Cannot find a resource with the given key." at run-time.

A simple repro example. Lets say you have a UWP app with a single TextBox containing dequehead's name. Here's the code-behind:

public sealed partial class MainPage : Page
{
    public string Fullname { get; set; } = "dequehead"; // dq: nice, init?

    public MainPage()
    {
        this.InitializeComponent();
        this.DataContext = this;
    }
}
and the XAML:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.Resources>
        <local:CamelConverter x:Key="camelConv"/>
        <Style TargetType="TextBox">
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
    </Grid.Resources>
    <TextBox x:Name="textBox" Text="{Binding Fullname, Mode=TwoWay, Converter={StaticResource camelConv}}"/>
</Grid>
You can see the reference to camelConv in Grid.Resources, and here is its definition:
public class CamelConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var cc = Encoding.UTF8.GetBytes(value as string);
        cc[0] = (Byte)Char.ToUpper((Char)cc[0]); // hack dq
        cc[5] = (Byte)Char.ToUpper((Char)cc[5]);
        return Encoding.UTF8.GetString(cc, 0, cc.Length);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException(); // dq: we'll let this throw for now
    }
}
It just capitalises the 'd' and 'h'. If you paste all that into a fresh UWP project and run it you will see exactly what you might expect - the dequehead in the backing store is displayed as DequeHead in the TextBox. I love converters.

Now let's change it to use compiled binding. The data context is already the MainPage code-behind, so all we need to do is change Binding to x:Bind:

<TextBox x:Name="textBox" Text="{x:Bind Fullname, Mode=TwoWay, Converter={StaticResource camelConv}}"/>
As predicted, barf. So, it appears that converters are broken in compiled binding. What to do?

Well, I wanted a workaround that kept, as far as possible, all the MVVM patterns intact. So I kept the converter instantiation in the XAML, but I decided to do the actual conversion in the view model for now. There are other valid approaches.

First, we need to make the converter visible in code by changing x:Key to x:Name. I like names to start with an upper-case letter (and keys start with a lower-case letter), so I did that as well.

<local:CamelConverter x:Name="CamelConv"/>
Then the auto-property has to be opened up, and the getter does the conversion using the converter, which is now a public property on the page. (Remind me to blog about snippets and the weird backing property name.)
private string _Fullname = "dequehead";
public string Fullname
{
    get { return this.CamelConv.Convert(this._Fullname, typeof(String), null, null) as string; }
    set { this._Fullname = value; }
}
Last we remove the converter from the binding:
<TextBox x:Name="textBox" Text="{x:Bind Fullname, Mode=TwoWay}"/>
If you run that, it will work. You'll also see that the generated update method does not have the dictionary lookup that was causing the problem:
private void Update_Fullname(global::System.String obj, int phase)
{
    if (phase == NOT_PHASED || phase == DATA_CHANGED || phase == 0)
    {
        XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBox_Text(this.obj3, obj, null);
    }
}
It is a shame that there is not really an easy global search-and-replace that can convert from this hack to proper converter use when the bug is fixed, if anyone has any ideas please leave a comment.

When wrestling with this issue, dequehead found plenty of quotes like "x:Bind just drops straight in...blah blah...and converters work as before...", but no actual examples of converters used in conjunction with x:Bind. The extensive Windows 10 samples don't appear to have one. If anyone has seen a working example dequehead would like to hear about it. Asidedly, he thinks that the NOT_PHASED constant in the generated code above is worth a blog, at some point.

3 comments:

  1. I ran into this. I had to use the Page.Resources for the dictionary to get it to work. Great write-up though. It helped me a lot.

    ReplyDelete
  2. 11.03.2016. Converters still not work.

    ReplyDelete
  3. 29.07.2018 Converters still not work

    ReplyDelete