Skip to content

Commit e28fcc4

Browse files
Copilotjaviercn
andcommitted
Fix Blazor persistent component state issue and add comprehensive tests
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
1 parent cbf0000 commit e28fcc4

File tree

2 files changed

+124
-2
lines changed

2 files changed

+124
-2
lines changed

src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ internal void RestoreProperty()
143143
Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
144144
var sequence = new ReadOnlySequence<byte>(data!);
145145
_lastValue = _customSerializer.Restore(_propertyType, sequence);
146+
_ignoreComponentPropertyValue = true;
146147
if (!skipNotifications)
147148
{
148-
_ignoreComponentPropertyValue = true;
149149
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
150150
}
151151
}
@@ -160,9 +160,9 @@ internal void RestoreProperty()
160160
{
161161
Log.RestoredValueFromPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
162162
_lastValue = value;
163+
_ignoreComponentPropertyValue = true;
163164
if (!skipNotifications)
164165
{
165-
_ignoreComponentPropertyValue = true;
166166
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
167167
}
168168
}

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,4 +708,126 @@ public void Constructor_WorksCorrectly_ForPublicProperty()
708708
Assert.NotNull(subscription);
709709
subscription.Dispose();
710710
}
711+
712+
[Fact]
713+
public void RestoreProperty_SetsIgnoreComponentPropertyValueUnconditionally_WhenRestoringFromState()
714+
{
715+
// Arrange
716+
var initialState = new Dictionary<string, byte[]>();
717+
var state = new PersistentComponentState(initialState, [], []);
718+
var renderer = new TestRenderer();
719+
var component = new TestComponent { State = "initial-value" };
720+
var componentState = CreateComponentState(renderer, component, null, null);
721+
722+
// Pre-populate the state with serialized data
723+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState, nameof(TestComponent.State));
724+
initialState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value", JsonSerializerOptions.Web);
725+
state.InitializeExistingState(initialState, RestoreContext.LastSnapshot);
726+
727+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
728+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
729+
var logger = NullLogger.Instance;
730+
731+
var subscription = new PersistentValueProviderComponentSubscription(
732+
state, componentState, cascadingParameterInfo, serviceProvider, logger);
733+
734+
// Initialize the subscription so it has a _lastValue set
735+
subscription.GetOrComputeLastValue();
736+
737+
// Act - Call RestoreProperty to simulate restoration after component re-creation
738+
var restoreMethod = typeof(PersistentValueProviderComponentSubscription)
739+
.GetMethod("RestoreProperty", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
740+
restoreMethod.Invoke(subscription, null);
741+
742+
// Assert - The ignoreComponentPropertyValue flag should cause the restored value to be returned
743+
var result = subscription.GetOrComputeLastValue();
744+
Assert.Equal("persisted-value", result);
745+
746+
subscription.Dispose();
747+
}
748+
749+
[Fact]
750+
public void GetOrComputeLastValue_ReturnsRestoredValue_AfterComponentRecreation()
751+
{
752+
// Arrange
753+
var initialState = new Dictionary<string, byte[]>();
754+
var state = new PersistentComponentState(initialState, [], []);
755+
var renderer = new TestRenderer();
756+
var component = new TestComponent { State = "component-property-value" };
757+
var componentState = CreateComponentState(renderer, component, null, null);
758+
759+
// Pre-populate the state with serialized data
760+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState, nameof(TestComponent.State));
761+
initialState[key] = JsonSerializer.SerializeToUtf8Bytes("restored-value", JsonSerializerOptions.Web);
762+
state.InitializeExistingState(initialState, RestoreContext.LastSnapshot);
763+
764+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
765+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
766+
var logger = NullLogger.Instance;
767+
768+
var subscription = new PersistentValueProviderComponentSubscription(
769+
state, componentState, cascadingParameterInfo, serviceProvider, logger);
770+
771+
// Act - First call initializes and restores from persistent state
772+
var firstResult = subscription.GetOrComputeLastValue();
773+
774+
// Assert - Should return the restored value, not the component's property value
775+
Assert.Equal("restored-value", firstResult);
776+
777+
// Simulate component property being updated after restoration
778+
component.State = "updated-component-value";
779+
780+
// Second call should return the updated component value since the flag was reset
781+
var secondResult = subscription.GetOrComputeLastValue();
782+
Assert.Equal("updated-component-value", secondResult);
783+
784+
subscription.Dispose();
785+
}
786+
787+
[Fact]
788+
public void RestoreProperty_WorksCorrectly_ForComponentsWithoutKey()
789+
{
790+
// Arrange - This test simulates components being added/removed without @key
791+
var initialState = new Dictionary<string, byte[]>();
792+
var state = new PersistentComponentState(initialState, [], []);
793+
var renderer = new TestRenderer();
794+
795+
// First component instance
796+
var component1 = new TestComponent { State = "initial-value-1" };
797+
var componentState1 = CreateComponentState(renderer, component1, null, null);
798+
799+
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State));
800+
initialState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value-from-previous-session", JsonSerializerOptions.Web);
801+
state.InitializeExistingState(initialState, RestoreContext.LastSnapshot);
802+
803+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
804+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
805+
var logger = NullLogger.Instance;
806+
807+
var subscription1 = new PersistentValueProviderComponentSubscription(
808+
state, componentState1, cascadingParameterInfo, serviceProvider, logger);
809+
810+
// Act - Simulate component being destroyed and recreated (like during navigation)
811+
var result1 = subscription1.GetOrComputeLastValue();
812+
subscription1.Dispose();
813+
814+
// Simulate a new component instance being created (like after navigation back)
815+
var component2 = new TestComponent { State = "initial-value-2" };
816+
var componentState2 = CreateComponentState(renderer, component2, null, null);
817+
818+
// Re-populate state for the new component instance
819+
initialState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value-from-previous-session", JsonSerializerOptions.Web);
820+
state.InitializeExistingState(initialState, RestoreContext.LastSnapshot);
821+
822+
var subscription2 = new PersistentValueProviderComponentSubscription(
823+
state, componentState2, cascadingParameterInfo, serviceProvider, logger);
824+
825+
var result2 = subscription2.GetOrComputeLastValue();
826+
827+
// Assert - Both instances should restore the persisted value correctly
828+
Assert.Equal("persisted-value-from-previous-session", result1);
829+
Assert.Equal("persisted-value-from-previous-session", result2);
830+
831+
subscription2.Dispose();
832+
}
711833
}

0 commit comments

Comments
 (0)