maui icon indicating copy to clipboard operation
maui copied to clipboard

TemplatedView and TemplateBinding not working anymore in NET9

Open XceedBoucherS opened this issue 1 year ago • 16 comments
trafficstars

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

XceedBoucherS avatar Sep 26 '24 19:09 XceedBoucherS

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?

drasticactions avatar Sep 27 '24 06:09 drasticactions

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.

XceedBoucherS avatar Sep 27 '24 11:09 XceedBoucherS

@simonrozsival is this related to binding compilation ?

StephaneDelcroix avatar Sep 27 '24 11:09 StephaneDelcroix

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

simonrozsival avatar Sep 27 '24 13:09 simonrozsival

@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 avatar Sep 27 '24 13:09 drasticactions

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.

drasticactions avatar Sep 27 '24 14:09 drasticactions

@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

StephaneDelcroix avatar Oct 14 '24 20:10 StephaneDelcroix

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

simonrozsival avatar Oct 14 '24 20:10 simonrozsival

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 avatar Oct 15 '24 07:10 simonrozsival

@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

simonrozsival avatar Oct 15 '24 07:10 simonrozsival

@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

XceedBoucherS avatar Oct 15 '24 13:10 XceedBoucherS

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

simonrozsival avatar Oct 15 '24 13:10 simonrozsival

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

XceedBoucherS avatar Oct 15 '24 15:10 XceedBoucherS

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

simonrozsival avatar Oct 15 '24 16:10 simonrozsival

Going to reopen this one for some continued discussion next week

PureWeen avatar Oct 18 '24 22:10 PureWeen

This issue has been verified using Visual Studio 17.12.0 Preview 3(9.0.0-rc.2.24503.2 & 9.0.0-rc.1.24453.9 & 8.0.92 &8.0.82). Can repro this issue on windows platform. 8.0.92 & 8.0.82 works fine.

Zhanglirong-Winnie avatar Oct 23 '24 09:10 Zhanglirong-Winnie

Looking at the way WPF does things, you are supposed to be able to have multiple ContentPresenters in one control. Here are some examples: https://www.iditect.com/faq/csharp/multiple-content-presenters-in-a-wpf-user-control.html

I think the issue is that we made a change, and @simonrozsival has a fallback to a reflection lookup to get old behaviour.

However, for MAUI, we should not use the fallback nor do I think we should be changing the controls. We should rather be updating our control templates to correctly bind the Content property of the ContentPresenter to the correct property. For example, the RadioButton should do this in its template definition:

contentPresenter.SetBinding(
    ContentPresenter.ContentProperty,
    static (RadioButton radio) => radio.Content,
    source: RelativeBindingSource.TemplatedParent,
    converter: new ContentConverter(),
    converterParameter: contentPresenter);

We already do this for all the other props:

contentPresenter.SetBinding(
    MarginProperty,
    static (RadioButton radio) => radio.Padding,
    BindingMode.OneWay,
    source: RelativeBindingSource.TemplatedParent);

mattleibow avatar Nov 20 '24 16:11 mattleibow

Looking at the way WPF does things, you are supposed to be able to have multiple ContentPresenters in one control. Here are some examples: https://www.iditect.com/faq/csharp/multiple-content-presenters-in-a-wpf-user-control.html

I think the issue is that we made a change, and @simonrozsival has a fallback to a reflection lookup to get old behaviour.

However, for MAUI, we should not use the fallback nor do I think we should be changing the controls. We should rather be updating our control templates to correctly bind the Content property of the ContentPresenter to the correct property. For example, the RadioButton should do this in its template definition:

contentPresenter.SetBinding( ContentPresenter.ContentProperty, static (RadioButton radio) => radio.Content, source: RelativeBindingSource.TemplatedParent, converter: new ContentConverter(), converterParameter: contentPresenter); We already do this for all the other props:

contentPresenter.SetBinding( MarginProperty, static (RadioButton radio) => radio.Padding, BindingMode.OneWay, source: RelativeBindingSource.TemplatedParent);

I think that's currently not working https://github.com/dotnet/maui/issues/23797

PureWeen avatar Nov 22 '24 21:11 PureWeen

I can confirm this is not working on Maui version 9.0.30, for ControlTemplate used on ContentPages

pictos avatar Jan 28 '25 02:01 pictos