Microsoft-ui-xaml: Proposal: {x:Bind} as binding function argument

Created on 21 Jan 2020  路  14Comments  路  Source: microsoft/microsoft-ui-xaml

Proposal: {x:Bind} as binding function argument

Summary


This proposal is for adding support for the {x:Bind} target as argument for function bindings, in addition to x:True, x:False, x:Null, constant numbers and the other supported arguments as specified in the docs. This addition is needed to support bindings to the source DataContext, which is currently not possible, forcing devs to fallback to classic converters in that case, which bring a whole set of downsides associated with them (more overhead, weakly typed, boxing for value types, etc.).


Code example (click to expand)

Model:

public enum Option
{
    A,
    B,
    C
}

Converter:

public static class OptionConverter
{
    public static string Convert(Option value)
    {
        return value switch
        {
            Option.A => "Cat",
            Option.B => "Dog",
            Option.C => "Giraffe",
            _ => throw new ArgumentOutOfRangeException();
        }
    }
}

XAML:

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:enums="using:MyEnums"
xmlns:converters="using:MyConverters"

<DataTemplate x:DataType="enums:Option">
    <TextBlock Text="{x:Bind converters:OptionConverter.Convert({x:Bind})}"/>
</DataTemplate>

As far as I know, this is just not possible at all with function bindings. Which is unfortunate: they work amazingly well for whatever property you want to bind to, but they just can't target the whole source object for now. Without a function instead, once can use x:Bind Converter=... without specifying a path, and the binding will correctly target the entire source model. This should simply apply to function binding arguments as well, with the same syntax.

Rationale

  • This feature is already supported in both classic bindings and compiled bindings. It should be supported with function bindings as well.
  • Allow devs to get rid of converters when binding to entire models
  • Converters should be avoided as much as possible:

    • They're weakly typed

    • They don't give you compile type errors

    • They require instantiating converter instances (which either adds memory/overhead to templates, or simply adds the overhead for the StaticResource lookup)

    • They box value type parameters

    • They only take an object as parameters (forcing you to eg. pass numeric values as strings and then parse them every time, unless you use a resource lookup, which is not only slower but also, again, causes boxing)

    • They box value type return values

Supporting the self {x:Bind} as argument for function binding would allow developers to move from converters to function bindings in a variety of additional situations.

Scope


| Capability | Priority |
| :---------- | :------- |
| Support {x:Bind} as function binding argument | Must |

feature proposal team-Markup

Most helpful comment

Here's a trick I just discovered!

You're right, the native bind parser doesn't provide a keyword to represent this as a function parameter BUT one thing I just found out is that native bindings support pathless casting {x:bind (x:String)} and can be used as a function parameter:

