Skip to content

Instantly share code, notes, and snippets.

@HTD
Last active November 21, 2021 18:05
Show Gist options
  • Select an option

  • Save HTD/f056f2bc9bf9872d9212452e59332605 to your computer and use it in GitHub Desktop.

Select an option

Save HTD/f056f2bc9bf9872d9212452e59332605 to your computer and use it in GitHub Desktop.
Blazor spurious updates workaround
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
namespace Woof.Blazor;
/// <summary>
/// Use this instead of <see cref="ComponentBase"/> to prevent <see cref="EventCallback"/>s triggers from changing the component's state and resetting all parameters.
/// </summary>
public abstract class ProtectedComponentBase : ComponentBase {
/// <summary>
/// Gets or sets a value indicating that every updates should be accepted, AKA default ComponentBase behavior.
/// </summary>
public bool AcceptAllUpdates { get; set; }
/// <summary>
/// Gets or sets a value indicating that every update to the component state should be ignored, basically making the component static.
/// Set this in fully manual updates scenario. It disables the data binding completely.
/// </summary>
public bool RejectAllUpdates { get; set; }
/// <summary>
/// Prevents re-rendering if protected.
/// </summary>
/// <returns></returns>
protected override bool ShouldRender() => IsReallyChanged;
/// <summary>
/// Checks if the component parameters really changed since the last call ignoring <see cref="EventCallback"/> changes.
/// </summary>
/// <param name="parameters">The special collection of parameters to be set on binding.</param>
/// <returns>Task completed when the state tracking is done.</returns>
public override async Task SetParametersAsync(ParameterView parameters) {
IsReallyChanged = false;
Changed.Clear();
if (!parameters.GetEnumerator().MoveNext()) { // special case - no parameters, if that does't pass, initialization fails.
IsReallyChanged = true;
await base.SetParametersAsync(parameters);
return;
}
if (!Properties.GetEnumerator().MoveNext()) { // another special case - first pass with parameters, necessary for proper component initialization.
foreach (var parameter in parameters)
Properties.Add(parameter.Name, GetType().GetProperty(parameter.Name));
IsReallyChanged = true;
await base.SetParametersAsync(parameters);
return;
}
if (RejectAllUpdates) return;
if (AcceptAllUpdates) await base.SetParametersAsync(parameters);
foreach (var parameter in parameters) { // actual spurious update detection, we comparing parameters Blazor claims changed with actual component properties.
var property = Properties[parameter.Name];
if (property is null) continue;
if (property.GetValue(this)?.Equals(parameter.Value) != true && // value differs
property.PropertyType != typeof(EventCallback) && // also NOT EventCallback
property.PropertyType != typeof(EventCallback<>)) { // and NOT EventCallback<> (because it's the most harmful spurious case)
Changed.Add(parameter.Name, parameter.Value);
IsReallyChanged = true;
}
}
if (IsReallyChanged) await base.SetParametersAsync(ParameterView.FromDictionary(Changed));
}
/// <summary>
/// Notifies the component that its state has changed. When applicable, this will cause the component to be re-rendered.
/// </summary>
public new void StateHasChanged() {
IsReallyChanged = true;
base.StateHasChanged();
IsReallyChanged = false;
}
/// <summary>
/// True if the component's state seems to really be changed.
/// </summary>
private bool IsReallyChanged;
/// <summary>
/// Component parameter properties reflection cache.
/// </summary>
private readonly Dictionary<string, PropertyInfo> Properties = new();
/// <summary>
/// A temporary directory of the changed component state values to build a corrected <see cref="ParameterView"/>.
/// </summary>
private readonly Dictionary<string, object> Changed = new();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment