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

Commit

Permalink
Rework state management and injections
Browse files Browse the repository at this point in the history
commit 9a00c1e
Author: Yuriy Durov <[email protected]>
Date:   Wed May 22 21:09:44 2024 +0400

    Rework state management

commit ecfddfe
Author: Vladimir Seldemirov <[email protected]>
Date:   Wed May 22 13:37:33 2024 +0400

    Split state restoring
  • Loading branch information
YuriyDurov committed May 22, 2024
1 parent c75a630 commit d49f102
Show file tree
Hide file tree
Showing 29 changed files with 567 additions and 196 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
@inherits ComponentBase<CounterViewModel>

@if (ViewModel.State is not null)
{
<div class="card">

<h4>@ViewModel.NameOnPage</h4>

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

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

<small style="margin-top:1rem;">@ViewModel.State.Text</small>

<small style="margin-top:1rem;">@Description</small>

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;

namespace BitzArt.Blazor.MVVM.SampleApp.Client.Components;

public partial class Counter : ComponentBase<CounterViewModel>
{
[Parameter, EditorRequired]
public string? Description { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.card {
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
width: 400px;
}

.card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Current render mode: @RenderingEnvironment</p>

<p>@ViewModel.State?.Text</p>

<Counter ViewModel="ViewModel.Counter1" />
<Counter ViewModel="ViewModel.Counter2" />
<Counter
ViewModel="ViewModel.Counter1"
Description="This counter was initialized while prerendering" />

@if (RenderingEnvironment.IsPrerender)
{
<h4 style="margin-top: 2rem;">
Awaiting interactivity...
</h4>
}
else
{
<Counter
ViewModel="ViewModel.Counter2"
Description="This counter's value was increased by a parent" />

<Counter
ViewModel="ViewModel.Counter3"
Description="This counter's value is being increased by a timer"/>
}

<ComponentStateContainer ViewModel="ViewModel" />
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
using Microsoft.AspNetCore.Components;
namespace BitzArt.Blazor.MVVM.SampleApp;

namespace BitzArt.Blazor.MVVM.SampleApp;

public class CounterPageViewModel(
RenderingEnvironment renderingEnvironment)
public class CounterPageViewModel(RenderingEnvironment renderingEnvironment)
: ViewModel<CounterPageViewModelState>
{
[Inject]
[NestViewModel]
public CounterViewModel Counter1 { get; set; } = null!;

[Inject]
[NestViewModel]
public CounterViewModel Counter2 { get; set; } = null!;

[NestViewModel]
public CounterViewModel Counter3 { get; set; } = null!;

public override void InitializeState()
{
State.Text = $"ViewModel State initialized on: {renderingEnvironment}";
Counter2.State!.Count = 100;
State.Text = $"Page State initialized on: {renderingEnvironment}";
}

protected override void OnDependenciesInjected()
{
Counter2.OnStateInitialized += (_) =>
{
Counter2.State!.Count += 100;
};
}
}

public class CounterPageViewModelState
public class CounterPageViewModelState : ComponentState
{
public string Text { get; set; } = "State not initialized";
public string Text { get; set; } = "Page State not initialized";
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
namespace BitzArt.Blazor.MVVM.SampleApp;

public class CounterViewModel : ViewModel<CounterState>
public class CounterViewModel(RenderingEnvironment renderingEnvironment)
: ViewModel<CounterState>, IDisposable
{
private readonly Timer _timer;
[ParentViewModel]
public CounterPageViewModel Parent { get; set; } = null!;

public CounterViewModel()
private Timer? _timer;

public string NameOnPage
{
_timer = new Timer(TimerIncrementCount, null, 1000, 1000);
get
{
if (this == Parent.Counter1) return "Counter 1";
if (this == Parent.Counter2) return "Counter 2";
if (this == Parent.Counter3) return "Counter 3";
return string.Empty;
}
}

public override void InitializeState()
{
State.Count = 0;
State.Text = $"Counter State initialized on: {renderingEnvironment}";
}

private void TimerIncrementCount(object? state)
protected override void OnDependenciesInjected()
{
if (State is null) return;
if (this == Parent.Counter3) _timer = new Timer(IncrementCount, null, 1000, 1000);
}

public void IncrementCount(object? state = null)
{
State.Count++;
ComponentStateHasChanged();
}

public void IncrementCount()
public void Dispose()
{
if (State is null) return;

State.Count++;
ComponentStateHasChanged();
_timer?.Dispose();
GC.SuppressFinalize(this);
}
}

public class CounterState
public class CounterState : ComponentState
{
public int? Count { get; set; }
public int Count { get; set; } = 0;

public string Text { get; set; } = "Counter State not initialized";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BitzArt.Blazor.MVVM.SampleApp</a>
<a class="navbar-brand" href="">BitzArt.Blazor.MVVM</a>
</div>
</div>

Expand Down
6 changes: 6 additions & 0 deletions src/BitzArt.Blazor.MVVM/Attributes/NestViewModelAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BitzArt.Blazor.MVVM;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class NestViewModelAttribute : Attribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BitzArt.Blazor.MVVM;

// TODO: Implement ParentAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ParentViewModelAttribute : Attribute
{
}
3 changes: 2 additions & 1 deletion src/BitzArt.Blazor.MVVM/Builder/BlazorMvvmBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public interface IBlazorMvvmBuilder
{
public IServiceCollection ServiceCollection { get; }

public IViewModelFactory Factory { get; }
internal IViewModelFactory Factory { get; }
}

internal class BlazorMvvmBuilder : IBlazorMvvmBuilder
Expand All @@ -27,5 +27,6 @@ public BlazorMvvmBuilder(

ServiceCollection.AddSingleton(Factory);
ServiceCollection.AddSingleton<BlazorViewModelStateManager>();
ServiceCollection.AddScoped<PageStateDictionaryContainer>();
}
}
95 changes: 95 additions & 0 deletions src/BitzArt.Blazor.MVVM/Components/ComponentBase{TViewModel}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BitzArt.Blazor.MVVM;

public abstract class ComponentBase<TViewModel> : ComponentBase, IStateComponent
where TViewModel : ViewModel
{
[Parameter, EditorRequired]
public TViewModel ViewModel { get; set; } = null!;

[Inject]
protected IServiceProvider ServiceProvider { get; set; } = null!;

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

[Inject]
internal PageStateDictionaryContainer PageStateDictionaryContainer { get; set; } = null!;

[Inject]
private protected BlazorViewModelStateManager StateManager { get; set; } = null!;

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

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

/// <summary>
/// Method invoked when the component is ready to start, having received its initial
/// parameters from its parent in the render tree. Override this method if you will
/// perform an asynchronous operation and want the component to refresh when that
/// operation is completed.
/// </summary>
/// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
ViewModel.OnComponentStateChanged += async (_) =>
{
await InvokeAsync(StateHasChanged);
};

await SetupStateAsync();
}

private async Task SetupStateAsync()
{
var shouldRestoreState = ShouldRestoreState();

if (shouldRestoreState) await RestoreStateAsync();
else await InitializeStateAsync();
}

protected virtual bool ShouldRestoreState()
{
return !RenderingEnvironment.IsPrerender;
}

protected virtual async Task InitializeStateAsync()
{
if (ViewModel is not IStatefulViewModel statefulViewModel) return;

await StateManager.InitializeStateAsync(ViewModel);

statefulViewModel.State.IsInitialized = true;

statefulViewModel.OnStateInitialized?.Invoke(ViewModel);
statefulViewModel.OnStateInitializedAsync?.Invoke(ViewModel);

StateHasChanged();
}

protected virtual async Task RestoreStateAsync()
{
if (ViewModel is not IStatefulViewModel statefulViewModel) return;

await PageStateDictionaryContainer!.WaitUntilConfiguredAsync();
var state = PageStateDictionaryContainer!.PageStateDictionary!.Get(ViewModel.Signature);
if (state is null)
{
await InitializeStateAsync();
return;
}
if (state!.GetType() != statefulViewModel.StateType) throw new InvalidOperationException("State type mismatch.");
statefulViewModel.State = state;
statefulViewModel.State.IsInitialized = true;
}

void IStateComponent.StateHasChanged() => InvokeAsync(StateHasChanged);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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<TViewModel>, IStateComponent
public abstract class PageBase<TViewModel> : ComponentBase<TViewModel>, IStateComponent, IDisposable
where TViewModel : ViewModel
{
[Inject]
Expand All @@ -19,33 +19,19 @@ public abstract class PageBase<TViewModel> : ComponentBase<TViewModel>, IStateCo
set => base.ViewModel = value;
}

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

private const string StateKey = "state";

protected override async Task RestoreStateAsync()
{
await RestoreComponentStateAsync(ViewModel);
}

private async Task RestoreComponentStateAsync(ViewModel viewModel)
{
var isPrerender = RenderingEnvironment.IsPrerender;
var state = isPrerender
? null
: await Js.InvokeAsync<string?>("getInnerText", [StateKey]);
var state = await Js.InvokeAsync<string?>("getInnerText", [StateKey]);
var buffer = Convert.FromBase64String(state!);
var json = Encoding.UTF8.GetString(buffer);

if (state is not null)
{
var buffer = Convert.FromBase64String(state);
var json = Encoding.UTF8.GetString(buffer);
var pageState = await StateManager.RestoreStateAsync(ViewModel, json);
PageStateDictionaryContainer.PageStateDictionary = pageState;
PageStateDictionaryContainer.MarkConfigured();

await StateManager.RestoreStateAsync(viewModel, json);
}
else
{
await StateManager.InitializeStateAsync(viewModel);
}
await base.RestoreStateAsync();
}

/// <summary>
Expand All @@ -57,6 +43,11 @@ public override Task SetParametersAsync(ParameterView parameters)

return base.SetParametersAsync(parameters);
}

public void Dispose()
{
PageStateDictionaryContainer.Dispose();
}
}

internal static class StateJsonOptionsProvider
Expand Down
4 changes: 2 additions & 2 deletions src/BitzArt.Blazor.MVVM/Extensions/AddViewModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static IBlazorMvvmBuilder AddViewModel(this IBlazorMvvmBuilder builder, T
builder.ServiceCollection.AddTransient(viewModelType, serviceProvider =>
{
var factory = serviceProvider.GetRequiredService<IViewModelFactory>();
return factory.Create(serviceProvider, viewModelType);
return factory.Create(serviceProvider, viewModelType, new RootComponentSignature());
});

return builder;
Expand All @@ -92,7 +92,7 @@ public static IBlazorMvvmBuilder AddViewModel<TViewModel>(this IBlazorMvvmBuilde
builder.ServiceCollection.AddTransient(serviceProvider =>
{
var factory = serviceProvider.GetRequiredService<IViewModelFactory>();
return factory.Create<TViewModel>(serviceProvider);
return factory.Create<TViewModel>(serviceProvider, new RootComponentSignature());
});

return builder;
Expand Down
Loading

0 comments on commit d49f102

Please sign in to comment.