Skip to content

Commit e0d6b84

Browse files
csharpfritzCopilot
andcommitted
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>
1 parent 3dc1afc commit e0d6b84

7 files changed

Lines changed: 344 additions & 42 deletions

File tree

src/BlazorWebFormsComponents.Test/ClientScriptShimTests.cs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -350,43 +350,76 @@ public async Task FlushAsync_ReRegistering_AfterFlush_Works()
350350

351351
#endregion
352352

353-
#region Unsupported Methods
353+
#region PostBack / Callback Methods
354354

355-
// NOTE: Exact method signatures depend on Cyclops's implementation.
356-
// These tests verify the throw behavior — signatures may need adjustment
357-
// once the shim is finalized.
355+
// These methods now return working JavaScript strings instead of throwing.
358356

359357
[Fact]
360-
public void GetPostBackEventReference_ThrowsNotSupported()
358+
public void GetPostBackEventReference_ReturnsDoPostBackJs()
361359
{
362360
var shim = CreateShim();
363361

364-
var ex = Should.Throw<NotSupportedException>(() =>
365-
shim.GetPostBackEventReference(null!, "arg"));
362+
var result = shim.GetPostBackEventReference(new object(), "arg");
366363

367-
ex.Message.ShouldNotBeNullOrWhiteSpace();
364+
result.ShouldContain("__doPostBack");
365+
result.ShouldContain("arg");
368366
}
369367

370368
[Fact]
371-
public void GetPostBackClientHyperlink_ThrowsNotSupported()
369+
public void GetPostBackEventReference_NullControl_ReturnsUnknownTarget()
372370
{
373371
var shim = CreateShim();
374372

375-
var ex = Should.Throw<NotSupportedException>(() =>
376-
shim.GetPostBackClientHyperlink(null!, "arg"));
373+
var result = shim.GetPostBackEventReference(null!, "arg");
377374

378-
ex.Message.ShouldNotBeNullOrWhiteSpace();
375+
result.ShouldContain("__doPostBack('unknown',");
379376
}
380377

381378
[Fact]
382-
public void GetCallbackEventReference_ThrowsNotSupported()
379+
public void GetPostBackEventReference_NullArgument_DefaultsToEmpty()
383380
{
384381
var shim = CreateShim();
385382

386-
var ex = Should.Throw<NotSupportedException>(() =>
387-
shim.GetCallbackEventReference(null!, "arg", "callback", "ctx", "errorCb", false));
383+
var result = shim.GetPostBackEventReference(new object(), null!);
388384

389-
ex.Message.ShouldNotBeNullOrWhiteSpace();
385+
result.ShouldContain("__doPostBack(");
386+
result.ShouldContain("''");
387+
}
388+
389+
[Fact]
390+
public void GetPostBackClientHyperlink_ReturnsJavascriptUrl()
391+
{
392+
var shim = CreateShim();
393+
394+
var result = shim.GetPostBackClientHyperlink(new object(), "arg");
395+
396+
result.ShouldStartWith("javascript:");
397+
result.ShouldContain("__doPostBack");
398+
}
399+
400+
[Fact]
401+
public void GetCallbackEventReference_ReturnsCallbackJs()
402+
{
403+
var shim = CreateShim();
404+
405+
var result = shim.GetCallbackEventReference(
406+
new object(), "arg", "onSuccess", "ctx", "onError", false);
407+
408+
result.ShouldContain("__bwfc_callback");
409+
result.ShouldContain("arg");
410+
result.ShouldContain("onSuccess");
411+
result.ShouldContain("onError");
412+
}
413+
414+
[Fact]
415+
public void GetCallbackEventReference_NullCallbacks_UsesNull()
416+
{
417+
var shim = CreateShim();
418+
419+
var result = shim.GetCallbackEventReference(
420+
new object(), "arg", null!, null, null!, false);
421+
422+
result.ShouldContain("null");
390423
}
391424

392425
#endregion

src/BlazorWebFormsComponents/ClientScriptShim.cs

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Text.RegularExpressions;
44
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Components;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.JSInterop;
78

@@ -192,45 +193,46 @@ public async Task FlushAsync(IJSRuntime jsRuntime)
192193
_scriptIncludes.Clear();
193194
}
194195

195-
// ─── Unsupported Methods ───────────────────────────────────────────
196+
// ─── PostBack / Callback Methods ──────────────────────────────────
196197

