In a MAUI Hybrid Blazor application, a login page may need to do more than validate a username and password. In a real application, we may also need to detect the client's public IP address, check whether that IP is allowed to use the application, keep a few login values in sessionStorage, and finally redirect to the correct module.
This article shows a practical pattern for coordinating those operations with IJSRuntime, OnAfterRenderAsync, and a small JavaScript bridge between the page and Blazor.
๐งฉ The Problem
The login page has several jobs:
- Wait for the page and JavaScript environment to be ready.
- Call JavaScript to obtain the public IP address.
- Send that IP to an API to determine whether the user has full access.
- Enable or disable the login controls based on the access result.
- Store selected plant and login information in browser session storage.
- Perform the normal authentication request.
- Redirect the user to the module returned by the authentication API.
The important part is timing. Browser-dependent JavaScript work should not be forced into OnInitialized when the required DOM or JavaScript state is not ready yet.
โ๏ธ Step 1: Wait Until the Component Has Rendered
The login page first registers its JavaScript callback in OnAfterRenderAsync:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await RegisterJs();
await Task.Delay(1000);
isVideoReady = true;
await InvokeAsync(StateHasChanged);
}
}
OnAfterRenderAsync is a better place for browser interop that depends on the rendered page. The first-render check also prevents the registration from running repeatedly.
๐ Step 2: Create a JavaScript-to-Blazor Bridge
A DotNetObjectReference lets JavaScript call a method on the Blazor component:
private DotNetObjectReference<Login_DTVS>? objRef;
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
private async Task RegisterJs()
{
await JS.InvokeVoidAsync(
"myInterop.register",
objRef);
}
public void Dispose()
{
objRef?.Dispose();
}
The JavaScript object keeps the .NET reference:
window.myInterop = {
dotNetHelper: null,
register: function (dotNetHelper) {
this.dotNetHelper = dotNetHelper;
window.hasregistered = true;
},
callBlazor: function () {
this.dotNetHelper.invokeMethodAsync(
"UpdateMessage",
"Hello from JavaScript!"
);
}
};
The Dispose method is important because the JavaScript reference points back to the Blazor component. Releasing the DotNetObjectReference helps avoid keeping the component alive longer than necessary.
๐ Step 3: Get the Public IP Address
The browser can call a public IP service directly:
async function getPublicIP() {
try {
const response = await fetch(
'https://api.ipify.org?format=json'
);
const data = await response.json();
console.log("Your IP Address is:", data.ip);
return data.ip;
}
catch (error) {
console.error("Error fetching IP from ipify:", error);
}
}
Because the request is asynchronous, the calling C# code must await the JavaScript result:
string res = await JS.InvokeAsync<string>("getPublicIP");
clientIp = res;
This is preferable to guessing the public address from the local device network interface, especially when the MAUI application is behind NAT, Wi-Fi routers, corporate networks, or mobile networks.
๐ก๏ธ Step 4: Check Whether the IP Has Access
Once the public IP is known, the Blazor page can call the application's API:
var response = await Http.GetFromJsonAsync<AccessResponse>(
"http://serverip:serverport/Auth/check-access/" + res);
if (response != null)
{
hasFullAccess = response.HasAccess;
clientIp = response.IpAddress;
}
A simple response model can represent the result:
public class AccessResponse
{
public bool HasAccess { get; set; }
public string IpAddress { get; set; }
}
The result can then control the login UI:
<button class="login-btn"
@onclick="HandleLogin"
disabled="@isLoading || !@hasFullAccess">
Sign in
</button>
This creates a simple gate: the user can see the login screen, but authentication controls remain unavailable until the access check completes successfully.
๐พ Step 5: Store Login Context in sessionStorage
After validation, the selected plant and login ID can be saved in browser session storage:
await JS.InvokeVoidAsync(
"sessionStorage.setItem",
"PlantCode",
selectedPlant);
await JS.InvokeVoidAsync(
"sessionStorage.setItem",
"LoginId",
username);
await JS.InvokeVoidAsync(
"sessionStorage.setItem",
"CompCode",
selectedPlant);
Only non-sensitive state should be persisted this way. In particular, the example deliberately does not store the password in sessionStorage.
๐ Step 6: Perform the Login API Request
The request can contain the selected plant, credentials, and public IP:
var reqq = new AuthApiService.LoginRequest
{
category = selectedPlant == "1108" ? "E" : "S",
UserID = username,
LoginPwd = password,
IpAddress = clientIp
};
var lr = await AuthService.LoginAsync(reqq);
The response can carry additional session information such as employee number, supplier code, photo path, focus target, or the destination module.
๐ฆ Step 7: Handle Errors Before Redirecting
A login API can return an error message instead of a redirect target:
if (!string.IsNullOrWhiteSpace(lr.Error))
{
ToastMessage = lr.Error;
ShowToast = true;
StateHasChanged();
await Task.Delay(3000);
ShowToast = false;
return;
}
Keeping this branch before the final navigation makes the login experience much easier to understand for the user.
โก๏ธ Step 8: Redirect to the Target Module
When the API provides a valid destination, Blazor navigation can take over:
if (!string.IsNullOrWhiteSpace(lr.RedirectTo))
{
await AuthService.AutoRejectPendingApprovalsAsync(selectedPlant);
Nav.NavigateTo("/" + lr.RedirectTo);
}
This keeps the login page responsible for authentication while allowing the server to decide which module should open next.
๐ง Why the JavaScript Timing Matters
One of the easiest mistakes in a MAUI Hybrid Blazor application is to assume that every JavaScript call can be executed from OnInitialized. Browser APIs and DOM elements may not be available at that point.
The practical sequence is:
- Blazor constructs the component.
- The component renders.
OnAfterRenderAsync(firstRender: true)runs.- JavaScript interop is registered.
- JavaScript waits until the bridge exists.
- JavaScript calls the Blazor method.
- Blazor gets the public IP and performs the access check.
- The login controls become available when access is granted.
This approach avoids trying to call browser-only functionality before the page is ready.
๐งน Cleanup Matters
Because a DotNetObjectReference is created, the component should release it:
public void Dispose()
{
objRef?.Dispose();
}
This is especially useful for pages that can be entered and left multiple times during the lifetime of the application.
โ Final Pattern
The complete flow can be summarized as:
Page renders
โ
OnAfterRenderAsync(firstRender)
โ
Register JavaScript interop
โ
JavaScript calls Blazor
โ
Fetch public IP
โ
API checks IP access
โ
Enable login controls
โ
Authenticate user
โ
sessionStorage stores non-sensitive context
โ
Navigate to the target module
This pattern works well for MAUI Hybrid applications where Blazor provides the UI, JavaScript supplies browser APIs, and an ASP.NET Core API remains responsible for authentication and server-side authorization decisions.
๐ Conclusion
MAUI Hybrid Blazor gives you a useful middle ground: you can build the entire login UI with Razor components while still using JavaScript interop for browser features such as public IP detection and session storage.
The key is to respect the component lifecycle. Register browser-dependent JavaScript after the first render, use a controlled callback between JavaScript and .NET, await asynchronous calls, avoid storing passwords in browser storage, and dispose of the .NET object reference when the component is finished.
Written for AlgoLassi โ practical .NET, Blazor, MAUI, SQL Server, and developer tutorials.
๐ฌ Comments
Sign in with Google to publish immediately, or comment anonymously and wait for approval.
Loading comments...