diff --git a/src/Try.Core/CompilationService.cs b/src/Try.Core/CompilationService.cs index 171b884a..1c2deba2 100644 --- a/src/Try.Core/CompilationService.cs +++ b/src/Try.Core/CompilationService.cs @@ -38,12 +38,6 @@ public class CompilationService "@using MudBlazor" ]; - private const string MudBlazorServices = @" - - - -"; - // Creating the initial compilation + reading references is on the order of 250ms without caching // so making sure it doesn't happen for each run. private static CSharpCompilation _baseCompilation; @@ -71,6 +65,7 @@ public static unsafe Task InitAsync() typeof(AssemblyTargetedPatchBandAttribute).Assembly, // System.Private.CoreLib typeof(NavLink).Assembly, // Microsoft.AspNetCore.Components.Web typeof(IQueryable).Assembly, // System.Linq.Expressions + typeof(Queryable).Assembly, // System.Linq.Queryable typeof(HttpClientJsonExtensions).Assembly, // System.Net.Http.Json typeof(HttpClient).Assembly, // System.Net.Http typeof(IJSRuntime).Assembly, // Microsoft.JSInterop @@ -129,6 +124,7 @@ public async Task CompileToAssemblyAsync( var codeHash = ComputeCodeHash(codeFiles); if (_cachedResult != null && _lastCodeHash == codeHash) { + _cachedResult.IsFromCache = true; return _cachedResult; } @@ -258,9 +254,7 @@ private async Task> CompileToCSharpAsync( { if (codeFile.Type == CodeFileType.Razor) { - var fileContent = index == 0 ? MudBlazorServices : string.Empty; - fileContent += codeFile.Content; - var projectItem = CreateRazorProjectItem(codeFile.Path, fileContent); + var projectItem = CreateRazorProjectItem(codeFile.Path, codeFile.Content); var codeDocument = projectEngine.ProcessDeclarationOnly(projectItem); var cSharpDocument = codeDocument.GetCSharpDocument(); diff --git a/src/Try.Core/CompileToAssemblyResult.cs b/src/Try.Core/CompileToAssemblyResult.cs index e1cbe298..a5379d21 100644 --- a/src/Try.Core/CompileToAssemblyResult.cs +++ b/src/Try.Core/CompileToAssemblyResult.cs @@ -10,5 +10,7 @@ public class CompileToAssemblyResult public IEnumerable Diagnostics { get; set; } = []; public byte[] AssemblyBytes { get; set; } + + public bool IsFromCache { get; set; } } } diff --git a/src/Try.Core/CoreConstants.cs b/src/Try.Core/CoreConstants.cs index 9dcb5d24..f3ec261a 100644 --- a/src/Try.Core/CoreConstants.cs +++ b/src/Try.Core/CoreConstants.cs @@ -20,9 +20,6 @@ void ButtonOnClick() } }"; - public const string DefaultUserComponentsAssemblyBytes = - "TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAATAEDAF0sHZkAAAAAAAAAAOAAIiALATAAAAgAAAAGAAAAAAAA9iYAAAAgAAAAQAAAAAAAEAAgAAAAAgAABAAAAAAAAAAEAAAAAAAAAACAAAAAAgAAAAAAAAMAYIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAKImAABPAAAAAEAAANQDAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAwAAACoJQAAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAACAAAAAAAAAAAAAAACCAAAEgAAAAAAAAAAAAAAC50ZXh0AAAA/AYAAAAgAAAACAAAAAIAAAAAAAAAAAAAAAAAACAAAGAucnNyYwAAANQDAAAAQAAAAAQAAAAKAAAAAAAAAAAAAAAAAABAAABALnJlbG9jAAAMAAAAAGAAAAACAAAADgAAAAAAAAAAAAAAAAAAQAAAQgAAAAAAAAAAAAAAAAAAAADWJgAAAAAAAEgAAAACAAUAXCAAAEwFAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYqHgIoDAAACioAAEJTSkIBAAEAAAAAAAwAAAB2NC4wLjMwMzE5AAAAAAUAbAAAAKQBAAAjfgAAEAIAAEwCAAAjU3RyaW5ncwAAAABcBAAABAAAACNVUwBgBAAAEAAAACNHVUlEAAAAcAQAANwAAAAjQmxvYgAAAAAAAAACAAABRxUAAAkAAAAA+gEzABYAAAEAAAAOAAAAAgAAAAIAAAABAAAADAAAAAsAAAABAAAAAgAAAAAAhQEBAAAAAAAGAOsA6gEGAD0B6gEGADcA1wEPAAoCAAAGACQBowEGALQAowEGAHEAowEGAI4AowEGAAsBowEGAEsAowEGANMA6gEKAGIAGQIKACkAGQIKALUBWwEAAAAAAQAAAAAAAQABAAEAEACcATkCNQABAAEAUCAAAAAAxAAKACcAAQBSIAAAAACGGNEBBgACAAAAAQDHAQkA0QEBABEA0QEGABkA0QEKACkA0QEQADEA0QEQADkA0QEQAEEA0QEQAEkA0QEQAFEA0QEQAFkA0QEBAGEA0QEQAGkA0QEGACcAUwDRAC4ACwAtAC4AEwA2AC4AGwBVAC4AIwBeAC4AKwB2AC4AMwCDAC4AOwCQAC4AQwBeAC4ASwBeAEMAWwDEAASAAAABAAAAAAAAAAAAAAAAADkCAAAJAAAAAAAAAAAAAAAVABoAAAAAAAkAAAAAAAAAAAAAAB4AGQIAAAAAAAAAAAA8TW9kdWxlPgBCdWlsZFJlbmRlclRyZWUAU3lzdGVtLlJ1bnRpbWUAQ29tcG9uZW50QmFzZQBEZWJ1Z2dhYmxlQXR0cmlidXRlAEFzc2VtYmx5VGl0bGVBdHRyaWJ1dGUAUm91dGVBdHRyaWJ1dGUAQXNzZW1ibHlGaWxlVmVyc2lvbkF0dHJpYnV0ZQBBc3NlbWJseUluZm9ybWF0aW9uYWxWZXJzaW9uQXR0cmlidXRlAEFzc2VtYmx5Q29uZmlndXJhdGlvbkF0dHJpYnV0ZQBSZWZTYWZldHlSdWxlc0F0dHJpYnV0ZQBDb21waWxhdGlvblJlbGF4YXRpb25zQXR0cmlidXRlAEFzc2VtYmx5UHJvZHVjdEF0dHJpYnV0ZQBBc3NlbWJseUNvbXBhbnlBdHRyaWJ1dGUAUnVudGltZUNvbXBhdGliaWxpdHlBdHRyaWJ1dGUATWljcm9zb2Z0LkFzcE5ldENvcmUuQ29tcG9uZW50cy5SZW5kZXJpbmcAVHJ5LlVzZXJDb21wb25lbnRzLmRsbABfX01haW4AU3lzdGVtLlJlZmxlY3Rpb24AUmVuZGVyVHJlZUJ1aWxkZXIAX19idWlsZGVyAC5jdG9yAFN5c3RlbS5EaWFnbm9zdGljcwBTeXN0ZW0uUnVudGltZS5Db21waWxlclNlcnZpY2VzAERlYnVnZ2luZ01vZGVzAE1pY3Jvc29mdC5Bc3BOZXRDb3JlLkNvbXBvbmVudHMAVHJ5LlVzZXJDb21wb25lbnRzAAAAAAAdJaLVPNvETL37jSE7YZGLAAQgAQEIAyAAAQUgAQEREQQgAQEOCLA/X38R1Qo6CK25eTgp3a5gBSABARI5CAEACAAAAAAAHgEAAQBU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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; - public const string DefaultRazorFileContentFormat = "

