microsoft-ui-xaml icon indicating copy to clipboard operation
microsoft-ui-xaml copied to clipboard

x:Bind outside data templates is extremely inconsistent

Open Sergio0694 opened this issue 5 years ago • 43 comments

Describe the bug

Apparently it is possible to use x:Bind to bind to a field in code behind outside a data template. Basically, escaping from the data template in use. Not entirely sure whether this is by design or just a lucky quirk of the XAML compiler and codegen (especially since this is not mentioned anywhere in the docs), but it works.

As a code example:

<UserControl
    x:Class="MyNamespace.Views.MyUserControl"
    xmlns:viewModels="using:MyNamespace.ViewModels"
    models="using:MyNamespace.Models">
    <UserControl.DataContext>
        <viewModels:MyViewModel x:Name="ViewModel"/>
    </UserControl.DataContext>
    <UserControl.Resources>

        <DataTemplate
            x:Name="SomeDataTemplate"
            x:DataTye="models:SomeModelTyppe">

            <!--This works-->
            <TextBlock Text="{x:Bind ViewModel.SomeText}"/>
        </DataTemplate>
    </UserControl.Resources>

    <ListView
        ItemsSource="{x:Bind ViewModel.MyItems}"
        ItemTemplate="{StaticResource SomeDataTemplate}"/>
</UserControl>

You can see how the binding is escaping the data template and just targeting a field in code behind. The problem with this is that it's extremely fragile and inconsistent:

  • x:Bind to a property outside the data template works fine ✅
  • x:Bind to that same property but with function binding fails to build ❌
  • x:Bind to that same property from a visual state setter crashes at runtime ❌
  • x:Bind to that same property with a converter crashes at runtime ❌

Steps to reproduce the bug

Steps to reproduce the behavior:

  1. Download this repro
  2. Open the solution, run the app

Expected behavior

🤷‍♂️ 🤷‍♂️ 🤷‍♂️

Actual behavior

The binding to the outside field (the view model) works just fine

Screenshots

image

Windows 10 version Saw the problem?
Insider Build (xxxxx)
November 2019 Update (18363) Yes
May 2019 Update (18362) Yes
October 2018 Update (17763)
April 2018 Update (17134)
Fall Creators Update (16299)
Creators Update (15063)
Device form factor Saw the problem?
Desktop Yes
Mobile
Xbox
Surface Hub
IoT

cc. @MikeHillberg

Sergio0694 avatar May 20 '20 22:05 Sergio0694

@fabiant3 For FYI

StephenLPeters avatar May 20 '20 23:05 StephenLPeters

This almost looks like it shouldn't work at all. The DataTemplate has a defined DataType which it uses for its bindings.

I know with old-style Binding, this was possible with ElementName but I think there's an argument for having this functionality work correctly with x:Bind.

jamesmcroft avatar May 21 '20 07:05 jamesmcroft

{Binding} and ElementName will walk up the element tree at runtime. IIRC x:Bind will statically search up the naming scopes -- the DataTemplate then the x:Class.

MikeHillberg avatar May 21 '20 08:05 MikeHillberg

@jamesc I actually believe this was in fact supported, in particular because VS even has dedicated warnings for this. Like, if your x:DataType model has a property with the same name as the one from code behind, and you x:Bind to it, VS will say something like:

Warning, binding to property will cause the binding to reference the one from the outer scope.

As in, it looks like the system is perfectly aware of the possibility of binding outside of a data template, as @MikeHillberg said. I'd also argue this feature is extremely useful in many cases.

@MikeHillberg is it safe to say then that the issue is just that using anything other than standard x:Bind in this case (eg. function binding, binding from markup extension, etc.) breaks down? But like, that in theory this is a supported scenario that should work? 🤔

Sergio0694 avatar May 21 '20 10:05 Sergio0694

Yes, it should work. I expect some of the code for the (1903) feature went into a not-common code path, so missed the function binding case.

MikeHillberg avatar May 21 '20 21:05 MikeHillberg

Thanks Mike for digging into this - yes, x:Bind'ing to named elements outside of the scope is an intentional feature. This is meant to be similar to the ElementName feature in {Binding}, allowing for binding to named elements outside of the current namescope. But unlike {Binding} which walks the visual tree at runtime to perform the name lookup, x:Bind walks the "markup tree" at compile time so it can perform compile time validation. You could see behavior differences when those don't align, like when using a Binding/x:Bind in a template from a ResourceDictionary. The Binding walks the visual tree from where the template is used, whereas x:Bind walks the markup tree from where the template is defined.

