Technology: ASP.NET Core 8, Blazor Web App, .NET 8 Demo Application: WeatherApp (OpenWeatherMap API integration) Level: Intermediate | Estimated Duration: ~2h30
Table of Contents
- Introduction to JavaScript Interop
- Calling JavaScript from C#
- Calling C# from JavaScript
- Error Handling and Debugging
- Complete Application — WeatherApp
- Advanced Patterns
- Best Practices and Common Pitfalls
- Practical Exercises
- Glossary
1. Introduction to JavaScript Interop
1.1 Why JavaScript Interop?
Blazor allows you to build web applications with C#, but the browser remains a fundamentally JavaScript environment. Some features are only available via JavaScript:
flowchart LR
subgraph CSharp["What C# can do alone"]
C1[Business logic]
C2[HTTP API calls]
C3[Validation]
C4[Model manipulation]
end
subgraph JavaScript["What requires JavaScript Interop"]
J1["Browser APIs\n(Geolocation, Camera, Clipboard)"]
J2["Existing JS libraries\n(Chart.js, Leaflet, Three.js)"]
J3["Direct DOM manipulation\n(Focus, complex animations)"]
J4["Local storage\n(localStorage, sessionStorage)"]
J5["Recent browser APIs\n(Notifications, WebRTC, etc.)"]
end
Bridge[JavaScript\nInterop\nIJSRuntime] --> CSharp
Bridge --> JavaScript
1.2 How JavaScript Interop Works
sequenceDiagram
participant Blazor as Blazor Component (C#)
participant JSRuntime as IJSRuntime
participant JS as JavaScript
Note over Blazor,JS: C# → JavaScript call
Blazor->>JSRuntime: InvokeVoidAsync("functionName", args)
JSRuntime->>JS: Call via interop channel
JS->>JS: Execute function
JS-->>JSRuntime: Result (optional)
JSRuntime-->>Blazor: Returned value
Note over Blazor,JS: JavaScript → C# call
JS->>JS: DotNet.invokeMethodAsync("Assembly", "MethodName")
JS-->>Blazor: Call to [JSInvokable] method
Blazor->>Blazor: Execute C# method
Blazor-->>JS: Result
1.3 Key Limitations
| Limitation | Description | Workaround |
|---|---|---|
| Timing | The DOM must be rendered before calling JS | Use OnAfterRenderAsync(firstRender) |
| Coupling | Mixing C# + JS = difficult to maintain code | Centralize JS calls in services |
| Security | External JS code can be malicious | Validate sources, CSP headers |
| Debugging | Errors crossing both languages | Use debugging tools in both environments |
| Performance | Each call = one interop round-trip | Minimize calls, use modules |
2. Calling JavaScript from C#
2.1 IJSRuntime — Fundamentals
IJSRuntime is the Blazor service for invoking JavaScript from C#. It is injected like any other service:
@inject IJSRuntime JSRuntime
@* Or in code-behind: *@
@code {
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
}
Main methods:
| Method | Return | Use case |
|---|---|---|
InvokeVoidAsync(name, args) | ValueTask | Call with no return value |
InvokeAsync<T>(name, args) | ValueTask<T> | Call with return value |
InvokeVoid(name, args) | void | Synchronous (not recommended) |
Invoke<T>(name, args) | T | Synchronous with return (not recommended) |
Recommendation: Always use the
asyncversions to avoid blocking the render thread.
2.2 Global JavaScript Functions
JavaScript exposes many global functions accessible directly:
// ─── ALERTS AND DIALOGS ──────────────────────────────────────
// alert()
await JSRuntime.InvokeVoidAsync("alert", "Message to the user");
// confirm() — returns bool
var confirmed = await JSRuntime.InvokeAsync<bool>(
"confirm", "Are you sure you want to delete?");
if (confirmed) { /* ... */ }
// prompt() — returns string?
var userInput = await JSRuntime.InvokeAsync<string?>(
"prompt", "Enter your name:", "Default value");
// ─── CONSOLE ─────────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("console.log", "Debug from C#");
await JSRuntime.InvokeVoidAsync("console.error", "Error from C#");
await JSRuntime.InvokeVoidAsync("console.warn", "Warning message");
// ─── NAVIGATION ──────────────────────────────────────────────
// Redirect
await JSRuntime.InvokeVoidAsync("window.location.replace", "https://example.com");
// Open in a new tab
await JSRuntime.InvokeVoidAsync("window.open",
"https://example.com", "_blank");
// ─── CLIPBOARD ───────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText",
"Text copied to clipboard");
// ─── LOCALSTORAGE ────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", "dark");
var theme = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "theme");
await JSRuntime.InvokeVoidAsync("localStorage.removeItem", "theme");
// ─── FOCUS ───────────────────────────────────────────────────
// Focus an element by ID
await JSRuntime.InvokeVoidAsync("document.getElementById('myInput')?.focus");
// Or using ElementReference (recommended)
await JSRuntime.InvokeVoidAsync("eval",
"document.getElementById('searchInput').focus()");
// ─── SCROLL ──────────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("window.scrollTo", 0, 0); // Top of page
2.3 Custom JavaScript Functions
Recommended structure:
wwwroot/
├── js/
│ ├── weather.js ← ES6 module for weather
│ ├── notifications.js ← ES6 module for notifications
│ └── mapUtils.js ← ES6 module for maps
└── index.html
Two approaches:
Option 1 — Global functions (window.xxx):
// wwwroot/js/custom.js — Referenced in index.html
// <script src="js/custom.js"></script>
window.checkWeather = function(cityName) {
// JavaScript logic
return `Weather for ${cityName}`;
};
window.showToast = function(message, type = 'info') {
// Create a Bootstrap toast
const toast = document.createElement('div');
toast.className = `toast bg-${type}`;
toast.textContent = message;
document.getElementById('toastContainer').appendChild(toast);
new bootstrap.Toast(toast).show();
};
// Call from C#
var result = await JSRuntime.InvokeAsync<string>("checkWeather", "London");
await JSRuntime.InvokeVoidAsync("showToast", "Operation successful!", "success");
Option 2 — ES6 Modules (recommended):
// wwwroot/js/weather.js — ES6 module (no window.xxx)
export async function checkWeather(cityName, apiKey) {
const url = `https://api.openweathermap.org/data/2.5/weather` +
`?q=${cityName}&appid=${apiKey}&units=metric&lang=en`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return await response.json();
}
export function showNotification(title, message, type = 'info') {
// Create notification via browser API
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body: message });
}
}
// Load and use an ES6 module in C#
private IJSObjectReference? _weatherModule;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Import the ES6 module (relative to wwwroot)
_weatherModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/weather.js");
}
}
private async Task<WeatherData?> GetWeatherAsync(string city)
{
if (_weatherModule is null) return null;
return await _weatherModule.InvokeAsync<WeatherData>("checkWeather", city, _apiKey);
}
// Always release modules
public async ValueTask DisposeAsync()
{
if (_weatherModule is not null)
await _weatherModule.DisposeAsync();
}
2.4 ElementReference — Reference to a DOM Element
@* Access a DOM element directly *@
<input @ref="_searchInput" type="text" @bind="_searchTerm" class="form-control" />
<button @onclick="FocusSearch">Focus search</button>
@code {
private ElementReference _searchInput;
private string _searchTerm = string.Empty;
private async Task FocusSearch()
{
// Give focus to the DOM element
await _searchInput.FocusAsync();
// Or via JSRuntime for more control:
await JSRuntime.InvokeVoidAsync("focusElement", _searchInput);
}
}
// JS function receiving an ElementReference
window.focusElement = function(element) {
element.focus();
element.select(); // Also select the text
};
2.5 Asynchronous Calls with Return Values
// Retrieve data from a third-party API via JavaScript
private WeatherResponse? _weatherData;
private async Task LoadWeatherAsync(string cityName)
{
_isLoading = true;
_error = null;
try
{
// JavaScript call returning a complex object
// The JSON is automatically deserialized to the requested C# type
_weatherData = await JSRuntime.InvokeAsync<WeatherResponse>(
"checkWeather", cityName);
}
catch (JSException jsEx)
{
// JavaScript error
_error = $"JavaScript error: {jsEx.Message}";
}
catch (Exception ex)
{
_error = $"Unexpected error: {ex.Message}";
}
finally
{
_isLoading = false;
}
}
DTOs for the JavaScript response:
// DTOs/WeatherResponse.cs — Matches the JSON structure of the API
public class WeatherResponse
{
[JsonPropertyName("name")]
public string CityName { get; set; } = string.Empty;
[JsonPropertyName("coord")]
public Coordinates? Coordinates { get; set; }
[JsonPropertyName("weather")]
public WeatherInfo[]? Weather { get; set; }
[JsonPropertyName("main")]
public MainMetrics? Main { get; set; }
[JsonPropertyName("sys")]
public SystemInfo? Sys { get; set; }
}
public class WeatherInfo
{
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
}
public class MainMetrics
{
[JsonPropertyName("temp")]
public double Temperature { get; set; }
[JsonPropertyName("feels_like")]
public double FeelsLike { get; set; }
[JsonPropertyName("humidity")]
public int Humidity { get; set; }
}
3. Calling C# from JavaScript
3.1 Static Methods with [JSInvokable]
To call a C# method from JavaScript, two conditions must be met:
- The method must be decorated with
[JSInvokable] - Use
DotNet.invokeMethodAsync("AssemblyName", "MethodName", ...args)in JavaScript
// In a Blazor component
[JSInvokable]
public static async Task ShowFavoriteButton()
{
// This method can be called from JavaScript
// ⚠️ Must be STATIC for invocation without an instance
_isShowFavorite = true;
// Note: StateHasChanged() does not work in static methods
}
[JSInvokable]
public static async Task UpdateCityName(string cityName)
{
_selectedCity = cityName;
}
// JavaScript — Call to a static C# method
async function afterWeatherLoaded(cityName) {
// DotNet.invokeMethodAsync("AssemblyName", "MethodName", ...args)
await DotNet.invokeMethodAsync("WeatherApp", "ShowFavoriteButton");
await DotNet.invokeMethodAsync("WeatherApp", "UpdateCityName", cityName);
}
// Synchronous call (less recommended)
DotNet.invokeMethod("WeatherApp", "ShowFavoriteButton");
3.2 Instance Methods with DotNetObjectReference
To call non-static methods on a specific instance:
// Components/Pages/Home.razor.cs
public partial class Home : IAsyncDisposable
{
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
// Reference to pass to JavaScript
private DotNetObjectReference<Home>? _dotNetRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Create the reference
_dotNetRef = DotNetObjectReference.Create(this);
// Pass the reference to JavaScript
await JSRuntime.InvokeVoidAsync("setupInterop", _dotNetRef);
}
}
// Method callable from JavaScript (non-static)
[JSInvokable]
public async Task FavoriteCity(string cityName)
{
// Save to localStorage
await JSRuntime.InvokeVoidAsync("localStorage.setItem",
$"favorite_{cityName}", cityName);
// Update Blazor UI
_favoriteCities.Add(cityName);
StateHasChanged(); // ← Possible because it's an instance method
}
[JSInvokable]
public void OnLocationReceived(double latitude, double longitude)
{
_userLocation = (latitude, longitude);
StateHasChanged();
}
// IMPORTANT: Release the reference
public async ValueTask DisposeAsync()
{
_dotNetRef?.Dispose();
}
}
// wwwroot/js/app.js
let dotNetRef = null;
// Receives the reference from C#
window.setupInterop = function(ref) {
dotNetRef = ref;
// Configure event listeners that call C#
document.getElementById('favoriteBtn')?.addEventListener('click', async () => {
const city = document.getElementById('cityInput').value;
await dotNetRef.invokeMethodAsync('FavoriteCity', city);
});
};
// Geolocation — calls C# when position is known
window.requestUserLocation = function() {
if (!navigator.geolocation) {
console.warn("Geolocation not available");
return;
}
navigator.geolocation.getCurrentPosition(
async (position) => {
await dotNetRef?.invokeMethodAsync(
'OnLocationReceived',
position.coords.latitude,
position.coords.longitude
);
},
(error) => console.error("Geolocation error:", error)
);
};
4. Error Handling and Debugging
4.1 Detailed Errors in Development
// Program.cs — Configuration by environment
if (app.Environment.IsDevelopment())
{
// Detailed error page with stack trace
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
// Generic error page in production
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
4.2 Error Boundaries — Isolating Errors
@* Wrapping a component that might crash *@
<ErrorBoundary>
<ChildContent>
@* If this component throws an exception, only this area is affected *@
<WeatherWidget City="@_city" ApiKey="@_apiKey" />
</ChildContent>
<ErrorContent Context="ex">
@* Content displayed on error *@
<div class="alert alert-warning">
<strong>⚠️ Weather error</strong>
Unable to load data.
@if (IsDevelopment)
{
@* In development, show details *@
<details class="mt-2">
<summary>Technical details</summary>
<pre class="text-danger small">@ex.Message</pre>
</details>
}
<button class="btn btn-sm btn-outline-secondary mt-2" @onclick="Retry">
Retry
</button>
</div>
</ErrorContent>
</ErrorBoundary>
@* ErrorBoundary with ErrorBoundaryBase for more control *@
@inherits ErrorBoundaryBase
@if (CurrentException is null)
{
@ChildContent
}
else
{
<div class="error-container">
<h4>An error occurred</h4>
<button @onclick="Recover">Recover</button>
</div>
}
@code {
protected override Task OnErrorAsync(Exception exception)
{
// Log the error
Logger.LogError(exception, "Error in ErrorBoundary");
return base.OnErrorAsync(exception);
}
}
4.3 Global Exception Handler
// Helpers/GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
=> _logger = logger;
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An internal error occurred",
Detail = httpContext.RequestServices
.GetRequiredService<IWebHostEnvironment>().IsDevelopment()
? exception.ToString()
: null
};
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true; // Exception handled
}
}
// Program.cs — Registration
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
// In the pipeline:
app.UseExceptionHandler();
4.4 Breakpoints and Debugging Types
Types of breakpoints in Visual Studio:
| Type | Description | Usage |
|---|---|---|
| Line breakpoint | Pause at a specific line | Variable inspection |
| Conditional breakpoint | Pause only if condition is true | Ex: cityId == 1 |
| Exception breakpoint | Pause when an exception is thrown | Find error source |
| Hit count breakpoint | Pause after N executions | Bugs in loops |
| Data breakpoint | Pause when a variable changes | Track state changes |
Conditional breakpoint example:
// Set a breakpoint at this line
// Then right-click → Conditions → cityName == "London"
var weatherData = await _weatherModule!.InvokeAsync<WeatherResponse>(
"checkWeather", cityName); // ← Conditional breakpoint here
4.5 Logging with Serilog
// Install packages:
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.File
// dotnet add package Serilog.Sinks.Console
// Program.cs — Serilog configuration
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/weather-app-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
// Usage in a component
public partial class WeatherPage
{
[Inject] private ILogger<WeatherPage> Logger { get; set; } = default!;
private async Task LoadWeather(string city)
{
Logger.LogInformation("Loading weather for {City}", city);
try
{
var data = await WeatherService.GetAsync(city);
Logger.LogDebug("Weather received: {Temp}°C in {City}", data.Temperature, city);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "Network error loading weather for {City}", city);
_error = "Unable to load weather. Check your connection.";
}
}
}
5. Complete Application — WeatherApp
5.1 Application Architecture
flowchart TB
subgraph Blazor["Blazor WeatherApp"]
Home[Home.razor\nMain page]
Favorites[Favorites.razor\nFavorite cities]
NavMenu[NavMenu.razor]
end
subgraph JS["JavaScript (wwwroot/js/)"]
Custom[custom.js\nmain functions]
Storage[storage.js\nlocalStorage helpers]
end
subgraph External["External APIs"]
OWM[OpenWeatherMap API\nhttps://api.openweathermap.org]
end
Home -->|"IJSRuntime"| Custom
Custom -->|"fetch()"| OWM
OWM -->|"JSON response"| Custom
Custom -->|"WeatherResponse"| Home
Favorites -->|"localStorage"| Storage
5.2 Complete Implementation
@* Components/Pages/Home.razor *@
@page "/weather"
@rendermode InteractiveServer
@inject IJSRuntime JSRuntime
@inject ILogger<Home> Logger
@implements IAsyncDisposable
<PageTitle>Weather — WeatherApp</PageTitle>
<div class="weather-container">
<div class="search-section">
<h1>🌤️ Real-Time Weather</h1>
<div class="input-group mb-3">
<input type="text"
class="form-control form-control-lg"
placeholder="Enter a city name..."
@bind="_cityName"
@bind:event="oninput"
@onkeyup="@(e => { if (e.Key == "Enter") _ = LoadWeatherAsync(); })" />
<button class="btn btn-primary btn-lg" @onclick="LoadWeatherAsync"
disabled="@(_isLoading || string.IsNullOrWhiteSpace(_cityName))">
@if (_isLoading)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
Search
</button>
</div>
</div>
@if (_error is not null)
{
<div class="alert alert-danger">@_error</div>
}
@if (_weatherData is not null)
{
<div class="weather-card card shadow">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h2>@_weatherData.CityName, @_weatherData.Sys?.Country</h2>
<p class="text-muted">@_weatherData.Weather?[0].Description</p>
</div>
@if (_weatherData.Weather?[0].Icon is not null)
{
<img src="https://openweathermap.org/img/wn/@(_weatherData.Weather[0].Icon)@@2x.png"
alt="Weather icon" width="80" />
}
</div>
<div class="row g-3 mt-2">
<div class="col-md-3 text-center">
<div class="fs-1 fw-bold text-primary">
@(_weatherData.Main?.Temperature.ToString("F1"))°C
</div>
<small>Temperature</small>
</div>
<div class="col-md-3 text-center">
<div class="fs-3">@(_weatherData.Main?.FeelsLike.ToString("F1"))°C</div>
<small>Feels like</small>
</div>
<div class="col-md-3 text-center">
<div class="fs-3">@_weatherData.Main?.Humidity%</div>
<small>Humidity</small>
</div>
</div>
@if (_isShowFavorite)
{
<button class="btn btn-outline-warning mt-3" @onclick="FavoriteCurrentCity">
⭐ Favorite — @_cityName
</button>
}
</div>
</div>
}
</div>
// Components/Pages/Home.razor.cs
public partial class Home : IAsyncDisposable
{
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
[Inject] private ILogger<Home> Logger { get; set; } = default!;
private IJSObjectReference? _weatherModule;
private DotNetObjectReference<Home>? _dotNetRef;
private string _cityName = string.Empty;
private WeatherResponse? _weatherData;
private bool _isLoading;
private bool _isShowFavorite;
private string? _error;
private const string ApiKey = "your_openweathermap_api_key";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_weatherModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/weather.js");
_dotNetRef = DotNetObjectReference.Create(this);
await JSRuntime.InvokeVoidAsync("setupWeatherCallbacks", _dotNetRef);
}
}
private async Task LoadWeatherAsync()
{
if (string.IsNullOrWhiteSpace(_cityName)) return;
_isLoading = true;
_error = null;
_isShowFavorite = false;
try
{
Logger.LogInformation("Loading weather for {City}", _cityName);
_weatherData = await _weatherModule!.InvokeAsync<WeatherResponse>(
"checkWeather", _cityName, ApiKey);
_isShowFavorite = _weatherData is not null;
Logger.LogDebug("Weather loaded: {Temp}°C in {City}",
_weatherData?.Main?.Temperature, _cityName);
}
catch (JSException ex) when (ex.Message.Contains("404"))
{
_error = $"City '{_cityName}' not found. Check the spelling.";
}
catch (JSException ex)
{
_error = $"API error: {ex.Message}";
Logger.LogError(ex, "JS error loading weather for {City}", _cityName);
}
catch (Exception ex)
{
_error = "An unexpected error occurred.";
Logger.LogError(ex, "Unexpected error for {City}", _cityName);
}
finally
{
_isLoading = false;
}
}
private async Task FavoriteCurrentCity()
{
if (string.IsNullOrWhiteSpace(_cityName)) return;
await JSRuntime.InvokeVoidAsync("localStorage.setItem",
$"fav_{_cityName}", _cityName);
await JSRuntime.InvokeVoidAsync("alert", $"'{_cityName}' added to favorites!");
}
[JSInvokable]
public static void ShowFavoriteButton() => _isShowFavStatic = true;
private static bool _isShowFavStatic;
public async ValueTask DisposeAsync()
{
if (_weatherModule is not null)
await _weatherModule.DisposeAsync();
_dotNetRef?.Dispose();
}
}
6. Advanced Patterns
6.1 Abstraction Service for JavaScript
Encapsulate JS calls in a dedicated service:
// Services/IStorageService.cs
public interface IStorageService
{
Task<T?> GetItemAsync<T>(string key);
Task SetItemAsync<T>(string key, T value);
Task RemoveItemAsync(string key);
Task ClearAsync();
}
// Services/LocalStorageService.cs
public class LocalStorageService : IStorageService
{
private readonly IJSRuntime _jsRuntime;
public LocalStorageService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
public async Task<T?> GetItemAsync<T>(string key)
{
var json = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
return json is null ? default : JsonSerializer.Deserialize<T>(json);
}
public async Task SetItemAsync<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
}
public async Task RemoveItemAsync(string key)
=> await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
public async Task ClearAsync()
=> await _jsRuntime.InvokeVoidAsync("localStorage.clear");
}
// Registration in Program.cs
builder.Services.AddScoped<IStorageService, LocalStorageService>();
6.2 Geolocation
// Services/IGeolocationService.cs
public interface IGeolocationService
{
Task<(double Latitude, double Longitude)?> GetCurrentPositionAsync();
}
// Services/GeolocationService.cs
public class GeolocationService : IGeolocationService
{
private readonly IJSRuntime _jsRuntime;
public GeolocationService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
public async Task<(double Latitude, double Longitude)?> GetCurrentPositionAsync()
{
try
{
// JavaScript call for geolocation
var position = await _jsRuntime.InvokeAsync<GeolocationPosition?>(
"getCurrentPosition");
return position is null
? null
: (position.Latitude, position.Longitude);
}
catch
{
return null; // User denied or geolocation not available
}
}
}
public class GeolocationPosition
{
[JsonPropertyName("latitude")]
public double Latitude { get; set; }
[JsonPropertyName("longitude")]
public double Longitude { get; set; }
}
// wwwroot/js/geolocation.js
window.getCurrentPosition = function() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error("Geolocation not available"));
return;
}
navigator.geolocation.getCurrentPosition(
position => resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude
}),
error => reject(error),
{ timeout: 10000, maximumAge: 60000 }
);
});
};
7. Best Practices and Common Pitfalls
7.1 Timing — OnAfterRenderAsync vs OnInitializedAsync
// ❌ BAD — DOM not yet rendered during OnInitialized
protected override async Task OnInitializedAsync()
{
// The DOM does not exist yet!
await JSRuntime.InvokeVoidAsync("initMap", "mapContainer"); // FAILS
}
// ✅ GOOD — DOM available after first render
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// The DOM is now rendered, JS can access it
await JSRuntime.InvokeVoidAsync("initMap", "mapContainer"); // OK
}
}
7.2 Releasing Resources — IAsyncDisposable
// ❌ BAD — Memory leak!
public class MyComponent
{
private IJSObjectReference? _module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
_module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.js");
}
// No Dispose → the JS module stays in memory!
}
// ✅ GOOD — Proper cleanup
public class MyComponent : IAsyncDisposable
{
private IJSObjectReference? _module;
private DotNetObjectReference<MyComponent>? _dotNetRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.js");
_dotNetRef = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("setup", _dotNetRef);
}
}
public async ValueTask DisposeAsync()
{
if (_module is not null)
{
await _module.InvokeVoidAsync("cleanup"); // JS cleanup
await _module.DisposeAsync();
}
_dotNetRef?.Dispose();
}
}
7.3 Comparison of Approaches
| Approach | Advantages | Disadvantages | Recommended for |
|---|---|---|---|
| window.xxx (global) | Simple, fast | Pollutes global namespace | Small scripts, prototypes |
| ES6 Module | Encapsulated, modern | Requires import setup | Production, libraries |
| Inline script | No dependencies | Hard to debug | Simple emergency cases |
| NuGet Blazor package | Native integration | Licensing costs | Reusable components |
8. Practical Exercises
Exercise 1 — LocalStorage (Beginner, ~45min)
Goal: Save user preferences (dark/light theme) in localStorage.
Starter code:
@inject IJSRuntime JSRuntime
@rendermode InteractiveServer
<button @onclick="ToggleTheme">Mode: @(_isDark ? "Dark" : "Light")</button>
@code {
private bool _isDark = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// TODO: Read theme from localStorage
}
}
private async Task ToggleTheme()
{
_isDark = !_isDark;
// TODO: Save to localStorage
// TODO: Apply CSS class to body via JS
}
}
Exercise 2 — Third-Party API with ES6 Module (Intermediate, ~2h)
Goal: Create a weather page using the weather.js module.
Steps:
- Create
wwwroot/js/weather.jswith thecheckWeather()function - Create the
WeatherResponseDTO - Load the module in
OnAfterRenderAsync - Call the function and display the data
- Handle errors (unknown city, no network)
Exercise 3 — Geolocation (Advanced, ~2h)
Goal: Get the user’s GPS position and automatically display the weather.
Steps:
- Create
wwwroot/js/geolocation.jswithgetCurrentPosition() - Create
IGeolocationServiceand its implementation - At page load, request the user’s position
- Call the weather API with the coordinates
- Display the weather for the current location
9. Glossary
| Term | Definition |
|---|---|
| DotNet (JS) | JavaScript object exposed by Blazor to call C# code |
| DotNetObjectReference | .NET reference passed to JavaScript to invoke instance methods |
| ElementReference | C# reference to a Blazor DOM element (@ref) |
| Error Boundary | Blazor component that catches exceptions in its child components |
| fetch() | Native JavaScript API for making HTTP requests |
| GlobalExceptionHandler | ASP.NET Core class handling all unhandled exceptions |
| IAsyncDisposable | .NET interface for releasing asynchronous resources |
| IJSRuntime | Blazor injection service for invoking JavaScript |
| import() | Dynamic loading of an ES6 module |
| InvokeAsync | JavaScript call returning a value of type T |
| InvokeVoidAsync | JavaScript call with no return value |
| JSException | Exception thrown when a JavaScript error occurs |
| JSInvokable | Attribute marking a C# method callable from JavaScript |
| localStorage | Persistent browser storage (key/value) |
| ES6 Module | Modern JavaScript module format with import/export |
| OnAfterRenderAsync | Blazor method called after the component has been rendered in the DOM |
| openweathermap.org | Weather API used in the demo application |
| ProblemDetails | RFC 7807 format for HTTP error responses |
| Serilog | Popular .NET structured logging library |
| sessionStorage | Browser storage cleared when the tab is closed |
| ValueTask | Async return type optimized for operations that are often synchronous |
| window.xxx | Convention for global JavaScript functions |
Additional Resources
- Official documentation: learn.microsoft.com/aspnet/core/blazor/javascript-interoperability
- OpenWeatherMap API: openweathermap.org/api — Weather API used in demos
- Serilog: serilog.net — .NET structured logging
- MDN Web APIs: developer.mozilla.org/docs/Web/API — Browser API reference
- Blazor.JS NuGet: Community packages encapsulating popular JS libraries
Summary of Patterns
┌─────────────────────────────────────────────────────────────────┐
│ JAVASCRIPT INTEROP PATTERNS │
│ │
│ C# → JS: │
│ ───────── │
│ InvokeVoidAsync("fn") → No return value │
│ InvokeAsync<T>("fn", args...) → Typed return value │
│ Wait for OnAfterRenderAsync → DOM available │
│ │
│ JS → C#: │
│ ───────── │
│ [JSInvokable] static → DotNet.invokeMethodAsync │
│ [JSInvokable] instance → DotNetObjectReference │
│ │
│ Errors: │
│ ──────── │
│ ErrorBoundary → UI error isolation │
│ IExceptionHandler → Global handling │
│ IsDevelopment() → Details vs generic messages │
└─────────────────────────────────────────────────────────────────┘
Search Terms
asp.net · javascript · interop · core · blazor · web · ui · c# · .net · development · application · calling · debugging · error · errors · functions · geolocation · global · methods · patterns · resources