ClientScript Shim: Zero-Rewrite Migration
Your Web Forms code-behind calls ClientScript.RegisterStartupScript(...) today.
With the ClientScriptShim, those same calls work in Blazor — no rewrite needed.
💡 Recommended Migration Path
The ClientScriptShim lets you keep your existing
Page.ClientScript calls intact. Just remove the Page. prefix and the
shim handles everything — queuing, deduplication, and execution via IJSRuntime.
1. RegisterStartupScript — Same Code, Works in Blazor
Web Forms Code-Behind (unchanged)
protected void btnGreet_Click(object sender, EventArgs e)
{
ClientScript.RegisterStartupScript(
GetType(),
"greeting",
"alert('Hello from the server!');",
true);
}Blazor with ClientScriptShim (identical!)
private void HandleGreet()
{
ClientScript.RegisterStartupScript(
GetType(),
"greeting",
"alert('Hello from the server!');",
true);
// FlushAsync() runs automatically in OnAfterRenderAsync
}Live Demo — Startup Script
Click the button to queue a startup script. It executes after the next render cycle,
just like Web Forms' RegisterStartupScript.
Key point: The ClientScript.RegisterStartupScript() call is
identical to what you'd write in Web Forms code-behind. The shim queues the script
and flushes it via IJSRuntime during OnAfterRenderAsync.
2. RegisterClientScriptInclude — Dynamic Script Loading
Web Forms Code-Behind
protected void Page_Load(object sender, EventArgs e)
{
ClientScript.RegisterClientScriptInclude(
"moment",
"https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js");
}Blazor with ClientScriptShim
private void LoadMomentJs()
{
ClientScript.RegisterClientScriptInclude(
"moment",
"https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js");
}Live Demo — Script Include
Click to dynamically load the Moment.js library. The shim appends a <script> tag to the document head — the same effect as RegisterClientScriptInclude in Web Forms.
Include registered: Not yet
Key point: The API call is the same. The shim dynamically injects the
<script src="..."> tag — no need for layout file changes or static references.
3. Deduplication — Same Key Runs Once
Web Forms Pattern
// Web Forms automatically deduplicates by key:
if (!ClientScript.IsStartupScriptRegistered(GetType(), "init"))
{
ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Initialized!');", true);
}
// Registering the same key again is a no-op
ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Duplicate — ignored!');", true);Blazor — Identical Behavior
// Same deduplication logic works in Blazor:
if (!ClientScript.IsStartupScriptRegistered(GetType(), "init"))
{
ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Initialized!');", true);
}
// Same key = same no-op behavior
ClientScript.RegisterStartupScript(
GetType(), "init",
"console.log('Duplicate — ignored!');", true);Live Demo — Deduplication
Click the button multiple times. The IsStartupScriptRegistered check prevents
duplicate registration, and even without the check, the shim uses dictionary-based keys
so the same key overwrites rather than duplicates.
Registration attempts: 0
Was already registered: No (registered)
Key point: IsStartupScriptRegistered() works exactly like Web
Forms — check before registering to avoid duplicate execution. The shim's internal dictionary
also deduplicates by key, matching Web Forms behavior.
4. Side-by-Side: Manual IJSRuntime vs. ClientScriptShim
Compare the two migration approaches. The left column shows the rewrite you'd need
with direct IJSRuntime. The right column shows the zero-rewrite shim approach.
Every ClientScript call must be found, understood, and rewritten:
@inject IJSRuntime JS
@code {
// ❌ Must rewrite: RegisterStartupScript → OnAfterRenderAsync
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("eval",
"document.getElementById('msg').innerText = 'Ready!';");
}
}
// ❌ Must rewrite: RegisterClientScriptInclude → dynamic import
private async Task LoadScript()
{
await JS.InvokeVoidAsync("eval",
"(function(){var s=document.createElement('script');" +
"s.src='lib.js';document.head.appendChild(s);})()");
}
// ❌ Must rewrite: IsStartupScriptRegistered → manual tracking
private HashSet<string> _registered = new();
private async Task RegisterOnce(string key, string script)
{
if (!_registered.Contains(key))
{
_registered.Add(key);
await JS.InvokeVoidAsync("eval", script);
}
}
}Your Web Forms code-behind stays the same:
@inject ClientScriptShim ClientScript
@code {
// ✅ Unchanged: RegisterStartupScript works as-is
protected override void OnInitialized()
{
ClientScript.RegisterStartupScript(
GetType(), "init",
"document.getElementById('msg').innerText = 'Ready!';",
true);
}
// ✅ Unchanged: RegisterClientScriptInclude works as-is
private void LoadScript()
{
ClientScript.RegisterClientScriptInclude(
"myLib", "lib.js");
}
// ✅ Unchanged: IsStartupScriptRegistered works as-is
private void RegisterOnce(string key, string script)
{
if (!ClientScript.IsStartupScriptRegistered(
GetType(), key))
{
ClientScript.RegisterStartupScript(
GetType(), key, script, true);
}
}
}Live Demo — Both Approaches, Same Result
Click each button to see the same effect achieved two different ways:
Via IJSRuntime (manual):
Via ClientScriptShim (zero-rewrite):
⏳ Click a button above...
Bottom line: Both produce the same result, but the shim approach preserves your original code-behind logic. Less rewriting = fewer bugs = faster migration.
Migration Quick Reference
| Web Forms API | ClientScriptShim Method | Change Needed |
|---|---|---|
Page.ClientScript.RegisterStartupScript(...) |
ClientScript.RegisterStartupScript(...) |
Remove Page. prefix only |
Page.ClientScript.RegisterClientScriptBlock(...) |
ClientScript.RegisterClientScriptBlock(...) |
Remove Page. prefix only |
Page.ClientScript.RegisterClientScriptInclude(...) |
ClientScript.RegisterClientScriptInclude(...) |
Remove Page. prefix only |
Page.ClientScript.IsStartupScriptRegistered(...) |
ClientScript.IsStartupScriptRegistered(...) |
Remove Page. prefix only |
Page.ClientScript.GetPostBackEventReference(...) |
❌ Not supported | Use @onclick / EventCallback |
Page.ClientScript.GetCallbackEventReference(...) |
❌ Not supported | Use IJSRuntime JS-to-.NET interop |
How to Get the Shim
- Components inheriting
BaseWebFormsComponent: Access it asClientScriptproperty — it's built in. - Razor pages or non-BWFC components: Inject it with
@inject ClientScriptShim ClientScript - Service registration: Call
builder.Services.AddBlazorWebFormsComponents()inProgram.cs
Source Code
Complete @code block for this page:
@inject BlazorWebFormsComponents.ClientScriptShim ClientScript
@inject IJSRuntime JS
@code {
private bool _momentRegistered;
private string _momentStatus = "";
private int _dedupAttempts;
private bool _wasAlreadyRegistered;
private string _dedupMessage = "";
// Demo 1: RegisterStartupScript — identical to Web Forms code-behind
private void HandleGreet()
{
ClientScript.RegisterStartupScript(
GetType(), "greeting",
"alert('Hello from the ClientScriptShim!');", true);
}
// Demo 2: RegisterClientScriptInclude — same API as Web Forms
private void LoadMomentJs()
{
ClientScript.RegisterClientScriptInclude(
"moment",
"https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js");
_momentRegistered = ClientScript.IsClientScriptIncludeRegistered("moment");
}
// Demo 3: Deduplication with IsStartupScriptRegistered
private void RegisterWithDedup()
{
_dedupAttempts++;
_wasAlreadyRegistered = ClientScript.IsStartupScriptRegistered(GetType(), "dedup-demo");
if (!_wasAlreadyRegistered)
{
ClientScript.RegisterStartupScript(GetType(), "dedup-demo",
"document.getElementById('dedup-output').innerText = " +
"'✅ Script executed (first time only!)';", true);
_dedupMessage = "Registered and queued for execution.";
}
else
{
_dedupMessage = $"Already registered — skipped (attempt #{_dedupAttempts}).";
}
}
// Demo 4: Side-by-side comparison handlers
private async Task SetMessageViaJs()
{
await JS.InvokeVoidAsync("eval",
"document.getElementById('comparison-output').innerText = " +
"'✅ Message set via IJSRuntime (manual rewrite)';");
}
private void SetMessageViaShim()
{
ClientScript.RegisterStartupScript(GetType(), "comparison",
"document.getElementById('comparison-output').innerText = " +
"'✅ Message set via ClientScriptShim (zero rewrite!)';", true);
}
// Flush queued scripts after each render
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await ClientScript.FlushAsync(JS);
}
// Reset helpers
private void CheckMomentLoaded()
{
_momentRegistered = ClientScript.IsClientScriptIncludeRegistered("moment");
_momentStatus = _momentRegistered
? "Moment.js include is registered ✓"
: "Moment.js not yet registered";
}
private void ResetDedup()
{
_dedupAttempts = 0;
_wasAlreadyRegistered = false;
_dedupMessage = "";
}
}