You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Blazor] Support for declaratively persisting component and services state (#60634)
# Adds a declarative model for persistent component and services state
This PR augments the persistent component state feature with a declarative model that allows the developer to place an attribute on components and services properties to indicate that they should be persisted during prerendering so that it is accessible when the application becomes interactive.
## Scenarios
### Serializing state for a component
```razor
@page "/counter"
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @OnClick="IncrementCount">Click me</button>
@code {
[SupplyParameterFromPersistentComponentState]
private int CurrentCount { get; set; }
private void IncrementCount()
{
CurrentCount++;
}
}
```
* Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized and deserialized during prerendering.
### Serializing state for multiple components of the same type
**ParentComponent.razor**
```razor
@page "/parent"
@foreach (var element in elements)
{
<ChildComponent @key="element.Name" />
}
```
**ChildComponent.razor**
```razor
<div>
<p>Current count: @Element.CurrentCount</p>
<button class="btn btn-primary" @OnClick="IncrementCount">Click me</button>
</div>
@code {
[SupplyParameterFromPersistentComponentState]
public State Element { get; set; }
private void IncrementCount()
{
Element.CurrentCount++;
}
protected override void OnInitialized()
{
Element ??= new State();
}
private class State
{
public int CurrentCount { get; set; }
}
}
```
* Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized and deserialized during prerendering.
* The `@key` directive is used to ensure that the state is correctly associated with the component instance.
* The `Element` property is initialized in the `OnInitialized` method to avoid null reference exceptions similarly to how we do it for
query parameters and form data.
### Serializing state for a service
**CounterService.cs**
```csharp
public class CounterService
{
[SupplyParameterFromPersistentComponentState]
public int CurrentCount { get; set; }
public void IncrementCount()
{
CurrentCount++;
}
}
```
**Program.cs**
```
builder.Services.AddRazorComponents().RegisterPersistentService<CounterService>(RenderMode.InteractiveAuto);
```
* Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized during prerendering and deserialized when the application becomes interactive.
* The `AddPersistentService` method is used to register the service for persistence.
* The render mode is required as can't be inferred from the service type.
* `RenderMode.Server` - The service will be available for interactive server mode.
* `RenderMode.Webassembly` - The service will be available for interactive webassembly mode.
* `RenderMode.InteractiveAuto` - The service will be available for both interactive server and webassembly modes if a component renders in any of those modes.
* The service will be resolved during interactive mode initialization and the properties annotated with `[SupplyParameterFromPersistentComponentState]` will be deserialized.
## Implementation details
### Key Computation
#### For components
We need to generate a unique key for each property that needs to be persisted. For components, this key is computed based on:
- The parent component type
- The component type
- The property name
- The `@key` directive if present and serializable (e.g., `Guid`, `DateOnly`, `TimeOnly`, and primitive types)
The key computation ensures that even if multiple instances of the same component are present on the page (for example, in a loop),
each instance's state can be uniquely identified and persisted.
The key computation algorithm only takes into account a small subset of a component hierarchy for performance reasons.
This limits the ability to persist state on recursive component hierarchies.
Our recommendation for those scenarios is to persist the state at the top level of the hierarchy.
#### For services
Only persisting scoped services is supported. We need to generate a unique key for each property that needs to be persisted.
The key for services is derived from:
- The type used to register the persistent service
- Assembly
- Full type name
- Property name
Properties to be serialized are identified from the actual service instance.
- This approach allows marking an abstraction as a persistent service.
- Enables actual implementations to be internal or different types.
- Supports shared code in different assemblies.
- Each instance must expose the same properties.
### Serialization and Deserialization
By default properties are serialized using the System.Text.Json serializer with default settings.
Note that this method is not trimmer safe and requires the user to ensure that the types used are preserved through some other means.
This is consistent with our usage of System.Text.Json across other areas of the product, like root component parameters or JSInterop.
0 commit comments