Blazor 将 ValidationMessage 传递给扩展的 InputText 组件

Blazor pass ValidationMessage to extended InputText component

我有一个 ExtendedInputText 组件继承自 InputText

@inherits InputText

<div class="flex">
    <label class="w-1/2">
        @Label 
        @if(Required){
            <span class="text-red-500 ml-1">*</span>
        }
    </label>
    <InputText
        class="flex-1 border border-gray-200 bg-white p-2 rounded"
        placeholder="@Label"
        Value="@Value"
        ValueChanged="@ValueChanged"
        ValueExpression="@ValueExpression"
        Required="@Required"
    />
    
</div>

@code
{

    [Parameter]
    public bool Required { get; set; }

    [Parameter]
    public string Label { get; set; }
}

我打算用它来代替这个

<EditForm Model="Command" OnValidSubmit="OnValidSubmit">

  <FluentValidationValidator />
  <ValidationSummary />

  <div class="">
    <label>Title <span class="text-red-500">*</span></label>
    <InputText id="Title" @bind-Value="Command.Title" />
    <ValidationMessage For="@(() => Command.Title)" />
  </div>

  <button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>

</EditForm>

有了这个

<EditForm Model="Command" OnValidSubmit="OnValidSubmit">

  <FluentValidationValidator />
  <ValidationSummary />

  <ExtendedInputText Label="Title" Required="true" @bind-Value="Command.Title"/>

  <button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>

</EditForm>

我如何将 <ValidationMessage For="@(() => Command.Title)" /> 传递给 ExtendedInputText 组件并从内部渲染它?

我将以下代码用于我创建的组件 LabelText 但应该用于您的情况:

    public partial class LabelText<T>: ComponentBase
    {
        [Parameter] public Expression<Func<T>> For { get; set; }
        [Parameter] public RenderFragment ChildContent { get; set; }
        private FieldIdentifier _fieldIdentifier;

...
protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "label");
            builder.AddMultipleAttributes(1, AdditionalAttributes);
            builder.AddAttribute(2, "for", _fieldIdentifier.FieldName);
            builder.AddContent(3, label + GetRequired());
            builder.CloseElement();
        }

protected override void OnParametersSet()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{GetType()} requires a cascading parameter " +
                    $"of type {nameof(EditContext)}. For example, you can use {GetType()} inside " +
                    $"an {nameof(EditForm)}.");
            }

            if (For == null) // Not possible except if you manually specify T
            {
                throw new InvalidOperationException($"{GetType()} requires a value for the " +
                    $"{nameof(For)} parameter.");
            }
            _fieldIdentifier = FieldIdentifier.Create(For);
        }

更新

我无法比@MrC 的优秀代码更好地解释

private RenderFragment ValidationFragment => (builder) =>
        {
            if (this.ShowValidation && !this.IsValid)
            {
                builder.OpenElement(310, "div");
                builder.AddAttribute(320, "class", MessageCss);
                builder.OpenComponent<ValidationMessage<TValue>>(330);
                builder.AddAttribute(340, "For", this.ValueExpression);
                builder.CloseComponent();
                builder.CloseElement();
            }
            else if (!string.IsNullOrWhiteSpace(this.HelperText))
            {
                builder.OpenElement(350, "div");
                builder.AddAttribute(360, "class", MessageCss);
                builder.AddContent(370, this.HelperText);
                builder.CloseElement();
            }
        };

您只需要添加一个参数,例如 ValidationMessage="(() => Command.Title)",这个 RenderFragment 会为您完成这项工作。

类似组件的完整代码。注意组件获取的是Validation消息,不需要传递。

我想我已经包括了唯一的依赖者 class,但我可能漏掉了一些东西。

/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================

using Blazr.SPA.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq;
using System.Linq.Expressions;