{x:Bind MethodName( ( )}
--> Equivalent of {x:Bind MethodName( this )}

Example:
Text="{x:Bind local:MainPage.GenerateSongTitle((local:SongItem))}"
Text="{x:Bind local:MainPage.GenerateStr(x:False, (local:SongItem), 12)}"

Enjoy 馃槈

Sample app:

<Page
    x:Class="AppSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:AppSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid>
        <ListView ItemsSource="{x:Bind Songs}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:SongItem">
                    <TextBlock
                        Margin="12"
                        FontSize="40"
                        Text="{x:Bind local:MainPage.GenerateSongTitle((local:SongItem))}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>
namespace AppSample
{
    public class SongItem
    {
        public string TrackName { get; private set; }
        public string ArtistName { get; private set; }

        public SongItem(string trackName, string artistName)
        {
            ArtistName = artistName;
            TrackName = trackName;
        }
    }

    public sealed partial class MainPage : Page
    {
        public List<SongItem> Songs { get; }
        public MainPage()
        {
            Songs = new List<SongItem>()
            {
                new SongItem("Imagine", "John Lennon"),
                new SongItem("U2", "One"),
                new SongItem("The Eagles", "Hotel California")
            };

            this.InitializeComponent();
        }

        public static string GenerateSongTitle(SongItem song)
        {
            return $"{song.TrackName} - {song.ArtistName}";
        }
    }
}

All 14 comments

@danzil and @fabiant3 as FYI

In the function binding syntax, the function parameters are really nested bindings, but in the syntax you only need to provide the Path, you don't need to spell out the whole syntax. But if you could spell out the whole syntax, you could handle this this case, and have access to other binding functions, like FallbackValue. So just add squiggles around the xBind in the proposal:

 <TextBlock Text="{x:Bind converters:OptionConverter.Convert( {x:Bind} )}"/>

Another possible solution here would be to support '.' syntax like {Binding} has, so:

 <TextBlock Text="{x:Bind converters:OptionConverter.Convert( . )}"/>

Hi @MikeHillberg - thanks for chiming in!
I've updated the proposal with your feedback, thanks 馃憤

The reason why I had originally not used {x:Bind} but just x:Bind was just to follow suit with the other supported values like x:True and x:False, which don't require brackets when used as arguments for a function binding. Though I'll admit that that was somewhat counter intuitive to me at first, so yeah I agree that using {x:Bind} would probably be clearer anyway 馃槃

I personally don't favor expressing the source via _{x:Bind}_. I like _x:Bind_ or other options better. If used with squiggles, you expect full {x:Bind} support in the parameter and, if I'm reading the suggestion right, only a blank {x:Bind} statement would be supported. Without squiggles - _x:Bind_, it expresses a constant: _x:True_, _x:Null_, so it maybe better suited for expressing an _"x:Source"_, similar to the _this_ pointer in C#. To be consistent with _Binding_, and allow the possibility to also reference the Item in an ItemsCollection, maybe . (dot) should be the notation for source and / (backslash) for item. How about _x:Source_ or _x:This_?

I personally like {x:Bind} better for three main reasons:

  • It doesn't introduce a new XAML keyword/expression (x:Bind is widely used already)
  • It perfectly mimics the behavior of compiled bindings targeting the entire model. They use just {x:Bind} in exactly the same way. I think this would make it more intuitive to developers in general, since it's literally the same expression used in the same exact way - binding to the entire model.
  • Conceptually, as you said, x:True, x:False and x:Null are constants, {x:Bind} is not. It's just a reference to the current model (ie. the DataContext exposed as the type specified in x:DataType). As such, it can change and supports notifications. It's just not a constant, it's an expression. I think differentiating that from other constants (so by adding the squiggly brackets) is again something that would make it more explicit and clear for developers in general.

Of course these are just my thoughts on it, so everyone feel free to chime in 馃槉

The thing is, function parameters are also mini x:Binds that inherit the main x:Bind's characteristics (Mode). If we end up using {x:Bind}, then I would also expect all the x:Bind universe to work inside a function parameter. This would have to be valid:

<TextBlock Text="{x:Bind local:Foo( {x:Bind local:Bar( {x:Bind Abc.Def} ), Mode=OneTime} ), Mode=OneWay }" />

I'm afraid that this explodes into a complex situation, both on the implementation side, code-gen, and also the markup side, encouraging increasingly more complex expressions that would be better suited to live in code rather than in markup.

That's just my 2c, I have not given this too much thought.

Yeah no, I get what you're saying, what I meant was that the {x:Bind} usage could easily be restricted to literally just {x:Bind} when used as a function parameter. That wouldn't be handled as a whole nested expression, but just as {x:Bind} meaning the equivalent of "bind to this".

Anything more would simply not compile at all, and that could just be mentioned in the docs as it's the case for the other allowed expressions. Generally speaking, the supported parameters for function bindings are already pretty limited, so I personally don't see a problem with just supporting {x:Bind} as in _literally_ just {x:Bind}, if it's well documented and explained in the docs. This proposal is not meant to be for a complex new feature, but simply for a very small and specific detail that's missing 馃槃

What I like is the simplicity of saying that while the function arguments can be an implicit x:Bind (today's story), it's OK to alternatively provide an explicit x:Bind.

A restriction would be required that an x:Bind used as an explicit function argument not specify a TwoWay Mode. But I think the other xBind properties make sense, and even OneWay vs OneTime.

By the way, if you have access to the data type, another workaround is to give it a property that returns itself:

c# public class TheData { public TheData This => this; }

Yeah that This => this syntax is a workaround that we've joked about for a while in the UWP Community, where we first started discussing about this feature proposal, before opening the issue 馃槅

While it does work (although not as efficiently, but still), it's unfortunately not viable for cases like the one mentioned in the first post, ie. when binding to primitive types or enums. And also, it's obviously not ideal to have to modify the actual models just to make a UI-related feature work.

Still though, I'm glad there's some interest about this feature, so I'm hoping to hear some official feedback on that and eventually an ETA or an approximate timeframe for when such a feature could actually be implemented 馃槃

Here's a trick I just discovered!

You're right, the native bind parser doesn't provide a keyword to represent this as a function parameter BUT one thing I just found out is that native bindings support pathless casting {x:bind (x:String)} and can be used as a function parameter:

{x:Bind MethodName( ( )}
--> Equivalent of {x:Bind MethodName( this )}

Example:
Text="{x:Bind local:MainPage.GenerateSongTitle((local:SongItem))}"
Text="{x:Bind local:MainPage.GenerateStr(x:False, (local:SongItem), 12)}"

Enjoy 馃槈

Sample app:

<Page
    x:Class="AppSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:AppSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid>
        <ListView ItemsSource="{x:Bind Songs}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:SongItem">
                    <TextBlock
                        Margin="12"
                        FontSize="40"
                        Text="{x:Bind local:MainPage.GenerateSongTitle((local:SongItem))}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>
namespace AppSample
{
    public class SongItem
    {
        public string TrackName { get; private set; }
        public string ArtistName { get; private set; }

        public SongItem(string trackName, string artistName)
        {
            ArtistName = artistName;
            TrackName = trackName;
        }
    }

    public sealed partial class MainPage : Page
    {
        public List<SongItem> Songs { get; }
        public MainPage()
        {
            Songs = new List<SongItem>()
            {
                new SongItem("Imagine", "John Lennon"),
                new SongItem("U2", "One"),
                new SongItem("The Eagles", "Hotel California")
            };

            this.InitializeComponent();
        }

        public static string GenerateSongTitle(SongItem song)
        {
            return $"{song.TrackName} - {song.ArtistName}";
        }
    }
}

Nice! I opened this as a doc issue.

@rudyhuyn You're the real MVP, nice catch! 馃殌
And thanks @MikeHillberg for opening the doc issue as well!

Looks like there are no reasons left to use an IValueConverter ever again! 馃帀

Wow, that's genius! I implemented function bindings and didn't know about this unintended effect of casting. I bet Drew, who implemented x:Bind casting didn't know about this either. What a happy junction and what a great find!

Was this page helpful?
0 / 5 - 0 ratings