Custom Disconnect Popup in Blazor
Author: Lance Wright (Magic Coding Man)
Published: October 10, 2024
One common frustration among Blazor developers is the default disconnect overlay. When users leave a Blazor server or hybrid site open for too long, they may get disconnected and presented with an overlay message, which can appear intimidating and misleading.
This message is essential when there's a real connection issue, but often, it's just a simple case of the user being inactive for too long. Most users don't realize a simple refresh will resolve the issue, and I’ve received many support calls because of this confusion.
A Lightweight Solution
After testing several options to replace this overlay, many solutions felt too invasive or required significant maintenance with Blazor framework updates. I wanted a simple, non-intrusive solution that could automatically handle reconnections without custom reconnect logic. Here’s how I tackled it.
The disconnect overlay and message have a unique ID, “components-reconnect-modal.” Using CSS and JavaScript, I created a system to detect this ID’s visibility, display a custom popup, and hide it when the connection is restored. The result is a smooth, user-friendly experience that’s easy to implement.
Here's the actual visual I created for this website when the user is disconnected:
The Code
First, here's the CSS I used to design the popup that appears when the disconnect occurs:
/* Fullscreen popup overlay */
#disconnect-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8); /* Black background with opacity */
display: flex;
justify-content: center;
align-items: center;
z-index: 100000 !important; /* Ensure it's above everything */
visibility: hidden;
}
/* Popup content */
#disconnect-popup-content {
text-align: center;
color: white;
background-color: #333;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* Refresh button style */
#refresh-button {
margin-top: 20px;
padding: 10px 20px;
font-size: 16px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#refresh-button:hover {
background-color: #0056b3;
}
/* Image div styling */
#disconnect-image-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
width: 100%;
height: auto; /* Ensure the image resizes with the screen */
}
#disconnect-image {
display: block;
max-width: 100%;
height: auto;
}
Next, the JavaScript checks for the disconnect state and shows/hides the popup accordingly:
const imageCacheKey = 'cachedSleepyRobotImage'; // Key to store/retrieve from localStorage
window.checkUserConnectionWithoutDotnet = () => {
console.log('Initializing connection check...');
let isUserDisconnected = false;
// Function to dynamically insert the image when showing the popup
function showDisconnectPopup() {
console.log('Showing disconnect popup...');
const popup = document.getElementById('disconnect-popup');
popup.style.visibility = 'visible';
// Dynamically insert the image into the container
insertImageFromCache();
hideReconnectModal();
}
// Function to hide the disconnect popup
function hideDisconnectPopup() {
console.log('Hiding disconnect popup...');
const popup = document.getElementById('disconnect-popup');
popup.style.visibility = 'hidden';
}
// Function to preload the image from the cache or load from localStorage if not found
function checkAndRetrieveCachedImage() {
console.log('Checking if cachedImageDataUrl is available...');
// First, check if the cached image exists in localStorage
let cachedImageDataUrl = localStorage.getItem(imageCacheKey);
if (!cachedImageDataUrl || cachedImageDataUrl.trim() === "") {
console.error('No valid cached image found in localStorage.');
return null; // If nothing in the cache, return null
}
console.log('Cached image found in localStorage.');
return cachedImageDataUrl; // Return the cached Data URL
}
// Function to insert the image from cache into the DOM dynamically
function insertImageFromCache() {
const imageContainer = document.getElementById('disconnect-image-container');
const cachedImageDataUrl = checkAndRetrieveCachedImage();
if (cachedImageDataUrl) {
// Check if an existing <img> with this ID already exists, if so, remove it
let existingImage = document.getElementById('disconnect-image');
if (existingImage) {
console.log('Removing existing image.');
imageContainer.removeChild(existingImage);
}
// Dynamically create a new <img> element
const newImg = document.createElement('img');
newImg.id = 'disconnect-image'; // Set the ID for the new image
newImg.src = cachedImageDataUrl;
newImg.style.maxWidth = '100%';
newImg.style.height = 'auto';
// Append the new image into the container
console.log('Inserting new image into container.');
imageContainer.appendChild(newImg);
} else {
console.error('No image to insert, the cache is empty.');
}
}
// Function to preload the image and store it in localStorage (only called once when the page is first loaded)
function preloadImageAsDataUrl(src) {
const img = new Image();
img.src = src;
img.crossOrigin = "Anonymous"; // Important if loading images from different origins
img.loading = 'lazy'; // Lazy loading to minimize impact on page load
img.onload = () => {
console.log('Image successfully loaded from source:', src);
// Create a canvas to draw the image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions to match the image
canvas.width = img.width;
canvas.height = img.height;
// Draw the image onto the canvas
ctx.drawImage(img, 0, 0);
// Convert the canvas to a Data URL (Base64 encoded image)
const dataUrl = canvas.toDataURL('image/webp');
console.log('Image converted to Base64 Data URL:', dataUrl);
// Store the preloaded image Data URL into localStorage for persistent caching
localStorage.setItem(imageCacheKey, dataUrl);
console.log('Image Data URL stored in localStorage');
};
// Handle image loading errors
img.onerror = (err) => {
console.error('Error preloading image:', err);
};
}
// Function to periodically check the status of the reconnect modal
function checkConnectionStatus() {
const reconnectModal = document.getElementById('components-reconnect-modal');
if (reconnectModal) {
const visibility = window.getComputedStyle(reconnectModal).visibility;
// Check if the modal is visible (user is disconnected)
if (visibility === 'visible' && !isUserDisconnected) {
console.log('User is disconnected, showing popup.');
showDisconnectPopup();
isUserDisconnected = true;
}
// If the modal is no longer visible, the user reconnected
else if (visibility !== 'visible' && isUserDisconnected) {
console.log('User reconnected, hiding popup.');
hideDisconnectPopup();
isUserDisconnected = false;
}
} else if (isUserDisconnected) {
// If the modal element is not found, the user reconnected
console.log('Reconnect modal not found, assuming user is reconnected.');
hideDisconnectPopup();
isUserDisconnected = false;
}
// Continue checking every second
setTimeout(checkConnectionStatus, 1000);
}
// Preload the image and cache it in localStorage when the page first loads
preloadImageAsDataUrl('/Images/Sleepy_Robot_Dark_Mode__438x428_50c.webp');
// Start the connection check loop
checkConnectionStatus();
};
function hideReconnectModal() {
var modal = document.getElementById("components-reconnect-modal");
if (modal) {
modal.style.opacity = "0";
console.log('Hiding reconnect modal.');
}
}
// Start the connection check without needing Blazor interop
window.checkUserConnectionWithoutDotnet();
How It Works
The CSS handles the popup’s appearance, while the JavaScript watches for the Blazor disconnect modal’s visibility. When the user is disconnected, my popup is displayed, along with a pre-cached image for quicker load times and a simple button to refresh the page.
Finally, I invoke the JavaScript in my `NavMenu.razor` on the first render:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("checkUserConnectionWithoutDotnet");
}
}
This approach provides a lightweight solution, letting Blazor handle reconnections seamlessly while offering users a much friendlier message and experience when disconnected.
Conclusion
With this simple solution, you can ensure a smoother user experience for your Blazor applications. It’s easy to implement, non-intrusive, and customizable to your needs. I don't believe this is the best solution or an end game level solution. But I really do believe it's an easy and simple method to implement! With only slight alterations to my code, you can create the same thing for yourself. I hope this helps you improve your Blazor projects as well!