is it safe to say then that the issue is just that using anything other than standard x:Bind in this case (eg. function binding, binding from markup extension, etc.) breaks down? But like, that in theory this is a supported scenario that should work? 🤔

Correct, these should work

RealTommyKlein avatar May 27 '20 21:05 RealTommyKlein

I believe this is still an issue; is there any indication on when we can use x:Bind for this rather than Binding?

Imagine I have a UserControl containing a dependency property IsEditing (bool), I want to be able to set the visibility of a TextBlock vs a TextBox inside my DataTemplates.

With x:Bind that seems not possible because it only allows selecting items from the configured x:DataType, and not from the codebehind of the UserControl

Pseudocode, only to explain the use case of wanting to access data outside the DataType scope using x:Bind:

<UserControl Class="local:CarsList">
   <UserControl.Resources>
      <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
      <DataTemplate x:Key="listItemTemplate" x:DataType:"models.Car">
        <Grid>
            <TextBlock Text="{x:Bind Brand}"
                               Visibility="{x:Bind IsEditing, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=True, Mode=OneWay}" />
            <TextBox Text="{x:Bind Brand}"
                            Visibility="{x:Bind IsEditing, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
        </Grid>
      </DataTemplate x:Key="listItemTemplate">
   </UserControl.Resources>
</UserControl>

Doing the above will result in The property 'IsEditing' was not found in type 'Car'.

Workaround is giving the usercontrol an x:Name and fall back to Binding. Quite annoying 😕.

<UserControl Class="local:CarsList" x:Name="workaroundNameForControl">
   <UserControl.Resources>
      <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
      <DataTemplate x:Key="listItemTemplate" x:DataType:"models.Car">
        <Grid>
            <TextBlock Text="{x:Bind Brand}"
                               Visibility="{Binding Path=IsEditing, ElementName=workaroundNameForControl, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=True, Mode=OneWay}" />
            <TextBox Text="{x:Bind Brand}"
                            Visibility="{Binding Path=IsEditing, ElementName=workaroundNameForControl, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
        </Grid>
      </DataTemplate x:Key="listItemTemplate">
   </UserControl.Resources>
</UserControl>

Other workarounds for this case would be using a DataTemplateSelector but that won't work for all cases.

hansmbakker avatar Sep 26 '20 11:09 hansmbakker

Unfortunately, I don't believe we can investigate this before the official WinUI 3 release in 2021. For your scenario in particular, you could try adding an x:Name to your root UserControl, then x:Bind using the named element (e.g. x:Bind RootUserControl.IsEditing). When looking outside of the namescope it was used in, x:Bind only looks for named elements, and not properties on the root data context.

RealTommyKlein avatar Oct 01 '20 20:10 RealTommyKlein

Thanks for thinking along! Actually I tried that (x:Bind workaroundNameForControl.IsEditing), but then I get the error The property 'IsEditing' was not found in type 'Car'.. I'm using VS 16.8 Preview 3.2 with an UWP project.

hansmbakker avatar Oct 01 '20 20:10 hansmbakker

@hansmbakker does using {x:Bind ElementName=workaroundNameForControl, Path=IsEditing} work?

stevenbrix avatar Nov 18 '20 23:11 stevenbrix

@stevenbrix Considering that x:Bind has no ElementName, no, it does not... :-) And even if it did, it would only work for plain property binding, not functions where the property would need to be passed as an argument, for instance.

I'm also struggling with this, and this is very important. Now that we have functions in x:Bind and prefer to avoid converters, reaching out to two places: data from the context of the data template and functions from the underlying control (data conversions, checks for visibility, disabled state, etc), this became very important. And Binding isn't even an alternative any more, not because it's quirkier and slower but because it doesn't do functions.

Conversion and other functions can be specified static and called as such (not nice but definitely a workaround) but properties of the control can't. Naming the control doesn't work, either, just as Hans pointed out above. And while the OP's solution might be OK for MVVMWHATEVERTHECURRENTBUZZWORDIS, it doesn't work for a plain user control: if we refer to the control itself this way, there will be complaints about IType_BindingsScopeConnector. Basically, with this trick, you can reach out to two data elements (template data context and control data context) but not the control itself as the second.

deakjahn avatar Dec 03 '20 13:12 deakjahn

any update?

HppZ avatar Mar 18 '21 03:03 HppZ

Hey all, we're still heads-down on major feature work/bug fixes for WinUI and Project Reunion, so there likely won't be any movement on this for several months. Thanks!

