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"; +}