{0}

"; public static readonly string DefaultCSharpFileContentFormat = diff --git a/src/TryMudBlazor.Client/App.razor b/src/TryMudBlazor.Client/App.razor index 36693d37..2fe6392b 100644 --- a/src/TryMudBlazor.Client/App.razor +++ b/src/TryMudBlazor.Client/App.razor @@ -1,11 +1,34 @@ +@using System.Reflection +@using TryMudBlazor.Client.Pages +@using TryMudBlazor.Client.Services +@implements IDisposable + + AdditionalAssemblies="@_additionalAssemblies" + NotFoundPage="typeof(NotFound)"> - - -

Sorry, there's nothing at this address.

-
-
+ +@code { + private readonly Assembly[] _additionalAssemblies; + private readonly IDisposable _subscription; + + public App(UserComponentsAssemblyService assemblyService) + { + _additionalAssemblies = [assemblyService.Assembly]; + _subscription = assemblyService.Subscribe(HandleAssemblyChanged); + } + + private Task HandleAssemblyChanged(Assembly assembly) + { + _additionalAssemblies[0] = assembly; + return InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + _subscription.Dispose(); + } +} diff --git a/src/TryMudBlazor.Client/Extensions/WebAssemblyHostBuilderExtensions.cs b/src/TryMudBlazor.Client/Extensions/WebAssemblyHostBuilderExtensions.cs new file mode 100644 index 00000000..7c30b5f2 --- /dev/null +++ b/src/TryMudBlazor.Client/Extensions/WebAssemblyHostBuilderExtensions.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Try.UserComponents; + +namespace TryMudBlazor.Client.Extensions; + +internal static class WebAssemblyHostBuilderExtensions +{ + private static readonly MethodInfo ConfigureMethod = ResolveConfigureMethod(); + + public static void TryInvokeUserStartup(this WebAssemblyHostBuilder builder) + => ConfigureMethod?.Invoke(null, [builder]); + + private static MethodInfo ResolveConfigureMethod() + { + var assembly = typeof(__Main).Assembly; + + var startupType = + assembly.GetType("UserStartup", throwOnError: false, ignoreCase: true) ?? + assembly.GetType("Try.UserComponents.UserStartup", throwOnError: false, ignoreCase: true); + + var method = startupType?.GetMethod("Configure", BindingFlags.Static | BindingFlags.Public); + if (method is null) + return null; + + var parameters = method.GetParameters(); + if (parameters.Length != 1 || parameters[0].ParameterType != typeof(WebAssemblyHostBuilder)) + return null; + + return method; + } +} diff --git a/src/TryMudBlazor.Client/Models/TryConstants.cs b/src/TryMudBlazor.Client/Models/TryConstants.cs index 44218526..16543555 100644 --- a/src/TryMudBlazor.Client/Models/TryConstants.cs +++ b/src/TryMudBlazor.Client/Models/TryConstants.cs @@ -20,6 +20,8 @@ public static class Editor public static class CodeExecution { public const string UpdateUserComponentsDll = "Try.CodeExecution.updateUserComponentsDll"; + public const string ClearUserComponentsDll = "Try.CodeExecution.clearUserComponentsDll"; + public const string HotReloadIframe = "Try.CodeExecution.hotReloadIframe"; } } } diff --git a/src/TryMudBlazor.Client/Pages/NotFound.razor b/src/TryMudBlazor.Client/Pages/NotFound.razor new file mode 100644 index 00000000..e70ef2ad --- /dev/null +++ b/src/TryMudBlazor.Client/Pages/NotFound.razor @@ -0,0 +1,4 @@ +@page "/not-found" +@layout EmptyLayout + +

