Skip to content

Commit 792fcff

Browse files
committed
Do not update properties when updated after restoring
1 parent 6fa70d8 commit 792fcff

File tree

2 files changed

+97
-57
lines changed

2 files changed

+97
-57
lines changed
Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
5+
46
namespace Microsoft.AspNetCore.Components;
57

68
/// <summary>
@@ -20,26 +22,27 @@ namespace Microsoft.AspNetCore.Components;
2022
/// public string? NonPrerenderingData { get; set; }
2123
/// </code>
2224
/// </example>
25+
/// <remarks>
26+
/// Initializes a new instance of the <see cref="RestoreStateOnPrerenderingAttribute"/> class.
27+
/// </remarks>
28+
/// <param name="enable">
29+
/// <see langword="true"/> to enable state restoration during prerendering (default);
30+
/// <see langword="false"/> to disable state restoration during prerendering.
31+
/// </param>
2332
[AttributeUsage(AttributeTargets.Property)]
24-
public sealed class RestoreStateOnPrerenderingAttribute : Attribute, IPersistentStateFilter
33+
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
34+
public sealed class RestoreStateOnPrerenderingAttribute(bool enable = true) : Attribute, IPersistentStateFilter
2535
{
26-
/// <summary>
27-
/// Initializes a new instance of the <see cref="RestoreStateOnPrerenderingAttribute"/> class.
28-
/// </summary>
29-
/// <param name="enable">
30-
/// <see langword="true"/> to enable state restoration during prerendering (default);
31-
/// <see langword="false"/> to disable state restoration during prerendering.
32-
/// </param>
33-
public RestoreStateOnPrerenderingAttribute(bool enable = true)
34-
{
35-
Filter = enable ? WebPersistenceFilter.Prerendering : new WebPersistenceFilter(WebPersistenceScenarioType.Prerendering, enabled: false);
36-
}
37-
38-
internal WebPersistenceFilter Filter { get; }
36+
internal WebPersistenceFilter Filter { get; } = enable ? WebPersistenceFilter.Prerendering : new WebPersistenceFilter(WebPersistenceScenarioType.Prerendering, enabled: false);
3937

4038
bool IPersistentStateFilter.SupportsScenario(IPersistentComponentStateScenario scenario)
4139
=> Filter.SupportsScenario(scenario);
4240

4341
bool IPersistentStateFilter.ShouldRestore(IPersistentComponentStateScenario scenario)
4442
=> Filter.ShouldRestore(scenario);
43+
44+
private string GetDebuggerDisplay()
45+
{
46+
return $"RestoreStateOnPrerenderingAttribute: (Enabled: {enable})";
47+
}
4548
}

src/Components/Components/src/PersistentStateValueProvider.cs

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,12 @@ public bool SupportsScenario(IPersistentComponentStateScenario scenario)
394394

