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

Multi-Project solution support #7

Open
StefanKoell opened this issue May 6, 2024 · 16 comments
Open

Multi-Project solution support #7

StefanKoell opened this issue May 6, 2024 · 16 comments

Comments

@StefanKoell
Copy link

Hi!

I was wondering if there is a plan to support more complex solutions with multiple projects containing controls?

cheers,
Stefan

@Kir-Antipov
Copy link
Owner

Kir-Antipov commented May 7, 2024

Hello! As stated in the README:

Currently, HotAvalonia does not watch for controls located in referenced projects. In other words, hot reload only works for controls defined within the entry assembly. While this limitation exists, it is technically feasible to implement support for this feature in the future.

So, yes, there are definitely plans to add proper support for referenced projects. The only thing that's missing here is establishing a relevance between a given referenced control assembly and its source project location. For instance, if you were to manually do something like this:

var context = AvaloniaHotReloadContext.FromAsembly(typeof(MyControl).Assembly, "/path/to/project/root/");
context.EnableHotReload(); // Note: the context needs to be saved somewhere, so the GC doesn't throw it away.

It should already work. I just haven't found an elegant way to:

  1. Determine all referenced assemblies that contain Avalonia controls.
  2. Find the project root for each of them.

Also, if you (or somebody else) would make a PR that moves some controls from the Demo app to another project for testing/demonstration purposes - that would be nice! :)

@StefanKoell
Copy link
Author

Thanks for the quick response. I tried to make it work in my app but it doesn't do anything :( Need to dig further in when I have time. I would be happy with the ability to specify projects manually like you described. I'm wondering why hot reload isn't working for me at all. I guess there's no logging/tracing which can be enabled, right?

@ClxS
Copy link

ClxS commented May 8, 2024

It worked well for me, and fortunately it appears this can happen after AvaloniaXamlLoader.Load(this), as my plugin system wouldn't have kicked in by then.

Here is what I ended up doing

private void SetupHotReload()
{
    foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        string? path = TryLocateAssembly(assembly);
        if (path is null)
        {
            continue;
        }
        
        AvaloniaHotReloadContext context = AvaloniaHotReloadContext.FromAssembly(assembly, path);
        context.EnableHotReload();

        contexts.Add(context);
    }
}

private string? TryLocateAssembly(Assembly assembly, [CallerFilePath] string? appFilePath = null)
{
    if (appFilePath is null)
    {
        return null;
    }

    string? assemblyName = assembly.GetName().Name;
    if (assemblyName is null || assemblyName.StartsWith("System."))
    {
        return null;
    }
    
    Uri root = new(Path.Combine(appFilePath, "../../.."));
    foreach (string project in Directory.EnumerateFiles(root.LocalPath, "*.csproj", SearchOption.AllDirectories))
    {
        if (Path.GetFileNameWithoutExtension(project) == assemblyName)
        {
            return Path.GetDirectoryName(project);
        }
    }

    return null;
}

