Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x:Bind outside data templates is extremely inconsistent #2508

Open
Sergio0694 opened this issue May 20, 2020 · 38 comments
Open

x:Bind outside data templates is extremely inconsistent #2508

Sergio0694 opened this issue May 20, 2020 · 38 comments
Assignees
Labels
area-Binding bug Something isn't working team-Markup Issue for the Markup team wct

Comments

@Sergio0694
Copy link
Member

Sergio0694 commented May 20, 2020

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

@msft-github-bot msft-github-bot added the needs-triage Issue needs to be triaged by the area owners label May 20, 2020
@StephenLPeters StephenLPeters added the team-Markup Issue for the Markup team label May 20, 2020
@StephenLPeters
Copy link
Contributor

@fabiant3 For FYI

@jamesmcroft
Copy link
Member

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.

@MikeHillberg
Copy link
Contributor

{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.

@Sergio0694
Copy link
Member Author

@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? 🤔

@MikeHillberg
Copy link
Contributor

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.

@RealTommyKlein
Copy link
Contributor

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 RealTommyKlein self-assigned this May 27, 2020
@hansmbakker
Copy link

hansmbakker commented Sep 26, 2020

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.

@RealTommyKlein
Copy link
Contributor

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.

@hansmbakker
Copy link

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.

@stevenbrix
Copy link
Contributor

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

@stevenbrix stevenbrix removed the needs-triage Issue needs to be triaged by the area owners label Nov 18, 2020
@deakjahn
Copy link

deakjahn commented Dec 3, 2020

@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.

@HppZ
Copy link

HppZ commented Mar 18, 2021

any update?

@RealTommyKlein
Copy link
Contributor

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!

@HppZ
Copy link

HppZ commented Mar 25, 2021

{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
Copy link

HppZ commented Mar 26, 2021

@Sergio0694 not work either.
image

@HppZ
Copy link

HppZ commented May 11, 2021

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

@HppZ
Copy link

HppZ commented May 11, 2021

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
Copy link

HppZ commented May 11, 2021

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

image

@michael-hawker
Copy link
Collaborator

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.

@ArsenijK
Copy link

ArsenijK commented Mar 23, 2022

@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!

@bogdan-patraucean
Copy link

bogdan-patraucean commented Apr 10, 2022

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>

@hawkerm
Copy link

hawkerm commented May 12, 2022

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

@Sergio0694
Copy link
Member Author

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 😄

@michael-hawker
Copy link
Collaborator

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

@Sergio0694
Copy link
Member Author

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

@Sergio0694
Copy link
Member Author

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 😥

@HppZ
Copy link

HppZ commented Jul 22, 2022

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

@LeonSpors
Copy link

LeonSpors commented Oct 27, 2022

@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.

@Sergio0694
Copy link
Member Author

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 🙂

@LeonSpors
Copy link

Any updates so far?

@Sergio0694
Copy link
Member Author

Bumping this (see #8638).

@ghost1372
Copy link
Contributor

@duncanmacmichael any update?

@bogdan-patraucean
Copy link

They said they are working on x:Bind improvements for the 1.6 version of WinAppSdk, let's hope this will be part of the update.

@michael-hawker
Copy link
Collaborator

michael-hawker commented Oct 15, 2024

Will call out that this type of issue will prevent adoption of AoT as the fallback is to do things with regular Binding or alternatives compared to keeping things sleek and simple with x:Bind. Linked an issue we have with a sample from the WCT in a related but also tangential issue with ItemsPanelTemplate. See the related issue we filed here for that: #10070

@michael-hawker
Copy link
Collaborator

Here's another scenario we have currently which relies on Binding, which means we can't migrate this to be AOT compatible:

<DataTemplate x:DataType="local:TemplateInformation">
    <StackPanel>
        <TextBox Name="CodeValidator"
                 ui:TextBoxExtensions.Regex="{x:Bind Regex, Mode=OneWay}"
                 Header="{x:Bind Header, Mode=OneWay}"
                 PlaceholderText="{x:Bind PlaceholderText, Mode=OneWay}" />
        <TextBlock Text="Thanks for entering a valid code!"
                   Visibility="{Binding (ui:TextBoxExtensions.IsValid), ElementName=CodeValidator}" />
    </StackPanel>
</DataTemplate>

We should be able to just do:

<TextBlock Text="Thanks for entering a valid code!"
   Visibility="{x:Bind CodeValidator.(ui:TextBoxExtensions.IsValid)}" />

Like we could if we were outside the data template, but within the template, CodeValidator can't be found.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Nov 27, 2024

Does this not work?

<TextBlock
    Text="Thanks for entering a valid code!"
    Visibility="{x:Bind ui:TextBoxExtensions.IsValid(CodeValidator), Mode=OneWay}" />

That should work fine, it's just a normal binding to function 🤔

@michael-hawker
Copy link
Collaborator

Does this not work?


That should work fine, it's just a normal binding to function 🤔

IsValid is an attached property not a function. Plus, I would think that it still wouldn't find the CodeValidator element within the template, as it's over aggressive about only looking for things on the DataType of the data template.

@AndrewKeepCoding
Copy link
Contributor

I would think that it still wouldn't find the CodeValidator element within the template

Confirmed. x:Bind won't look for elements inside a DataTemplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Binding bug Something isn't working team-Markup Issue for the Markup team wct
Projects
None yet
Development

No branches or pull requests