#nullable enable
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace Blazr.UIComponents
{
    public class FormEditControl<TValue> : ComponentBase
    {
        [Parameter]
        public TValue? Value { get; set; }

        [Parameter] public EventCallback<TValue> ValueChanged { get; set; }

        [Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }

        [Parameter] public string? Label { get; set; }

        [Parameter] public string? HelperText { get; set; }

        [Parameter] public string DivCssClass { get; set; } = "mb-2";

        [Parameter] public string LabelCssClass { get; set; } = "form-label";

        [Parameter] public string ControlCssClass { get; set; } = "form-control";

        [Parameter] public Type ControlType { get; set; } = typeof(InputText);

        [Parameter] public bool ShowValidation { get; set; }

        [Parameter] public bool ShowLabel { get; set; } = true;

        [Parameter] public bool IsRequired { get; set; }

        [Parameter] public bool IsRow { get; set; }

        [CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;

        private readonly string formId = Guid.NewGuid().ToString();

        private bool IsLabel => this.ShowLabel && (!string.IsNullOrWhiteSpace(this.Label) || !string.IsNullOrWhiteSpace(this.FieldName));

        private bool IsValid;

        private FieldIdentifier _fieldIdentifier;

        private ValidationMessageStore? _messageStore;

        private string? DisplayLabel => this.Label ?? this.FieldName;
        private string? FieldName
        {
            get
            {
                string? fieldName = null;
                if (this.ValueExpression != null)
                    ParseAccessor(this.ValueExpression, out var model, out fieldName);
                return fieldName;
            }
        }

        private string MessageCss => CSSBuilder.Class()
            .AddClass("invalid-feedback", !this.IsValid)
            .AddClass("valid-feedback", this.IsValid)
            .Build();

        private string ControlCss => CSSBuilder.Class(this.ControlCssClass)
            .AddClass("is-valid", this.IsValid)
            .AddClass("is-invalid", !this.IsValid)
            .Build();

        protected override void OnInitialized()
        {
            if (CurrentEditContext is null)
                throw new InvalidOperationException($"No Cascading Edit Context Found!");

            if (ValueExpression is null)
                throw new InvalidOperationException($"No ValueExpression defined for the Control!  Define a Bind-Value.");

            if (!ValueChanged.HasDelegate)
                throw new InvalidOperationException($"No ValueChanged defined for the Control! Define a Bind-Value.");

            CurrentEditContext.OnFieldChanged += FieldChanged;
            CurrentEditContext.OnValidationStateChanged += ValidationStateChanged;
            _messageStore = new ValidationMessageStore(this.CurrentEditContext);
            _fieldIdentifier = FieldIdentifier.Create(ValueExpression);
            if (_messageStore is null)
                throw new InvalidOperationException($"Cannot set the Validation Message Store!");

            var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
            var showHelpText = (messages.Count == 0) && this.IsRequired && this.Value is null;
            if (showHelpText && !string.IsNullOrWhiteSpace(this.HelperText))
                _messageStore.Add(_fieldIdentifier, this.HelperText);
        }

        protected void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
        {
            var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
            if (messages != null || messages.Count > 1)
            {
                _messageStore.Clear();
            }
        }

        protected void FieldChanged(object sender, FieldChangedEventArgs e)
        {
            if (e.FieldIdentifier.Equals(_fieldIdentifier))
                _messageStore.Clear();
        }

        protected override void OnParametersSet()
        {
            this.IsValid = true;
            {
                if (this.IsRequired)
                {
                    this.IsValid = false;
                    var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
                    if (messages is null || messages.Count == 0)
                        this.IsValid = true;
                }
            }
        }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            if (IsRow)
                builder.AddContent(1, RowFragment);
            else
                builder.AddContent(2, BaseFragment);
        }

        private RenderFragment BaseFragment => (builder) =>
        {
            builder.OpenElement(0, "div");
            builder.AddAttribute(10, "class", this.DivCssClass);
            builder.AddContent(40, this.LabelFragment);
            builder.AddContent(60, this.ControlFragment);
            builder.AddContent(70, this.ValidationFragment);
            builder.CloseElement();
        };

        private RenderFragment RowFragment => (builder) =>
        {
            builder.OpenElement(0, "div");
            builder.AddAttribute(10, "class", "row form-group");
            builder.OpenElement(20, "div");
            builder.AddAttribute(30, "class", "col-12 col-md-3");
            builder.AddContent(40, this.LabelFragment);
            builder.CloseElement();
            builder.OpenElement(40, "div");
            builder.AddAttribute(50, "class", "col-12 col-md-9");
            builder.AddContent(60, this.ControlFragment);
            builder.AddContent(70, this.ValidationFragment);
            builder.CloseElement();
            builder.CloseElement();
        };

        private RenderFragment LabelFragment => (builder) =>
        {
            if (this.IsLabel)
            {
                builder.OpenElement(110, "label");
                builder.AddAttribute(120, "for", this.formId);
                builder.AddAttribute(130, "class", this.LabelCssClass);
                builder.AddContent(140, this.DisplayLabel);
                builder.CloseElement();
            }
        };


        private RenderFragment ControlFragment => (builder) =>
        {
            builder.OpenComponent(210, this.ControlType);
            builder.AddAttribute(220, "class", this.ControlCss);
            builder.AddAttribute(230, "Value", this.Value);
            builder.AddAttribute(240, "ValueChanged", EventCallback.Factory.Create(this, this.ValueChanged));
            builder.AddAttribute(250, "ValueExpression", this.ValueExpression);
            builder.CloseComponent();
        };

        private RenderFragment ValidationFragment => (builder) =>
        {
            if (this.ShowValidation && !this.IsValid)
            {
                builder.OpenElement(310, "div");
                builder.AddAttribute(320, "class", MessageCss);
                builder.OpenComponent<ValidationMessage<TValue>>(330);
                builder.AddAttribute(340, "For", this.ValueExpression);
                builder.CloseComponent();
                builder.CloseElement();
            }
            else if (!string.IsNullOrWhiteSpace(this.HelperText))
            {
                builder.OpenElement(350, "div");
                builder.AddAttribute(360, "class", MessageCss);
                builder.AddContent(370, this.HelperText);
                builder.CloseElement();
            }
        };

        // Code lifted from FieldIdentifier.cs
        private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
        {
            var accessorBody = accessor.Body;
            if (accessorBody is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Type == typeof(object))
                accessorBody = unaryExpression.Operand;

            if (!(accessorBody is MemberExpression memberExpression))
                throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");

            fieldName = memberExpression.Member.Name;
            if (memberExpression.Expression is ConstantExpression constantExpression)
            {
                if (constantExpression.Value is null)
                    throw new ArgumentException("The provided expression must evaluate to a non-null value.");
                model = constantExpression.Value;
            }
            else if (memberExpression.Expression != null)
            {
                var modelLambda = Expression.Lambda(memberExpression.Expression);
                var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
                var result = modelLambdaCompiled();
                if (result is null)
                    throw new ArgumentException("The provided expression must evaluate to a non-null value.");
                model = result;
            }
            else
                throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
        }
    }
}
#pragma warning restore CS8622
#pragma warning restore CS8602
#nullable disable

