dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.12k stars 1.73k forks source link

TemplatedView and TemplateBinding not working anymore in NET9 #24949

Open XceedBoucherS opened 3 weeks ago

XceedBoucherS commented 3 weeks ago

Description

in .NET8, I can use TemplateBinding on a property from a CustomControl in a ControlTemplate while it's not working anymore in NET9.

Steps to Reproduce

Create a new .NET8 app and use the attached files -MyBorder (a Custom Border control, deriving from TemplatedView with a ContentPresenter as ControlTemplate) -MyButton (a Custom Button control, deriving from TemplatedView) -App.xaml (defining the ControlTemplate for MyButton, using MyBorder and a ContentView TemplateBinding as content. -MainPage.xaml (using MyButton with a Label Content)

When running, everything is fine: the Label can be seen.

Create a new .NET9 app and use the same attached files. When running, the Label can't be seen ! Something has changed in ContentPresenter or BindingContext.

App.xaml.txt MainPage.xaml.txt MyBorder.cs.txt MyButton.cs.txt

Link to public reproduction project repository

No response

Version with bug

9.0.0-rc.1.24453.9

Is this a regression from previous behavior?

Yes, this used to work in .NET MAUI

Last version that worked well

8.0.82 SR8.2

Affected platforms

Windows

Affected platform versions

No response

Did you find any workaround?

No

Relevant log output

No response

drasticactions commented 3 weeks ago

As you only provided txt files, I tried to reproduce what you're seeing based on those on .NET 8

https://github.com/drasticactions/MauiRepoRedux/tree/bindingview

But I couldn't get it working in net8.0 or net9.0 on any platform. I could have made a mistake when recreating this with what you sent, or maybe there's something else wrong that I'm missing. Could you please create a reproduceable sample of this working in net8.0 and failing in net9.0?

XceedBoucherS commented 3 weeks ago

Hi drasticactions, thank you for your feedback.

The style for MyButton was missing in App.xaml. I tried to do a PullRequest with it. Not sure if I did it the right way. If not, try to copy the style of MyButton from app.xaml.txt attached earlier in your repo.

Thank you again.

StephaneDelcroix commented 3 weeks ago

@simonrozsival is this related to binding compilation ?

simonrozsival commented 3 weeks ago

@StephaneDelcroix I don't see the label even in Debug so without any compilation. There's just 1 binding in the code and there's no x:DataType, so there should be no binding compilation.

drasticactions commented 3 weeks ago

@XceedBoucherS Thank you for catching that!

Yeah, I updated my code. As far as I can tell playing around with it, MyButton works fine if you remove that inner MyBorder code from it and call the Control Template directly. MyBorder doesn't work at all in .net9.0 with what's there, it does in net8.0

[DefaultProperty( "Content" )]
[ContentProperty( "Content" )]
public class MyBorder : TemplatedView
{
    private ContentPresenter m_contentPresenter;

    public MyBorder()
    {
        m_contentPresenter = new ContentPresenter();
        this.ControlTemplate = new ControlTemplate( () => m_contentPresenter );
    }

    public static readonly BindableProperty ContentProperty = BindableProperty.Create( nameof( Content ), typeof( View ), typeof( MyBorder ) );

    public View Content
    {
        get => (View)GetValue( ContentProperty );
        set => SetValue( ContentProperty, value );
    }
}

Maybe the act of setting the inner ContentPresenter and ControlTemplate does something different in net9.0? MyButton doesn't do that and it works fine.

drasticactions commented 3 weeks ago

Also, this isn't just Windows. I tested it on iOS, Android, and Catalyst too and it happens on all of them. I don't think it's platform specific.

StephaneDelcroix commented 1 week ago

@simonrozsival I was able to find the root cause of this, it's because of the SetBinding change in ContentPresenter constructor.

Changing it back to

        public ContentPresenter()
        {
            #pragma warning disable IL2026
            SetBinding(ContentProperty, new Binding(ContentProperty.PropertyName, source: RelativeBindingSource.TemplatedParent,
                converterParameter: this, converter: new ContentConverter()));
            // this.SetBinding(
            //  ContentProperty,
            //  static (IContentView view) => view.Content,
            //  source: RelativeBindingSource.TemplatedParent,
            //  converter: new ContentConverter(),
            //  converterParameter: this);
        }

fixes it.

the branch fix_24949_90 contains a unit test for this. Could you have a look ??? Thanks

simonrozsival commented 1 week ago

@StephaneDelcroix good find! I'll look into this first thing in the morning.

simonrozsival commented 6 days ago

To get the component code work with the compiled binding in ContentPresenter the custom control needs to implement IContentView.Content:

// ...
-public class MyBorder : TemplatedView
+public class MyBorder : TemplatedView, IContentView
{
    // ...

    public View Content
    {
        get => (View)GetValue( ContentProperty );
        set => SetValue( ContentProperty, value );
    }

+    object IContentView.Content => Content;
}

Now the question is if this is an OK migration step that's required when moving from .NET 8 to .NET 9 and this is just an issue of missing documentation, or if we need to revert the binding in ContentPresenter and rethink how to make this work with compiled bindings.

simonrozsival commented 6 days ago

@XceedBoucherS do you have a specific reason why you're using TemplatedView as the base class? Our docs recommend using ContentView: https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/controltemplate?view=net-maui-8.0

XceedBoucherS commented 6 days ago

@simonrozsival the MyBorder class is one of the many custom control I have created in NET MAUI. All of them actually derives from a MyControl class, with many common properties for all my custom control. This MyControl class derives from TemplatedView so that all my custom control have access to a ControlTemplate property to redefine their look. If, instead of deriving the MyControl class from TemplatedView, I derive it from ContentView, all my custom control will have a Content property. This is not a wanted property in all my custom controls. If, instead of deriving the MyBorder class from MyControl (which derives from TemplatedView), I derive it from ContentView, MyBorder works well in NET9, but I have to copy all of my custom properties from the MyControl class in MyBorder. And I'll have to do the same for all my custom controls. Tell me if it's not clear enough.

In the end, I have many properties that I want to be available for all my custom control and I want to redefine the ControlTemplate of all of my Custom controls, but I don't want a Content property for all of them. The solution that worked in .NET8 and before was to derive my custom controls from a MyControl class with my common properties and derives this MyControl class from TemplatedView.

The documentation for the TemplatedView class only says: "A view that displays content with a control template, and the base class for ContentView." https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.templatedview?view=net-maui-8.0&devlangs=csharp&f1url=%3FappId%3DDev17IDEF1%26l%3DEN-US%26k%3Dk(Microsoft.Maui.Controls.TemplatedView)%3Bk(DevLang-csharp)%26rd%3Dtrue I understand that TemplatedView is the base class for ContentView, but I was hoping to only use the TemplatedView class for my own need : redefine a ControlTemplate for my custom controls.

What should I expect in NET9 ? Or should I change something in my code to pass from .NET8 to .NET9, without dupplicating code ?

Thank you

simonrozsival commented 6 days ago

@XceedBoucherS Thanks for the detailed explanation. In your case, it seems that the easiest way forward for you would be to implement IContentView for all your classes that have the Content property. If you want to avoid duplicating code, maybe you can consider introducing a MyContentControl which will implement IContentView and contain the ContentProperty. This way your controls will work with the ContentPresenter in .NET MAUI 9. Is this an acceptable solution for you?

XceedBoucherS commented 6 days ago

@simonrozsival I have 2 options: 1) MyBorder no longer derives from MyControl(which derives from TemplatedView), but instead derives from ContentView. I do not need this anymore in the MyBorder constructor: m_contentPresenter = new ContentPresenter(); this.ControlTemplate = new ControlTemplate( () => m_contentPresenter ); and everything works, but I have to dupplicate all the properties from the MyControl class in the MyBorder class.

2) MyBorder continues to derive from MyControl(which derives from TemplatedView), I keep the following in the MyBorder constructor: m_contentPresenter = new ContentPresenter(); this.ControlTemplate = new ControlTemplate( () => m_contentPresenter ); and I now derive MyBorder from IContentView and add the following line: object IContentView.Content => Content; and everything works as expected with very few changes.

I think I'll go with option 2, since only have 1 line to add and I do not dupplicate code. I have tested in NET8 and NET9 and it is working. So the conclusion, starting at .NET9, if I use a ContentPresenter in a ControlTemplate, I need to derives my control from IContentView and add: object IContentView.Content => Content;

Thank you for your help. Just poke me if you change something related to this in NET9.

simonrozsival commented 6 days ago

@XceedBoucherS thanks for the summary. Since there is a workaround for your problem, I'm closing the issue now.

I don't think we'll make any changes to this in .NET 9 anymore (we're close to GA) but I will keep you in the loop if anything changes.

PureWeen commented 3 days ago

Going to reopen this one for some continued discussion next week