Skip to content

Conversation

@simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Mar 12, 2025

This PR supersedes #28115

Description of Change

This proof of concept explores how we could get rid of the long DI setup code in AppHostBuilderExtensions:

internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCollection handlersCollection)
{
    handlersCollection.AddHandler<CollectionView, CollectionViewHandler>();
    handlersCollection.AddHandler<CarouselView, CarouselViewHandler>();
    handlersCollection.AddHandler<Application, ApplicationHandler>();
    handlersCollection.AddHandler<ActivityIndicator, ActivityIndicatorHandler>();
    handlersCollection.AddHandler<BoxView, BoxViewHandler>();
    // ...

Listing all the classes this way makes it impossible for the trimmer to remove any of them.

The idea in this PR is to connect the element and the element handler using an attribute instead of looking up the handler in the DI container:

[ElementHandler<LabelHandler>]
partial class Label
{
    static Label()
    {
        RemappingHelper.RemapIfNeeded(typeof(VisualElement), VisualElement.RemapIfNeeded);

        // ...
    }
}

EDIT: The IRemappable interface used in earlier iteration has been removed.

if the control isn't used anywhere in the app, the trimmer will remove the element type. When the type is removed, the handler type is removed as well, because the only link to the handler type is the custom class attribute.

The DI mechanism is still checked first before looking for the attribute for two reasons:

  1. Backwards compatibility: if the developer registers their own handlers in their app/library through DI today, it should continue working. They will need to make changes to their code to make their controls trimmable as well.
  2. Overwriting the defaults: It should be possible to change the default handlers for built in controls. I'm not sure how useful this is, but we should keep that possibility around.

Notes:

  • I'm sharing this Draft to get early feedback. I'm still not 100% sure this is the right way to do it. I'm especially not confident in the way RemapForControls would be handled.
  • This PR shows what the changes would look like for two controls: Label and CheckBox. These two are chosen because Label is in the dotnet new maui app and so we can test that the handler for that control is created correctly, and CheckBox is NOT in the sample app, so we can verify that the trimmer removes this class + the CheckBoxHandler when trim mode is set to full.

Feedback is very welcome!

Fixes

/cc @jonathanpeppers @PureWeen @mattleibow @ivanpovazan


internal interface IRemappable
{
void RemapForControls();
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

oops, i see i used static ctors which are random in AOT, so pretend I did something else in that bit of code.

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 tried to use static abstract methods, but I struggled to get that working with netstandard. I think I would have to completely remove the Remap methods in netstandard which seems weird. Do we have some precedent for that already?

Copy link
Member

@mattleibow mattleibow Mar 13, 2025

Choose a reason for hiding this comment

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

Yes. Netstandard is not supported. We just use it to access type information in controls. And I am pretty sure that is not being used. Just for vs as well.

We are using static interface members and default interface members already.

Copy link
Member Author

@simonrozsival simonrozsival Mar 14, 2025

Choose a reason for hiding this comment

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

So I ran into the problem that the static abstract methods can only be called via constrained generic type. This means we would need to have two different attributes for handlers:

[RemappableElementHandler<CheckBoxHandler>]
class CheckBox;

[ElementHandler<ProgressBarHandler>]
class ProgressBar;

If we kept this as a simple interface with an instance method, we can use pattern matching in RemappingHelper to see if the handler implements IRemappable or not.

Alternatively, we could put the remapping logic into the static constructor of the handler. That would be likely more efficient, but we would have even less control over the order in which the remappings are applied. If I understand it correctly, the order of remapping matters and it is not desirable to remap a base type after the subclass because it could shadow the more specialized remappings.

We could maybe just do something like this:

class VisualElement
{
    static VisualElement() => RemapForControls();
    internal new static void RemapForControls()
    {
        // make sure we don't override any previously registered remappings of other derived classes
        if (!RemappingHelper.ShouldRemap(typeof(VisualElement))
            return;

        Element.RemapForControls();
        // ... do what VisualElement needs to do
}

class CheckBox
{
    static CheckBox() => RemapForControls();
    internal new static void RemapForControls()
    {
        if (!RemappingHelper.ShouldRemap(typeof(CheckBox))
            remap;

        VisualElement.RemapForControls(); // the base ctor will run on its own, but there is no guarantee that the base cctor runs BEFORE the derived class cctor. to achieve the right ordering, we need to call the base remapping ourselves
        // ... do what CheckBox needs to do
    }
}

class Label
{
    static Label() => RemapForControls();    
    internal new static void RemapForControls()
    {
        if (!RemappingHelper.ShouldRemap(typeof(Label))
            return;

        VisualElement.RemapForControls(); 
        // ... do the label remapping
    }
}

It would be less lazy than what is in this PR, but it wouldn't be any worse than today. It would still be trimmable and it would require one less interface.

(I will need to work on the thread-safety aspect of the code. I don't think the code with the early returns is actually correct.)

public override IElementHandler CreateHandler()
{
var handler = new THandler();
RemappingHelper.RemapIfNeeded(typeof(THandler), handler);
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 realized this doesn't work. It's the element that can be remapped, not the handler. I need to figure out how to address this without having to change the API drastically. The cctors approach might be the best after all.

@mattleibow mattleibow added the area-core-hosting Extensions / Hosting / AppBuilder / Startup label Mar 17, 2025
@simonrozsival simonrozsival marked this pull request as ready for review March 19, 2025 21:28
@simonrozsival simonrozsival requested a review from a team as a code owner March 19, 2025 21:28
@rmarinho
Copy link
Member

/rebase

@github-actions github-actions bot force-pushed the dev/simonrozsival/net10.0-trimmable-view-handlers branch from 52a5cff to 2cdf173 Compare April 20, 2025 00:14
@mattleibow
Copy link
Member

@jfversluis and @StephaneDelcroix and FYI and source generators and thoughts?

@PureWeen PureWeen added this to the .NET 10.0-preview5 milestone May 7, 2025
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks like a red flag for trimming.

Copy link
Member Author

Choose a reason for hiding this comment

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

if I remember correctly, this is necessary to use this extension method, which is trimming and AOT-friendly:

viewType.GetCustomAttribute<ElementHandlerAttribute>()

@github-project-automation github-project-automation bot moved this from Todo to Approved in MAUI SDK Ongoing May 13, 2025
@PureWeen PureWeen merged commit 368000a into net10.0 May 13, 2025
130 checks passed
@PureWeen PureWeen deleted the dev/simonrozsival/net10.0-trimmable-view-handlers branch May 13, 2025 14:15
@github-project-automation github-project-automation bot moved this from Approved to Done in MAUI SDK Ongoing May 13, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Jun 13, 2025
@simonrozsival simonrozsival changed the title [net10.0] Trimmable view handlers [net11.0] Trimmable view handlers Dec 1, 2025
@simonrozsival simonrozsival changed the title [net11.0] Trimmable view handlers [net10.0] Trimmable view handlers Dec 1, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-core-hosting Extensions / Hosting / AppBuilder / Startup proposal/open

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants