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

[Trimming] Use typed bindings internally #20567

Merged
merged 12 commits into from Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions docs/design/FeatureSwitches.md
@@ -1,18 +1,23 @@
# Feature Switches

Certain features of MAUI can be enabled or disabled using feature switches. The easiest way to control the features is by putting the corresponding MSBuild property into the app's project file. Disabling unnecessary features can help reducing the app size when combined with the [`full` trimming mode](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options).
Certain features of MAUI can be enabled or disabled using feature switches. The easiest way to control the features is by putting the corresponding MSBuild property into the app's project file. Disabling unnecessary features can help reducing the app size when combined with the [`full` trimming mode](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options).

| MSBuild Property Name | AppContext Setting | Description |
|-|-|-|
| MauiXamlRuntimeParsingSupport | Microsoft.Maui.RuntimeFeature.IsXamlRuntimeParsingSupported | When disabled, all XAML loading at runtime will throw an exception. This will affect usage of APIs such as the `LoadFromXaml` extension method. This feature can be safely turned off when all XAML resources are compiled using XamlC (see [XAML compilation](https://learn.microsoft.com/en-us/dotnet/maui/xaml/xamlc)). This feature is enabled by default for all configurations except for NativeAOT. |
| MauiXamlRuntimeParsingSupport | Microsoft.Maui.RuntimeFeature.IsXamlRuntimeParsingSupported | When disabled, all XAML loading at runtime will throw an exception. This will affect usage of APIs such as the `LoadFromXaml` extension method. This feature can be safely turned off when all XAML resources are compiled using XamlC (see [XAML compilation](https://learn.microsoft.com/dotnet/maui/xaml/xamlc)). This feature is enabled by default for all configurations except for NativeAOT. |
| MauiEnableIVisualAssemblyScanning | Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled | When enabled, MAUI will scan assemblies for types implementing `IVisual` and for `[assembly: Visual(...)]` attributes and register these types. |
| MauiShellSearchResultsRendererDisplayMemberNameSupported | Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported | When disabled, it is necessary to always set `ItemTemplate` of any `SearchHandler`. Displaying search results through `DisplayMemberName` will not work. |

## MauiXamlRuntimeParsingSupport

When this feature is disabled, the following APIs are affected:
- [`LoadFromXaml` extension methods](https://learn.microsoft.com/en-us/dotnet/maui/xaml/runtime-load) will throw runtime exceptions.
- [Disabling XAML compilation](https://learn.microsoft.com/en-us/dotnet/maui/xaml/xamlc#disable-xaml-compilation) using `[XamlCompilation(XamlCompilationOptions.Skip)]` on pages and controls or whole assemblies will cause runtime exceptions.
- [`LoadFromXaml` extension methods](https://learn.microsoft.com/dotnet/maui/xaml/runtime-load) will throw runtime exceptions.
- [Disabling XAML compilation](https://learn.microsoft.com/dotnet/maui/xaml/xamlc#disable-xaml-compilation) using `[XamlCompilation(XamlCompilationOptions.Skip)]` on pages and controls or whole assemblies will cause runtime exceptions.

## MauiEnableIVisualAssemblyScanning

When this feature is not enabled, custom and third party `IVisual` types will not be automatically discovered and registerd.

## MauiShellSearchResultsRendererDisplayMemberNameSupported

When this feature is disabled, any value set to [`SearchHandler.DisplayMemberName`](https://learn.microsoft.com/dotnet/api/microsoft.maui.controls.searchhandler.displaymembername) will be ignored. Consider implementing a custom `ItemTemplate` to define the appearance of search results (see [Shell search documentation](https://learn.microsoft.com/dotnet/maui/fundamentals/shell/search#define-search-results-item-appearance)).
Expand Up @@ -210,6 +210,7 @@
<PropertyGroup>
<MauiXamlRuntimeParsingSupport Condition="'$(MauiXamlRuntimeParsingSupport)' == '' and '$(PublishAot)' == 'true'">false</MauiXamlRuntimeParsingSupport>
<MauiEnableIVisualAssemblyScanning Condition="'$(MauiEnableIVisualAssemblyScanning)' == ''">false</MauiEnableIVisualAssemblyScanning>
<MauiShellSearchResultsRendererDisplayMemberNameSupported Condition="'$(MauiShellSearchResultsRendererDisplayMemberNameSupported)' == '' and '$(PublishAot)' == 'true'">false</MauiShellSearchResultsRendererDisplayMemberNameSupported>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.IsXamlRuntimeParsingSupported"
Expand All @@ -220,6 +221,10 @@
Condition="'$(MauiEnableIVisualAssemblyScanning)' != ''"
Value="$(MauiEnableIVisualAssemblyScanning)"
Trim="true" />
<RuntimeHostConfigurationOption Include="Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported"
Condition="'$(MauiShellSearchResultsRendererDisplayMemberNameSupported)' != ''"
Value="$(MauiShellSearchResultsRendererDisplayMemberNameSupported)"
Trim="true" />
</ItemGroup>
</Target>

Expand Down
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Specialized;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Extensions.Logging;
using ObjCRuntime;
using UIKit;

Expand Down Expand Up @@ -37,7 +38,21 @@ DataTemplate DefaultTemplate
return _defaultTemplate ?? (_defaultTemplate = new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, SearchHandler.DisplayMemberName ?? ".");

if (RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported)
{
label.SetBinding(Label.TextProperty, SearchHandler.DisplayMemberName ?? ".");
}
else
{
if (SearchHandler.DisplayMemberName is not null)
jonathanpeppers marked this conversation as resolved.
Show resolved Hide resolved
{
Application.Current?.FindMauiContext()?.CreateLogger<ShellSearchResultsRenderer>().LogWarning(TrimmerConstants.SearchHandlerDisplayMemberNameNotSupportedWarning);
Copy link
Member

Choose a reason for hiding this comment

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

Should this throw? and then we put [RequiresUnreferencedCode] on the setter of the DisplayMemberName property?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wasn't sure if this should throw or not. When I'm running the app from the terminal, I don't see any stack trace or the exception message when the app crashes. Maybe do both?

The problem with [RequiresUnreferencedCode] on DisplayMemberName is that it causes ~50 trimming warnings similar to this one:

/.../maui/src/Controls/src/Core/Shell/SearchHandler.cs(398): Trim analysis warning IL2026: Microsoft.Maui.Controls.SearchHandler..cctor(): Using member 'Microsoft.Maui.Controls.SearchHandler.DisplayMemberName.set' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. DisplayMemberName is not supported. Consider implementing custom ItemTemplate instead. Alternatively, enable DisplayMemberName by setting the $(MauiShellSearchResultsRendererDisplayMemberNameSupported) MSBuild property to true. Note: DisplayMemberName is not trimming-safe and it might not work as expected in NativeAOT or fully trimmed apps. [/.../MyMauiApp/MyMauiApp.csproj::TargetFramework=net9.0-maccatalyst]

I am not sure why the static constructor would cause such a warning, but unfortunately, I don't know what to do about it.

Copy link
Member

Choose a reason for hiding this comment

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

So if we have:

public static readonly BindableProperty DisplayMemberNameProperty =
	BindableProperty.Create(nameof(DisplayMemberName), typeof(string), typeof(SearchHandler), null, BindingMode.OneTime);

//...

public string DisplayMemberName
{
	get { return (string)GetValue(DisplayMemberNameProperty); }
	[RequiresUnreferencedCode]
	set { SetValue(DisplayMemberNameProperty, value); }
}

Was it warning about nameof(DisplayMemberName)? I think we can probably leave the BindableProperty alone and only add the attribute to the setter of DisplayMemberName , if that is possible?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is exactly the code that I had locally, and it produced the flood of trimming warnings (we can't apply the attribute to fields BTW). Maybe the warnings are a bug in ILC? @vitek-karas might know what's going on when he's back from vacation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, it's probably not a bug, but it's still strange. We get 2 exactly same warnings on each line where we use typeof(SearchHandler), for example:

// /.../maui/src/Controls/src/Core/Shell/SearchHandler.cs(145): Trim analysis warning IL2026: Microsoft.Maui.Controls.SearchHandler..cctor(): Using member 'Microsoft.Maui.Controls.SearchHandler.DisplayMemberName.set' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Use ItemTemplate instead. [/.../MyMauiApp/MyMauiApp.csproj::TargetFramework=net9.0-maccatalyst]
// /.../maui/src/Controls/src/Core/Shell/SearchHandler.cs(145): Trim analysis warning IL2026: Microsoft.Maui.Controls.SearchHandler..cctor(): Using member 'Microsoft.Maui.Controls.SearchHandler.DisplayMemberName.set' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Use ItemTemplate instead. [/.../MyMauiApp/MyMauiApp.csproj::TargetFramework=net9.0-maccatalyst]
145: public static readonly BindableProperty CancelButtonColorProperty = BindableProperty.Create(nameof(CancelButtonColor), typeof(Color), typeof(SearchHandler), default(Color));

At the same time, the compiled XAML doesn't produce any warnings. That is because XamlC doesn't use the setter method, but it sets the value using BindableObject.SetValue:

<Shell.SearchHandler>
    <local:AnimalSearchHandler Placeholder="Enter search term"
                               ShowsResults="true"
                               DisplayMemberName="Name" />
</Shell.SearchHandler>
// decompiled IL
animalSearchHandler.SetValue(SearchHandler.DisplayMemberNameProperty, "Name");

Copy link
Member

Choose a reason for hiding this comment

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

This is very likely a problem in the trimmer - and possibly even in NativeAOT, but that I'm not sure of. The root cause is that the parameter to which we're passing typeof(SearchHandler) has DAM(PublicProperties | PublicMethods).

The PublicProperties will mark the DisplayMemberName property as reflection accessible, which in turn marks the getter of it as reflection accessible -> warning.
The PublicMethods will mark the getter direcly as reflection accessible -> warning.

@sbomer as FYI.

}

label.SetBinding(Label.TextProperty, TypedBinding.ForSingleNestingLevel(string.Empty, static (object o) => o));
}

label.HorizontalTextAlignment = TextAlignment.Center;
label.VerticalTextAlignment = TextAlignment.Center;

Expand Down Expand Up @@ -88,7 +103,9 @@ public override UITableViewCell GetCell(UITableView tableView, NSIndexPath index
var template = SearchHandler.ItemTemplate;

if (template == null)
{
template = DefaultTemplate;
}

var cellId = ((IDataTemplateController)template.SelectDataTemplate(context, _context.Shell)).IdString;

Expand Down
22 changes: 12 additions & 10 deletions src/Controls/src/Core/ContentConverter.cs
Expand Up @@ -66,29 +66,31 @@ static Label ConvertToLabel(string textContent, ContentPresenter presenter)

static void BindTextProperties(BindableObject content)
{
BindProperty(content, TextElement.TextColorProperty, typeof(ITextElement));
BindProperty(content, TextElement.CharacterSpacingProperty, typeof(ITextElement));
BindProperty(content, TextElement.TextTransformProperty, typeof(ITextElement));
BindProperty(content, TextElement.TextColorProperty, static (ITextElement te) => te.TextColor);
BindProperty(content, TextElement.CharacterSpacingProperty, static (ITextElement te) => te.CharacterSpacing);
BindProperty(content, TextElement.TextTransformProperty, static (ITextElement te) => te.TextTransform);
}

static void BindFontProperties(BindableObject content)
{
BindProperty(content, FontElement.FontAttributesProperty, typeof(IFontElement));
BindProperty(content, FontElement.FontSizeProperty, typeof(IFontElement));
BindProperty(content, FontElement.FontFamilyProperty, typeof(IFontElement));
BindProperty(content, FontElement.FontAttributesProperty, static (IFontElement fe) => fe.FontAttributes);
BindProperty(content, FontElement.FontSizeProperty, static (IFontElement fe) => fe.FontSize);
BindProperty(content, FontElement.FontFamilyProperty, static (IFontElement fe) => fe.FontFamily);
}

static void BindProperty(BindableObject content, BindableProperty property, Type type)
static void BindProperty<TSource, TProperty>(
BindableObject content,
BindableProperty property,
Func<TSource, TProperty> getter)
{
if (content.IsSet(property) || content.GetIsBound(property))
{
// Don't override the property if user has already set it
return;
}

content.SetBinding(property,
new Binding(property.PropertyName,
source: new RelativeBindingSource(RelativeBindingSourceMode.FindAncestor, type)));
content.SetBinding(property, TypedBinding.ForSingleNestingLevel(
property.PropertyName, getter, source: new RelativeBindingSource(RelativeBindingSourceMode.FindAncestor, typeof(TSource))));
}

static bool HasTemplateAncestor(ContentPresenter presenter, Type type)
Expand Down
11 changes: 9 additions & 2 deletions src/Controls/src/Core/ContentPresenter.cs
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
using Microsoft.Maui.Controls.Internals;

namespace Microsoft.Maui.Controls
{
Expand All @@ -16,8 +17,14 @@ public class ContentPresenter : Compatibility.Layout, IContentView
/// <include file="../../docs/Microsoft.Maui.Controls/ContentPresenter.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
public ContentPresenter()
{
SetBinding(ContentProperty, new Binding(ContentProperty.PropertyName, source: RelativeBindingSource.TemplatedParent,
converterParameter: this, converter: new ContentConverter()));
SetBinding(
ContentProperty,
TypedBinding.ForSingleNestingLevel(
nameof(IContentView.Content),
static (IContentView view) => view.Content,
source: RelativeBindingSource.TemplatedParent,
converter: new ContentConverter(),
converterParameter: this));
}

/// <include file="../../docs/Microsoft.Maui.Controls/ContentPresenter.xml" path="//Member[@MemberName='Content']/Docs/*" />
Expand Down
23 changes: 12 additions & 11 deletions src/Controls/src/Core/Items/CarouselView.cs
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Microsoft.Maui.Controls.Internals;

namespace Microsoft.Maui.Controls
{
Expand Down Expand Up @@ -208,17 +209,17 @@ static void LinkToIndicatorView(CarouselView carouselView, IndicatorView indicat
if (indicatorView == null)
return;

indicatorView.SetBinding(IndicatorView.PositionProperty, new Binding
{
Path = nameof(CarouselView.Position),
Source = carouselView
});

indicatorView.SetBinding(IndicatorView.ItemsSourceProperty, new Binding
{
Path = nameof(ItemsView.ItemsSource),
Source = carouselView
});
indicatorView.SetBinding(IndicatorView.PositionProperty, TypedBinding.ForSingleNestingLevel(
nameof(CarouselView.Position),
getter: static (CarouselView carousel) => carousel.Position,
setter: static (carousel, val) => carousel.Position = val,
source: carouselView));

indicatorView.SetBinding(IndicatorView.ItemsSourceProperty, TypedBinding.ForSingleNestingLevel(
nameof(CarouselView.ItemsSource),
getter: static (CarouselView carousel) => carousel.ItemsSource,
setter: static (carousel, val) => carousel.ItemsSource = val,
source: carouselView));
}

/// <include file="../../../docs/Microsoft.Maui.Controls/CarouselView.xml" path="//Member[@MemberName='IsScrolling']/Docs/*" />
Expand Down
8 changes: 7 additions & 1 deletion src/Controls/src/Core/ListView/ListView.cs
Expand Up @@ -411,7 +411,13 @@ public void ScrollTo(object item, object group, ScrollToPosition position, bool
protected override Cell CreateDefault(object item)
{
TextCell textCell = new TextCell();
textCell.SetBinding(TextCell.TextProperty, ".", converter: _toStringValueConverter);
textCell.SetBinding(
TextCell.TextProperty,
TypedBinding.ForSingleNestingLevel(
propertyName: string.Empty,
getter: static (object cell) => cell,
mode: BindingMode.OneWay,
converter: _toStringValueConverter));
return textCell;
}

Expand Down
74 changes: 48 additions & 26 deletions src/Controls/src/Core/RadioButton/RadioButton.cs
Expand Up @@ -444,37 +444,48 @@ void HandleRadioButtonGroupValueChanged(Element layout, RadioButtonGroupValueCha
SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
}

static void BindToTemplatedParent(BindableObject bindableObject, params BindableProperty[] properties)
{
foreach (var property in properties)
{
bindableObject.SetBinding(property, new Binding(property.PropertyName,
source: RelativeBindingSource.TemplatedParent));
}
}

static View BuildDefaultTemplate()
{
Border border = new Border()
{
Padding = 6
};

BindToTemplatedParent(border, BackgroundColorProperty, HorizontalOptionsProperty,
MarginProperty, OpacityProperty, RotationProperty, ScaleProperty, ScaleXProperty, ScaleYProperty,
TranslationYProperty, TranslationXProperty, VerticalOptionsProperty);

border.SetBinding(Border.StrokeProperty,
new Binding(BorderColorProperty.PropertyName,
source: RelativeBindingSource.TemplatedParent));

border.SetBinding(Border.StrokeShapeProperty,
new Binding(CornerRadiusProperty.PropertyName, converter: new CornerRadiusToShape(),
source: RelativeBindingSource.TemplatedParent));
void BindToTemplatedParent<TProperty>(
BindableProperty property,
Func<RadioButton, TProperty> getter,
string radioButtonPropertyName = null,
IValueConverter converter = null)
{
border.SetBinding(property, TypedBinding.ForSingleNestingLevel(radioButtonPropertyName ?? property.PropertyName,
getter, source: RelativeBindingSource.TemplatedParent, converter: converter));
}

border.SetBinding(Border.StrokeThicknessProperty,
new Binding(BorderWidthProperty.PropertyName,
source: RelativeBindingSource.TemplatedParent));
BindToTemplatedParent(BackgroundColorProperty, static (RadioButton rb) => rb.BackgroundColor);
BindToTemplatedParent(HorizontalOptionsProperty, static (RadioButton rb) => rb.HorizontalOptions);
BindToTemplatedParent(MarginProperty, static (RadioButton rb) => rb.Margin);
BindToTemplatedParent(OpacityProperty, static (RadioButton rb) => rb.Opacity);
BindToTemplatedParent(RotationProperty, static (RadioButton rb) => rb.Rotation);
BindToTemplatedParent(ScaleProperty, static (RadioButton rb) => rb.Scale);
BindToTemplatedParent(ScaleXProperty, static (RadioButton rb) => rb.ScaleX);
BindToTemplatedParent(ScaleYProperty, static (RadioButton rb) => rb.ScaleY);
BindToTemplatedParent(TranslationYProperty, static (RadioButton rb) => rb.TranslationY);
BindToTemplatedParent(TranslationXProperty, static (RadioButton rb) => rb.TranslationX);
BindToTemplatedParent(VerticalOptionsProperty, static (RadioButton rb) => rb.VerticalOptions);

BindToTemplatedParent(
Border.StrokeProperty,
static (RadioButton rb) => rb.BorderColor,
radioButtonPropertyName: nameof(RadioButton.BorderColor));
BindToTemplatedParent(
Border.StrokeShapeProperty,
static (RadioButton rb) => rb.CornerRadius,
radioButtonPropertyName: nameof(RadioButton.CornerRadius),
converter: new CornerRadiusToShape());
BindToTemplatedParent(
Border.StrokeThicknessProperty,
static (RadioButton rb) => rb.BorderWidth,
radioButtonPropertyName: nameof(RadioButton.BorderWidth));

var grid = new Grid
{
Expand Down Expand Up @@ -573,9 +584,20 @@ static View BuildDefaultTemplate()
out checkMarkFillVisualStateDark);
}

contentPresenter.SetBinding(MarginProperty, new Binding("Padding", source: RelativeBindingSource.TemplatedParent));
contentPresenter.SetBinding(BackgroundColorProperty, new Binding(BackgroundColorProperty.PropertyName,
source: RelativeBindingSource.TemplatedParent));
contentPresenter.SetBinding(
MarginProperty,
TypedBinding.ForSingleNestingLevel(
nameof(RadioButton.Padding),
static (RadioButton radio) => radio.Padding,
mode: BindingMode.OneWay,
source: RelativeBindingSource.TemplatedParent));
contentPresenter.SetBinding(
BackgroundColorProperty,
TypedBinding.ForSingleNestingLevel(
nameof(RadioButton.BackgroundColor),
static (RadioButton radio) => radio.BackgroundColor,
mode: BindingMode.OneWay,
source: RelativeBindingSource.TemplatedParent));

grid.Add(normalEllipse);
grid.Add(checkMark);
Expand Down
41 changes: 36 additions & 5 deletions src/Controls/src/Core/Shell/BaseShellItem.cs
Expand Up @@ -339,7 +339,7 @@ BindableObject NonImplicitParent
}
}

internal static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, string iconBinding)
internal static DataTemplate CreateDefaultFlyoutItemCell(BindableObject bo)
{
return new DataTemplate(() =>
{
Expand Down Expand Up @@ -426,9 +426,37 @@ internal static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, str
columnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
defaultGridClass.Setters.Add(new Setter { Property = Grid.ColumnDefinitionsProperty, Value = columnDefinitions });

Binding automationIdBinding = new Binding(Element.AutomationIdProperty.PropertyName);
BindingBase automationIdBinding = TypedBinding.ForSingleNestingLevel(
nameof(Element.AutomationId),
static (Element element) => element.AutomationId,
static (element, val) => element.AutomationId = val);
defaultGridClass.Setters.Add(new Setter { Property = Element.AutomationIdProperty, Value = automationIdBinding });

BindingBase imageBinding = null;
BindingBase labelBinding = null;
if (bo is MenuItem)
{
imageBinding = TypedBinding.ForSingleNestingLevel(
nameof(MenuItem.IconImageSource),
getter: static (MenuItem item) => item.IconImageSource,
setter: static (item, val) => item.IconImageSource = val);
labelBinding = TypedBinding.ForSingleNestingLevel(
nameof(MenuItem.Text),
getter: static (MenuItem item) => item.Text,
setter: static (item, val) => item.Text = val);
}
else
{
imageBinding = TypedBinding.ForSingleNestingLevel(
nameof(BaseShellItem.FlyoutIcon),
getter: static (BaseShellItem item) => item.FlyoutIcon,
setter: static (item, val) => item.FlyoutIcon = val);
labelBinding = TypedBinding.ForSingleNestingLevel(
nameof(BaseShellItem.Title),
getter: static (BaseShellItem item) => item.Title,
setter: static (item, val) => item.Title = val);
}

var image = new Image();

double sizeRequest = -1;
Expand All @@ -453,13 +481,11 @@ internal static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, str
defaultImageClass.Setters.Add(new Setter { Property = Image.MarginProperty, Value = new Thickness(12, 0, 12, 0) });
}

Binding imageBinding = new Binding(iconBinding);
defaultImageClass.Setters.Add(new Setter { Property = Image.SourceProperty, Value = imageBinding });

grid.Add(image);

var label = new Label();
Binding labelBinding = new Binding(textBinding);
defaultLabelClass.Setters.Add(new Setter { Property = Label.TextProperty, Value = labelBinding });

grid.Add(label, 1, 0);
Expand Down Expand Up @@ -527,7 +553,12 @@ internal static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, str
// just bind the semantic description to the title
if (!g.IsSet(SemanticProperties.DescriptionProperty))
{
g.SetBinding(SemanticProperties.DescriptionProperty, TitleProperty.PropertyName);
g.SetBinding(
SemanticProperties.DescriptionProperty,
TypedBinding.ForSingleNestingLevel(
nameof(BaseShellItem.Title),
static (BaseShellItem item) => item.Title,
static (item, val) => item.Title = val));
}
}
}
Expand Down