395395
internal class ComponentSubscription
396396
{
397+
private static readonly object _uninitializedValue = new();
397398
private readonly PersistingComponentStateSubscription? _persistingSubscription;
398399
private readonly RestoringComponentStateSubscription? _restoringSubscription;
399-
private object? _lastValue;
400-
private bool _hasComputedValueFirstTime;
400+
private object? _lastValue = _uninitializedValue;
401+
private bool _hasPendingInitialValue;
402+
private bool _ignoreUpdatedValues;
401403
private readonly PersistentComponentState _state;
402404
private readonly ComponentState _subscriber;
403405
private readonly string _propertyName;
@@ -434,45 +436,38 @@ public ComponentSubscription(
434436

435437
internal object? GetOrComputeLastValue()
436438
{
437-
if (!_hasComputedValueFirstTime)
439+
var isInitialized = !ReferenceEquals(_lastValue, _uninitializedValue);
440+
if (!isInitialized)
438441
{
439-
_hasComputedValueFirstTime = true;
440-
RestoreProperty();
441-
}
442-
443-
return _lastValue;
444-
}
445-
446-
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
447-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
448-
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
449-
[UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
450-
private Task PersistProperty()
451-
{
452-
// The key needs to be computed here, do not move this outside of the lambda.
453-
var storageKey = ComputeKey(_subscriber, _propertyName);
454-
455-
var property = _propertyGetter.GetValue(_subscriber.Component);
456-
if (property == null)
457-
{
458-
Log.SkippedPersistingNullValue(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
459-
return Task.CompletedTask;
442+
// Remove the uninitialized sentinel.
443+
_lastValue = null;
444+
if (_hasPendingInitialValue)
445+
{
446+
RestoreProperty();
447+
_hasPendingInitialValue = false;
448+
}
460449
}
461-
462-
if (_customSerializer != null)
450+
else
463451
{
464-
Log.PersistingValueToState(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
465-
466-
using var writer = new PooledArrayBufferWriter<byte>();
467-
_customSerializer.Persist(_propertyType, property, writer);
468-
_state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray());
469-
return Task.CompletedTask;
452+
if (_ignoreUpdatedValues)
453+
{
454+
// At this point, we just received a value update from `RestoreProperty`.
455+
// The property value might have been modified by the component and in this
456+
// case we want to overwrite it with the value we just restored.
457+
_ignoreUpdatedValues = false;
458+
return _lastValue;
459+
}
460+
else
461+
{
462+
// In this case, the component might have modified the property value after
463+
// we restored it from the persistent state. We don't want to overwrite it
464+
// with a previously restored value.
465+
var currentPropertyValue = _propertyGetter.GetValue(_subscriber.Component);
466+
return currentPropertyValue;
467+
}
470468
}
471469

472-
// Fallback to JSON serialization
473-
Log.PersistingValueToState(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
474-
_state.PersistAsJson(storageKey, property, _propertyType);
475-
return Task.CompletedTask;
470+
return _lastValue;
476471
}
477472

478473
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
@@ -481,12 +476,14 @@ private Task PersistProperty()
481476
[UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
482477
private void RestoreProperty()
483478
{
484-
if (!_hasComputedValueFirstTime)
479+
var skipNotifications = _hasPendingInitialValue;
480+
if (ReferenceEquals(_lastValue, _uninitializedValue) && !_hasPendingInitialValue)
485481
{
486-
// We'll get invoked right away upon subscription. This is too early to try and restore the value as
487-
// the component state might not be fully initialized yet.
488-
// For that reason, skip the restore operation until GetOrComputeLastValue is called.
489-
// It will trigger the restore operation at the right time.
482+
// Upon subscribing, the callback might be invoked right away,
483+
// but this is too early to restore the first value since the component state
484+
// hasn't been fully initialized yet.
485+
// For that reason, we make a mark to restore the state on GetOrComputeLastValue.
486+
_hasPendingInitialValue = true;
490487
return;
491488
}
492489

@@ -500,7 +497,11 @@ private void RestoreProperty()
500497
Log.RestoringValueFromState(_logger, storageKey, _propertyType.Name, _propertyName);
501498
var sequence = new ReadOnlySequence<byte>(data!);
502499
_lastValue = _customSerializer.Restore(_propertyType, sequence);
503-
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
500+
if (!skipNotifications)
501+
{
502+
_ignoreUpdatedValues = true;
503+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
504+
}
504505
}
505506
else
506507
{
@@ -513,7 +514,11 @@ private void RestoreProperty()
513514
{
514515
Log.RestoredValueFromPersistentState(_logger, storageKey, _propertyType.Name, "null", _propertyName);
515516
_lastValue = value;
516-
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
517+
if (!skipNotifications)
518+
{
519+
_ignoreUpdatedValues = true;
520+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
521+
}
517522
}
518523
else
519524
{
@@ -522,6 +527,38 @@ private void RestoreProperty()
522527
}
523528
}
524529

530+
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
531+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
532+
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
533+
[UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
534+
private Task PersistProperty()
535+
{
536+
// The key needs to be computed here, do not move this outside of the lambda.
537+
var storageKey = ComputeKey(_subscriber, _propertyName);
538+
539+
var property = _propertyGetter.GetValue(_subscriber.Component);
540+
if (property == null)
541+
{
542+
Log.SkippedPersistingNullValue(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
543+
return Task.CompletedTask;
544+
}
545+
546+
if (_customSerializer != null)
547+
{
548+
Log.PersistingValueToState(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
549+
550+
using var writer = new PooledArrayBufferWriter<byte>();
551+
_customSerializer.Persist(_propertyType, property, writer);
552+
_state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray());
553+
return Task.CompletedTask;
554+
}
555+
556+
// Fallback to JSON serialization
557+
Log.PersistingValueToState(_logger, storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
558+
_state.PersistAsJson(storageKey, property, _propertyType);
559+
return Task.CompletedTask;
560+
}
561+
525562
public void Dispose()
526563
{
527564
_persistingSubscription?.Dispose();

0 commit comments

Comments
 (0)