-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Is there an existing issue for this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe the problem.
There have been various suggestions in the past for ways to get Blazor to detect when the state of an instance of a reference type is unaltered and therefore a child will not need to re-render when its parent renders.
- Blazor change detection too liberal #13610
- Use IEquatable for Blazor object parameters to optimize rendering by default #40867
- Extend API for component attributes with render-comparer different from its value #21915
All have been closed, but I am here to hash out a new idea :)
Describe the solution you'd like
In the ChangeDetection class, have it also check if the new value implements IStructuralEquality
. If so, then ask it if it is structurally equal to the old value.
That way, we can write code like this when working with immutable reference types.
public readonly record ImmutablePerson : IStructuralEquatable
{
public required string FamilyName { get; init; }
public required string PersonalName { get; init; }
bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer) =>
ReferenceEquals(this, other);
int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
GetHashCode();
}
Or like this for mutable reference types (as Array already implements it).
public class Order : IStructuralEquatable
{
public int Id { get; set; }
public int CustomerId { get; set; }
public OrderLine[] Lines { get; set; } = [];
bool IStructuralEquatable.Equals(object? obj, IEqualityComparer comparer) =>
obj is Order other
&& other.Id == Id
&& other.CustomerId == CustomerId
&& StructuralComparisons.StructuralEqualityComparer.Equals(Lines, other.Lines);
int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
HashCode.Combine(
Id,
CustomerId,
StructuralComparisons.StructuralEqualityComparer.GetHashCode(Lines)
);
}
public class OrderLine : IStructuralEquatable
{
public int ProductId { get; set; }
public uint Quantity { get; set; }
bool IStructuralEquatable.Equals(object? obj, IEqualityComparer comparer) =>
obj is OrderLine other
&& other.ProductId == ProductId
&& other.Quantity == Quantity;
int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
HashCode.Combine(ProductId, Quantity);
}
Additional context
I think the following changes to ChangeDetection.MayHaveChanged
might suffice.
static bool MayHaveChanged<T1, T2>(T1 oldValue, T2 newValue)
{
var oldIsNotNull = oldValue != null;
var newIsNotNull = newValue != null;
if (oldIsNotNull != newIsNotNull)
{
return true; // One's null and the other isn't, so different
}
else if (oldIsNotNull) // i.e., both are not null (considering previous check)
{
// *** New check for StructuralEquatable ***
if (oldValue is IStructuralEquatable structuralEquatable)
{
return !structuralEquatable.Equals(newValue, EqualityComparer<T1>.Default);
}
var oldValueType = oldValue!.GetType();
var newValueType = newValue!.GetType();
if (oldValueType != newValueType // Definitely different
|| !IsKnownImmutableType(oldValueType) // Maybe different
|| !oldValue.Equals(newValue)) // Somebody says they are different
{
return true;
}
}
// By now we know either both are null, or they are the same immutable type
// and ThatType::Equals says the two values are equal.
return false;
}
Note: Related issue in .Net runtime that would ensure this works for parameters of type List<T>
etc - dotnet/runtime#117199