ClientScript Migration Patterns

This sample demonstrates how to migrate common ASP.NET Web Forms Page.ClientScript and ScriptManager JavaScript patterns to Blazor using IJSRuntime, component lifecycle events, and native Blazor event handling.

Migration Principle

Prefer IJSRuntime over wrapper shims. Blazor's component model replaces the Web Forms postback lifecycle — use OnAfterRenderAsync for DOM-touching JavaScript and @onclick / EventCallback for event handling.


1. Startup Script Migration

Before (Web Forms)

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        Page.ClientScript.RegisterStartupScript(
            this.GetType(),
            "InitializeUI",
            "initializePage('Page loaded!');",
            addScriptTags: true);
    }
}

After (Blazor)

@inject IJSRuntime JS

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("initializePage",
                "Page loaded via IJSRuntime!");
        }
    }
}
Live Demo — Startup Script

The message below was set by IJSRuntime calling initializePage() during OnAfterRenderAsync(firstRender: true):

⏳ Waiting for OnAfterRenderAsync...

Key point: OnAfterRenderAsync with if (firstRender) is the Blazor equivalent of if (!IsPostBack) + RegisterStartupScript. It runs after the component's DOM is available.


2. Script Include Migration

Before (Web Forms)

protected void Page_Load(object sender, EventArgs e)
{
    Page.ClientScript.RegisterClientScriptInclude(
        "customScript",
        ResolveUrl("~/Scripts/custom.js"));
}

After (Blazor)

<!-- Option A: Static tag in App.razor or layout -->
<script src="js/clientscript-demo.js"></script>

<!-- Option B: Dynamic import via IJSRuntime -->
@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Dynamic module import
            await JS.InvokeVoidAsync("import", "./js/my-module.js");
        }
    }
}
Live Demo — Script Include

This page includes js/clientscript-demo.js via a <script> tag in the layout. The startup script demo above calls a function defined in that file.

Script include status: Loaded ✓

Key point: Web Forms' RegisterClientScriptInclude() becomes a static <script src="..."> tag. For conditional loading, use IJSRuntime.InvokeAsync("import", ...).


3. Focus Management (replacing ScriptManager.SetFocus)

Before (Web Forms)

protected void Page_Load(object sender, EventArgs e)
{
    // Using ScriptManager
    ScriptManager.GetCurrent(Page).SetFocus(txtSearch);

    // Or using ClientScript
    Page.SetFocus("txtSearch");
}

After (Blazor)

// Option A: ElementReference + FocusAsync (preferred)
<input @ref="_searchBox" type="text" />

@code {
    private ElementReference _searchBox;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await _searchBox.FocusAsync();
        }
    }
}
Live Demo — Focus Management

Click the button to set focus to the search box using ElementReference.FocusAsync():

Focus set count: 0

Key point: ElementReference.FocusAsync() is the direct replacement for ScriptManager.SetFocus(). No JavaScript needed — it's built into Blazor.


4. Event Handler Migration (replacing GetPostBackEventReference)

Before (Web Forms)

// IPostBackEventHandler implementation
public class MyControl : UserControl, IPostBackEventHandler
{
    public void RaisePostBackEvent(string eventArgument)
    {
        if (eventArgument == "delete")
            OnDeleteRequested?.Invoke(this, EventArgs.Empty);
    }

    // Generate client-side postback trigger
    string script = Page.ClientScript.GetPostBackEventReference(
        new PostBackOptions(this, "delete"));
    // Produces: __doPostBack('ctl00$MyControl','delete')
}

After (Blazor)

// Direct event binding — no postback needed
<button @onclick="HandleDelete">Delete</button>
<button @onclick='() => HandleAction("archive")'>Archive</button>

@code {
    [Parameter]
    public EventCallback OnDeleteRequested { get; set; }

    private async Task HandleDelete()
    {
        await OnDeleteRequested.InvokeAsync();
    }

    private async Task HandleAction(string action)
    {
        _lastAction = action;
    }
}
Live Demo — Event Handlers

In Web Forms, events went through __doPostBack() and IPostBackEventHandler. In Blazor, events are direct method calls:

Key point: Blazor's @onclick and EventCallback completely replace __doPostBack() and GetPostBackEventReference(). Events are strongly-typed C# method calls — no string-based event arguments needed.


5. DOM Manipulation (replacing RegisterClientScriptBlock)

Before (Web Forms)

Page.ClientScript.RegisterClientScriptBlock(
    this.GetType(),
    "HighlightRow",
    "function highlightRow(id) { " +
    "  document.getElementById(id).style.backgroundColor = '#ffc'; " +
    "}",
    addScriptTags: true);

After (Blazor)

// Define function in a .js file, call via IJSRuntime
await JS.InvokeVoidAsync("highlightElement",
    "my-element", "#ffffcc");

// Or better — use Blazor's built-in binding:
<div style="@(_isHighlighted ? "background:#ffc" : "")">
    Content
</div>
Live Demo — DOM Manipulation

Two approaches: JS interop for complex DOM work, or Blazor binding for simple styling:

Via IJSRuntime:
This box is highlighted via IJSRuntime
Via Blazor Binding (Preferred):
This box is highlighted via Blazor @bind

Key point: For simple styling, use Blazor's binding (style="@(...)"). Reserve IJSRuntime for cases where you need to call third-party libraries or manipulate the DOM in ways Blazor can't express.


Source Code

Complete @code block for this page:

@inject IJSRuntime JS

@code {
    private ElementReference _searchBox;
    private int _focusCount;
    private bool _isHighlighted;
    private string _scriptIncludeStatus = "Loaded ✓";
    private List<string> _actionLog = new();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Startup script — equivalent of RegisterStartupScript
            await JS.InvokeVoidAsync("initializePage", "Page loaded via IJSRuntime!");
        }
    }

    private async Task SetFocusToSearch()
    {
        // Equivalent of ScriptManager.SetFocus()
        await _searchBox.FocusAsync();
        _focusCount++;
    }

    private void HandleDelete()
    {
        _actionLog.Insert(0, $"[{DateTime.Now:HH:mm:ss}] 🗑️ Delete requested");
    }

    private void HandleAction(string action)
    {
        _actionLog.Insert(0, $"[{DateTime.Now:HH:mm:ss}] Action: {action}");
    }

    private async Task HighlightViaJs()
    {
        await JS.InvokeVoidAsync("highlightElement", "js-highlight-target", "#fff3cd");
    }

    private void ToggleHighlight()
    {
        _isHighlighted = !_isHighlighted;
    }
}

Migration Quick Reference

Web Forms Pattern Blazor Equivalent Notes
RegisterStartupScript() OnAfterRenderAsync + IJSRuntime Guard with if (firstRender)
RegisterClientScriptInclude() <script src="..."> in layout Static includes go in App.razor
RegisterClientScriptBlock() Named JS function + IJSRuntime Move inline JS to .js files
ScriptManager.SetFocus() ElementReference.FocusAsync() No JS interop needed
GetPostBackEventReference() @onclick / EventCallback Direct method calls replace __doPostBack
IPostBackEventHandler [Parameter] EventCallback Strongly-typed events replace string arguments
if (!IsPostBack) if (firstRender) In OnAfterRenderAsync