// 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);
                        }
                    }
                }
Beispiel #2
0
        /// <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();
            }
        }