Sorry, there's nothing at this address.

diff --git a/src/TryMudBlazor.Client/Pages/Repl.razor.cs b/src/TryMudBlazor.Client/Pages/Repl.razor.cs index 04d5521b..647ce1a4 100644 --- a/src/TryMudBlazor.Client/Pages/Repl.razor.cs +++ b/src/TryMudBlazor.Client/Pages/Repl.razor.cs @@ -18,7 +18,6 @@ public partial class Repl : IDisposable { [Inject] private LayoutService LayoutService { get; set; } - private const string MainComponentCodePrefix = "@page \"/__main\"\n"; private const string MainUserPagePath = "/__main"; private DotNetObjectReference dotNetInstance; @@ -166,19 +165,10 @@ private async Task CompileAsync() await Task.Yield(); CompileToAssemblyResult compilationResult = null; - CodeFile mainComponent = null; - string originalMainComponentContent = null; try { this.UpdateActiveCodeFileContent(); - // Add the necessary main component code prefix and store the original content so we can revert right after compilation. - if (this.CodeFiles.TryGetValue(CoreConstants.MainComponentFilePath, out mainComponent)) - { - originalMainComponentContent = mainComponent.Content; - mainComponent.Content = MainComponentCodePrefix + originalMainComponentContent.Replace(MainComponentCodePrefix, ""); - } - compilationResult = await this.CompilationService.CompileToAssemblyAsync( this.CodeFiles.Values, this.UpdateLoaderTextAsync); @@ -193,21 +183,13 @@ private async Task CompileAsync() } finally { - if (mainComponent != null) - { - mainComponent.Content = originalMainComponentContent; - } - this.Loading = false; } if (compilationResult?.AssemblyBytes?.Length > 0) { - // Make sure the DLL is updated before reloading the user page this.JsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, compilationResult.AssemblyBytes); - - // TODO: Add error page in iframe - this.JsRuntime.InvokeVoid(Try.ReloadIframe, "user-page-window", MainUserPagePath); + this.JsRuntime.InvokeVoid(Try.CodeExecution.HotReloadIframe, "user-page-window", MainUserPagePath); } } diff --git a/src/TryMudBlazor.Client/Pages/UserMainPage.razor b/src/TryMudBlazor.Client/Pages/UserMainPage.razor new file mode 100644 index 00000000..809df2be --- /dev/null +++ b/src/TryMudBlazor.Client/Pages/UserMainPage.razor @@ -0,0 +1,91 @@ +@page "/__main" +@using Microsoft.JSInterop +@using System.Runtime.Loader +@using Try.UserComponents +@using TryMudBlazor.Client.Services +@inject IJSRuntime JSRuntime +@inject UserComponentsAssemblyService AssemblyService +@implements IAsyncDisposable + + + + + + +@code { + private Type _type = typeof(__Main); + private AssemblyLoadContext _loadContext; + private readonly Lazy> _ref; + + public UserMainPage() + { + _ref = new Lazy>(CreateDotNetObject()); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JSRuntime.InvokeVoidAsync("Try.registerHotReload", _ref.Value); + } + } + + [JSInvokable] + public async Task HotReload() + { + try + { + var base64 = await JSRuntime.InvokeAsync("sessionStorage.getItem", "TryMudBlazor.UserComponentsDllBase64"); + if (base64 == null) + { + // sessionStorage was cleared (e.g. crash recovery) — fall back to a full reload + // so loadBootResource picks up the safe static DLL. + await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); + return; + } + + var bytes = Convert.FromBase64String(base64); + + // Swap to a fresh collectible context; previous one becomes eligible for GC + // once Blazor disposes the old component instance below. + var previousContext = _loadContext; + _loadContext = new AssemblyLoadContext(name: null, isCollectible: true); + var assembly = _loadContext.LoadFromStream(new MemoryStream(bytes)); + previousContext?.Unload(); + + // UserStartup requires DI re-registration — fall back to full iframe reload + if (assembly.GetType("UserStartup") is not null || + assembly.GetType("Try.UserComponents.UserStartup") is not null) + { + await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); + return; + } + + var newType = assembly.GetType("Try.UserComponents.__Main"); + if (newType is not null) + { + // Update the Router's AdditionalAssemblies before rendering the new __Main + // so that any NavigateTo calls inside it can resolve user-defined @page routes. + await AssemblyService.UpdateAssemblyAsync(assembly); + _type = newType; + await InvokeAsync(StateHasChanged); + } + } + catch + { + await JSRuntime.InvokeVoidAsync("Try.requestFullReload", "/__main"); + } + } + + public async ValueTask DisposeAsync() + { + if (_ref.IsValueCreated) + { + await JSRuntime.InvokeVoidAsync("Try.unregisterHotReload"); + _ref.Value.Dispose(); + } + _loadContext?.Unload(); + } + + private DotNetObjectReference CreateDotNetObject() => DotNetObjectReference.Create(this); +} diff --git a/src/TryMudBlazor.Client/Program.cs b/src/TryMudBlazor.Client/Program.cs index ff92e761..8f9f14c8 100644 --- a/src/TryMudBlazor.Client/Program.cs +++ b/src/TryMudBlazor.Client/Program.cs @@ -17,6 +17,7 @@ namespace TryMudBlazor.Client using Services.UserPreferences; using Try.Core; using Try.UserComponents; + using TryMudBlazor.Client.Extensions; using TryMudBlazor.Client.Models; using TryMudBlazor.Client.Services; @@ -32,6 +33,7 @@ public static async Task Main(string[] args) builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(); builder.Services.AddSingleton(new CompilationService()); + builder.Services.AddSingleton(); builder.Services .AddOptions() @@ -46,7 +48,7 @@ public static async Task Main(string[] args) var jsRuntime = GetJsRuntime(); try { - ExecuteUserDefinedConfiguration(builder); + builder.TryInvokeUserStartup(); } catch (Exception exception) { @@ -55,28 +57,12 @@ public static async Task Main(string[] args) var actualException = exception is TargetInvocationException tie ? tie.InnerException : exception; await Console.Error.WriteLineAsync($"Error on app startup: {actualException}"); - jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes); + jsRuntime.InvokeVoid(Try.CodeExecution.ClearUserComponentsDll); } await builder.Build().RunAsync(); } - private static void ExecuteUserDefinedConfiguration(WebAssemblyHostBuilder builder) - { - var userComponentsAssembly = typeof(__Main).Assembly; - var startupType = userComponentsAssembly.GetType("UserStartup", throwOnError: false, ignoreCase: true) - ?? userComponentsAssembly.GetType("Try.UserComponents.UserStartup", throwOnError: false, ignoreCase: true); - if (startupType == null) - return; - var configureMethod = startupType.GetMethod("Configure", BindingFlags.Static | BindingFlags.Public); - if (configureMethod == null) - return; - var configureMethodParams = configureMethod.GetParameters(); - if (configureMethodParams.Length != 1 || configureMethodParams[0].ParameterType != typeof(WebAssemblyHostBuilder)) - return; - configureMethod.Invoke(obj: null, new object[] { builder }); - } - private static IJSInProcessRuntime GetJsRuntime() { const string defaultJsRuntimeTypeName = "DefaultWebAssemblyJSRuntime"; diff --git a/src/TryMudBlazor.Client/Properties/launchSettings.json b/src/TryMudBlazor.Client/Properties/launchSettings.json index d11c42d0..c21b86f6 100644 --- a/src/TryMudBlazor.Client/Properties/launchSettings.json +++ b/src/TryMudBlazor.Client/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "hotReloadEnabled": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -19,6 +20,7 @@ "BlazorRepl": { "commandName": "Project", "launchBrowser": true, + "hotReloadEnabled": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs b/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs index c0a761cb..a1d4ee93 100644 --- a/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs +++ b/src/TryMudBlazor.Client/Services/HandleCriticalUserComponentExceptionsLogger.cs @@ -28,7 +28,7 @@ public void Log( { if (exception?.ToString()?.Contains(CompilationService.DefaultRootNamespace) ?? false) { - _jsRuntime.InvokeVoid(Try.CodeExecution.UpdateUserComponentsDll, CoreConstants.DefaultUserComponentsAssemblyBytes); + _jsRuntime.InvokeVoid(Try.CodeExecution.ClearUserComponentsDll); } } diff --git a/src/TryMudBlazor.Client/Services/UserComponentsAssemblyService.cs b/src/TryMudBlazor.Client/Services/UserComponentsAssemblyService.cs new file mode 100644 index 00000000..fd191e59 --- /dev/null +++ b/src/TryMudBlazor.Client/Services/UserComponentsAssemblyService.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using Try.UserComponents; + +namespace TryMudBlazor.Client.Services; + +public class UserComponentsAssemblyService +{ + private readonly List> _callbacks = []; + + public Assembly Assembly { get; private set; } = typeof(__Main).Assembly; + + public IDisposable Subscribe(Func callback) + { + _callbacks.Add(callback); + return new Subscription(() => _callbacks.Remove(callback)); + } + + public async Task UpdateAssemblyAsync(Assembly assembly) + { + Assembly = assembly; + foreach (var callback in _callbacks) + { + await callback(assembly); + } + } + + private sealed class Subscription(Action unsubscribe) : IDisposable + { + public void Dispose() => unsubscribe(); + } +} diff --git a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj index d90f063e..0a38a313 100644 --- a/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj +++ b/src/TryMudBlazor.Client/TryMudBlazor.Client.csproj @@ -3,6 +3,7 @@ false true + true diff --git a/src/TryMudBlazor.Client/wwwroot/editor/main.js b/src/TryMudBlazor.Client/wwwroot/editor/main.js index d1376f65..18711b28 100644 --- a/src/TryMudBlazor.Client/wwwroot/editor/main.js +++ b/src/TryMudBlazor.Client/wwwroot/editor/main.js @@ -70,6 +70,16 @@ function throttle(func, timeFrame, id) { } } +// Listen for hot-reload signals when running inside the preview iframe +if (window.frameElement) { + window.addEventListener('message', function (e) { + if (e.origin !== window.location.origin) return; + if (e.data?.type === 'hotReload' && window._hotReloadRef) { + window._hotReloadRef.invokeMethodAsync('HotReload'); + } + }); +} + window.Try = { initialize: function (dotNetInstance) { _dotNetInstance = dotNetInstance; @@ -101,7 +111,18 @@ window.Try = { dispose: function () { _dotNetInstance = null; window.removeEventListener('keydown', onKeyDown); - } + }, + registerHotReload: function (dotNetRef) { + window._hotReloadRef = dotNetRef; + window._hotReloadReady = true; + }, + unregisterHotReload: function () { + window._hotReloadRef = null; + window._hotReloadReady = false; + }, + requestFullReload: function (src) { + window.parent.Try.reloadIframe('user-page-window', src); + }, } window.Try.Editor = window.Try.Editor || (function () { @@ -181,6 +202,22 @@ window.Try.CodeExecution = window.Try.CodeExecution || (function () { const USER_COMPONENTS_DLL_STORAGE_KEY = 'TryMudBlazor.UserComponentsDllBase64'; return { + hotReloadIframe: function (id, fallbackSrc) { + const iFrame = document.getElementById(id); + if (!iFrame) return; + + const iframeWindow = iFrame.contentWindow; + if (iframeWindow && iframeWindow._hotReloadReady) { + // Iframe is live — signal it to hot-reload from sessionStorage + iframeWindow.postMessage({ type: 'hotReload' }, window.location.origin); + } else { + // Iframe not yet ready (first run) — fall back to full navigation + Try.reloadIframe(id, fallbackSrc); + } + }, + clearUserComponentsDll: function () { + window.sessionStorage.removeItem(USER_COMPONENTS_DLL_STORAGE_KEY); + }, updateUserComponentsDll: function (dllData) { if (!dllData) return; diff --git a/src/TryMudBlazor.Client/wwwroot/index.html b/src/TryMudBlazor.Client/wwwroot/index.html index 3f0ac12c..2935a9b9 100644 --- a/src/TryMudBlazor.Client/wwwroot/index.html +++ b/src/TryMudBlazor.Client/wwwroot/index.html @@ -24,10 +24,13 @@ + - - - + + + + + @@ -42,6 +45,7 @@ gtag('config', 'G-33J60J6E14'); + @@ -57,9 +61,9 @@ Reload 🗙 - - - + + +