// It's internal because there isn't a known use case for user code comparing // ParameterCollection instances, and even if there was, it's unlikely it should // use these equality rules which are designed for their effect on rendering. internal bool DefinitelyEquals(ParameterCollection oldParameters) { // In general we can't detect mutations on arbitrary objects. We can't trust // things like .Equals or .GetHashCode because they usually only tell us about // shallow changes, not deep mutations. So we return false if both: // [1] All the parameters are known to be immutable (i.e., Type.IsPrimitive // or is in a known set of common immutable types) // [2] And all the parameter values are equal to their previous values // Otherwise be conservative and return false. // To make this check cheaper, since parameters are virtually always generated in // a deterministic order, we don't bother to account for reordering, so if any // of the names don't match sequentially we just return false too. // // The logic here may look kind of epic, and would certainly be simpler if we // used ParameterEnumerator.GetEnumerator(), but it's perf-critical and this // implementation requires a lot fewer instructions than a GetEnumerator-based one. var oldIndex = oldParameters._ownerIndex; var newIndex = _ownerIndex; var oldEndIndexExcl = oldIndex + oldParameters._frames[oldIndex].ComponentSubtreeLength; var newEndIndexExcl = newIndex + _frames[newIndex].ComponentSubtreeLength; while (true) { // First, stop if we've reached the end of either subtree oldIndex++; newIndex++; var oldFinished = oldIndex == oldEndIndexExcl; var newFinished = newIndex == newEndIndexExcl; if (oldFinished || newFinished) { return(oldFinished == newFinished); // Same only if we have same number of parameters } else { // Since neither subtree has finished, it's safe to read the next frame from both ref var oldFrame = ref oldParameters._frames[oldIndex]; ref var newFrame = ref _frames[newIndex]; // Stop if we've reached the end of either subtree's sequence of attributes oldFinished = oldFrame.FrameType != RenderTreeFrameType.Attribute; newFinished = newFrame.FrameType != RenderTreeFrameType.Attribute; if (oldFinished || newFinished) { return(oldFinished == newFinished); // Same only if we have same number of parameters } else { if (!string.Equals(oldFrame.AttributeName, newFrame.AttributeName, StringComparison.Ordinal)) { return(false); // Different names } var oldValue = oldFrame.AttributeValue; var newValue = newFrame.AttributeValue; if (ChangeDetection.MayHaveChanged(oldValue, newValue)) { return(false); } } }
/// <inheritdoc /> public void SetParameters(ParameterCollection parameters) { // Implementing the parameter binding manually, instead of just calling // parameters.AssignToProperties(this), is just a very slight perf optimization // and makes it simpler impose rules about the params being required or not. var hasSuppliedValue = false; var previousValue = Value; var previousFixed = IsFixed; Value = default; ChildContent = null; Name = null; IsFixed = false; foreach (var parameter in parameters) { if (parameter.Name.Equals(nameof(Value), StringComparison.OrdinalIgnoreCase)) { Value = (T)parameter.Value; hasSuppliedValue = true; } else if (parameter.Name.Equals(nameof(ChildContent), StringComparison.OrdinalIgnoreCase)) { ChildContent = (RenderFragment)parameter.Value; } else if (parameter.Name.Equals(nameof(Name), StringComparison.OrdinalIgnoreCase)) { Name = (string)parameter.Value; if (string.IsNullOrEmpty(Name)) { throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue<T>)}' does not allow null or empty values."); } } else if (parameter.Name.Equals(nameof(IsFixed), StringComparison.OrdinalIgnoreCase)) { IsFixed = (bool)parameter.Value; } else { throw new ArgumentException($"The component '{nameof(CascadingValue<T>)}' does not accept a parameter with the name '{parameter.Name}'."); } } if (_hasSetParametersPreviously && IsFixed != previousFixed) { throw new InvalidOperationException($"The value of {nameof(IsFixed)} cannot be changed dynamically."); } _hasSetParametersPreviously = true; // It's OK for the value to be null, but some "Value" param must be suppled // because it serves no useful purpose to have a <CascadingValue> otherwise. if (!hasSuppliedValue) { throw new ArgumentException($"Missing required parameter '{nameof(Value)}' for component '{nameof(Parameter)}'."); } // Rendering is most efficient when things are queued from rootmost to leafmost. // Given a components A (parent) -> B (child), you want them to be queued in order // [A, B] because if you had [B, A], then the render for A might change B's params // making it render again, so you'd render [B, A, B], which is wasteful. // At some point we might consider making the render queue actually enforce this // ordering during insertion. // // For the CascadingValue component, this observation is why it's important to render // ourself before notifying subscribers (which can be grandchildren or deeper). // If we rerendered subscribers first, then our own subsequent render might cause an // further update that makes those nested subscribers get rendered twice. _renderHandle.Render(Render); if (_subscribers != null && ChangeDetection.MayHaveChanged(previousValue, Value)) { NotifySubscribers(); } }