RealTommyKlein avatar Mar 18 '21 22:03 RealTommyKlein

{Binding} and ElementName will walk up the element tree at runtime. IIRC x:Bind will statically search up the naming scopes -- the DataTemplate then the x:Class.

@MikeHillberg @Sergio0694 what if in nested DataTemplate: image image

HppZ avatar Mar 25 '21 05:03 HppZ

@Sergio0694 not work either. image

HppZ avatar Mar 26 '21 02:03 HppZ

image error CS1503: Argument 1: cannot convert from 'System.WeakReference' to 'Windows.UI.Xaml.Controls.Page'

HppZ avatar May 11 '21 05:05 HppZ

Unfortunately, I don't believe we can investigate this before the official WinUI 3 release in 2021. For your scenario in particular, you could try adding an x:Name to your root UserControl, then x:Bind using the named element (e.g. x:Bind RootUserControl.IsEditing). When looking outside of the namescope it was used in, x:Bind only looks for named elements, and not properties on the root data context.

not work, see above.

HppZ avatar May 11 '21 05:05 HppZ

after change the generated code, it compiles. @RealTommyKlein image

image

HppZ avatar May 11 '21 05:05 HppZ

Seems related to #2237 as well?

My latest trick is to do something like this:

<Page x:Name="ThisPage"...>
   <ItemsControl>
       <ItemsControl.ItemTemplate>
           <DataTemplate x:DataType="local:MyType">
               <Button Command="{Binding ViewModel.SaveCommand, ElementName=ThisPage}" CommandParameter="{x:Bind (local:MyType)}"/>

But x:Bind should just let us break into the different scopes we need:

  • Local (data item itself)
  • Container (ListViewItem for instance)
  • "Parent" (ListView for instance)
  • "Global" (i.e. Page)

I think that'd cover the majority of scenarios I ever need when dealing with templating. The compiler should just be able to walk the scope up if it's not finding the reference path at each tier.

michael-hawker avatar Jan 14 '22 19:01 michael-hawker

@michael-hawker your latest trick doesn't work for ItemsRepeater, although it does for ListView/ItemsControl. That is mind blowing! I really want to have at least some workaround, but I can't, unless I switch from ItemsRepeater to ListView/ItemsControl and only then can switch to use Binding? Crazy! Something is wrong here even with Binding. Please look into it!

ArsenijK avatar Mar 23 '22 14:03 ArsenijK

For me it actually worked only by giving a name to my UserControl (which is used in a ListView), and then replacing x:Bind with Binding. They should really fix this.

This works:

<controls:CustomControl Visibility="{Binding ElementName=ControlName, Path=Object.IsReady, Mode=OneWay}"></controls:CustomControl>

This works but it fails eventually to evaluate IsReady when it's true and the control remains hidden only for certain items in the list.

<controls:CustomControl Visibility="{x:Bind Object.IsReady, Mode=OneWay}"></controls:CustomControl>

bogdan-patraucean avatar Apr 10 '22 20:04 bogdan-patraucean

This also comes up for ItemsPanelTemplates, x:Bind isn't usable in those, which is frustrating. Sometimes you want to configure your layout panel change for ItemsControl based on other properties.

Instead trying to bind to a page property gets you: XamlCompiler error WMC1111: DataTemplates containing x:Bind need a DataType to be specified using 'x:DataType', which isn't a thing for ItemsPanelTemplate (if you try you get XamlCompiler error WMC0908: DataType is only allowed for DataTemplate.)

Related SO post as well: https://stackoverflow.com/questions/11307531/binding-inside-the-itemspaneltemplate-from-parent-in-windows-phone

hawkerm avatar May 12 '22 01:05 hawkerm

Also found myself needing this the other day in the Store. I just couldn't for the love of me get this to work, as the XAML compiler would just refuse to compile code with a binding to something outside of a template, even if visible in XAML. Had to come up with some proxy object to bind to to, as I just couldn't get a normal binding to the control's property directly to work.

A fix for this would really, really be welcome 😄

Sergio0694 avatar May 12 '22 10:05 Sergio0694

Also found myself needing this the other day in the Store. I just couldn't for the love of me get this to work, as the XAML compiler would just refuse to compile code with a binding to something outside of a template, even if visible in XAML. Had to come up with some proxy object to bind to to, as I just couldn't get a normal binding to the control's property directly to work.

A fix for this would really, really be welcome 😄

@Sergio0694 Find Ancestor extension in the Toolkit? https://docs.microsoft.com/windows/communitytoolkit/extensions/frameworkelementextensions#ancestortype

