Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

ViewLocator conflict with trimming functionality in Visual Studio when publishing #14507

Closed
creamIcec opened this issue Feb 6, 2024 · 4 comments

Comments

@creamIcec
Copy link

creamIcec commented Feb 6, 2024

Hi I'm new to Avalonia and trying to use view locator for switching views. But I encountered some unexpected consequence when publishing the app.

Describe the bug

I'm using the view locator code available on Avalonia documentation: https://docs.avaloniaui.net/zh-Hans/docs/tutorials/todo-list-app/locating-views. It works good under IDE debugging and running, but when I publish my app with trimming code option checked, the result is that app launched but just displaying not found message on screen. Below are my actions trying to solve this issue, but unfortunately none of them worked:

  1. When I firstly encountered the issue, I believe there must be some difference between debugging in IDE and publishing. So I turned to search engine and added an option to my project file:
    <PreserveCompilationContext>true</PreserveCompilationContext>
    which means to preserve all things in runtime considered when debugging. But this didn't work, view locator still return the 'not found' textblock.

  2. Then I turned to some apps recorded in Awesome Avalonia for references. I found they are using the registration mechanism which register mapping of viewmodels to views. This may be a better solution and I tried, following are the modified view locator Build() method:

    public Control Build(object data) {
        var type = data.GetType();
        Registration.TryGetValue(type, out type);
        if (type != null) {
            return (Control)Activator.CreateInstance(type);
        }
        else {
            return new TextBlock { Text = "Not Found: " + type };
        }
    }

    And below is the registration dictionary, included in ViewLocator.cs:

    private static Dictionary<Type, Type> Registration = new Dictionary<Type, Type> ();

    And App.xaml.cs, the modified Initialize() Method:

    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    
        ViewLocator.Register<HomeViewModel, HomeView>();
        ViewLocator.Register<FocusModeViewModel, FocusModeView>();
        ViewLocator.Register<ManageAudioViewModel, ManageAudioView>();
        ViewLocator.Register<MixAudioViewModel, MixAudioView>();
        ViewLocator.Register<SettingsViewModel, SettingsView>();
        ViewLocator.Register<AddAudioDetailViewModel, AddAudioDetailView>();
        ViewLocator.Register<AddAudioViewModel, AddAudioView>();
        ViewLocator.Register<PlayStatusBarViewModel, PlayStatusBarView>();
        ViewLocator.Register<SetCardViewModel, SetCardView>();
        ViewLocator.Register<SoundCardViewModel, SoundCardView>();
    
    }

    The type templates are all my viewmodels and views.
    Then the result became even worse, as app started to fail to launch. For this I turned on console window for published app, and received the error message below:
    Unhandled exception. System.MissingMethodException: Cannot dynamically create an instance of type 'Calmy.Views.HomeView' Reason: No parameterless constructor defined. Where Calmy.Views is the namespace for my views. Accroding to stack trace, I believe view locator found corresponding view, but due to some reason the app was unable to display it. To get a better understanding, I printed all class names in the assembly with this:

    var q = from t in Assembly.GetExecutingAssembly().GetTypes()
        where t.IsClass
        select t;
    q.ToList().ForEach(t => Console.WriteLine(t.FullName));

    and found something strange. The views classes are not printed. Then I realized it may be caused by trmming code option. Views code-behind and xamls are trimmed, since they are accessed by reflection in my previous view locator(the one doesn't use registration). So I turned to find a way to preserve my views not to be trimmed. Finally I found this blog: https://devblogs.microsoft.com/dotnet/customizing-trimming-in-net-core-5/ and followed some steps described. But this time, the condition get even worse, as the xml indicating trimming for preserving didn't work, the view classes names still not printed.

    In addition, there was a warning from ViewLocator when publishing:
    Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.

After doing these research, I believe there are two major issues:

  1. The ViewLocator provided by To-do List app sample uses reflection, but views will be trimmed when enabled trimming.
  2. When using registration(as view types are registered, they would not be trimmed), view instances couldn't be created.

whole stacktrace for my issue 2:

Unhandled exception. System.MissingMethodException: Cannot dynamically create an instance of type 'Calmy.Views.HomeView'. Reason: No parameterless constructor defined.
   at System.RuntimeType.ActivatorCache..ctor(RuntimeType)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean, Boolean)
   at System.Activator.CreateInstance(Type, Boolean, Boolean)
   at System.Activator.CreateInstance(Type , Boolean)
   at System.Activator.CreateInstance(Type )
   at Calmy.Utils.ViewLocator.Build(Object) in F:\Projects\Calmy\src\Calmy\Utils\ViewLocator.cs:line 38
   at Avalonia.Controls.Presenters.ContentPresenter.CreateChild(Object, Control, IDataTemplate)
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild(Object)
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild()
   at Avalonia.Controls.Presenters.ContentPresenter.ApplyTemplate()
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Controls.DockPanel.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.LayoutHelper.MeasureChild(Layoutable, Size, Thickness, Thickness)
   at Avalonia.Controls.Presenters.ContentPresenter.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Controls.Grid.MeasureCell(Int32, Boolean)
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32, Size, Boolean, Boolean, Boolean& )
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32, Size, Boolean, Boolean)
   at Avalonia.Controls.Grid.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Controls.DockPanel.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.LayoutHelper.MeasureChild(Layoutable, Size, Thickness, Thickness)
   at Avalonia.Controls.Presenters.ContentPresenter.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.LayoutHelper.MeasureChild(Layoutable, Size, Thickness, Thickness)
   at Avalonia.Controls.Presenters.ContentPresenter.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.LayoutHelper.MeasureChild(Layoutable, Size, Thickness)
   at Avalonia.Controls.Decorator.MeasureOverride(Size)
   at Avalonia.Controls.Primitives.VisualLayerManager.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Layout.Layoutable.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size)
   at Avalonia.Controls.Window.MeasureOverride(Size)
   at Avalonia.Controls.WindowBase.MeasureCore(Size)
   at Avalonia.Layout.Layoutable.Measure(Size)
   at Avalonia.Layout.LayoutManager.Measure(Layoutable)
   at Avalonia.Layout.LayoutManager.ExecuteInitialLayoutPass()
   at Avalonia.Controls.Window.ShowCore(Window)
   at Avalonia.Controls.Window.Show()
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.ShowMainWindow()
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[])
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder, String[], ShutdownMode )
   at Calmy.Desktop.Program.Main(String[]) in F:\Projects\Calmy\src\Calmy.Desktop\Program.cs:line 14