(NOTE: This cannot be combined with .EnableHotReload() on the app, or else you'll get an ExecutionEngineException)

Usage is to call SetupHotReload at the end of my OnFrameworkInitializationCompleted.

You'll have to alter how TryLocateAssembly works as that was just a quick hack to make it work my project layout.

@Kir-Antipov
Copy link
Owner

I'm wondering why hot reload isn't working for me at all

@StefanKoell, do you have an MRE I can check? If hot reload doesn't work for you at all, it may be worth opening a separate issue!

I guess there's no logging/tracing which can be enabled, right?

Welp, not really. I've added logging here and there, but only where something could actively go wrong. So, if you don't see any errors in the debug output, it means there were none.

Are you trying to debug a regular desktop app? What kind of code editor/IDE do you use? Have you tried if hot reload works when saving a file via a basic notepad of some kind?


Here is what I ended up doing

@ClxS, yeah, looks about right for a personal setup. Don't forget to wrap this with #if DEBUG so your code can compile successfully whenever you decide to release the app, though ;)

fortunately it appears this can happen after AvaloniaXamlLoader.Load(this)

This is not a hard requirement. The only thing is HotAvalonia needs to be initialized before a control is created so that it can be hot reloaded as is. If control(s) are initialized before that, HotAvalonia won't know that they exist until they are rebuilt (e.g., by closing and then opening a containing view again).

This cannot be combined with .EnableHotReload() on the app, or else you'll get an ExecutionEngineException

You cannot create the AvaloniaHotReloadContext for the same assembly twice because of some black magic currently being used behind the scene. Thus, since AppDomain.CurrentDomain.GetAssemblies() also includes the currently running assembly, which was already processed by .EnableHotReload(), it breaks. Maybe I need to do something about this :D

You'll have to alter how TryLocateAssembly works as that was just a quick hack to make it work my project layout.

That's why this feature isn't built into HotAvalonia yet. It's easy to wire it for a project at hand, but getting all the required information dynamically for a project with an unknown layout is challenging. I have an idea that involves debug symbols, which of course contain all the information we need. However, a part of me wants to find a solution that may work even in release mode, where debug symbols are usually not present, if somebody, for some reason, decides to incorporate HotAvalonia into their app as a feature and not as a development tool.

@StefanKoell
Copy link
Author

@ClxS your solution works well for me. Thanks for sharing! @Kir-Antipov thanks again for all the help and the great work!

@Kir-Antipov
Copy link
Owner

solution works well for me

That's great to hear! :)

However, I'm gonna leave this issue open, as it's something HotAvalonia needs to learn how to do out of the box.

@Kir-Antipov Kir-Antipov reopened this May 9, 2024
@Epacik
Copy link

Epacik commented May 13, 2024

So, yes, there are definitely plans to add proper support for referenced projects. The only thing that's missing here is establishing a relevance between a given referenced control assembly and its source project location. For instance, if you were to manually do something like this:

var context = AvaloniaHotReloadContext.FromAsembly(typeof(MyControl).Assembly, "/path/to/project/root/");
context.EnableHotReload(); // Note: the context needs to be saved somewhere, so the GC doesn't throw it away.

It should already work. I just haven't found an elegant way to:

  1. Determine all referenced assemblies that contain Avalonia controls.
  2. Find the project root for each of them.

It probably won't be the solution, but maybe it's possible to use a source generator to generate those contexts for referenced projects? At least as a temporary solution.

@Kir-Antipov
Copy link
Owner

While it's definitely a solution, it has its downsides. Namely:

  1. For this approach to work, all projects containing Avalonia controls need to be explicitly referenced and accessible during compile time. However, this is not always the case. For example, ClxS mentioned that they use a plugin system that only kicks in during runtime, rendering source generators ineffective for them.

  2. I'm not really sold on using source generators in such cases due to how high level they are. You either need to maintain 3 copies of the same source generators to support The Big Three (i.e., C#, F#, and VB.NET), or go a different route like most .NET developers do and pretend that C# is the only .NET language, which couldn't be further from the truth. And, to be quite frankly honest with you, I don't want to do either of those.

Thus, I think I'm going to stick with the debug symbols-related solution.

@Epacik
Copy link

Epacik commented May 15, 2024

That makes sense.
May I ask how long would it take, to fix that problem properly, using debug symbols?

If it's going to take a longer while, I could volunteer some of my time to create some simple generators for The Big Three, and to keep them working.

Once you'd finish the debug symbols based solution, source generator could get deprecated and subsequently removed.

That would only make sense, if the solution you want to go with would take considerably longer to be made, hence my question.

@Kir-Antipov
Copy link
Owner

how long would it take

In my better years, I could have done this in a day. However, without going into too much detail, I'm not in my most productive form at the moment, so it's more like a "will do it in an hour within a week" kind of deal.

Since this is going to be a breaking change anyways (I need to abstract AvaloniaHotReloadContext so I can combine different contexts and support dynamic assembly loading), I'm going to tackle a bit more than just multi-project support. For example, I also want to implement support for asset hot reloading, which in turn requires a better different injection technique. My homebrewed one only supports injection into methods contained within assemblies built in the Debug mode; and for asset hot reloading, I would need to inject code into Avalonia itself, which has obviously been built with optimizations enabled, thus rendering my injection method irrelevant.

So, what I'm saying here is it will take some time, but not a lot, really.

I could volunteer some of my time to create some simple generators for The Big Three

The sentiment is very much appreciated! However, it will definitely be faster for me to implement the solution I already settled on :)

@Epacik
Copy link

Epacik commented May 18, 2024

All right, in that case I will patiently wait!
And who knows, maybe I'll be able to help with something else down the line 😉

@Kir-Antipov
Copy link
Owner

@ClxS, does your plugin system load assemblies via Assembly.Load(string path) or Assembly.Load(byte[] rawAssembly)?

@Kir-Antipov
Copy link
Owner

On a slightly more generic note, I'm currently trying to think of a good way for users to provide location hints for their assemblies when it's really truly needed.

Within the current model, where hot reload is only enabled for a single project, it's easy - .EnableHotReload() accepts a path, which is usually provided by the compiler itself (thanks to the CallerFilePathAttribute), but can be easily overridden by a user themselves:

this.EnableHotReload("/path/to/control.cs");

However, a single string is not going to cut it in a multi-project setup. Any bright ideas, folks?


P.S. - by the way, everything else is already implemented, so I hope you will be able to at least test the new multi-project-oriented system in a few days when I'm done wiring up this last thing :)

@ClxS
Copy link

ClxS commented May 25, 2024

@Kir-Antipov at the moment it's the string path form. I might move it towards the byte[] eventually but that's a long way off

@Kir-Antipov
Copy link
Owner

@ClxS, that's great, actually, because assemblies loaded via .Load(byte[]) do not have their Location property set, as they were loaded from the memory and not from the disk.

Since you use .Load(string), the new .EnableHotReload() should work for you out of the box when I'm done!

Kir-Antipov added a commit that referenced this issue Jun 1, 2024
Now `AvaloniaHotReloadExtensions` uses new AppDomain-wide `IHotReloadContext` out of the box.

Closes #7
@Kir-Antipov
Copy link
Owner

I think I'm done here. .EnableHotReload() should work out of the box now, even if you dynamically load assemblies into the current application domain.

If HotAvalonia cannot determine the location of some assembly's source project (e.g., there are no debug symbols available, or projects are located in a different place compared to where they were when the assemblies were compiled, etc.), there's a new overload that lets you pass a callback function capable of resolving project paths:

this.EnableHotReload(
  asm => TryLocateAssemblyProject(asm, out string? directoryName) ? directoryName : null
);

However, I genuinely think that you won't need that with your current setups.

Anyway, if you folks could test it on your end as well, it would be very much appreciated :)
You can wire your projects directly to the sources of the development branch, or grab pre-built binaries from run#9329750366 and put them into a local NuGet repository (which is just a fancy name for a directory mentioned in the nuget.config).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants