User Profile
Gavin-Williams
Brass Contributor
Joined 3 years ago
User Widgets
Recent Discussions
Using browser local storage with Blazor
Project based on .NET Blazor Web App in Server render mode. Blazor currently has no built-in function for utilizing local browser storage. However this feature can come in useful for storing user information or partially completed forms etc. Luckily implementing a class for facilitating this function is straightforward enough. Here I’ll run over the steps required to get local storage up and running in your Blazor web app. Note: This method uses JSRuntime so make sure if you want to call it on page/component load you will need to run it on “OnAfterRender” rather than “OnInitialize” otherwise you will get an error. Here is a screenshot of my file structure so you can see the additional files I’ve made and where I’ve placed them: Making the javascript functions: Within the wwwroot of your app, make a new javascript file (I’ve called mine LocalStorage.js) and enter the following code: window.LocalStorageActions = { setItem: function (key, value) { localStorage.setItem(key, value); }, getItem: function (key) { return localStorage.getItem(key); }, removeItem: function (key) { localStorage.removeItem(key); }, clearData: function () { localStorage.clear(); } } These are the four functions we will need for setting, getting, removing and clearing local storage in the browser. Make a C# class to call the functions: To keep things tidy, I’ve put the C# class for calling the local storage functions and it’s interface into a folder called helpers, but you can put this wherever it’s most appropriate in your solution. You can create a new .cs file and add the following code: using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; namespace BlazorLocalStorage.Helpers { public class LocalStorageHelper : ILocalStorageHelper { private readonly IJSRuntime _javaScript; public LocalStorageHelper(IJSRuntime javaScript) { _javaScript = javaScript; } public async Task SetItem(string key, string value) { await _javaScript.InvokeAsync<string>("LocalStorageActions.setItem", key, value); } public async Task<string> GetItem(string key) { return await _javaScript.InvokeAsync<string>("LocalStorageActions.getItem", key); } public async Task RemoveItem(string key) { await _javaScript.InvokeAsync<string>("LocalStorageActions.removeItem", key); } public async Task ClearData() { await _javaScript.InvokeAsync<string>("LocalStorageActions.clearData"); } } } To quickly produce the interface for this class you can right click the class name (LocalStorageHelper in this case) and select “Quick Actions and Refactoring” and then select “Extract Interface” This will open a popup for extracting an interface from the class that you can select OK and a new file called ILocalStorageHelper.cs will appear and the LocalStorageHelper class will now inherit from the interface that’s been created. Registering the new files ready for use: Firstly we want to reference the new javascript file for use in the application. To do this go to the App.razor file and add the following line in the <body> section of the file. <script src="LocalStorage.js"></script> Next, we need to register the LocalStorageHelper so it can be injected wherever we want to use it. To do this open the Program.cs file and add the following line before the builder.Build() method it called. builder.Services.AddScoped<ILocalStorageHelper, LocalStorageHelper>(); Let’s try it out: I’ve made a little example of the new class being used by modifying the Home.razor file to use the various functions we’ve created. Before adding the main code I have created a little css class to centre the content of the page to keep things neat. So you can add the following code snippet to app.css. .center-screen { display: flex; justify-content: center; align-items: center; text-align: center; min-height: 70vh; } Now we can go into the Home.razor file and replace the content with the following. PAGE "/" @using BlazorLocalStorage.Helpers @rendermode InteractiveServer <PageTitle>Home</PageTitle> <div class="center-screen"> <div> <div class="row"> <label>Text-1</label> <InputText class="m-2" @bind-Value="Text1" /> </div> <div class="row"> <button class="btn btn-success m-2" @onclick="Save">Save</button> <button class="btn btn-info m-2" @onclick="Load">Load</button> <button class="btn btn-warning m-2" @onclick="Remove">Remove</button> <button class="btn btn-danger m-2" @onclick="Clear">Clear</button> </div> </div> </div> @code { [Inject] ILocalStorageHelper LocalStorage { get; set; } public string Text1 { get; set; } public async Task Save() { await LocalStorage.SetItem("textData", Text1); } public async Task Load() { Text1 = await LocalStorage.GetItem("textData"); } public async Task Remove() { await LocalStorage.RemoveItem("textData"); } public async Task Clear() { await LocalStorage.ClearData(); } } Apologies for the garish use of coloured buttons but it’s just meant to be an obvious demo. If you open the browsers developer tools and navigate to “Application” the select “Local Storage” you will be able to see the data stored, updated or removed as you use the demo. If you refresh the page or close and open the browser and select load it will pull the stored data back in to the textbox. That’s all there is to it! If you want to see the project, I've stuck a copy up on github that you can have a look at: https://github.com/DrGav/BlazorLocalStorage Have yourselves a wonderful day, Gavin.1.5KViews0likes0CommentsRe: Blazor WASM PWA – Applications updates, cache busting with notification or force refresh
Amazing! Thanks for investigating the functionality in .NET8, that was going to be my next move so you've saved me a job there. Glad to know it will be resolved after an upgrade. Fortunately my project isn't going to production till next year and I've planned in a .NET8 migration.10KViews0likes0CommentsRe: Blazor WASM PWA – Applications updates, cache busting with notification or force refresh
Strange, I did all of this on .NET7 but didn't encounter that error. Just out of interest, could you try a clean and rebuild on that? Wondering if there's some reference to previous dlls that may be able to be cleared.11KViews0likes2CommentsBlazor WASM PWA – Applications updates, cache busting with notification or force refresh
My team is currently working on a Blazor application and after several bouts of confusion between developers and testers we stuck a version number on the home screen and discovered the updates weren’t going out to everyone as we were expecting. We ended up with several people on different versions of the application and fortunately we spotted this issue early in development. I’ve put together the options and info I’ve found while trying to find a solution to this that works for our particular situation in hopes that it will help someone else dealing with this and hopefully open a discussion on how people are handling this issue and find any alternative solutions. To look at what was happening behind the scenes, I set up a simple WASM PWA on .NET7 and have it hosted on an Azure App Service. I stuck a version number on the home page so I can easily see when the update has taken place. The behaviour: Blazor WASM PWA applications load the application into the browser based on the cache pulled in by the service workers (Javascript assets that act as a proxy between browsers and web servers for offline support and performance). In dev tools you can see what’s in the cache storage for the app (go to dev tools > application tab > cache storage). The picture below shows that the cache contains “blazor-resources” and “offline-cache”. The “offline-cache” is the important one that determines what the site is displaying and on pushing an update the “blazor-resources” is unaffected so for this purpose can be ignored. Now the standard behaviour on pushing an update to the app is that the service worker will install the new app cache in the background and enter the “waiting” state and will not activate the newly cached app until the current application service worker is released, by either closing the browser or hard refreshing the app (Ctrl + F5). This behaviour is by design to prevent having two tabs open with different versions of the application running at once. However users may never close the application or browser or whatever and that would mean they could be running long retired versions of your application for an indeterminant length of time. Below you can see the cache storage after an update has been pushed stacking up the old and new version of the offline-cache. Once the service worker has been released then the old cache is removed the latest will be activated. This will then display the new version of the app. The solution: Now, what if we want to change this behaviour to either prompt and update or force one? Either way we will want to remove the need to close browsers or hard refresh as that can be a bit of a tricky one to get end users to get on board with. The service workers actually have a method that can skip the “waiting” stage of the service worker lifecycle clear the old cache and activate the latest. This is the SkipWaiting method. In the Blazor solution there is the service-worker.published.js file found in the wwwroot folder. This file contains the methods for the service worker. In this file there is a OnInstall method and can be modified to the following adding in the SkipWaiting method: async function onInstall(event) { console.info('Service worker: Install'); self.skipWaiting(); // Fetch and cache all matching items from the assets manifest const assetsRequests = self.assetsManifest.assets .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); } This additional line will now allow a standard refresh to update the application rather than requiring a hard refresh or closing the browser completely. When looking at the cache storage when an update is pushed you will see the second cache appear then the old cache disappear immediately. Note: Adding this behaviour will overwrite the intended behaviour and allow the potential of different version of the application running in different tabs. However we can look at avoiding that by forcing update later. Next, regardless of whether we want to prompt the user for an update or force it on them, we need a way of letting the site know it needs an update. My javascript is less than brilliant so I’ve used a solution by Wouter Huysentruit, full description here: https://whuysentruit.medium.com/blazor-wasm-pwa-adding-a-new-update-available-notification-d9f65c4ad13 This involves adding a js file called sw-registrator.js to the wwwroot folder that will allow an event to be triggered that can link to updating the site: window.updateAvailable = new Promise((resolve, reject) => { if (!('serviceWorker' in navigator)) { const errorMessage = `This browser doesn't support service workers`; console.error(errorMessage); reject(errorMessage); return; } navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.info(`Service worker registration successful (scope: ${registration.scope})`); registration.onupdatefound = () => { const installingServiceWorker = registration.installing; installingServiceWorker.onstatechange = () => { if (installingServiceWorker.state === 'installed') { resolve(!!navigator.serviceWorker.controller); } } }; }) .catch(error => { console.error('Service worker registration failed with error:', error); reject(error); }); }); window.registerForUpdateAvailableNotification = (caller, methodName) => { window.updateAvailable.then(isUpdateAvailable => { if (isUpdateAvailable) { caller.invokeMethodAsync(methodName).then(); } }); }; Within index.html in the wwwroot folder we will need to register this new file instead of the service-worker.js. So we need to replace the reference to service-worker.js with the following: <script src="sw-registrator.js"></script> This will now raise an event once the cache refresh happens that can be used to trigger a prompt on the application or a refresh of the application. Now we need a razor component that will listen for the event and either throw up a refresh prompt or just force a refresh. Firstly I’ll do the prompt. For this make a new razor component and add the following code: @inject IJSRuntime _jsRuntime @inject NavigationManager uriHelper @if (_newVersionAvailable) { <button type="button" class="btn btn-danger shadow floating-update-button" onclick="window.location.reload()"> A new version of the application is available. Click here to upgrade. </button> } @code { private bool _newVersionAvailable = false; protected override async Task OnInitializedAsync() { await RegisterForUpdateAvailableNotification(); } private async Task RegisterForUpdateAvailableNotification() { await _jsRuntime.InvokeAsync<object>( identifier: "registerForUpdateAvailableNotification", DotNetObjectReference.Create(this), nameof(OnUpdateAvailable)); } [JSInvokable(nameof(OnUpdateAvailable))] public Task OnUpdateAvailable() { _newVersionAvailable = true; StateHasChanged(); return Task.CompletedTask; } } I personally like a red bar across the top as it’s very attention grabbing so you can add the following css file called WhateverYouCalledTheComponent.razor.css: .floating-update-button { position: relative; top: 0; padding: 1rem 1.5rem; width: 100%; } This component can then be placed at the top of MainLayout.razor above the body to ensure it appears anywhere on the app the user happens to be. This will then produce the following result once an update is pushed out: Once the banner is clicked, it will reload the site and the new update will be running. This is the setup that I will be implementing on my teams project as I wouldn’t want the site to force updates in case a user was in the middle of data entry. However if a forced refresh is desired, it’s a quick change to the razor component to allow that. Change the components code to the following: @inject IJSRuntime _jsRuntime @inject NavigationManager uriHelper @code { protected override async Task OnInitializedAsync() { await RegisterForUpdateAvailableNotification(); } private async Task RegisterForUpdateAvailableNotification() { await _jsRuntime.InvokeAsync<object>( identifier: "registerForUpdateAvailableNotification", DotNetObjectReference.Create(this), nameof(OnUpdateAvailable)); } [JSInvokable(nameof(OnUpdateAvailable))] public Task OnUpdateAvailable() { uriHelper.NavigateTo(uriHelper.Uri, forceLoad: true); StateHasChanged(); return Task.CompletedTask; } } Now on pushing an update you will see that the update is installed in the cache and once the event is triggered the old cache is removed and the site reloads wherever it happens to be. I’d be really interested to hear other peoples takes on this or if they’ve found other methods of achieving this, I’ve scoured the web and this was the combination of the best methods I came across but certainly open to improvement.13KViews0likes5CommentsConvert the standard Blazor navigation menu to a collapsible icon menu
While I admittedly love Blazor I’ve always changed the out-of-the-box navigation menu that comes with it. It’s the first manoeuvre I pull when spinning up a new Blazor app, stripping out the purple gradient and getting it in, what I consider, a “blank slate state”. The other change I’ve wanted to make to the out-the-box look is one of those deluxe collapsible menus that leave just the icons showing. Anyone that’s used Azure DevOps will know what I’m talking about. I’ve included a picture to show DevOps example of what I’d like to see in my Blazor app. It gives a load of extra screen real estate which is always a priority for me in business applications particularly with complex or intensive workflows. Plus it gives the user the option to remove the text prompts once they are familiar with the system which is supported with carefully selected icon choices. As with most tasks that I assume will be an obvious solution I hit my search engine of choice and looked to avoid reinventing the wheel. However I found no source of pre-written changes to achieve this and was directed to fairly expensive third party controls to solve this one for me, which, being tight fisted, pushed me to do it for myself. Here I hope you save you the trouble of paying a pretty penny or having to wrestle the CSS into submission and provide a guide for producing a nice collapsible icon navigation menu by altering the existing out of the box menu in Blazor. In the following example I have left all the standard styling as is with the menu and just done the changes required to make the collapsible menu. The three files that require changes are MainLayout.razor, NavMenu.razor and NavMenu.razor.css. The code changes are shown below: Firstly the NavMenu.razor requires a bool value (IconMenuActive) to indicate whether the icon menu is showing or not, then wrap the text of the each NavItem in an if statement dependent on this bool. Then a method for toggling this bool and EventCalBack to send a bool to the MainLayout.razor for shrinking the width of the sidebar. Lastly there needs to be the control for switching menu views (I used the standard io icon arrows). NavMenu.razor <div class="top-row ps-3 navbar navbar-dark"> <div class="container-fluid"> <span class="oi oi-monitor" style="color:white;" aria-hidden="true"></span> @if (!@IconMenuActive) { <a class="navbar-brand" href="">The Menu Title Here</a> } <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> </div> <div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <nav class="flex-column"> <div class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> @if (!@IconMenuActive) { <label>Home</label> } </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> @if (!@IconMenuActive) { <label>Counter</label> } </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> @if (!@IconMenuActive) { <label>Fetch data</label> } </NavLink> </div> </nav> </div> <div class="bottom-row"> <div class="icon-menu-arrow"> @if (!@IconMenuActive) { <span class="oi oi-arrow-left" style="color: white;" @onclick="ToggleIconMenu"></span> } else { <span class="oi oi-arrow-right" style="color: white;" @onclick="ToggleIconMenu"></span> } </div> </div> @code { //bool to send to MainLayout for shrinking sidebar and showing/hide menu text private bool IconMenuActive { get; set; } = false; //EventCallback for sending bool to MainLayout [Parameter] public EventCallback<bool> ShowIconMenu { get; set; } private bool collapseNavMenu = true; private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } //Method to toggle IconMenuActive bool and send bool via EventCallback private async Task ToggleIconMenu() { IconMenuActive = !IconMenuActive; await ShowIconMenu.InvokeAsync(IconMenuActive); } } Next I add in a bit of CSS in to NavMenu.razor.css to put the arrow for toggling the menu at the bottom of the sidebar and a media query to make sure it doesn't show up in mobile view. The CSS classes added are .bottom-row and .icon-menu-arrow. NavMenu.razor.css .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); } .top-row { height: 3.5rem; background-color: rgba(0,0,0,0.4); } .bottom-row { position: absolute; bottom: 0; padding-bottom: 10px; text-align: right; width: 100%; padding-right: 28px; } .icon-menu-arrow { text-align: right; } .navbar-brand { font-size: 1.1rem; } .oi { width: 2rem; font-size: 1.1rem; vertical-align: text-top; top: -2px; } .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; } .nav-item:first-of-type { padding-top: 1rem; } .nav-item:last-of-type { padding-bottom: 1rem; } .nav-item ::deep a { color: #d7d7d7; border-radius: 4px; height: 3rem; display: flex; align-items: center; line-height: 3rem; } .nav-item ::deep a.active { background-color: rgba(255,255,255,0.25); color: white; } .nav-item ::deep a:hover { background-color: rgba(255,255,255,0.1); color: white; } @media (min-width: 641px) { .navbar-toggler { display: none; } .collapse { /* Never collapse the sidebar for wide screens */ display: block; } } @media (max-width: 640px) { .bottom-row { display: block; } } Finally I add in the handler for the EventCallback to MainLayout.razor and a method to alter the width of the sidebar. MainLayout.razor @inherits LayoutComponentBase <div class="page"> <div class="sidebar" style="@IconMenuCssClass"> <NavMenu ShowIconMenu="ToggleIconMenu"/> </div> <main> <div class="top-row px-4"> <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a> </div> <article class="content px-4"> @Body </article> </main> </div> @code{ private bool _iconMenuActive { get; set; } private string? IconMenuCssClass => _iconMenuActive ? "width: 80px;" : null; protected void ToggleIconMenu(bool iconMenuActive) { _iconMenuActive = iconMenuActive; } } The final product of these little changes are shown in the pictures below: I'd love to hear if anyone has tackled this in a different way to me and if they've got any ideas on making it cleaner. Have yourselves a wonderful day, Gav76KViews10likes19CommentsRe: Mobile pages
JohnStraumann Hi John, Looks like you're calling RenderBody() on line 41 of _layoutMobile which is above where you are laying out the content of the body. I did a quick test to check this is the case with the code you provided and took the following section of code and put it just before the </body> tag: @{ var level = ""; var pcurrent = 0; var pcomplete = "No"; //Getting session object if (Session["MembershipLevel"] != null) { level = Session["MembershipLevel"].ToString(); pcurrent = Convert.ToInt32(Session["MembershipCurrent"]); pcomplete = Session["ProfileCompleted"].ToString(); } //Generate variables for nav showing var psale = false; var mdir = false; var pwanted = false; var pisale = false; var pisearch = false; if (pcomplete == "Yes" && pcurrent == 1) //show for both basic and premium { psale = true; pisale = true; pisearch = true; pwanted = true; mdir = true; } if (IsSectionDefined("scripts")) { @RenderSection("scripts", required: false) } @RenderBody() } By putting the render as the last part of your mobile layout body it will always produce it below the logo and nav menu that you produce above it. You probably don't need all of the code above in that body section but hopefully this will give you enough of an idea about your issue to clean it up. Also note that if you plan on adding a footer then that would go below the section I mentioned above so the body renders between the header and footer. I've attached a picture of how it looks with the change I described above. Have yourself a wonderful day, Gav981Views0likes0CommentsRe: How is going Blazor?
phcmiguez I'll try to avoid bias because I'm a massive .NET fanboy. I'm currently pushing all of my companies business application development to Blazor for a few reasons: It removes the need (for the most part) for using JavaScript/TypeScript which makes the hiring process easier as I can take pure .NET (desktop or web) developers and get them learning Blazor framework without the need for getting to grips with unfamiliar syntax or nuances of a different language. The learning curve is tiny in my opinion, if you've already been coding in C#. As an anecdotal example, I took on a junior developer 8 months ago as his first development role and he is producing some very professional frontend work using just Blazor and the out-the-box bootstrap with very little intervention from myself or the seniors on my teams. I tentatively kept an eye on Blazor when the first preview was released as you never know how it's going to play out but Microsoft have absolutely convinced me of the potential and provided some brilliant road maps for the future. I'm particularly interested in the way various web techs are now slotting together. First there's the MAUI/Blazor hybrid bringing web and mobile closer and then news of Blazor United to unify server side and WASM Blazor hopefully coming fr .NET8. As far as the market goes on the wider scheme, I know there are few Blazor developers about in comparison to React, Angular, etc, as it is an emerging technology. I usually hire through recruiters as they have a large pool of developers to contact and they confirm the lack of developers with significant production experience but I do see that changing as I know a few other development managers who have started eyeballing Blazor for their new web applications. If anyone was to ask for the purpose of learning to go for a Blazor specific position, I'd say give it a go and spin up a couple personal projects. Anyone with C# experience can make a couple components and get some data passing with EventCallbacks/Cascading parameters and they'll be well on their way. Have yourself a wonderful day, Gav1.9KViews1like1Comment
Recent Blog Articles
No content to show