197198
/// <summary>
198-
/// Not supported in Blazor. Throws <see cref="NotSupportedException"/>
199-
/// with migration guidance.
199+
/// Returns a JavaScript string that triggers a postback, mirroring the
200+
/// Web Forms <c>ClientScriptManager.GetPostBackEventReference</c> behavior.
201+
/// The returned string calls <c>__doPostBack(eventTarget, eventArgument)</c>,
202+
/// which is defined in <c>bwfc-postback.js</c>.
200203
/// </summary>
201-
/// <exception cref="NotSupportedException">Always thrown.</exception>
204+
/// <param name="control">The control initiating the postback (used to resolve an ID).</param>
205+
/// <param name="argument">The event argument string.</param>
206+
/// <returns>A JavaScript expression string, e.g. <c>__doPostBack('Button1', '')</c>.</returns>
202207
public string GetPostBackEventReference(object control, string argument)
203208
{
204-
throw new NotSupportedException(
205-
"GetPostBackEventReference is not supported in Blazor. " +
206-
"Use @onclick / EventCallback<T> instead. " +
207-
"See: docs/Migration/ClientScriptMigrationGuide.md");
209+
var id = ResolveControlId(control);
210+
return $"__doPostBack('{EscapeJsString(id)}', '{EscapeJsString(argument ?? string.Empty)}')";
208211
}
209212

210213
/// <summary>
211-
/// Not supported in Blazor. Throws <see cref="NotSupportedException"/>
212-
/// with migration guidance.
214+
/// Returns a <c>javascript:</c> URL that triggers a postback, mirroring
215+
/// <c>ClientScriptManager.GetPostBackClientHyperlink</c>.
213216
/// </summary>
214-
/// <exception cref="NotSupportedException">Always thrown.</exception>
217+
/// <param name="control">The control initiating the postback.</param>
218+
/// <param name="argument">The event argument string.</param>
219+
/// <returns>A <c>javascript:__doPostBack(...)</c> URL string.</returns>
215220
public string GetPostBackClientHyperlink(object control, string argument)
216221
{
217-
throw new NotSupportedException(
218-
"GetPostBackClientHyperlink is not supported in Blazor. " +
219-
"Use NavigationManager or <a href=\"...\"> instead. " +
220-
"See: docs/Migration/ClientScriptMigrationGuide.md");
222+
return $"javascript:{GetPostBackEventReference(control, argument)}";
221223
}
222224

223225
/// <summary>
224-
/// Not supported in Blazor. Throws <see cref="NotSupportedException"/>
225-
/// with migration guidance.
226+
/// Returns a JavaScript expression that invokes the BWFC callback bridge,
227+
/// mirroring <c>ClientScriptManager.GetCallbackEventReference</c>.
228+
/// The returned expression calls <c>__bwfc_callback</c> defined in
229+
/// <c>bwfc-postback.js</c>.
226230
/// </summary>
227-
/// <exception cref="NotSupportedException">Always thrown.</exception>
228-
public string GetCallbackEventReference(object control, string argument, string clientCallback, string context, string clientErrorCallback, bool useAsync)
231+
public string GetCallbackEventReference(object control, string argument,
232+
string clientCallback, string context, string clientErrorCallback, bool useAsync)
229233
{
230-
throw new NotSupportedException(
231-
"GetCallbackEventReference is not supported in Blazor. " +
232-
"Use IJSRuntime / EventCallback<T> for JS-to-.NET interop. " +
233-
"See: docs/Migration/ClientScriptMigrationGuide.md");
234+
var id = ResolveControlId(control);
235+
return $"__bwfc_callback('{EscapeJsString(id)}', '{EscapeJsString(argument ?? string.Empty)}', {clientCallback ?? "null"}, '{EscapeJsString(context ?? string.Empty)}', {clientErrorCallback ?? "null"})";
234236
}
235237

