diff --git a/.github/workflows/ci-hotreload.yml b/.github/workflows/ci-hotreload.yml new file mode 100644 index 000000000..f2cfa452d --- /dev/null +++ b/.github/workflows/ci-hotreload.yml @@ -0,0 +1,34 @@ +name: CI - HotReload + +on: + push: + branches: [main] + paths: + - 'src/HotReload/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main] + paths: + - 'src/HotReload/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + +jobs: + build: + uses: ./.github/workflows/_build.yml + with: + project-path: src/HotReload/HotReload.slnf + project-name: hotreload + run-tests: false + pack: true + install-workloads: false diff --git a/AGENTS.md b/AGENTS.md index 58ef251cd..642f4618d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ This repository hosts experimental .NET MAUI packages. It is a **multi-product m | **Cli** | `Microsoft.Maui.Cli` (global tool: `maui`) | Unified MAUI command-line tool: environment diagnostics (`maui doctor`), Android SDK/JDK/emulator management, Apple platform management, device listing, `maui go` for rapid prototyping, `maui profile startup` for performance tracing, and the `maui devflow` automation surface. | | **DevFlow** | `Microsoft.Maui.DevFlow.*` packages plus the unified `maui devflow` CLI surface | Runtime MAUI automation toolkit. In-app agent with HTTP API, visual tree inspection, CDP bridge for Blazor WebViews, MCP server for AI agents, cross-platform driver library. | | **Comet** | `Comet`, `Comet.SourceGenerator`, `Comet.Layout.Yoga` | Experimental MVU UI framework for .NET MAUI — C# fluent UI, signals/reactive state, Yoga layout. | +| **HotReload** | `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload — `IHotReloadable` interface with per-instance `OnHotReload()` callbacks, backed by a Roslyn source generator. | | **Go** | `Microsoft.Maui.Go.Server` + Comet Go companion app | Single-file Comet apps server and companion app for rapid prototyping (alpha; sister to Comet). | | **Essentials.AI** | `Microsoft.Maui.Essentials.AI` | On-device AI for .NET MAUI — semantic search, chat completion, embeddings, and tool use against local models. | | **AIExtensions** | `Microsoft.Maui.AI.Attributes` | Source-generated AI tool bindings — turns decorated C# methods into `Microsoft.Extensions.AI`-callable tools using Roslyn, with DI parameter binding and AOT support. | @@ -128,6 +129,11 @@ maui-labs/ │ ├── Linux.Gtk4/ # Linux GTK4 platform backend │ ├── MacOS/ # macOS AppKit platform backend │ └── Windows.WPF/ # WPF platform backend +├── src/ +│ └── HotReload/ # HotReload product +│ ├── Microsoft.Maui.HotReload/ # Shipping package (IHotReloadable, registry, handler) +│ ├── Microsoft.Maui.HotReload.SourceGen/ # Roslyn source generator (bundled as analyzer) +│ └── HotReload.slnf # Solution filter ├── samples/ # Sample MAUI apps (not shipped) ├── playground/ # Manual test/scratch apps ├── eng/ # Shared build infrastructure diff --git a/MauiLabs.slnx b/MauiLabs.slnx index a4ceebe82..007094080 100644 --- a/MauiLabs.slnx +++ b/MauiLabs.slnx @@ -17,6 +17,10 @@ + + + + diff --git a/README.md b/README.md index b9a7f8fda..33ae3df20 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,14 @@ Built artifacts are exposed as `@(MauiAppArtifact)` items with `ArtifactType`, ` |---------|-------------| | `Microsoft.Maui.Build.AppProjectReference` | Build-time app project reference with artifact discovery | +### HotReload + +A `MetadataUpdateHandler` for .NET MAUI that delivers per-instance hot reload callbacks via the `IHotReloadable` interface, backed by a Roslyn source generator. + +| Package | Description | +|---------|-------------| +| `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload with `IHotReloadable` callbacks | + ## Agent Skills This repository is also a marketplace for distributable agent skills for .NET MAUI development. Skills are organized as plugins compatible with Copilot CLI, Claude Code, and VS Code. diff --git a/eng/pipelines/devflow-official.yml b/eng/pipelines/devflow-official.yml index f49a5e9f8..5ee56df9a 100644 --- a/eng/pipelines/devflow-official.yml +++ b/eng/pipelines/devflow-official.yml @@ -44,6 +44,10 @@ parameters: displayName: 'Publish EssentialsAI packages to NuGet.org' type: boolean default: false +- name: publishHotReloadNuget + displayName: 'Publish HotReload packages to NuGet.org' + type: boolean + default: false variables: - template: /eng/pipelines/common-variables.yml@self @@ -387,7 +391,33 @@ extends: -projects $(Build.SourcesDirectory)\platforms\MacOS\MacOS-libs.slnf $(_OfficialBuildArgs) displayName: Build, Test and Pack macOS AppKit - + + # HotReload targets netstandard2.0 + net10.0 with no MAUI workloads. + # Builds and packs on Windows for MicroBuild/ESRP signing. + - job: HotReload + displayName: HotReload - Windows + pool: + name: NetCore1ESPool-Internal + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 + strategy: + matrix: + Release: + _BuildConfig: Release + _OfficialBuildArgs: /p:DotNetSignType=$(_SignType) + /p:TeamName=$(_TeamName) + /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + steps: + - task: UseDotNet@2 + displayName: Install .NET SDK + inputs: + version: 10.0.105 + - script: eng\common\cibuild.cmd + -configuration $(_BuildConfig) + -prepareMachine + -projects $(Build.SourcesDirectory)\src\HotReload\HotReload.slnf + $(_OfficialBuildArgs) + displayName: Build and Pack HotReload + - template: /eng/common/templates-official/post-build/post-build.yml@self parameters: enableSourceLinkValidation: false @@ -834,3 +864,58 @@ extends: packageParentPath: '$(Pipeline.Workspace)/MacOSPackages' nuGetFeedType: external publishFeedCredentials: 'nuget.org (dotnetframework)' + + # Publish HotReload packages to NuGet.org + - ${{ if eq(parameters.publishHotReloadNuget, true) }}: + - stage: publish_hotreload_nuget + displayName: 'Publish HotReload to NuGet.org' + dependsOn: + - Validate + - publish_using_darc + jobs: + - job: PrepareArtifacts + displayName: 'Prepare HotReload Artifacts' + timeoutInMinutes: 15 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + outputs: + - output: pipelineArtifact + displayName: Publish HotReload Packages + targetPath: '$(Pipeline.Workspace)/HotReloadPackages' + artifactName: HotReloadPackagesForNuGet + steps: + - download: current + artifact: PackageArtifacts + displayName: Download PackageArtifacts + - powershell: | + New-Item -ItemType Directory -Force -Path '$(Pipeline.Workspace)/HotReloadPackages' + Copy-Item '$(Pipeline.Workspace)/PackageArtifacts/Microsoft.Maui.HotReload.*.nupkg' '$(Pipeline.Workspace)/HotReloadPackages/' -Verbose + displayName: Filter HotReload packages + + - job: PublishNuGet + displayName: 'Push HotReload to NuGet.org' + dependsOn: PrepareArtifacts + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: HotReloadPackagesForNuGet + targetPath: '$(Pipeline.Workspace)/HotReloadPackages' + steps: + - task: 1ES.PublishNuget@1 + displayName: 'Push HotReload to NuGet.org' + inputs: + useDotNetTask: false + packagesToPush: '$(Pipeline.Workspace)/HotReloadPackages/*.nupkg' + packageParentPath: '$(Pipeline.Workspace)/HotReloadPackages' + nuGetFeedType: external + publishFeedCredentials: 'nuget.org (dotnetframework)' diff --git a/src/HotReload/HotReload.slnf b/src/HotReload/HotReload.slnf new file mode 100644 index 000000000..121a4ecc7 --- /dev/null +++ b/src/HotReload/HotReload.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "../../MauiLabs.slnx", + "projects": [ + "src\\HotReload\\Microsoft.Maui.HotReload\\Microsoft.Maui.HotReload.csproj", + "src\\HotReload\\Microsoft.Maui.HotReload.SourceGen\\Microsoft.Maui.HotReload.SourceGen.csproj" + ] + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..7519a8a8d --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..5e79268c7 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MUH0001 | HotReload | Info | HotReloadInitialize not called in constructor diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs new file mode 100644 index 000000000..59f1dd1b0 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs @@ -0,0 +1,205 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Maui.Labs.HotReload.SourceGen +{ + /// + /// Generates a HotReloadInitialize() method for every partial class that + /// implements Microsoft.Maui.Labs.HotReload.IHotReloadAware. Also emits an informational + /// diagnostic if no constructor appears to call it. + /// + [Generator] + public sealed class HotReloadAwareGenerator : IIncrementalGenerator + { + const string IHotReloadAwareFullName = "Microsoft.Maui.Labs.HotReload.IHotReloadAware"; + + static readonly DiagnosticDescriptor MissingInitCallDescriptor = new( + id: "MUH0001", + title: "HotReloadInitialize not called", + messageFormat: "'{0}' implements IHotReloadAware but no constructor calls HotReloadInitialize(). Add a call to HotReloadInitialize() in your constructor to enable hot reload notifications.", + category: "HotReload", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Types implementing IHotReloadAware must call the generated HotReloadInitialize() in their constructor for hot reload registration to take effect."); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var candidates = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsPartialClassDeclaration, + transform: GetHotReloadableClassInfo) + .Where(static info => info is not null) + .Select(static (info, _) => info!); + + // Deduplicate: a class split across multiple partial declarations will produce multiple + // candidates with the same HintName. Only the first one should emit source. + context.RegisterSourceOutput(candidates.Collect(), EmitAllSources); + } + + static bool IsPartialClassDeclaration(SyntaxNode node, System.Threading.CancellationToken _) => + node is ClassDeclarationSyntax c && + c.Modifiers.Any(SyntaxKind.PartialKeyword) && + c.BaseList is not null; + + static HotReloadableClassInfo? GetHotReloadableClassInfo(GeneratorSyntaxContext ctx, System.Threading.CancellationToken ct) + { + var classDecl = (ClassDeclarationSyntax)ctx.Node; + if (ctx.SemanticModel.GetDeclaredSymbol(classDecl, ct) is not INamedTypeSymbol classSymbol) + return null; + + // Skip generic types and nested types: the simple partial class codegen doesn't + // handle type parameters or containing-type wrappers. Users of those shapes must + // call HotReloadRegistry.Register manually. + if (classSymbol.IsGenericType || classSymbol.ContainingType != null) + return null; + + // Check interfaces: prefer semantic model, but fall back to syntax name matching + // so the generator works even when the referenced assembly isn't fully loaded + // in the compilation (e.g. net10.0 lib consumed by a net11.0-* project). + bool implementsIHotReloadAware = false; + + var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadAwareFullName); + if (iface is not null) + { + // Semantic path: exact type identity check + foreach (var i in classSymbol.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(i, iface)) + { + implementsIHotReloadAware = true; + break; + } + } + } + else + { + // Syntax fallback: match by short or fully-qualified name in the base list + foreach (var baseType in classDecl.BaseList?.Types ?? default) + { + var typeName = baseType.Type switch + { + IdentifierNameSyntax id => id.Identifier.Text, + QualifiedNameSyntax q => q.Right.Identifier.Text, + _ => null + }; + if (typeName == "IHotReloadAware") + { + implementsIHotReloadAware = true; + break; + } + } + } + + if (!implementsIHotReloadAware) + return null; + + // Check whether any constructor in this partial declaration calls HotReloadInitialize. + // Walk identifier tokens (skipping trivia/comments) so we don't get false positives + // from documentation or comments mentioning "HotReloadInitialize". + bool hasInitCall = false; + foreach (var member in classDecl.Members) + { + if (member is not ConstructorDeclarationSyntax ctor) + continue; + + SyntaxNode? body = (SyntaxNode?)ctor.Body ?? ctor.ExpressionBody; + if (body is null) + continue; + + foreach (var id in body.DescendantNodes().OfType()) + { + if (id.Identifier.ValueText == "HotReloadInitialize") + { + hasInitCall = true; + break; + } + } + + if (hasInitCall) + break; + } + + return new HotReloadableClassInfo( + ns: classSymbol.ContainingNamespace.IsGlobalNamespace + ? null + : classSymbol.ContainingNamespace.ToDisplayString(), + className: classSymbol.Name, + hintName: classSymbol.ToDisplayString().Replace('.', '_').Replace('<', '_').Replace('>', '_'), + location: classDecl.GetLocation(), + hasInitCall: hasInitCall); + } + + static void EmitAllSources(SourceProductionContext ctx, System.Collections.Immutable.ImmutableArray allCandidates) + { + var seen = new System.Collections.Generic.HashSet(StringComparer.Ordinal); + foreach (var info in allCandidates) + { + // Only emit once per unique class (multiple partial declarations yield the same HintName). + if (!seen.Add(info.HintName)) + continue; + + EmitSource(ctx, info); + } + } + + static void EmitSource(SourceProductionContext ctx, HotReloadableClassInfo info) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + + if (info.Namespace is not null) + { + sb.AppendLine($"namespace {info.Namespace}"); + sb.AppendLine("{"); + } + + string indent = info.Namespace is not null ? " " : string.Empty; + sb.AppendLine($"{indent}partial class {info.ClassName}"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} /// Auto-generated. Call from your constructor to register this instance for hot reload notifications."); + sb.AppendLine($"{indent} private void HotReloadInitialize()"); + sb.AppendLine($"{indent} => global::Microsoft.Maui.Labs.HotReload.HotReloadRegistry.Register(this);"); + sb.AppendLine($"{indent}}}"); + + if (info.Namespace is not null) + sb.AppendLine("}"); + + ctx.AddSource($"{info.HintName}.HotReload.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + + if (!info.HasInitCall) + { + ctx.ReportDiagnostic(Diagnostic.Create( + MissingInitCallDescriptor, + info.Location, + info.ClassName)); + } + } + + sealed class HotReloadableClassInfo + { + public string? Namespace { get; } + public string ClassName { get; } + public string HintName { get; } + public Location Location { get; } + public bool HasInitCall { get; } + + public HotReloadableClassInfo(string? ns, string className, string hintName, Location location, bool hasInitCall) + { + Namespace = ns; + ClassName = className; + HintName = hintName; + Location = location; + HasInitCall = hasInitCall; + } + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj new file mode 100644 index 000000000..2da57d100 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + Microsoft.Maui.Labs.HotReload.SourceGen + Microsoft.Maui.Labs.HotReload.SourceGen + true + true + false + enable + latest + + + + + + + + + + + + + diff --git a/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs new file mode 100644 index 000000000..2d59ff52f --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs @@ -0,0 +1,5 @@ +#if !NETSTANDARD +using System.Reflection.Metadata; + +[assembly: MetadataUpdateHandler(typeof(Microsoft.Maui.Labs.HotReload.MauiMetadataUpdateHandler))] +#endif diff --git a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs new file mode 100644 index 000000000..b41f7101c --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs @@ -0,0 +1,129 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Microsoft.Maui.Labs.HotReload +{ + /// + /// Registry that tracks live instances so they can be notified + /// when their type is updated via .NET Hot Reload. + /// + /// + /// Instances are held via so registration does not prevent garbage collection. + /// + public static class HotReloadRegistry + { + static readonly object _lock = new(); + static readonly Dictionary>> _registry = new(); + + /// + /// Registers so it receives + /// callbacks when its type (or any base type) is updated at runtime. + /// + public static void Register(IHotReloadAware instance) + { + if (instance is null) + return; + + var type = instance.GetType(); + lock (_lock) + { + if (!_registry.TryGetValue(type, out var list)) + _registry[type] = list = new List>(); + + // Compact dead references before adding to keep the list small. + list.RemoveAll(static w => !w.TryGetTarget(out _)); + list.Add(new WeakReference(instance)); + } + } + + /// + /// Removes from the registry. + /// Safe to call even if the instance was never registered. + /// + public static void Unregister(IHotReloadAware instance) + { + if (instance is null) + return; + + var type = instance.GetType(); + lock (_lock) + { + if (!_registry.TryGetValue(type, out var list)) + return; + + for (int i = list.Count - 1; i >= 0; i--) + { + if (!list[i].TryGetTarget(out var target)) + { + // Remove dead references opportunistically. + list.RemoveAt(i); + } + else if (ReferenceEquals(target, instance)) + { + list.RemoveAt(i); + break; + } + } + } + } + + /// + /// Notifies all registered instances whose runtime type is, or derives from, any of the + /// . Called by . + /// + internal static void NotifyInstances(Type[] updatedTypes) + { + List? toNotify = null; + + lock (_lock) + { + foreach (var updatedType in updatedTypes) + { + foreach (var kvp in _registry) + { + // Notify instances whose concrete type is updatedType or a subtype of it. + if (!updatedType.IsAssignableFrom(kvp.Key)) + continue; + + foreach (var weakRef in kvp.Value) + { + if (!weakRef.TryGetTarget(out var instance)) + continue; + + // Deduplicate by reference to avoid double-notifying when both a base + // type and derived type appear in updatedTypes simultaneously. + toNotify ??= new List(); + bool alreadyQueued = false; + for (int i = 0; i < toNotify.Count; i++) + { + if (ReferenceEquals(toNotify[i], instance)) + { + alreadyQueued = true; + break; + } + } + if (!alreadyQueued) + toNotify.Add(instance); + } + } + } + } + + if (toNotify is null) + return; + + foreach (var instance in toNotify) + { + try + { + instance.OnHotReload(updatedTypes); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MauiHotReload] Error in OnHotReload for {instance.GetType()}: {ex}"); + } + } + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs new file mode 100644 index 000000000..7d2cdf606 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs @@ -0,0 +1,25 @@ +// +#nullable enable +using System; + +namespace Microsoft.Maui.Labs.HotReload +{ + /// + /// Implemented by types that want to be notified when their code is updated via .NET Hot Reload. + /// + /// + /// To register for notifications, call HotReloadInitialize() in your constructor + /// (generated automatically by Microsoft.Maui.Labs.HotReload.SourceGen for partial classes). + /// + public interface IHotReloadAware + { + /// + /// Called when this instance's type (or a base type) has been updated via hot reload. + /// + /// + /// The set of types that were updated, as reported by the .NET Hot Reload host. + /// May be if the host did not provide type information. + /// + void OnHotReload(Type[]? updatedTypes); + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs new file mode 100644 index 000000000..aaaca86b0 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs @@ -0,0 +1,46 @@ +#nullable enable +using System; +#if !NETSTANDARD +using Microsoft.Maui.HotReload; +#endif + +namespace Microsoft.Maui.Labs.HotReload +{ + /// + /// Handles .NET Hot Reload metadata update notifications, forwarding them to registered + /// instances and the MAUI view-level hot reload infrastructure. + /// + public static class MauiMetadataUpdateHandler + { + /// + /// Called by the .NET Hot Reload host after metadata has been applied. + /// Notifies all registered instances whose types were updated, + /// then forwards to MAUI's MauiHotReloadHelper for view-level reload. + /// + /// + /// This method is invoked only during hot reload sessions (debug/development builds). + /// Trimming and hot reload are mutually exclusive, so trim-compatibility warnings are suppressed. + /// +#if !NETSTANDARD + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot Reload is not compatible with trimming.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Hot Reload is not compatible with AOT.")] +#endif + public static void UpdateApplication(Type[] types) + { + HotReloadRegistry.NotifyInstances(types); +#if !NETSTANDARD + MauiHotReloadHelper.UpdateApplication(types); +#endif + } + + /// + /// Called by the .NET Hot Reload host before metadata is applied, to clear any caches. + /// + public static void ClearCache(Type[] types) + { +#if !NETSTANDARD + MauiHotReloadHelper.ClearCache(types); +#endif + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj new file mode 100644 index 000000000..2eacc81a4 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj @@ -0,0 +1,53 @@ + + + + netstandard2.0;net10.0 + Microsoft.Maui.Labs.HotReload + Microsoft.Maui.Labs.HotReload + true + enable + latest + + + + + + + + + true + true + Microsoft.Maui.Labs.HotReload + Provides a MetadataUpdateHandler for .NET MAUI hot reload. Register any IHotReloadable instance to receive OnHotReload() callbacks when their type is hot-reloaded. + $(DefaultPackageTags);hotreload;developer-tools + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/HotReload/README.md b/src/HotReload/README.md new file mode 100644 index 000000000..7f6199857 --- /dev/null +++ b/src/HotReload/README.md @@ -0,0 +1,107 @@ +# Microsoft.Maui.HotReload + +A `MetadataUpdateHandler` for .NET MAUI that delivers per-instance hot reload callbacks. Implement `IHotReloadable` on any class, call the generated `HotReloadInitialize()` in its constructor, and receive `OnHotReload()` notifications whenever your type is modified during a hot reload session. + +> ⚠️ **Experimental** — APIs may change between releases. This package is not covered by the [.NET MAUI Support Policy](https://dotnet.microsoft.com/platform/support/policy/maui) and is provided as-is. + +## How It Works + +The package has two components: + +| Assembly | Role | +|----------|------| +| `Microsoft.Maui.HotReload` | Ships the `IHotReloadable` interface, `HotReloadRegistry`, and `MauiMetadataUpdateHandler` | +| `Microsoft.Maui.HotReload.SourceGen` | Roslyn incremental source generator bundled as an analyzer inside the NuGet package | + +When the .NET runtime applies a metadata update (hot reload), the `[assembly: MetadataUpdateHandler]` attribute routes the notification to `MauiMetadataUpdateHandler.UpdateApplication(Type[])`. This method: + +1. Walks the `HotReloadRegistry` to find all live `IHotReloadable` instances whose type (or a base type) was updated. +2. Calls `OnHotReload()` on each matched instance. +3. Forwards to MAUI's built-in `MauiHotReloadHelper` for view-level reload (on `net10.0`+). + +Instances are held via `WeakReference` so registration does not prevent garbage collection. + +## Quick Start + +Install the NuGet package: + +```bash +dotnet add package Microsoft.Maui.HotReload --prerelease +``` + +Implement `IHotReloadable` on a `partial` class and call `HotReloadInitialize()` in the constructor: + +```csharp +using Microsoft.Maui.HotReload; + +public partial class MainPage : ContentPage, IHotReloadable +{ + public MainPage() + { + HotReloadInitialize(); // Generated by the source generator + InitializeComponent(); + } + + public void OnHotReload() + { + // Re-apply dynamic state, refresh bindings, etc. + System.Diagnostics.Debug.WriteLine("MainPage was hot-reloaded!"); + } +} +``` + +That's it — during a hot reload session, `OnHotReload()` fires whenever `MainPage` (or any of its base types) is updated. + +## Source Generator + +The `Microsoft.Maui.HotReload.SourceGen` generator runs at compile time and, for every `partial` class implementing `IHotReloadable`: + +1. **Emits a `HotReloadInitialize()` method** that calls `HotReloadRegistry.Register(this)`. +2. **Reports diagnostic `MUH0001`** (Info severity) if no constructor in the class appears to call `HotReloadInitialize()`. + +### Limitations + +- **Generic types and nested types** are not supported by the generator. For those, call `HotReloadRegistry.Register(this)` manually in the constructor. +- The class **must be `partial`** for the generator to emit code into it. + +## Manual Registration + +For classes that cannot be `partial` or are generic/nested, register directly: + +```csharp +public class MyService : IHotReloadable +{ + public MyService() + { + HotReloadRegistry.Register(this); + } + + public void OnHotReload() + { + // Handle hot reload + } +} +``` + +Call `HotReloadRegistry.Unregister(this)` if you need to stop receiving notifications before the instance is garbage-collected. + +## Target Frameworks + +| TFM | Behavior | +|-----|----------| +| `net10.0` | Full integration — `MetadataUpdateHandler` registered, forwards to MAUI's `MauiHotReloadHelper` | +| `netstandard2.0` | `IHotReloadable` + `HotReloadRegistry` available for library authors; `MetadataUpdateHandler` not active | + +## Package + +| Package | Description | +|---------|-------------| +| `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload with `IHotReloadable` callbacks | + +## Building + +```bash +dotnet build src/HotReload/HotReload.slnf +``` + +No MAUI workload install is required — the project targets `netstandard2.0` and `net10.0` with a conditional `Microsoft.Maui.Core` reference.