Skip to content
This repository has been archived by the owner on Nov 1, 2024. It is now read-only.

Commit

Permalink
Use JsInterop instead of PersistentComponentState
Browse files Browse the repository at this point in the history
  • Loading branch information
YuriyDurov committed May 4, 2024
1 parent 5916dad commit 2d9e797
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[*.cs]


# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = none
13 changes: 9 additions & 4 deletions BitzArt.Blazor.MVVM.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6EE1DDAF-17B
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B22B408D-EEB8-4933-AED6-C5A225C50AFD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.MVVM", "src\BitzArt.Blazor.MVVM\BitzArt.Blazor.MVVM.csproj", "{1069F2AF-7A9C-47F2-91C2-E970295FEF38}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.MVVM", "src\BitzArt.Blazor.MVVM\BitzArt.Blazor.MVVM.csproj", "{1069F2AF-7A9C-47F2-91C2-E970295FEF38}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.MVVM.Tests", "tests\BitzArt.Blazor.MVVM.Tests\BitzArt.Blazor.MVVM.Tests.csproj", "{EDDA4260-98DF-42BD-A268-1FCE434916D0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.MVVM.Tests", "tests\BitzArt.Blazor.MVVM.Tests\BitzArt.Blazor.MVVM.Tests.csproj", "{EDDA4260-98DF-42BD-A268-1FCE434916D0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{5123F2D4-DA61-4184-A05D-3573882BCAB9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.MVVM.SampleApp", "sample\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp.csproj", "{807AE94D-9DD6-4B57-9ABA-93FA4EE68E90}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.MVVM.SampleApp", "sample\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp.csproj", "{807AE94D-9DD6-4B57-9ABA-93FA4EE68E90}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.MVVM.SampleApp.Client", "sample\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp.Client\BitzArt.Blazor.MVVM.SampleApp.Client.csproj", "{9FD22113-A902-4D92-8060-45F6E1CBC114}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.MVVM.SampleApp.Client", "sample\BitzArt.Blazor.MVVM.SampleApp\BitzArt.Blazor.MVVM.SampleApp.Client\BitzArt.Blazor.MVVM.SampleApp.Client.csproj", "{9FD22113-A902-4D92-8060-45F6E1CBC114}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B41215B5-9B6B-4165-BBBC-13364D482AA8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@

<p>This page is currently being rendered using: @(OperatingSystem.IsBrowser() ? "WASM" : "Server")</p>

<p>@ViewModel.State.Text</p>
@if (ViewModel.State is not null)
{
<p>@ViewModel.State.Text</p>

<p role="status">Current count: @ViewModel.State.Count</p>
<p role="status">Current count: @ViewModel.State.Count</p>

<button class="btn btn-primary" @onclick="ViewModel.IncrementCount">Click me</button>
<button class="btn btn-primary" @onclick="ViewModel.IncrementCount">Click me</button>
}

@{
base.BuildRenderTree(__builder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddRenderingEnvironment();
builder.Services.AddBlazorViewModels();

await builder.Build().RunAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function getInnerText(id) {
let element = document.getElementById(id);
if (element) {
return element.innerText;
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
<script src="app.js"></script>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using BitzArt.Blazor.MVVM.SampleApp.Client.Pages;
using BitzArt.Blazor.MVVM.SampleApp.Components;

namespace BitzArt.Blazor.MVVM.SampleApp;
Expand All @@ -14,6 +13,7 @@ public static void Main(string[] args)
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();

builder.Services.AddRenderingEnvironment();
builder.Services.AddBlazorViewModels();

var app = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;

namespace BitzArt.Blazor.MVVM;

public static class AddRenderingEnvironmentExtension
{
public static IServiceCollection AddRenderingEnvironment(this IServiceCollection services)
{
services.AddTransient(x => x.GetRenderingEnvironment());

return services;
}

private static RenderingEnvironment GetRenderingEnvironment(this IServiceProvider serviceProvider)
{
var isBrowser = OperatingSystem.IsBrowser();
var isServer = !isBrowser;

var isPrerender = isServer && serviceProvider.GetIsPrerender();

return new()
{
IsServer = !isBrowser,
IsClient = isBrowser,
IsPrerender = isPrerender
};
}

private static bool GetIsPrerender(this IServiceProvider serviceProvider)
{
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();

var JSRuntimeType = jsRuntime.GetType();
if (JSRuntimeType.Name != "RemoteJSRuntime") return false;

var IsInitializedProperty = jsRuntime.GetType().GetProperty("IsInitialized");
var isInitialized = IsInitializedProperty?.GetValue(jsRuntime);

return isInitialized is not true;
}
}
70 changes: 42 additions & 28 deletions src/BitzArt.Blazor.MVVM/Models/PageBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using System.Text;
using System.Text.Json;

namespace BitzArt.Blazor.MVVM;
Expand All @@ -7,30 +10,34 @@ namespace BitzArt.Blazor.MVVM;
/// Blazor page base class with view model support.
/// </summary>
/// <typeparam name="TViewModel">Type of this component's ViewModel</typeparam>
public abstract class PageBase<TViewModel> : ComponentBase, IPersistentComponent, IDisposable
public abstract partial class PageBase<TViewModel> : ComponentBase, IPersistentComponent
where TViewModel : ComponentViewModel
{
private const string StateKey = "state";

/// <summary>
/// This component's persistent state.
/// </summary>
[Inject]
public PersistentComponentState ComponentState { get; private set; } = null!;

/// <summary>
/// This page's ViewModel.
/// </summary>
[Inject]
protected TViewModel ViewModel { get; set; } = null!;

[Inject]
private IJSRuntime Js { get; set; } = default!;

[Inject]
private RenderingEnvironment RenderingEnvironment { get; set; } = null!;

/// <summary>
/// Navigation manager.
/// </summary>
[Inject]
protected NavigationManager NavigationManager { get; set; } = null!;

private PersistingComponentStateSubscription persistingSubscription;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var state = SerializeState();
if (state is not null) builder.AddMarkupContent(1, state);
}

/// <summary>
/// Method invoked when the component is ready to start, having received its initial
Expand All @@ -43,43 +50,55 @@ protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
ViewModel.Component = this;
persistingSubscription = ComponentState!.RegisterOnPersisting(PersistState);

await RestoreStateAsync();
}

private Task PersistState()
private string? SerializeState()
{
PersistComponentState(ViewModel, StateKey, strict: false);

return Task.CompletedTask;
return SerializeComponentState(ViewModel, StateKey, strict: false);
}

private void PersistComponentState(ComponentViewModel viewModel, string key, bool strict = true)
private string? SerializeComponentState(ComponentViewModel viewModel, string key, bool strict = true)
{
if (ViewModel is not IStatefulViewModel statefulViewModel)
{
if (strict) throw new InvalidOperationException($"ViewModel '{viewModel.GetType().Name}' must implement IStatefulViewModel");
return;
return null;
}

ComponentState.PersistAsJson(StateKey, statefulViewModel.ComponentState);
return Serialize(statefulViewModel.ComponentState, key);
}

private static string? Serialize(object state, string key)
{
if (state is null || OperatingSystem.IsBrowser())
return null;

var json = JsonSerializer.SerializeToUtf8Bytes(state, StateJsonOptionsProvider.Options);
var base64 = Convert.ToBase64String(json);
return $"<script id=\"{key}\" type=\"text/template\">{base64}</script>";
}

private async Task RestoreStateAsync()
{
await RestoreComponentStateAsync(ViewModel, StateKey);
var isPrerender = RenderingEnvironment.IsPrerender;
var state = isPrerender
? null
: await Js.InvokeAsync<string?>("getInnerText", [StateKey]);

await RestoreComponentStateAsync(ViewModel, state);
}

private async Task RestoreComponentStateAsync(ComponentViewModel viewModel, string key)
private async Task RestoreComponentStateAsync(ComponentViewModel viewModel, string? state)
{
if (viewModel is not IStatefulViewModel statefulViewModel) return;

var stateExists = ComponentState.TryTakeFromJson<JsonElement>(key, out var state);

if (stateExists)
if (state is not null)
{
statefulViewModel.ComponentState = JsonSerializer.Deserialize(state, statefulViewModel.StateType, PersistentStateJsonSerializerOptionsProvider.Options)!;
var buffer = Convert.FromBase64String(state);
var json = Encoding.UTF8.GetString(buffer);
statefulViewModel.ComponentState = JsonSerializer.Deserialize(json, statefulViewModel.StateType, StateJsonOptionsProvider.Options)!;
}
else
{
Expand All @@ -104,14 +123,9 @@ public override Task SetParametersAsync(ParameterView parameters)

return base.SetParametersAsync(parameters);
}

/// <summary>
/// Disposes of the page.
/// </summary>
public void Dispose() => persistingSubscription.Dispose();
}

internal static class PersistentStateJsonSerializerOptionsProvider
internal static class StateJsonOptionsProvider
{
public static readonly JsonSerializerOptions Options = new()
{
Expand Down
59 changes: 59 additions & 0 deletions src/BitzArt.Blazor.MVVM/Models/PersistentComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Text.Json;
using System.Text;

namespace BitzArt.Blazor.MVVM;

public abstract class PersistentComponentBase<TState> : ComponentBase
where TState : new()
{
[Inject]
private IJSRuntime Js { get; set; } = default!;
protected TState State { get; set; } = new();
private const string StateKey = "state";

protected override void BuildRenderTree(RenderTreeBuilder builder) =>
builder.AddMarkupContent(1, Serialize());

protected virtual Task InitializeStateAsync() => Task.CompletedTask;

protected override async Task OnInitializedAsync()
{
if (!OperatingSystem.IsBrowser())
{
await InitializeStateAsync();
return;
}

var stateJson = await Js.InvokeAsync<string?>($"document.getElementById({StateKey}).innerText");

if (string.IsNullOrWhiteSpace(stateJson))
{
await InitializeStateAsync();
return;
}

try
{
var buffer = Convert.FromBase64String(stateJson);
var json = Encoding.UTF8.GetString(buffer);
State = JsonSerializer.Deserialize<TState>(json)!;
}
catch
{
await InitializeStateAsync();
}
}

private string Serialize()
{
if (State is null || OperatingSystem.IsBrowser())
return "";

var json = JsonSerializer.SerializeToUtf8Bytes(State);
var base64 = Convert.ToBase64String(json);
return $"<script id=\"{StateKey}\" type=\"text/template\">{base64}</script>";
}
}
8 changes: 8 additions & 0 deletions src/BitzArt.Blazor.MVVM/Models/RenderingEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BitzArt.Blazor.MVVM;

public class RenderingEnvironment
{
public required bool IsPrerender { get; init; }
public required bool IsServer { get; init; }
public required bool IsClient { get; init; }
}

0 comments on commit 2d9e797

Please sign in to comment.