From 4d7c4eb62b278286b31bd07e80cc76728337b141 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 6 Apr 2026 21:54:20 -0400 Subject: [PATCH 1/7] docs: update ClientScript migration guide for Phase 2 - GetPostBackEventReference now documented as zero-rewrite (shim support) - ScriptManager.GetCurrent documented with ScriptManagerShim pattern - GetCallbackEventReference bridge pattern documented (new section 5) - Updated coverage summary to show full Phase 2 support - Updated BWFC022 analyzer to reference Phase 2 shim path - Updated BWFC024 analyzer to reference ScriptManager.GetCurrent() Phase 2 support - Renumbered sections 6-11 to accommodate new callback section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Analyzers/BWFC022.md | 14 +- docs/Analyzers/BWFC024.md | 45 +- docs/Migration/ClientScriptMigrationGuide.md | 437 ++++++++++++++++--- 3 files changed, 434 insertions(+), 62 deletions(-) diff --git a/docs/Analyzers/BWFC022.md b/docs/Analyzers/BWFC022.md index 17118ca7..3704ca9a 100644 --- a/docs/Analyzers/BWFC022.md +++ b/docs/Analyzers/BWFC022.md @@ -78,7 +78,7 @@ If you prefer to modernize your code now, rewrite to `IJSRuntime` directly. The | `RegisterStartupScript()` | `ClientScriptShim` (⭐ Easy) or `OnAfterRenderAsync()` + `IJSRuntime` (⭐⭐ Moderate) | ⭐ Easy | | `RegisterClientScriptInclude()` | `ClientScriptShim` (⭐ Easy) or `"); @@ -50,7 +49,7 @@ protected override async Task OnInitializedAsync() ClientScript.RegisterStartupScript(this.GetType(), "smScript", "alert('hello');", true); // Pattern 6: ScriptManager.GetCurrent - // TODO(bwfc-general): ScriptManager.GetCurrent() has no Blazor equivalent. Use IJSRuntime directly. + var sm = ScriptManager.GetCurrent(this); } } } diff --git a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs index 2809dd47..cb3ac137 100644 --- a/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs +++ b/tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/ClientScriptTransformTests.cs @@ -229,10 +229,10 @@ void Page_Load() #endregion - #region TC38 — GetPostBackEventReference and ScriptManager.GetCurrent + #region TC38 — GetPostBackEventReference and ScriptManager.GetCurrent (Phase 2: shim-preserving) [Fact] - public void TC38_GetPostBackEventReference_EmitsTodoWithEventCallbackGuidance() + public void TC38_GetPostBackEventReference_StripsPagePrefix() { var input = @"namespace MyApp { @@ -246,13 +246,14 @@ void DoWork() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("TODO(bwfc-general)", result); - Assert.Contains("@onclick", result); - Assert.Contains("EventCallback", result); + Assert.Contains("ClientScript.GetPostBackEventReference(btnSubmit", result); + Assert.DoesNotContain("Page.ClientScript", result); + Assert.DoesNotContain("// TODO(bwfc-general): Replace __doPostBack", result); + Assert.DoesNotContain("// Original:", result); } [Fact] - public void TC38_GetPostBackEventReference_PreservesOriginalAsComment() + public void TC38_GetPostBackEventReference_StripsThisPrefix() { var input = @"namespace MyApp { @@ -260,18 +261,38 @@ public partial class MyPage { void DoWork() { - var postbackRef = Page.ClientScript.GetPostBackEventReference(btnSubmit, ""validate""); + var postbackRef = this.ClientScript.GetPostBackEventReference(btnSubmit, ""validate""); } } }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("// Original:", result); - Assert.Contains("GetPostBackEventReference", result); + Assert.Contains("ClientScript.GetPostBackEventReference(btnSubmit", result); + Assert.DoesNotContain("this.ClientScript", result); + Assert.DoesNotContain("// Original:", result); } [Fact] - public void TC38_ScriptManagerGetCurrent_EmitsTodo() + public void TC38_GetPostBackEventReference_BareCall_SetsShimFlag() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void DoWork() + { + var postbackRef = ClientScript.GetPostBackEventReference(btnSubmit, ""validate""); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("ClientScript.GetPostBackEventReference(btnSubmit", result); + Assert.Contains("ClientScriptShim", result); + } + + [Fact] + public void TC38_ScriptManagerGetCurrent_ConvertsPageToThis() { var input = @"namespace MyApp { @@ -285,13 +306,32 @@ void Page_Load() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("TODO(bwfc-general)", result); - Assert.Contains("ScriptManager.GetCurrent()", result); - Assert.Contains("IJSRuntime", result); + Assert.Contains("ScriptManager.GetCurrent(this)", result); + Assert.DoesNotContain("ScriptManager.GetCurrent(Page)", result); + Assert.DoesNotContain("ScriptManager.GetCurrent() has no Blazor equivalent", result); + } + + [Fact] + public void TC38_ScriptManagerGetCurrent_WithThisDotPage_ConvertsToThis() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Page_Load() + { + var sm = ScriptManager.GetCurrent(this.Page); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("ScriptManager.GetCurrent(this)", result); + Assert.DoesNotContain("this.Page", result); } [Fact] - public void TC38_ScriptManagerGetCurrent_WithThis_EmitsTodo() + public void TC38_ScriptManagerGetCurrent_PreservesThis() { var input = @"namespace MyApp { @@ -305,8 +345,49 @@ void Page_Load() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.Contains("TODO(bwfc-general)", result); - Assert.Contains("ScriptManager.GetCurrent()", result); + Assert.Contains("ScriptManager.GetCurrent(this)", result); + Assert.DoesNotContain("ScriptManager.GetCurrent() has no Blazor equivalent", result); + Assert.Contains("ScriptManagerShim", result); + } + + [Fact] + public void TC38_ScriptManagerGetCurrent_AddsScriptManagerShimComment() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Page_Load() + { + var sm = ScriptManager.GetCurrent(Page); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("ScriptManagerShim", result); + Assert.Contains("ClientScriptShim", result); + } + + [Fact] + public void TC38_ScriptManagerGetCurrent_FullPattern_Preserved() + { + var input = @"namespace MyApp +{ + public partial class MyPage + { + void Page_Load() + { + ScriptManager sm = ScriptManager.GetCurrent(Page); + sm.RegisterStartupScript(this.GetType(), ""key"", ""script"", true); + } + } +}"; + var result = _transform.Apply(input, TestMetadata(input)); + + Assert.Contains("ScriptManager sm = ScriptManager.GetCurrent(this)", result); + Assert.Contains("sm.RegisterStartupScript(this.GetType()", result); + Assert.Contains("ScriptManagerShim", result); } #endregion @@ -342,10 +423,12 @@ void Page_Load() Assert.Contains("ClientScript.RegisterClientScriptBlock", result); // ScriptManager.RegisterStartupScript → ClientScript.RegisterStartupScript Assert.DoesNotContain("ScriptManager.RegisterStartupScript", result); - // Postback → TODO with EventCallback - Assert.Contains("@onclick", result); - // ScriptManager.GetCurrent → TODO - Assert.Contains("ScriptManager.GetCurrent() has no Blazor equivalent", result); + // Postback → preserved with prefix stripped + Assert.Contains("ClientScript.GetPostBackEventReference", result); + Assert.DoesNotContain("Page.ClientScript.GetPostBackEventReference", result); + // ScriptManager.GetCurrent → Page replaced with this + Assert.Contains("ScriptManager.GetCurrent(this)", result); + Assert.DoesNotContain("ScriptManager.GetCurrent(Page)", result); // ClientScriptShim comment injected (not IJSRuntime) Assert.Contains("ClientScriptShim", result); Assert.DoesNotContain("[Inject] private IJSRuntime JS", result); @@ -367,7 +450,7 @@ public partial class MyPage } [Fact] - public void DoesNotAddShimComment_WhenOnlyPostbackRef() + public void AddsShimComment_WhenOnlyPostbackRef() { var input = @"namespace MyApp { @@ -381,7 +464,7 @@ void DoWork() }"; var result = _transform.Apply(input, TestMetadata(input)); - Assert.DoesNotContain("ClientScriptShim", result); + Assert.Contains("ClientScriptShim", result); } [Fact] From e0d6b849864efdfd73c4b1ed0943d16a7d38819b Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Mon, 6 Apr 2026 21:58:34 -0400 Subject: [PATCH 3/7] feat: PostBack shim + ScriptManagerShim runtime (Phase 2) - GetPostBackEventReference() returns working __doPostBack JS string - GetPostBackClientHyperlink() returns javascript: URL - GetCallbackEventReference() returns working JS callback bridge - Created bwfc-postback.js with __doPostBack and callback interop - PostBackEventArgs for event handling - ScriptManagerShim with GetCurrent() factory pattern - WebFormsPageBase postback target registration via JS interop - Registered ScriptManagerShim in DI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ClientScriptShimTests.cs | 65 ++++++--- .../ClientScriptShim.cs | 62 +++++---- .../PostBackEventArgs.cs | 23 ++++ .../ScriptManagerShim.cs | 66 +++++++++ .../ServiceCollectionExtensions.cs | 1 + .../WebFormsPageBase.cs | 128 +++++++++++++++++- .../wwwroot/js/bwfc-postback.js | 41 ++++++ 7 files changed, 344 insertions(+), 42 deletions(-) create mode 100644 src/BlazorWebFormsComponents/PostBackEventArgs.cs create mode 100644 src/BlazorWebFormsComponents/ScriptManagerShim.cs create mode 100644 src/BlazorWebFormsComponents/wwwroot/js/bwfc-postback.js diff --git a/src/BlazorWebFormsComponents.Test/ClientScriptShimTests.cs b/src/BlazorWebFormsComponents.Test/ClientScriptShimTests.cs index b50887a9..11dbe951 100644 --- a/src/BlazorWebFormsComponents.Test/ClientScriptShimTests.cs +++ b/src/BlazorWebFormsComponents.Test/ClientScriptShimTests.cs @@ -350,43 +350,76 @@ public async Task FlushAsync_ReRegistering_AfterFlush_Works() #endregion - #region Unsupported Methods + #region PostBack / Callback Methods - // NOTE: Exact method signatures depend on Cyclops's implementation. - // These tests verify the throw behavior — signatures may need adjustment - // once the shim is finalized. + // These methods now return working JavaScript strings instead of throwing. [Fact] - public void GetPostBackEventReference_ThrowsNotSupported() + public void GetPostBackEventReference_ReturnsDoPostBackJs() { var shim = CreateShim(); - var ex = Should.Throw(() => - shim.GetPostBackEventReference(null!, "arg")); + var result = shim.GetPostBackEventReference(new object(), "arg"); - ex.Message.ShouldNotBeNullOrWhiteSpace(); + result.ShouldContain("__doPostBack"); + result.ShouldContain("arg"); } [Fact] - public void GetPostBackClientHyperlink_ThrowsNotSupported() + public void GetPostBackEventReference_NullControl_ReturnsUnknownTarget() { var shim = CreateShim(); - var ex = Should.Throw(() => - shim.GetPostBackClientHyperlink(null!, "arg")); + var result = shim.GetPostBackEventReference(null!, "arg"); - ex.Message.ShouldNotBeNullOrWhiteSpace(); + result.ShouldContain("__doPostBack('unknown',"); } [Fact] - public void GetCallbackEventReference_ThrowsNotSupported() + public void GetPostBackEventReference_NullArgument_DefaultsToEmpty() { var shim = CreateShim(); - var ex = Should.Throw(() => - shim.GetCallbackEventReference(null!, "arg", "callback", "ctx", "errorCb", false)); + var result = shim.GetPostBackEventReference(new object(), null!); - ex.Message.ShouldNotBeNullOrWhiteSpace(); + result.ShouldContain("__doPostBack("); + result.ShouldContain("''"); + } + + [Fact] + public void GetPostBackClientHyperlink_ReturnsJavascriptUrl() + { + var shim = CreateShim(); + + var result = shim.GetPostBackClientHyperlink(new object(), "arg"); + + result.ShouldStartWith("javascript:"); + result.ShouldContain("__doPostBack"); + } + + [Fact] + public void GetCallbackEventReference_ReturnsCallbackJs() + { + var shim = CreateShim(); + + var result = shim.GetCallbackEventReference( + new object(), "arg", "onSuccess", "ctx", "onError", false); + + result.ShouldContain("__bwfc_callback"); + result.ShouldContain("arg"); + result.ShouldContain("onSuccess"); + result.ShouldContain("onError"); + } + + [Fact] + public void GetCallbackEventReference_NullCallbacks_UsesNull() + { + var shim = CreateShim(); + + var result = shim.GetCallbackEventReference( + new object(), "arg", null!, null, null!, false); + + result.ShouldContain("null"); } #endregion diff --git a/src/BlazorWebFormsComponents/ClientScriptShim.cs b/src/BlazorWebFormsComponents/ClientScriptShim.cs index df05bead..da4530b6 100644 --- a/src/BlazorWebFormsComponents/ClientScriptShim.cs +++ b/src/BlazorWebFormsComponents/ClientScriptShim.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; @@ -192,45 +193,46 @@ public async Task FlushAsync(IJSRuntime jsRuntime) _scriptIncludes.Clear(); } - // ─── Unsupported Methods ─────────────────────────────────────────── + // ─── PostBack / Callback Methods ────────────────────────────────── /// - /// Not supported in Blazor. Throws - /// with migration guidance. + /// Returns a JavaScript string that triggers a postback, mirroring the + /// Web Forms ClientScriptManager.GetPostBackEventReference behavior. + /// The returned string calls __doPostBack(eventTarget, eventArgument), + /// which is defined in bwfc-postback.js. /// - /// Always thrown. + /// The control initiating the postback (used to resolve an ID). + /// The event argument string. + /// A JavaScript expression string, e.g. __doPostBack('Button1', ''). public string GetPostBackEventReference(object control, string argument) { - throw new NotSupportedException( - "GetPostBackEventReference is not supported in Blazor. " + - "Use @onclick / EventCallback instead. " + - "See: docs/Migration/ClientScriptMigrationGuide.md"); + var id = ResolveControlId(control); + return $"__doPostBack('{EscapeJsString(id)}', '{EscapeJsString(argument ?? string.Empty)}')"; } /// - /// Not supported in Blazor. Throws - /// with migration guidance. + /// Returns a javascript: URL that triggers a postback, mirroring + /// ClientScriptManager.GetPostBackClientHyperlink. /// - /// Always thrown. + /// The control initiating the postback. + /// The event argument string. + /// A javascript:__doPostBack(...) URL string. public string GetPostBackClientHyperlink(object control, string argument) { - throw new NotSupportedException( - "GetPostBackClientHyperlink is not supported in Blazor. " + - "Use NavigationManager or instead. " + - "See: docs/Migration/ClientScriptMigrationGuide.md"); + return $"javascript:{GetPostBackEventReference(control, argument)}"; } /// - /// Not supported in Blazor. Throws - /// with migration guidance. + /// Returns a JavaScript expression that invokes the BWFC callback bridge, + /// mirroring ClientScriptManager.GetCallbackEventReference. + /// The returned expression calls __bwfc_callback defined in + /// bwfc-postback.js. /// - /// Always thrown. - public string GetCallbackEventReference(object control, string argument, string clientCallback, string context, string clientErrorCallback, bool useAsync) + public string GetCallbackEventReference(object control, string argument, + string clientCallback, string context, string clientErrorCallback, bool useAsync) { - throw new NotSupportedException( - "GetCallbackEventReference is not supported in Blazor. " + - "Use IJSRuntime / EventCallback for JS-to-.NET interop. " + - "See: docs/Migration/ClientScriptMigrationGuide.md"); + var id = ResolveControlId(control); + return $"__bwfc_callback('{EscapeJsString(id)}', '{EscapeJsString(argument ?? string.Empty)}', {clientCallback ?? "null"}, '{EscapeJsString(context ?? string.Empty)}', {clientErrorCallback ?? "null"})"; } // ─── Helpers ─────────────────────────────────────────────────────── @@ -250,4 +252,18 @@ private static string EscapeJsString(string value) { return value.Replace("\\", "\\\\").Replace("'", "\\'"); } + + /// + /// Resolves a control reference to an ID string suitable for use in + /// JavaScript postback/callback expressions. + /// Prefers when available. + /// + private static string ResolveControlId(object control) + { + if (control is BaseWebFormsComponent bwfc && !string.IsNullOrEmpty(bwfc.ID)) + return bwfc.ID; + if (control is ComponentBase) + return control.GetType().Name; + return control?.GetType().Name ?? "unknown"; + } } diff --git a/src/BlazorWebFormsComponents/PostBackEventArgs.cs b/src/BlazorWebFormsComponents/PostBackEventArgs.cs new file mode 100644 index 00000000..44e06056 --- /dev/null +++ b/src/BlazorWebFormsComponents/PostBackEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace BlazorWebFormsComponents; + +/// +/// Event arguments for postback events, mirroring the Web Forms postback model. +/// Contains the (control ID) and +/// that were passed to __doPostBack(eventTarget, eventArgument). +/// +public class PostBackEventArgs : EventArgs +{ + /// The ID of the control that initiated the postback. + public string EventTarget { get; } + + /// The argument string associated with the postback. + public string EventArgument { get; } + + public PostBackEventArgs(string eventTarget, string eventArgument) + { + EventTarget = eventTarget; + EventArgument = eventArgument; + } +} diff --git a/src/BlazorWebFormsComponents/ScriptManagerShim.cs b/src/BlazorWebFormsComponents/ScriptManagerShim.cs new file mode 100644 index 00000000..2b1bea41 --- /dev/null +++ b/src/BlazorWebFormsComponents/ScriptManagerShim.cs @@ -0,0 +1,66 @@ +using System; + +namespace BlazorWebFormsComponents; + +/// +/// Compatibility shim for System.Web.UI.ScriptManager. +/// Provides the static factory method that Web Forms +/// code uses to obtain a ScriptManager instance from the page, and +/// delegates all RegisterXxx calls to . +/// +public class ScriptManagerShim +{ + private readonly ClientScriptShim _clientScript; + + /// + /// Initializes a new instance backed by the specified . + /// + public ScriptManagerShim(ClientScriptShim clientScript) + { + _clientScript = clientScript ?? throw new ArgumentNullException(nameof(clientScript)); + } + + /// + /// Static factory matching the Web Forms ScriptManager.GetCurrent(Page) pattern. + /// Resolves the from the page/component instance. + /// + /// + /// A or instance. + /// + /// A wrapping the component's ClientScript. + /// + /// Thrown when is not a recognized BWFC type or has no ClientScript. + /// + public static ScriptManagerShim GetCurrent(object page) + { + if (page is BaseWebFormsComponent component && component.ClientScript != null) + return new ScriptManagerShim(component.ClientScript); + if (page is WebFormsPageBase pageBase && pageBase.ClientScript != null) + return new ScriptManagerShim(pageBase.ClientScript); + + throw new InvalidOperationException( + "ScriptManager.GetCurrent() requires a component derived from " + + "BaseWebFormsComponent or WebFormsPageBase with a registered ClientScriptShim."); + } + + // ─── Delegated Registration Methods ─────────────────────────────── + + /// + public void RegisterStartupScript(Type type, string key, string script, bool addScriptTags) + => _clientScript.RegisterStartupScript(type, key, script, addScriptTags); + + /// + /// Overload accepting a control reference (ignored) to match the Web Forms + /// ScriptManager.RegisterStartupScript(Control, Type, String, String, Boolean) signature. + /// + public void RegisterStartupScript(object control, Type type, string key, string script, bool addScriptTags) + => _clientScript.RegisterStartupScript(type, key, script, addScriptTags); + + /// + public void RegisterClientScriptBlock(Type type, string key, string script, bool addScriptTags) + => _clientScript.RegisterClientScriptBlock(type, key, script, addScriptTags); + + /// + public void RegisterClientScriptInclude(Type type, string key, string url) + => _clientScript.RegisterClientScriptInclude(type, key, url); +} diff --git a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs index 32c760f3..fe57087f 100644 --- a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs +++ b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs @@ -43,6 +43,7 @@ public static IServiceCollection AddBlazorWebFormsComponents(this IServiceCollec services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(sp => new ScriptManagerShim(sp.GetRequiredService())); var options = new BlazorWebFormsComponentsOptions(); configure?.Invoke(options); diff --git a/src/BlazorWebFormsComponents/WebFormsPageBase.cs b/src/BlazorWebFormsComponents/WebFormsPageBase.cs index 1cbd00e7..894555e9 100644 --- a/src/BlazorWebFormsComponents/WebFormsPageBase.cs +++ b/src/BlazorWebFormsComponents/WebFormsPageBase.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; namespace BlazorWebFormsComponents; @@ -12,10 +14,10 @@ namespace BlazorWebFormsComponents; /// Base class for converted ASP.NET Web Forms pages. Provides Page.Title, /// Page.MetaDescription, Page.MetaKeywords, IsPostBack, Response.Redirect, /// Response.Cookies, Request.Cookies, Request.QueryString, Request.Url, -/// ViewState, and GetRouteUrl compatibility so that Web Forms code-behind -/// patterns survive migration with minimal changes. +/// ViewState, GetRouteUrl, ClientScript, and PostBack compatibility so that +/// Web Forms code-behind patterns survive migration with minimal changes. /// -public abstract class WebFormsPageBase : ComponentBase +public abstract class WebFormsPageBase : ComponentBase, IAsyncDisposable { [Inject] private IPageService _pageService { get; set; } = null!; [Inject] private NavigationManager _navigationManager { get; set; } = null!; @@ -25,6 +27,25 @@ public abstract class WebFormsPageBase : ComponentBase [Inject] private SessionShim _sessionShim { get; set; } = null!; [Inject] private IWebHostEnvironment _webHostEnvironment { get; set; } = null!; [Inject] private CacheShim _cacheShim { get; set; } = null!; +[Inject] private IJSRuntime _jsRuntime { get; set; } = null!; +[Inject] private ClientScriptShim _clientScriptShim { get; set; } = null!; + +/// +/// Provides access to client script registration methods, emulating +/// Page.ClientScript from ASP.NET Web Forms. +/// +public ClientScriptShim ClientScript => _clientScriptShim; + +// ─── PostBack Support ───────────────────────────────────────────── + +private DotNetObjectReference? _postBackRef; +private string? _postBackTargetId; + +/// +/// Raised when a postback is triggered from JavaScript via __doPostBack(). +/// Subscribe to this event in derived pages to handle postback actions. +/// +public event EventHandler? PostBack; /// /// Provides dictionary-style Session["key"] access, emulating @@ -237,4 +258,105 @@ private void RequireHttpContext(string memberName) $"rendering (WebSocket mode). Use {nameof(IsHttpContextAvailable)} to guard " + $"calls to this member, or ensure the page runs in SSR mode."); } + +// ─── PostBack JS Interop ────────────────────────────────────────── + +// Inline bootstrap ensures __doPostBack and registration functions exist +// before any postback target is registered. The standalone bwfc-postback.js +// file is also available for manual ", true); + + clientScript.IsStartupScriptRegistered(typeof(ScriptManagerShimTests), "tagged").ShouldBeTrue(); + } + + #endregion + + #region Delegation — RegisterClientScriptBlock + + [Fact] + public void RegisterClientScriptBlock_DelegatesToClientScript() + { + var clientScript = CreateClientScriptShim(); + var sm = new ScriptManagerShim(clientScript); + + sm.RegisterClientScriptBlock(typeof(ScriptManagerShimTests), "block1", "var x = 1;", false); + + clientScript.IsClientScriptBlockRegistered(typeof(ScriptManagerShimTests), "block1").ShouldBeTrue(); + } + + #endregion + + #region Delegation — RegisterClientScriptInclude + + [Fact] + public void RegisterClientScriptInclude_DelegatesToClientScript() + { + var clientScript = CreateClientScriptShim(); + var sm = new ScriptManagerShim(clientScript); + + sm.RegisterClientScriptInclude(typeof(ScriptManagerShimTests), "inc1", "https://cdn.example.com/lib.js"); + + clientScript.IsClientScriptIncludeRegistered("inc1").ShouldBeTrue(); + } + + #endregion +}