AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.95k stars 2.25k forks source link

Can't unload dynamically loaded Avalonia assembly #17129

Open MaxwellDAssistek opened 1 month ago

MaxwellDAssistek commented 1 month ago

Is your feature request related to a problem? Please describe.

We need to be able to dynamically load, unload and re-load modules containing UserControls throughout the lifecycle of our application. Although issue #13935 was recently closed and we did use the Unloading event handler suggested in the sample, it only fixes things for the simplest of modules. As soon as anything even slightly more complex is added, the assembly no longer totally unloads.

The biggest issue at first glance is Observables. We need a way to disconnect all observable listeners belonging to an assembly. For example, lets take a rather trivial library like AvaloniaProgressRing, which is basically all implemented using animation styles. The issue is that the animation system itself connects to Observables that get stuck even though the whole UserControl, which contains the progress ring in this case, is removed from the window before unloading the AssemblyLoadContext. Here is what I can see in dotMemory some time after the unload happens:

image

Describe the solution you'd like

It seems that there is still some more work to be done to make sure we can unload the assembly.

Describe alternatives you've considered

If I try to ignore the memory consequences of not having the module totally unloaded, I encounter another issue. I actually can't even reload any of the module's assemblies later because all avares references are still pointing to the old half-unloaded assembly due to how the AssemblyDescriptorResolver functions:

https://github.com/AvaloniaUI/Avalonia/blob/b8d8fda4eab0313fe26c5c3b574673d2b5c12521/src/Avalonia.Base/Platform/Internal/AssemblyDescriptorResolver.cs#L28-L29

The problem is that it just gets a list of all assemblies in the entire AppDomain and finds the first assembly with the requested name. The problem is that the half-unloaded assembly is the first one that it finds.

Since AssemblyDescriptorResolver is an internal Avalonia class that I can't extend, the only way I was able to work around the problem is using Harmony to hot-patch the GetAssembly method with a version that resolves assemblies through the correct AssemblyLoadContext.

This is obviously a very bad and fragile workaround, so it would be nice to get the module to fully unload in the first place.

Additional context

No response

timunie commented 1 month ago

Maybe you can file a draft PR to improve it on Avalonia side as you seem to already have an idea where to look and also you have samples to test. If the draft is accepted, you may add a unit test and make it a full PR.

MaxwellDAssistek commented 1 month ago

At this point, I'm not even really sure how to begin. Is the approach to create some kind of ObserverRegistry to keep track of all known observers? Maybe there is a better solution? I'm no expert in Avalonia internals. I feel like this is a bit too serious of a design decision to just throw into a draft PR.

ProjetNice commented 4 weeks ago

Yes, I also really hope to completely uninstall the UserControl. I've found that after each page display, it cannot truly be deleted from memory.