236238
// ─── Helpers ───────────────────────────────────────────────────────
@@ -250,4 +252,18 @@ private static string EscapeJsString(string value)
250252
{
251253
return value.Replace("\\", "\\\\").Replace("'", "\\'");
252254
}
255+
256+
/// <summary>
257+
/// Resolves a control reference to an ID string suitable for use in
258+
/// JavaScript postback/callback expressions.
259+
/// Prefers <see cref="BaseWebFormsComponent.ID"/> when available.
260+
/// </summary>
261+
private static string ResolveControlId(object control)
262+
{
263+
if (control is BaseWebFormsComponent bwfc && !string.IsNullOrEmpty(bwfc.ID))
264+
return bwfc.ID;
265+
if (control is ComponentBase)
266+
return control.GetType().Name;
267+
return control?.GetType().Name ?? "unknown";
268+
}
253269
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
3+
namespace BlazorWebFormsComponents;
4+
5+
/// <summary>
6+
/// Event arguments for postback events, mirroring the Web Forms postback model.
7+
/// Contains the <see cref="EventTarget"/> (control ID) and <see cref="EventArgument"/>
8+
/// that were passed to <c>__doPostBack(eventTarget, eventArgument)</c>.
9+
/// </summary>
10+
public class PostBackEventArgs : EventArgs
11+
{
12+
/// <summary>The ID of the control that initiated the postback.</summary>
13+
public string EventTarget { get; }
14+
15+
/// <summary>The argument string associated with the postback.</summary>
16+
public string EventArgument { get; }
17+
18+
public PostBackEventArgs(string eventTarget, string eventArgument)
19+
{
20+
EventTarget = eventTarget;
21+
EventArgument = eventArgument;
22+
}
23+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
3+
namespace BlazorWebFormsComponents;
4+
5+
/// <summary>
6+
/// Compatibility shim for <c>System.Web.UI.ScriptManager</c>.
7+
/// Provides the static <see cref="GetCurrent"/> factory method that Web Forms
8+
/// code uses to obtain a <c>ScriptManager</c> instance from the page, and
9+
/// delegates all <c>RegisterXxx</c> calls to <see cref="ClientScriptShim"/>.
10+
/// </summary>
11+
public class ScriptManagerShim
12+
{
13+
private readonly ClientScriptShim _clientScript;
14+
15+
/// <summary>
16+
/// Initializes a new instance backed by the specified <see cref="ClientScriptShim"/>.
17+
/// </summary>
18+
public ScriptManagerShim(ClientScriptShim clientScript)
19+
{
20+
_clientScript = clientScript ?? throw new ArgumentNullException(nameof(clientScript));
21+
}
22+
23+
/// <summary>
24+
/// Static factory matching the Web Forms <c>ScriptManager.GetCurrent(Page)</c> pattern.
25+
/// Resolves the <see cref="ClientScriptShim"/> from the page/component instance.
26+
/// </summary>
27+
/// <param name="page">
28+
/// A <see cref="BaseWebFormsComponent"/> or <see cref="WebFormsPageBase"/> instance.
29+
/// </param>
30+
/// <returns>A <see cref="ScriptManagerShim"/> wrapping the component's ClientScript.</returns>
31+
/// <exception cref="InvalidOperationException">
32+
/// Thrown when <paramref name="page"/> is not a recognized BWFC type or has no ClientScript.
33+
/// </exception>
34+
public static ScriptManagerShim GetCurrent(object page)
35+
{
36+
if (page is BaseWebFormsComponent component && component.ClientScript != null)
37+
return new ScriptManagerShim(component.ClientScript);
38+
if (page is WebFormsPageBase pageBase && pageBase.ClientScript != null)
39+
return new ScriptManagerShim(pageBase.ClientScript);
40+
41+
throw new InvalidOperationException(
42+
"ScriptManager.GetCurrent() requires a component derived from " +
43+
"BaseWebFormsComponent or WebFormsPageBase with a registered ClientScriptShim.");
44+
}
45+
46+
// ─── Delegated Registration Methods ───────────────────────────────
47+
48+
/// <inheritdoc cref="ClientScriptShim.RegisterStartupScript(Type, string, string, bool)"/>
49+
public void RegisterStartupScript(Type type, string key, string script, bool addScriptTags)
50+
=> _clientScript.RegisterStartupScript(type, key, script, addScriptTags);
51+
52+
/// <summary>
53+
/// Overload accepting a control reference (ignored) to match the Web Forms
54+
/// <c>ScriptManager.RegisterStartupScript(Control, Type, String, String, Boolean)</c> signature.
55+
/// </summary>
56+
public void RegisterStartupScript(object control, Type type, string key, string script, bool addScriptTags)
57+
=> _clientScript.RegisterStartupScript(type, key, script, addScriptTags);
58+
59+
/// <inheritdoc cref="ClientScriptShim.RegisterClientScriptBlock(Type, string, string, bool)"/>
60+
public void RegisterClientScriptBlock(Type type, string key, string script, bool addScriptTags)
61+
=> _clientScript.RegisterClientScriptBlock(type, key, script, addScriptTags);
62+
63+
/// <inheritdoc cref="ClientScriptShim.RegisterClientScriptInclude(Type, string, string)"/>
64+
public void RegisterClientScriptInclude(Type type, string key, string url)
65+
=> _clientScript.RegisterClientScriptInclude(type, key, url);
66+
}

src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public static IServiceCollection AddBlazorWebFormsComponents(this IServiceCollec
4343
services.AddScoped<ServerShim>();
4444
services.AddScoped<CacheShim>();
4545
services.AddScoped<ClientScriptShim>();
46+
services.AddScoped(sp => new ScriptManagerShim(sp.GetRequiredService<ClientScriptShim>()));
4647

4748
var options = new BlazorWebFormsComponentsOptions();
4849
configure?.Invoke(options);

0 commit comments

Comments
 (0)