【问题标题】:Blazor pass ValidationMessage to extended InputText componentBlazor 将 ValidationMessage 传递给扩展的 InputText 组件
【发布时间】:2021-08-18 20:08:55
【问题描述】:

我有一个继承自 InputTextExtendedInputText 组件

@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>

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

【问题讨论】:

  • 我有一个“复合”组件,我使用的组件与您正在查看的组件相似。如果您愿意,我将发布组件代码作为答案,您可以从中挑选您使用的内容。它构建了一个带有可选标签和验证信息的 BootStrap 风格的输入。您可以在此处查看控件 - cec-blazor-database.azurewebsites.net/fetchdata。编辑记录并清除温度。
  • 嗨@MrCakaShaunCurtis,如果您的组件允许我将相关的验证消息内容传递给组件,我非常想看看您有什么,谢谢!

标签: c# blazor blazor-editform


【解决方案1】:

在 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 中的相关错误

【讨论】:

    【解决方案2】:

    类似组件的完整代码。请注意,组件获取验证消息,您不需要传递它。

    我想我已经包含了唯一的依赖类,但我可能遗漏了一些东西。

    /// ============================================================
    /// 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();
            }
        }
    }
    

    实际效果如下:

    【讨论】:

    • 如果这没有帮助,我会删除答案。
    • 再次感谢您的意见,它确实帮助我指明了正确的方向。实际上与 EditForm/InputText 提供的内容有相当多的重叠,所以我实际上不必添加太多代码,我正在寻找的部分是私有 RenderFragment,它对我来说是一个新概念!
    • @CT14.IT。没问题,很高兴它有帮助。将您的答案标记为答案(我认为您可能需要等待几天才能这样做)并给我们的两个答案 +1。
    • 再次感谢,会做(当我被允许时)!
    【解决方案3】:

    我将以下代码用于我创建的组件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="(() =&gt; Command.Title)" 这样的参数,这个 RenderFragment 就可以为您完成这项工作。

    【讨论】:

    • 您好,感谢您提供建议的答案,抱歉,我不太了解如何使用它来将验证消息传递给组件
    • 再次感谢您的意见,它确实帮助我指明了正确的方向。实际上与 EditForm/InputText 提供的内容有相当多的重叠,所以我实际上不必添加太多代码,我正在寻找的部分是私有 RenderFragment,它对我来说是一个新概念!
    • 如果您解决了问题,请将其中一个答案标记为正确或有用。谢谢
    • 再次感谢,我会尽可能将自己的答案标记为已接受的答案。塔
    猜你喜欢
    • 2021-01-18
    • 1970-01-01
    • 2023-03-20
    • 2017-08-19
    • 1970-01-01
    • 2021-06-22
    • 2021-11-16
    • 2012-06-27
    • 2021-10-27
    相关资源
    最近更新 更多