CSSBuilder

/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================

using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace Blazr.SPA.Components
{
    public class CSSBuilder
    {
        private Queue<string> _cssQueue = new Queue<string>();

        public static CSSBuilder Class(string cssFragment = null)
        {
            var builder = new CSSBuilder(cssFragment);
            return builder.AddClass(cssFragment);
        }

        public CSSBuilder()
        {
        }

        public CSSBuilder (string cssFragment)
        {
            AddClass(cssFragment);
        }

        public CSSBuilder AddClass(string cssFragment)
        {
            if (!string.IsNullOrWhiteSpace(cssFragment)) _cssQueue.Enqueue(cssFragment);
            return this;
        }

        public CSSBuilder AddClass(IEnumerable<string> cssFragments)
        {
            if (cssFragments != null)
                cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
            return this;
        }

        public CSSBuilder AddClass(string cssFragment, bool WhenTrue)
        {
            if (WhenTrue) return this.AddClass(cssFragment);
            return this;
        }

        public CSSBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString());
            return this;
        }

        public CSSBuilder AddClassFromAttributes(IDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString());
            return this;
        }

        public string Build(string CssFragment = null)
        {
            if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
            if (_cssQueue.Count == 0)
                return string.Empty;
            var sb = new StringBuilder();
            foreach(var str in _cssQueue)
            {
                if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
            }
            return sb.ToString().Trim();
        }
    }
}

实际效果如下:

在 Nicola 和 Shaun 的帮助下,这是对我有用的解决方案。

@inherits InputText

<div class="flex">
    <label class="w-1/2 text-right font-semibold mr-1 py-2">
        @Label
        @if (Required)
        {
            <span class="text-red-500 ml-1">*</span>
        }
    </label>
    <div class="flex-1">
        <InputText class="w-full border border-gray-200 bg-white p-2 rounded"
                    placeholder="@Label"
                    Value="@Value"
                    ValueChanged="@ValueChanged"
                    ValueExpression="@ValueExpression"
                    Required="@Required"/>
        @ValidationFragment
    </div>
</div>

@code
{

    [Parameter]
    public bool Required { get; set; }

    [Parameter]
    public string Label { get; set; }

    private RenderFragment ValidationFragment => (builder) =>
    {
        var messages = EditContext.GetValidationMessages(FieldIdentifier).ToList();
        if(messages is not null && messages.Count > 0)
        {
            builder.OpenElement(310, "div");
            builder.AddAttribute(320, "class", "text-red-500 p-2 w-full");
            builder.OpenComponent<ValidationMessage<string>>(330);
            builder.AddAttribute(340, "For", ValueExpression);
            builder.CloseComponent();
            builder.CloseElement();
        }

    };

}

它们的关键部分是私有 RenderFragment ValidationFragment,它以编程方式构建以显示存储在级联 EditContext

中的相关错误