Skip to content

[Trimming] Add new API to create TypedBindings from expression#19995

Closed
simonrozsival wants to merge 11 commits intodotnet:net9.0from
simonrozsival:typed-binding-from-expression
Closed

[Trimming] Add new API to create TypedBindings from expression#19995
simonrozsival wants to merge 11 commits intodotnet:net9.0from
simonrozsival:typed-binding-from-expression

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Jan 19, 2024

Description of Change

TypedBinding is a great replacement for the reflection-based Binding class. Unfortunately, it can only be used through XamlC. This PR adds a new generic overload of the SetBinding extension method which will allow developers to use the typed binding from C#/F#.

I recommend reviewing the PR commit-by-commit.

Note: This PR depends on #20415 (support for relative binding sources).

Expressions

In this PR, I'm introducing a way to replace Binding with TypedBinding through a new SetBinding<TSource, TProperty> extension method. The extension method expects a getter expression instead of the string path. This has several advantages:

  • faster bindings (no reflection)
  • code completion in IDE
  • trimming-friendly, works well with NativeAOT

The extension method parses the expression and it uses it to automatically generate the handlers array the same way XamlC generates it. In most cases, the getter expression can be used to generate the setter as well. The only case where this is not possible to compile the setter is when the last part of the setter is a custom indexer (we don't have access to the setter method). In this case, the developer needs to pass the setter Action manually.

The "compiled" getters and setters are almost as fast as explicit getters and setters would be. This change doesn't increase the size of the final app bundle too much either: the .ipa build using NativeAOT was roughly 1.25% bigger than with just the reflection-based bindings.

Performance implications

Based on running a few benchmarks (BindingBenchmarker, modified TypedBindingBenchmarker), constructing TypedBindings from expressions is slightly slower than parsing the string path and the "compilation" also allocates more memory. I need to collect more data.

TODO:

  • Gather and present the performance data
  • Is there an impact on app startup?

Follow-ups

  • There are still many places where we use new Binding("...") and SetBinding("...") in the MAUI codebase. These can be migrated in some follow-up PR. There's no need to do the migration right now in this PR (it's quite big already).
    - ControlGallery
    - Controls.Sample
    - Controls.Sample.UITests
    - Windows specific code
    - Tizen specific code
    - Unit tests (Controls)
    - Examples in XML docs
  • ShellSearchResultsRenderer (iOS) and ShellSearchViewAdapter (Android) default template can't be migrated to typed binding and it won't work with typed binding. That's the (only) reason why I can't remove any trimming/AOT warnings in tests. We will need to disable it using a feature switch for native AOT in some follow-up PR.
  • Make the SetBinding<TSource, TProperty> method public + update PublicAPI.Unshipped.txt
    • I wasn't able to update PublicAPI.Unshipped.txt using VSCode or manually for some reason.

Issues Fixed

Contributes to #19397
Closes #19912

@simonrozsival simonrozsival changed the title [Trimming] Allow creating TypedBinding using an expression [Trimming] Allow creating TypedBinding from expressions Jan 19, 2024
@simonrozsival simonrozsival added the perf/app-size Application Size / Trimming (sub: perf) label Jan 19, 2024
@simonrozsival
Copy link
Copy Markdown
Member Author

Copy link
Copy Markdown
Contributor

@jsuarezruiz jsuarezruiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D:\a\_work\1\s\src\Compatibility\Core\src\Android\Renderers\ListViewAdapter.cs(739,5): error IL2026: Using member 'Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(BindableObject, BindableProperty, String, BindingMode, IValueConverter, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The use of string-based binding is not trimming safe. Use the expression-based SetBinding overloads instead. [D:\a\_work\1\s\src\Compatibility\Core\src\Compatibility.csproj::TargetFramework=net9.0-android]

@github-actions github-actions bot force-pushed the typed-binding-from-expression branch from fe8e866 to f5a5859 Compare January 29, 2024 10:56
@rmarinho rmarinho deleted the branch dotnet:net9.0 January 31, 2024 11:32
@rmarinho rmarinho closed this Jan 31, 2024
@simonrozsival simonrozsival deleted the typed-binding-from-expression branch January 31, 2024 12:41
@simonrozsival simonrozsival restored the typed-binding-from-expression branch January 31, 2024 12:41
@simonrozsival simonrozsival reopened this Jan 31, 2024
@simonrozsival simonrozsival force-pushed the typed-binding-from-expression branch from f5a5859 to d3465b7 Compare February 1, 2024 17:18
@simonrozsival simonrozsival changed the title [Trimming] Allow creating TypedBinding from expressions [Trimming] Improve TypedBindings Feb 1, 2024
@simonrozsival simonrozsival force-pushed the typed-binding-from-expression branch from 9c4799d to 3a929ed Compare February 2, 2024 12:52
@simonrozsival simonrozsival marked this pull request as ready for review February 2, 2024 13:00
@simonrozsival simonrozsival requested a review from a team as a code owner February 2, 2024 13:00
@simonrozsival
Copy link
Copy Markdown
Member Author

simonrozsival commented Feb 2, 2024

/cc @jonathanpeppers this is ready for review now