michael-hawker avatar May 12 '22 17:05 michael-hawker

I mean, yes, but I wanted a statically-typed and reflection-free solution, that's why x:Bind is cool 😅

Sergio0694 avatar May 12 '22 17:05 Sergio0694

Not sure whether this is a regression and if so, when it was introduced, but now I can't even get the only scenario that was working back when I first opened this issue to work at all anymore. Even just binding to the top viewmodel in a page fails to build. This is a pretty big issue because it blocks many scenarios where you need a backlink to a parent viewmodel for shared functionality (eg. for commands), and the only solution is to either use a static resource (which is only applicable in very few and specific cases), or having to add extra wrapping models just for that, which adds overhead. This is affecting other internal partners as well, and has been broken for years, with no ETA for a fix at all either. I'm pretty sure it's also broken on WinUI 3 too 😥

Sergio0694 avatar Jul 21 '22 22:07 Sergio0694

Not sure whether this is a regression and if so, when it was introduced, but now I can't even get the only scenario that was working back when I first opened this issue to work at all anymore. Even just binding to the top viewmodel in a page fails to build. This is a pretty big issue because it blocks many scenarios where you need a backlink to a parent viewmodel for shared functionality (eg. for commands), and the only solution is to either use a static resource (which is only applicable in very few and specific cases), or having to add extra wrapping models just for that, which adds overhead. This is affecting other internal partners as well, and has been broken for years, with no ETA for a fix at all either. I'm pretty sure it's also broken on WinUI 3 too 😥

noone cares

HppZ avatar Jul 22 '22 06:07 HppZ

@michael-hawker your latest trick doesn't work for ItemsRepeater, although it does for ListView/ItemsControl. That is mind blowing! I really want to have at least some workaround, but I can't, unless I switch from ItemsRepeater to ListView/ItemsControl and only then can switch to use Binding? Crazy! Something is wrong here even with Binding. Please look into it!

There is one workaround. You have to create a kinda display class. Just for the purpose of the example.

Code behind

[INotifyPropertyChanged]
public partial class FooViewModel {
    [ObserveableProperty] private List<FixedFooItem> _fooItems = new(); 

    private void Whenever() {
        List<FooItem> items = ...;        
        
        for(FooItem item in items) {
            _fooItems.Add(item, DoSomething)
        }
    }

    [RelayCommand]
    private void DoSomething(FooItem item) {
        //..
    }
}

[INotifyPropertyChanged]
public partial class FooItem {
     [ObserveableProperty] private string _name { get; set; }
}

[INotifyPropertyChanged]
public partial class FixedFooItem {
    [ObserveableProperty] private FooItem _data;
    public IRelayCommand DoSomethingCommand { get;set; }
    
    public FixedFooItem(FooItem item, IRelayCommand doSomethingCommand)
    {
        _data = item;
        DoSomethingCommand = doSomethingCommand;
    }
}

View

<ScrollViewer IsVerticalScrollChainingEnabled="True">
    <ItemsRepeater ItemsSource="{x:Bind ViewModel.FooItems, Mode=OneWay}">
        <ItemsRepeater.Layout>
            <UniformGridLayout MinItemWidth="100" MinItemHeight="100" MinRowSpacing="12" MinColumnSpacing="12"/>
        </ItemsRepeater.Layout>

        <ItemsRepeater.ItemTemplate>
            <DataTemplate x:DataType="local:FixedFooItems">
                <Button Content="{x:Bind Data.Name}" Command="{x:Bind DoSomethingCommand}" CommandParameter="{x:Bind Data}"/>
            </DataTemplate>
        </ItemsRepeater.ItemTemplate>
    </ItemsRepeater>
</ScrollViewer>

I hope there is coming a fix anytime soon.

LeonSpors avatar Oct 27 '22 14:10 LeonSpors

Unrelated, but want to call this out: @LeonSpors I'd strongly recommended not to use [INotifyPropertyChanged] and instead to inherit from ObservableObject if you can. I implemented the attribute to help in scenarios where you could not inherit from ObservableObject, eg. if you had to inherit from another type which didn't have the interface, so you could still get the functionality from it. But if you can, do prefer inheriting from ObservableObject instead, as it'll reduce code duplication and binary size, as every viwmodel will just share the same implementation of all the helper methods from the base class instead of each carrying its own private copy for no reason 🙂

Sergio0694 avatar Oct 27 '22 14:10 Sergio0694

Any updates so far?

LeonSpors avatar Apr 27 '23 09:04 LeonSpors