diff --git a/src/componentsBase/IIgniteUIBlazorRuntime.cs b/src/componentsBase/IIgniteUIBlazorRuntime.cs index fb991f99..f6d57cab 100644 --- a/src/componentsBase/IIgniteUIBlazorRuntime.cs +++ b/src/componentsBase/IIgniteUIBlazorRuntime.cs @@ -1,9 +1,10 @@ +using System.Runtime.CompilerServices; using Microsoft.JSInterop; namespace IgniteUI.Blazor.Controls { /// - /// The runtime behind the marker; the components and + /// The runtime behind ; the components and /// reach it through . /// internal interface IIgniteUIBlazorRuntime @@ -19,14 +20,29 @@ internal interface IIgniteUIBlazorRuntime internal static class IgniteUIBlazorRuntimeExtensions { + private static readonly ConditionalWeakTable _adopted = new(); + /// - /// The single conversion from the public marker to the runtime it is implemented by. + /// The single conversion from the public service to the runtime it is implemented by. /// internal static IIgniteUIBlazorRuntime AsRuntime(this IIgniteUIBlazor service) { - return service as IIgniteUIBlazorRuntime - ?? throw new InvalidOperationException( - $"The registered {nameof(IIgniteUIBlazor)} instance is of type {service?.GetType().Name ?? "null"}, which is not the runtime registered by AddIgniteUIBlazor."); + if (service is IIgniteUIBlazorRuntime runtime) + { + return runtime; + } + if (service != null && IsFullPackageRuntime(service.GetType())) + { + return _adopted.GetValue(service, s => new IgniteUIBlazor(s.JsRuntime)); + } + throw new InvalidOperationException( + $"The registered {nameof(IIgniteUIBlazor)} instance is of type {service?.GetType().Name ?? "null"}, which is not the runtime registered by AddIgniteUIBlazor."); } + + // Temporary, while the full IgniteUI.Blazor package deprecates its IgniteUIBlazor class: an app that registers + // that class instead of calling AddIgniteUIBlazor gets a runtime of its own, one per instance and so one per scope. + // Matched by name because that package depends on this one. + private static bool IsFullPackageRuntime(Type type) => + type.FullName == "IgniteUI.Blazor.Controls.IgniteUIBlazor" && type.Assembly.GetName().Name == "IgniteUI.Blazor"; } } diff --git a/src/componentsBase/IgbComponentBase.cs b/src/componentsBase/IgbComponentBase.cs index 87d3caa4..f51a04f7 100644 --- a/src/componentsBase/IgbComponentBase.cs +++ b/src/componentsBase/IgbComponentBase.cs @@ -3857,6 +3857,12 @@ internal IgniteUIBlazorSettings(IIgniteUIBlazorSettings settings) /// public interface IIgniteUIBlazor { + /// + /// The JS runtime the client resources load through. Supports the product infrastructure and is not + /// intended to be used directly from your code. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + IJSRuntime JsRuntime { get; } } internal class IgniteUIBlazor : IIgniteUIBlazor, IIgniteUIBlazorRuntime diff --git a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs index d24c32b7..e9cf564d 100644 --- a/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs +++ b/tests/IgniteUI.Blazor.Tests/ServiceRegistrationTests.cs @@ -1,3 +1,6 @@ +using System.Reflection; +using System.Reflection.Emit; +using Bunit; using IgniteUI.Blazor.Controls; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; @@ -180,6 +183,54 @@ public void AddIgniteUIBlazor_MutatingOriginalArray_DoesNotAffectRegisteredModul Assert.Equal([typeof(FirstModule)], SettingsOf(host).ModulesToLoad); } + [Fact] + public void Runtime_ExposesTheRegisteredJsRuntime() + { + using var host = new Host(s => s.AddIgniteUIBlazor()); + + Assert.Same(host.Services.GetRequiredService(), host.Services.GetRequiredService().JsRuntime); + } + + /// The full IgniteUI.Blazor package's deprecated IgniteUIBlazor, registered in place of AddIgniteUIBlazor. + [Fact] + public void Component_WithFullPackageRuntimeRegistered_RequestsItsModuleThroughItsJsRuntime() + { + using var context = new BunitContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + context.Services.AddScoped(sp => + { + var runtime = (HandRegisteredRuntime)Activator.CreateInstance(_fullPackageRuntimeType)!; + runtime.JsRuntime = sp.GetRequiredService(); + return runtime; + }); + + context.Render(); + + var load = Assert.Single(context.JSInterop.Invocations, v => v.Identifier == "igRequestLoad"); + Assert.Equal("WebButtonModule", load.Arguments[0]); + } + + [Fact] + public void Component_WithOtherRuntimeRegistered_Throws() + { + using var context = new BunitContext(); + context.Services.AddScoped(_ => new HandRegisteredRuntime()); + + var error = Assert.Throws(() => context.Render()); + Assert.Contains("not the runtime registered by AddIgniteUIBlazor", error.Message); + } + + public class HandRegisteredRuntime : IIgniteUIBlazor + { + public IJSRuntime JsRuntime { get; set; } = null!; + } + + // HandRegisteredRuntime under the full package's type and assembly name; this repository cannot reference that package. + private static readonly Type _fullPackageRuntimeType = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("IgniteUI.Blazor"), AssemblyBuilderAccess.Run) + .DefineDynamicModule("IgniteUI.Blazor") + .DefineType("IgniteUI.Blazor.Controls.IgniteUIBlazor", TypeAttributes.Public, typeof(HandRegisteredRuntime)) + .CreateType(); + [Fact] public void Runtime_IsScoped_SoEachScopeRegistersItsOwn() {