Comment on lines +432 to +448
if (bo is MenuItem)
{
imageBinding = TypedBindingFactory.Create(static (MenuItem item) => item.IconImageSource);
labelBinding = TypedBindingFactory.Create(static (MenuItem item) => item.Text);
}
else if (bo is BaseShellItem)
{
imageBinding = TypedBindingFactory.Create(static (BaseShellItem item) => item.FlyoutIcon);
labelBinding = TypedBindingFactory.Create(static (BaseShellItem item) => item.Title);
}
else
{
throw new InvalidOperationException("Invalid type for flyout item cell.");
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other types that need to work here besides MenuItem and BaseShellItem? What was the exception you would have gotten previously for using the wrong type inside a flyout?

Or did previously you got no error, and the bindings just "didn't work"?


namespace Microsoft.Maui.Controls
{
internal static class TypedBindingFactory
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code in here, it supports:

  • ParameterExpression
  • UnaryExpression
  • MemberExpression
  • MethodCallExpression
  • BinaryExpression

Does this need to be exhaustive yet? Should we at least put a <summary> comment of which expressions would work?

It seems like we'd need to implement them all if this was able to be used by customers -- which, isn't the case yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's perfectly OK to support only a subset and throw if the provided expression is too complex. ASP.NET uses similar trick and also implements only a limited subset of the expression tree support. For example https://github.com/dotnet/aspnetcore/blob/c2a9c205aa65af4031e0fd6471c2aa89bde095c0/src/Mvc/Mvc.ViewFeatures/src/ExpressionMetadataProvider.cs#L72C59-L72C94.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I focused only on mirroring what customers can do with binding paths - accessing members, arrays, and indexers (as described in https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/binding-path). From those 5 expression types, it also only supports a subset of them (for example unary operator - just Convert, AsType, method call - just special name methods get_Indexer, get_Item), but it's all that's necessary to support the expressions that are similar to the string paths in classic bindings. As @StephaneDelcroix told me, the subset of bindings I implemented is very close to one of the earlier expression-based APIs that Xamarin.Forms once had (https://github.com/xamarin/Xamarin.Forms/blob/5.0.0/Xamarin.Forms.Core/Binding.cs#L332).

You're definitely right that the documentation part is lacking. I'm assuming we'll need to update the official documentation if this feature goes in later, but I can definitely add that <summary> in this PR. Thanks for the suggestion.

groupProxy.HeaderContent = _itemsView.CreateDefault(ListProxy.ProxiedEnumerable);
groupProxy.HeaderContent.BindingContext = groupProxy;
groupProxy.HeaderContent.SetBinding(TextCell.TextProperty, "Name");
groupProxy.HeaderContent.SetBinding(TextCell.TextProperty, static (TemplatedItemsList<TView, TItem> list) => list.Name);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using BannedApiAnalyzers for a few things in this repo, do we need to also ban SetBinding(BindableProperty,string)? As the typed binding used here seems like it could be better for both performance and trimming/nativeaot.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that would be a good thing to do, but maybe in a follow-up PR that removes the remaining usages of this API in the app (mostly Windows and Tizen specific code). In fact, I think we should ban using the Binding class altogether.

We should also add [RequiresUnreferencedCode] to the Binding class so that customers get trimming warnings if they use it in their apps.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add [RequiresUnreferencedCode] to the Binding class so that customers get trimming warnings if they use it in their apps.

Won't we have to do this - to "fix" all the warnings - anyway?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vitek-karas we don't need it to fix all the warnings we get when we build the dotnet new maui app. Binding internally uses BindingExpression which has trimming-unsafe code so we get a warning anyway. I think we should add it so that when customers build their apps where they use Binding, the warnings appear in their codebase and not in the MAUI assembly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add it so that when customers build their apps where they use Binding, the warnings appear in their codebase and not in the MAUI assembly.

That was exactly the reason I had in mind :-). Thanks.

@ivanpovazan
Copy link
Copy Markdown
Member

Just for my understanding (I am still getting familiar with the context), regarding:

Unfortunately, it can only be used through XamlC and only in some limited cases where it can replace the old Binding (x:DataType annotation + no explicit source).

Would the changes in this PR enable us to improve XamlC codegen - covering more cases and making better use of TypedBindings as well, or it is purely to provide adequate API for using TypedBindings programatically?

@simonrozsival
Copy link
Copy Markdown
Member Author

Would the changes in this PR enable us to improve XamlC codegen - covering more cases and making better use of TypedBindings as well, or it is purely to provide adequate API for using TypedBindings programatically?

Yes, we should be able to compile a lot more bindings into typed bindings. I'll update the description to make it clear.

@simonrozsival simonrozsival force-pushed the typed-binding-from-expression branch from b2f2c8b to 7d7f60b Compare February 6, 2024 08:17
@simonrozsival simonrozsival marked this pull request as draft February 7, 2024 10:55
@simonrozsival simonrozsival changed the title [Trimming] Improve TypedBindings [Trimming] Add new API to create TypedBindings from expression Feb 7, 2024
@rmarinho
Copy link
Copy Markdown
Member

/rebase

1 similar comment
@rmarinho
Copy link
Copy Markdown
Member

/rebase

@simonrozsival
Copy link
Copy Markdown
Member Author

Closing this prototype for now. We might revive it later.

@github-actions github-actions bot locked and limited conversation to collaborators Mar 16, 2024
@Eilon Eilon added the area-publishing Issues with the app packaging/publishing process (ipk/apk/msix/trimming) label May 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-publishing Issues with the app packaging/publishing process (ipk/apk/msix/trimming) perf/app-size Application Size / Trimming (sub: perf)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] Add a new public API to allow defining TypedBinding in code

8 participants