is this due to the dependance injection progress happened behind scenes are also trimmed?

Expected behavior

Views display properly with code trimmed.

Environment

  • OS: Windows 11
  • Avalonia-Version: 11.0.4
  • .NET 7.0
  • Visual Studio 2022

As trimming will impressingly reduce my app size from > 300MB to about 130MB, I don't want to abandon it. Please help me.

@creamIcec creamIcec added the bug label Feb 6, 2024
@timunie timunie removed the bug label Feb 6, 2024
@maxkatz6
Copy link
Member

maxkatz6 commented Feb 6, 2024

You should avoid reflection based ViewLocator at any cost, if you want to use trimming. Which most apps need to do.
We still have it in our templates, as some developers requested to keep it for now.

I found they are using the registration mechanism which register mapping of viewmodels to views.

Yes, registration mechanism is a safe way to do it. But in their/your example there is a problem with Activator.CreateInstance reflection call which is still unsafe.

Consider something like this using factory methods in ViewLocator class:

private static Dictionary<Type, Func<Control>> Registration = new Dictionary<Type, Func<Control>> ();

public static void Register<TViewModel, TView>() where TView : new()
{
    Registration.Add(typeof(TViewModel), () => new TView());
}
public static void Register<TViewModel, TView>(Func<TView> factory)
{
    Registration.Add(typeof(TViewModel), factory);
}

public Control Build(object data) {
    var type = data.GetType();
    
    if (Registration.TryGetValue(type, out var factory)) {
        return factory();
    }
    else {
        return new TextBlock { Text = "Not Found: " + type };
    }
}

This way there is no reflection, and all types should be statically preserved. I think there should be no other changes in your code, as Initialize method looks good already.

@creamIcec
Copy link
Author

creamIcec commented Feb 6, 2024

Thank you! This really helps. I'm using ReactiveCommand to respond to button click and then switch page for user. Before this I'm using reflection to get ViewModel by its full name. Since reflection is not suitable, which means I can't use name string to find viewmodel, what's a better CommandParamter for the click command? I tried to pass the class name of viewmodel, but it just treat it as a string.

*edit: Write individual commands for every button seems an easy way, but it's redundant and not flexible if there are many views.

@creamIcec
Copy link
Author

Problem solved by following guide on https://docs.avaloniaui.net/docs/guides/data-binding/how-to-bind-to-a-command-with-reactiveui, thanks again!
By the way is there any way to solve conflict between trimming and refelection? Is this the .NET behaviour or by Avalonia design?
Also I'll appreciate if adding some tips to the view locator tutorial of this to help more newcomers like me.

@thevortexcloud
Copy link
Contributor

thevortexcloud commented Feb 6, 2024

The problem with reflection is the trimmer has no idea what code you need at compile time. So it will remove any code it's not sure about. If you don't need trimming just turn it off. It's generally only used for AOT/web assembly.

That said, there are ways to tell the trimmer about some things it needs to keep.

https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/incompatibilities

@AvaloniaUI AvaloniaUI locked and limited conversation to collaborators Feb 6, 2024
@timunie timunie converted this issue into discussion #14511 Feb 6, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants