Blazor Web Assembly Werte im Local Storage speichern
In diesem Tutorial zeige ich wie man in einer Blazor Web Assembly Applikation Werte im Local Storage speichern kann. Dieser Speicher ist optimal für Werte geeignet, die man früher in Cookies gespeichert hat.
Blazor Web Assembly Werte im Local Storage speichern
Meine Problemstellung sieht wie folgt aus: Ich habe eine neue Blazor Webassembly Applikation erstellt und ein Login über eine separate API eingebaut. Um die Benutzererfahrung zu verbessern möchte ich nun eine „Remember Me“ Checkbox hinzufügen. Damit werden die zuletzt verwendeten Logindaten im Browser gespeichert. Der Benutzer muss jedes weitere Mal nun noch Login klicken, die Daten sind im Formular vorausgefüllt. Das Datenmodell sieht dafür beispielsweise so aus:
public class LoginData { public LoginData() { UserName = string.Empty; Password = string.Empty; RememberMe = false; } public LoginData(string userName, string password, bool rememberMe) { UserName = userName; Password = password; RememberMe = rememberMe; } public string UserName { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } }
Bei modernen HTML 5 Browser ist der Local Storage der Speicherort der Wahl. Der Speicher ist groß genug für die Daten und ist über alle Tabs im Browser verfügbar. Außerdem ist der Datensatz permanent, zumindest so lange bis der Benutzer diesen aktiv löscht.
Implementierung
Die Implementierung für mein Projekt basiert auf diesem Tutorial. Der erste Schritt sind 3 JavaScript Funktionen, die man in der index.html Datei hinzufügt. Diese benötigt man um einen Wert im Local Storage zu setzen (Key-Value Paar), zu lesen (über einen Key) und einen Event Listener um über Änderungen im Programm informiert zu werden.
Als nächstes benötigen wir eine Klasse mit den Daten die gespeichert werden – die UserSettings. In meinem Fall sind das aktuell nur die Login Daten. Die Klasse kann aber nach belieben erweitert werden. Neben den Daten beinhaltet diese Klasse auch einen ChangeEventHandler über den man über eine Änderung der Daten informiert werden kann.
// The class that stores the user settings public class UserSettings : INotifyPropertyChanged { private LoginData loginData = default!; public LoginData LoginData { get => loginData; set { loginData = value; RaisePropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
In .NET Web Projekten läuft alles über Services die beim Start der Applikation registriert werden. Für die Interaktion aus dem Programm mit der Datenklasse UserSettings schreiben wir ein eigenes Service, den UserSettingsProvider:
public sealed class UserSettingsProvider { private const string KeyName = "state"; private readonly IJSRuntime _jsRuntime; private bool _initialized; private UserSettings _settings; public event EventHandler Changed; public bool AutoSave { get; set; } = true; public UserSettingsProvider(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async ValueTask Get() { if (_settings != null) return _settings; // Register the Storage event handler. This handler calls OnStorageUpdated when the storage changed. // This way, you can reload the settings when another instance of the application (tab / window) save the settings if (!_initialized) { // Create a reference to the current object, so the JS function can call the public method "OnStorageUpdated" var reference = DotNetObjectReference.Create(this); await _jsRuntime.InvokeVoidAsync("BlazorRegisterStorageEvent", reference); _initialized = true; } // Read the JSON string that contains the data from the local storage UserSettings result; var str = await _jsRuntime.InvokeAsync("BlazorGetLocalStorage", KeyName); if (str != null) { result = System.Text.Json.JsonSerializer.Deserialize(str) ?? new UserSettings(); } else { result = new UserSettings(); } // Register the OnPropertyChanged event, so it automatically persists the settings as soon as a value is changed result.PropertyChanged += OnPropertyChanged; _settings = result; return result; } public async Task Save() { var json = System.Text.Json.JsonSerializer.Serialize(_settings); await _jsRuntime.InvokeVoidAsync("BlazorSetLocalStorage", KeyName, json); } // Automatically persist the settings when a property changed private async void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (AutoSave) { await Save(); } } // This method is called from BlazorRegisterStorageEvent when the storage changed [JSInvokable] public void OnStorageUpdated(string key) { if (key == KeyName) { // Reset the settings. The next call to Get will reload the data _settings = null; Changed?.Invoke(this, EventArgs.Empty); } } }
Das Service benötigt die JSInteropt Bibliothek und stellt die Verbindung von der Blazor .NET Datenklasse und den Klassen in denen man die gespeicherten Werte verwenden will und die in index.html registrierten JavaScript Funktionen. Damit man dieses Service verwenden kann muss es noch in der Program.cs Datei registriert werden:
Jetzt kann man das Service in allen Komponenten verwenden. Effizient ist das nicht. Blazor verwendet für das UI Razor Components. Solche Komponenten sind wiederverwendbar und man kann diese sehr gut schachteln. Es macht Sinn für die Verwendung eines Services, dass an vielen Stellen in der Applikation benötigt wird eine eigene Komponente zu bauen. Ich erstelle die Datei Shared/UserSettingsComponent.razor.
@inject UserSettingsProvider UserSettingsProvider @implements IDisposable @if (state == null) { <p>loading...</p> } else { <CascadingValue Value="@state" IsFixed="false">@ChildContent</CascadingValue> } @code{ private UserSettings state = default!; [Parameter] public RenderFragment ChildContent { get; set; } = default!; protected override async Task OnInitializedAsync() { UserSettingsProvider.Changed += UserSettingsChanged; await Refresh(); } public void Dispose() { UserSettingsProvider.Changed -= UserSettingsChanged; } private async void UserSettingsChanged(object sender, EventArgs e) { await InvokeAsync(async () => { await Refresh(); StateHasChanged(); }); } private async Task Refresh() { state = await UserSettingsProvider.Get(); } }
Eine einfache Möglichkeit diese Komponente auf allen Seiten zu verwenden ist deren Verwendung in der App.razor Datei. Das Router Tag wird damit einfach eingeschlossen wie folgende Änderung meiner App.razor Datei zeigt:
Jetzt feht nur noch eines. Der eigentliche Grund für die ganze Arbeit. In der Login.razor Komponente sollen nun die Werte aus dem Formular aus dem Local Storage abgerufen bzw. dort hin gespeichert werden. Ich habe folgende Erweiterungen meines bestehenden Codes gemacht.
Der UserSettings State bilded die aktuell gespeicherten Werte ab. In der OnInitialized Methode kopiere ich die Daten aus dem Speicher in die im Formular verwendete Datenklasse LoginData. Das mache ich aber nur dann, wenn der Benutzer zuvor die „Remember Me“ Checkbox aktiviert hatte, beziehungsweise im Speicher überhaupt daten liegen (Remember Me ist default false). In der OnSubmit Methode werden die Werte sofern gewollt („Remember Me“ ist true) in den Local Storage des Browsers gespeichert. Dank dem ganzen Code reicht die Zuweisung aus, die Änderung des Zustands wird getriggert und der Wert im Speicher aktualisiert.
Fazit
Ich habe gezeigt wie ich Werte einer Blazor Web Assembly Applikation im Local Storage des Browsers gespeichert habe. Damit lassen sich Abläufe für den User vereinfachen, die User Experience verbessern. Der Local Storage kann mit der beschriebenen Methode recht einfach durch alle möglichen weiteren Werten ohne große Änderung am Code erweitert werden.