diff --git a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/Pages/CounterPage.razor b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/Pages/CounterPage.razor
index d731665..0c74ee3 100644
--- a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/Pages/CounterPage.razor
+++ b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/Pages/CounterPage.razor
@@ -10,7 +10,7 @@
@ViewModel.State?.Text
-
-
+
+
diff --git a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterPageViewModel.cs b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterPageViewModel.cs
index 61980d4..1083322 100644
--- a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterPageViewModel.cs
+++ b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterPageViewModel.cs
@@ -7,29 +7,19 @@ public class CounterPageViewModel(
: ViewModel
{
[Inject]
- public CounterViewModel Counter1ViewModel { get; set; } = null!;
+ public CounterViewModel Counter1 { get; set; } = null!;
[Inject]
- public CounterViewModel Counter2ViewModel { get; set; } = null!;
+ public CounterViewModel Counter2 { get; set; } = null!;
public override void InitializeState()
{
State.Text = $"ViewModel State initialized on: {renderingEnvironment}";
-
- OnStateRestored();
- Counter2ViewModel.State!.Count += 100;
- }
-
- public override void OnStateRestored()
- {
- Counter1ViewModel.State = State.Counter1State;
- Counter2ViewModel.State = State.Counter2State;
+ Counter2.State!.Count = 100;
}
}
public class CounterPageViewModelState
{
public string Text { get; set; } = "State not initialized";
- public CounterState Counter1State { get; set; } = new();
- public CounterState Counter2State { get; set; } = new();
}
diff --git a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterViewModel.cs b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterViewModel.cs
index d83503b..701af1b 100644
--- a/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterViewModel.cs
+++ b/sample/BitzArt.Blazor.MVVM.SampleApp/BitzArt.Blazor.MVVM.SampleApp.Client/ViewModels/CounterViewModel.cs
@@ -1,9 +1,7 @@
namespace BitzArt.Blazor.MVVM.SampleApp;
-public class CounterViewModel : ViewModel
+public class CounterViewModel : ViewModel
{
- public CounterState? State { get; set; }
-
private readonly Timer _timer;
public CounterViewModel()
@@ -11,6 +9,11 @@ public CounterViewModel()
_timer = new Timer(TimerIncrementCount, null, 1000, 1000);
}
+ public override void InitializeState()
+ {
+ State.Count = 0;
+ }
+
private void TimerIncrementCount(object? state)
{
if (State is null) return;
@@ -30,5 +33,5 @@ public void IncrementCount()
public class CounterState
{
- public int Count { get; set; } = 0;
+ public int? Count { get; set; };
}
diff --git a/src/BitzArt.Blazor.MVVM/BitzArt.Blazor.MVVM.csproj b/src/BitzArt.Blazor.MVVM/BitzArt.Blazor.MVVM.csproj
index 2d0448b..91c0333 100644
--- a/src/BitzArt.Blazor.MVVM/BitzArt.Blazor.MVVM.csproj
+++ b/src/BitzArt.Blazor.MVVM/BitzArt.Blazor.MVVM.csproj
@@ -15,14 +15,9 @@
https://bitzart.github.io/Blazor.MVVM/
README.md
-
-
-
-
-
+
-
diff --git a/src/BitzArt.Blazor.MVVM/Builder/BlazorMvvmBuilder.cs b/src/BitzArt.Blazor.MVVM/Builder/BlazorMvvmBuilder.cs
index 8b9b06a..1c5b144 100644
--- a/src/BitzArt.Blazor.MVVM/Builder/BlazorMvvmBuilder.cs
+++ b/src/BitzArt.Blazor.MVVM/Builder/BlazorMvvmBuilder.cs
@@ -15,7 +15,9 @@ internal class BlazorMvvmBuilder : IBlazorMvvmBuilder
public IViewModelFactory Factory { get; set; }
- public BlazorMvvmBuilder(IServiceCollection serviceCollection, IViewModelFactory factory)
+ public BlazorMvvmBuilder(
+ IServiceCollection serviceCollection,
+ IViewModelFactory factory)
{
ServiceCollection = serviceCollection;
Factory = factory;
@@ -24,5 +26,6 @@ public BlazorMvvmBuilder(IServiceCollection serviceCollection, IViewModelFactory
throw new InvalidOperationException("IViewModelFactory is already registered in this ServiceCollection.");
ServiceCollection.AddSingleton(Factory);
+ ServiceCollection.AddSingleton();
}
}
diff --git a/src/BitzArt.Blazor.MVVM/ComponentStateContainer.razor.cs b/src/BitzArt.Blazor.MVVM/ComponentStateContainer.razor.cs
deleted file mode 100644
index 806764c..0000000
--- a/src/BitzArt.Blazor.MVVM/ComponentStateContainer.razor.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Rendering;
-using Microsoft.JSInterop;
-using System.Text.Json;
-
-namespace BitzArt.Blazor.MVVM;
-
-public partial class ComponentStateContainer : ComponentBase
-{
- [Parameter] public ViewModel ViewModel { get; set; } = null!;
- [Parameter] public string StateKey { get; set; } = "state";
-
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- var state = SerializeState();
- if (state is not null) builder.AddMarkupContent(1, state);
- }
-
- private string? SerializeState()
- {
- return SerializeComponentState(ViewModel, StateKey, strict: false);
- }
-
- private string? SerializeComponentState(ViewModel 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 null;
- }
-
- return Serialize(statefulViewModel.State, 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 $"";
- }
-}
\ No newline at end of file
diff --git a/src/BitzArt.Blazor.MVVM/Factory/ViewModelFactory.cs b/src/BitzArt.Blazor.MVVM/Factory/ViewModelFactory.cs
index 7bfa7a7..11d6a4d 100644
--- a/src/BitzArt.Blazor.MVVM/Factory/ViewModelFactory.cs
+++ b/src/BitzArt.Blazor.MVVM/Factory/ViewModelFactory.cs
@@ -12,7 +12,7 @@ public interface IViewModelFactory
internal class ViewModelFactory : IViewModelFactory
{
- public ICollection InjectionMaps { get; set; }
+ private Dictionary InjectionMaps { get; set; }
public ViewModelFactory()
{
@@ -21,14 +21,13 @@ public ViewModelFactory()
public void AddViewModel(Type viewModelType, string registrationKey)
{
- if (!typeof(ViewModel).IsAssignableFrom(viewModelType)) throw new InvalidOperationException(
- $"Type {viewModelType.Name} is not a ViewModel.");
+ if (!typeof(ViewModel).IsAssignableFrom(viewModelType))
+ throw new InvalidOperationException($"Type {viewModelType.Name} is not a ViewModel.");
- if (InjectionMaps.Any(x => x.ViewModelType == viewModelType))
- throw new InvalidOperationException(
- $"ViewModel {viewModelType.Name} is already registered in the factory.");
+ if (InjectionMaps.ContainsKey(viewModelType))
+ throw new InvalidOperationException($"ViewModel {viewModelType.Name} is already registered in the factory.");
- InjectionMaps.Add(new(viewModelType, registrationKey));
+ InjectionMaps.Add(viewModelType, new(viewModelType, registrationKey));
}
public TViewModel Create(IServiceProvider serviceProvider)
@@ -37,9 +36,8 @@ public TViewModel Create(IServiceProvider serviceProvider)
public ViewModel Create(IServiceProvider serviceProvider, Type viewModelType)
{
- var viewModelMap = InjectionMaps.FirstOrDefault(x => x.ViewModelType == viewModelType)
- ?? throw new InvalidOperationException(
- $"ViewModel {viewModelType.Name} is not registered in the factory.");
+ var viewModelMap = InjectionMaps.GetValueOrDefault(viewModelType)
+ ?? throw new InvalidOperationException($"ViewModel {viewModelType.Name} is not registered in the factory.");
var viewModel = (ViewModel)serviceProvider.GetRequiredKeyedService(typeof(ViewModel), viewModelMap.RegistrationKey);
foreach (var injection in viewModelMap.Injections)
@@ -50,4 +48,12 @@ public ViewModel Create(IServiceProvider serviceProvider, Type viewModelType)
viewModel.OnDependenciesInjected();
return viewModel;
}
+
+ public ViewModelInjectionMap GetInjectionMap(Type viewModelType)
+ {
+ var injectionMap = InjectionMaps.GetValueOrDefault(viewModelType)
+ ?? throw new InvalidOperationException($"ViewModel {viewModelType.Name} is not registered in the factory.");
+
+ return injectionMap;
+ }
}
diff --git a/src/BitzArt.Blazor.MVVM/Models/PageBase.cs b/src/BitzArt.Blazor.MVVM/Models/PageBase.cs
index 4a4990e..95e1e4d 100644
--- a/src/BitzArt.Blazor.MVVM/Models/PageBase.cs
+++ b/src/BitzArt.Blazor.MVVM/Models/PageBase.cs
@@ -19,6 +19,8 @@ public abstract class PageBase : ComponentBase, IStateCo
set => base.ViewModel = value;
}
+ [Inject] private BlazorViewModelStateManager StateManager { get; set; } = null!;
+
private const string StateKey = "state";
protected override async Task RestoreStateAsync()
@@ -28,8 +30,6 @@ protected override async Task RestoreStateAsync()
private async Task RestoreComponentStateAsync(ViewModel viewModel)
{
- if (viewModel is not IStatefulViewModel statefulViewModel) return;
-
var isPrerender = RenderingEnvironment.IsPrerender;
var state = isPrerender
? null
@@ -39,24 +39,13 @@ private async Task RestoreComponentStateAsync(ViewModel viewModel)
{
var buffer = Convert.FromBase64String(state);
var json = Encoding.UTF8.GetString(buffer);
- statefulViewModel.State = JsonSerializer.Deserialize(json, statefulViewModel.StateType, StateJsonOptionsProvider.Options)!;
-
- statefulViewModel.OnStateRestored();
- await statefulViewModel.OnStateRestoredAsync();
+
+ await StateManager.RestoreStateAsync(viewModel, json);
}
else
{
- statefulViewModel.State = Activator.CreateInstance(statefulViewModel.StateType)!;
-
- statefulViewModel.OnStateChanged();
- await statefulViewModel.OnStateChangedAsync();
-
- statefulViewModel.InitializeState();
- await statefulViewModel.InitializeStateAsync();
+ await StateManager.InitializeStateAsync(viewModel);
}
-
- statefulViewModel.OnStateChanged();
- await statefulViewModel.OnStateChangedAsync();
}
///
@@ -75,6 +64,7 @@ internal static class StateJsonOptionsProvider
public static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
diff --git a/src/BitzArt.Blazor.MVVM/State/BlazorViewModelStateManager.cs b/src/BitzArt.Blazor.MVVM/State/BlazorViewModelStateManager.cs
new file mode 100644
index 0000000..efc6b19
--- /dev/null
+++ b/src/BitzArt.Blazor.MVVM/State/BlazorViewModelStateManager.cs
@@ -0,0 +1,123 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace BitzArt.Blazor.MVVM;
+
+internal class BlazorViewModelStateManager(IViewModelFactory viewModelFactory)
+{
+ private ViewModelFactory _viewModelFactory { get; } = (ViewModelFactory)viewModelFactory;
+
+ private const string _nestedStatePrefix = "n__";
+
+ ///
+ /// Serializes states in s hierarchy to JSON encoded as UTF-8 bytes.
+ ///
+ public byte[]? SerializeState(ViewModel viewModel)
+ {
+ var injectionMap = _viewModelFactory.GetInjectionMap(viewModel.GetType());
+ var state = GetState(viewModel, injectionMap);
+ if (state is null) return null;
+
+ return JsonSerializer.SerializeToUtf8Bytes(state, StateJsonOptionsProvider.Options);
+ }
+
+ private Dictionary? GetState(ViewModel viewModel, ViewModelInjectionMap injectionMap)
+ {
+ var state = new Dictionary();
+
+ if (viewModel is IStatefulViewModel statefulViewModel)
+ {
+ foreach (var property in statefulViewModel.StateType.GetProperties())
+ state.Add(property.Name, property.GetValue(statefulViewModel.State));
+ }
+
+ foreach (var injection in injectionMap.Injections)
+ {
+ var property = injection.Property;
+ var nestedViewModel = property.GetValue(viewModel) as ViewModel;
+ var nestedMap = _viewModelFactory.GetInjectionMap(injection.ViewModelType);
+ var nestedState = GetState(nestedViewModel!, nestedMap);
+
+ if (nestedState is not null)
+ state.Add($"{_nestedStatePrefix}{property.Name}", nestedState);
+ }
+
+ return state.Values.Count != 0 ? state : null;
+ }
+
+ ///
+ /// Restores states in s hierarchy from JSON string.
+ ///
+ public async Task RestoreStateAsync(ViewModel viewModel, string json)
+ {
+ var node = JsonNode.Parse(json);
+ if (node is null) return;
+
+ var injectionMap = _viewModelFactory.GetInjectionMap(viewModel.GetType());
+ await RestoreStateAsync(viewModel, node, injectionMap);
+ }
+
+ private async Task RestoreStateAsync(ViewModel viewModel, JsonNode node, ViewModelInjectionMap injectionMap)
+ {
+ foreach (var injection in injectionMap.Injections)
+ {
+ var property = injection.Property;
+ var jsonKey = $"{_nestedStatePrefix}{property.Name}";
+ var nestedNode = node[jsonKey];
+
+ if (nestedNode is null) continue;
+
+ var nestedViewModel = property.GetValue(viewModel) as ViewModel;
+ var nestedMap = _viewModelFactory.GetInjectionMap(injection.ViewModelType);
+
+ await RestoreStateAsync(nestedViewModel!, nestedNode, nestedMap);
+
+ (node as JsonObject)!.Remove(jsonKey);
+ }
+
+ if (viewModel is not IStatefulViewModel statefulViewModel) return;
+
+ var state = JsonSerializer.Deserialize(node, statefulViewModel.StateType, StateJsonOptionsProvider.Options);
+ statefulViewModel.State = state!;
+
+ statefulViewModel.OnStateRestored();
+ await statefulViewModel.OnStateRestoredAsync();
+
+ statefulViewModel.OnStateChanged();
+ await statefulViewModel.OnStateChangedAsync();
+ }
+
+ ///
+ /// Initializes states in s hierarchy.
+ ///
+ public async Task InitializeStateAsync(ViewModel viewModel)
+ {
+ var injectionMap = _viewModelFactory.GetInjectionMap(viewModel.GetType());
+ await InitializeStateAsync(viewModel, injectionMap);
+ }
+
+ private async Task InitializeStateAsync(ViewModel viewModel, ViewModelInjectionMap injectionMap)
+ {
+ foreach (var injection in injectionMap.Injections)
+ {
+ var property = injection.Property;
+ var nestedViewModel = property.GetValue(viewModel) as ViewModel;
+ var nestedMap = _viewModelFactory.GetInjectionMap(injection.ViewModelType);
+
+ await InitializeStateAsync(nestedViewModel!, nestedMap);
+ }
+
+ if (viewModel is not IStatefulViewModel statefulViewModel) return;
+
+ statefulViewModel.State = Activator.CreateInstance(statefulViewModel.StateType)!;
+
+ statefulViewModel.OnStateChanged();
+ await statefulViewModel.OnStateChangedAsync();
+
+ statefulViewModel.InitializeState();
+ await statefulViewModel.InitializeStateAsync();
+
+ statefulViewModel.OnStateChanged();
+ await statefulViewModel.OnStateChangedAsync();
+ }
+}
diff --git a/src/BitzArt.Blazor.MVVM/ComponentStateContainer.razor b/src/BitzArt.Blazor.MVVM/State/ComponentStateContainer.razor
similarity index 100%
rename from src/BitzArt.Blazor.MVVM/ComponentStateContainer.razor
rename to src/BitzArt.Blazor.MVVM/State/ComponentStateContainer.razor
diff --git a/src/BitzArt.Blazor.MVVM/State/ComponentStateContainer.razor.cs b/src/BitzArt.Blazor.MVVM/State/ComponentStateContainer.razor.cs
new file mode 100644
index 0000000..0711016
--- /dev/null
+++ b/src/BitzArt.Blazor.MVVM/State/ComponentStateContainer.razor.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace BitzArt.Blazor.MVVM;
+
+public partial class ComponentStateContainer : ComponentBase
+{
+ [Inject]
+ private BlazorViewModelStateManager StateManager { get; set; } = null!;
+
+ [Inject]
+ private RenderingEnvironment RenderingEnvironment { get; set; } = null!;
+
+ [Parameter]
+ public ViewModel ViewModel { get; set; } = null!;
+
+ [Parameter]
+ public string StateElementKey { get; set; } = "state";
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ var stateElement = BuildStateElement();
+ if (stateElement is not null)
+ builder.AddMarkupContent(1, stateElement);
+ }
+
+ private string? BuildStateElement()
+ {
+ if (!RenderingEnvironment.IsPrerender) return null;
+
+ var json = StateManager.SerializeState(ViewModel);
+ if (json is null) return null;
+
+ var stateEncoded = Convert.ToBase64String(json);
+
+ return $"";
+ }
+}
diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ServiceRegistrationTests.cs b/tests/BitzArt.Blazor.MVVM.Tests/ServiceRegistrationTests.cs
index 30523c4..c078da8 100644
--- a/tests/BitzArt.Blazor.MVVM.Tests/ServiceRegistrationTests.cs
+++ b/tests/BitzArt.Blazor.MVVM.Tests/ServiceRegistrationTests.cs
@@ -50,10 +50,9 @@ public void AddViewModels_OnBlazorMvvmBuilder_ShouldAddViewModelsFromAllAssembli
var factory = (ViewModelFactory)serviceProvider.GetRequiredService();
- Assert.NotEmpty(factory.InjectionMaps);
- Assert.Contains(factory.InjectionMaps, x => x.ViewModelType == typeof(TestParentViewModel));
- Assert.Contains(factory.InjectionMaps, x => x.ViewModelType == typeof(TestLayer1ViewModel));
- Assert.Contains(factory.InjectionMaps, x => x.ViewModelType == typeof(TestLayer2ViewModel));
+ Assert.NotNull(factory.GetInjectionMap(typeof(TestParentViewModel)));
+ Assert.NotNull(factory.GetInjectionMap(typeof(TestLayer1ViewModel)));
+ Assert.NotNull(factory.GetInjectionMap(typeof(TestLayer2ViewModel)));
}
[Fact]
@@ -70,10 +69,9 @@ public void AddViewModel_OnBlazorMvvmBuilder_ShouldMapViewModelInjections()
var factory = (ViewModelFactory)serviceProvider.GetRequiredService();
- Assert.NotEmpty(factory.InjectionMaps);
- Assert.Contains(factory.InjectionMaps, x => x.ViewModelType == typeof(TestParentViewModel));
+ Assert.NotNull(factory.GetInjectionMap(typeof(TestParentViewModel)));
- var parentInjectionMap = factory.InjectionMaps.First(x => x.ViewModelType == typeof(TestParentViewModel));
+ var parentInjectionMap = factory.GetInjectionMap(typeof(TestParentViewModel));
Assert.Single(parentInjectionMap.Injections);
}
diff --git a/tests/BitzArt.Blazor.MVVM.Tests/StateManagerTests.cs b/tests/BitzArt.Blazor.MVVM.Tests/StateManagerTests.cs
new file mode 100644
index 0000000..48350ca
--- /dev/null
+++ b/tests/BitzArt.Blazor.MVVM.Tests/StateManagerTests.cs
@@ -0,0 +1,163 @@
+using BitzArt.Blazor.MVVM.Tests.ViewModels;
+using Microsoft.Extensions.DependencyInjection;
+using System.Text;
+
+namespace BitzArt.Blazor.MVVM.Tests;
+
+public class StateManagerTests
+{
+ [Fact]
+ public void SerializeState_StatefulViewModel_ReturnsString()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+
+ // Act
+ var state = stateManager.SerializeState(viewModel);
+
+ // Assert
+ Assert.NotNull(state);
+ }
+
+ [Fact]
+ public void SerializeState_StatefulViewModelWithNestedViewModels_ReturnsString()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+
+ // Act
+ var state = stateManager.SerializeState(viewModel);
+
+ // Assert
+ Assert.NotNull(state);
+ }
+
+ [Fact]
+ public void SerializeState_ViewModelWithoutState_ReturnsNull()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+
+ // Act
+ var state = stateManager.SerializeState(viewModel);
+
+ // Assert
+ Assert.Null(state);
+ }
+
+ [Fact]
+ public async Task RestoreStateAsync_StatefulViewModel_RestoresState()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+
+ var viewModelTitle = nameof(TestLayer1ViewModel);
+ viewModel.State.Title = viewModelTitle;
+
+ var state = stateManager.SerializeState(viewModel);
+ viewModel.State = new();
+
+ // Act
+ await stateManager.RestoreStateAsync(viewModel, Encoding.UTF8.GetString(state!));
+
+ // Assert
+ Assert.Equal(viewModelTitle, viewModel.State.Title);
+ }
+
+ [Fact]
+ public async Task RestoreStateAsync_StatefulViewModelWithNestedViewModels_RestoresStateHierarchy()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+ var nestedViewModel = viewModel.TestNestedStatefulViewModel;
+
+ var viewModelTitle = nameof(TestStatefulParentViewModel);
+ viewModel.State.Title = viewModelTitle;
+
+ var nestedViewModelTitle = nameof(TestNestedStatefulViewModel);
+ nestedViewModel.State.Title = nestedViewModelTitle;
+
+ var state = stateManager.SerializeState(viewModel);
+ viewModel.State = new();
+ nestedViewModel.State = new();
+
+ // Act
+ await stateManager.RestoreStateAsync(viewModel, Encoding.UTF8.GetString(state!));
+
+ // Assert
+ Assert.Equal(viewModelTitle, viewModel.State.Title);
+ Assert.Equal(nestedViewModelTitle, nestedViewModel.State.Title);
+ }
+
+ [Fact]
+ public async Task InitializeStateAsync_StatefulViewModel_InitializesState()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+
+ var viewModelTitle = viewModel.State.Title;
+ viewModel.State.Title = nameof(TestNestedStatefulViewModel);
+
+ // Act
+ await stateManager.InitializeStateAsync(viewModel);
+
+ // Assert
+ Assert.Equal(viewModelTitle, viewModel.State.Title);
+ }
+
+ [Fact]
+ public async Task InitializeStateAsync_StatefulViewModelWithNestedViewModels_InitializesStateHierarchy()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddBlazorMvvm().AddViewModels();
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var stateManager = serviceProvider.GetRequiredService();
+ var viewModel = serviceProvider.GetRequiredService();
+ var nestedViewModel = viewModel.TestNestedStatefulViewModel;
+
+ var viewModelTitle = viewModel.State.Title;
+ viewModel.State.Title = nameof(TestStatefulParentViewModel);
+
+ var nestedViewModelTitle = nestedViewModel.State.Title;
+ nestedViewModel.State.Title = nameof(TestNestedStatefulViewModel);
+
+ // Act
+ await stateManager.InitializeStateAsync(viewModel);
+
+ // Assert
+ Assert.Equal(viewModelTitle, viewModel.State.Title);
+ Assert.Equal(nestedViewModelTitle, nestedViewModel.State.Title);
+ }
+}
diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedStatefulViewModel.cs b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedStatefulViewModel.cs
new file mode 100644
index 0000000..dc2d4bf
--- /dev/null
+++ b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedStatefulViewModel.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Components;
+
+namespace BitzArt.Blazor.MVVM.Tests.ViewModels;
+
+public class TestNestedStatefulViewModel : ViewModel
+{
+ [Inject]
+ public TestNestedViewModel TestNestedViewModel { get; set; } = null!;
+}
+
+public class TestNestedStatefulViewModelState
+{
+ public string? Title { get; set; } = "Title";
+}
\ No newline at end of file
diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedViewModel.cs b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedViewModel.cs
new file mode 100644
index 0000000..253a87a
--- /dev/null
+++ b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestNestedViewModel.cs
@@ -0,0 +1,5 @@
+namespace BitzArt.Blazor.MVVM.Tests.ViewModels;
+
+public class TestNestedViewModel : ViewModel
+{
+}
\ No newline at end of file
diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestStatefulParentViewModel.cs b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestStatefulParentViewModel.cs
new file mode 100644
index 0000000..d9a35f9
--- /dev/null
+++ b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/TestStatefulParentViewModel.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Components;
+
+namespace BitzArt.Blazor.MVVM.Tests.ViewModels;
+
+public class TestStatefulParentViewModel : ViewModel
+{
+ [Inject]
+ public TestNestedStatefulViewModel TestNestedStatefulViewModel { get; set; } = null!;
+}
+
+public class TestStatefulParentViewModelState
+{
+ public string Title { get; set; } = "Title";
+}