65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Blazor 中构建 DataList 控件

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2021 年 4 月 23 日

CPOL

5分钟阅读

viewsIcon

15656

如何在 Blazor 中构建 DataList 控件

引言

本文介绍如何在 Blazor 中构建基于 DataList 的输入控件,并使其行为类似于 SelectDataList 出现在 HTML5 中。一些浏览器,特别是 Safari,采纳较慢,因此在 HTML5 早期,其使用有点问题。如今,各种平台上的所有主流浏览器都支持它:您可以在 此处 查看支持列表。

我们将使用 Blazor 的 InputBase 作为基类来构建两个版本的控件,以适应现有的编辑表单框架。在此过程中,我们将深入了解 InputBase 的内部工作原理并探索控件绑定。

HTML DataList

Inputdatalist 关联时,它会根据 datalist 在用户键入时提供过滤建议。开箱即用,用户可以选择一个建议或输入任何文本值。控件的基本标记如下所示

<input type="text" list="countrylist" />

<datalist id="countrylist" />
    <option value="Algeria" />
    <option value="Australia" />
    <option value="Austria" />
<datalist>

示例站点和代码存储库

代码位于我的 Blazor.Database 存储库中,在此 Blazor.SPA/Components/FormControls 处。

您可以在我的 Blazor.Database 演示站点 上看到这些控件的实际运行情况。

探索测试控件中的绑定

在构建控件之前,让我们先探讨一下绑定的内容。如果您熟悉绑定三要素,可以跳过本节。

从一个标准的 Razor 组件和代码隐藏文件开始 - MyInput.razorMyInput.Razor.cs

将以下代码添加到 MyInput.razor.cs

  1. 我们拥有所谓的“绑定属性三要素”。
  2. Value 是要显示的实际值。
  3. ValueChanged 是一个回调,用于设置父级中的值。
  4. ValueExpression 是一个 lambda 表达式,指向父级中的源属性。它用于生成 FieldIdentifier,该 FieldIdentifier 用于验证和状态管理,以唯一标识字段。
  5. CurrentValue 是控件内部的 Value。更改时,它会更新 Value 并调用 ValueChanged
  6. AdditionalAttributes 用于捕获添加到控件的类和其他属性。
namespace MyNameSpace.Components
{
    public partial class MyInput
    {
        [Parameter] public string Value { get; set; }
        [Parameter] public EventCallback<string> ValueChanged { get; set; }
        [Parameter] public Expression<Func<string>> ValueExpression { get; set; }
        [Parameter(CaptureUnmatchedValues = true)] 
         public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }

        protected virtual string CurrentValue
        {
            get => Value;
            set
            {
                if (!value.Equals(this.Value))
                {
                    Value = value;
                    if (ValueChanged.HasDelegate)
                        _ = ValueChanged.InvokeAsync(value);
                }
            }
        }
    }
}

在 razor 文件中添加一个 Text input HTML 控件。

  1. 添加命名空间是为了在源文件数量增长时将 Components 分割到子文件夹中。
  2. @bind-value 指向控件的 CurrentValue 属性。
  3. @attributes 将控件属性添加到 input
@namespace MyNameSpace.Components

<input type="text" @bind-value="this.CurrentValue" @attributes="this.AdditionalAttributes" />

测试页面

将测试页面添加到 Pages - 如果您正在使用测试站点,则可以覆盖 index。我们将使用它来测试所有控件。

这不需要太多解释。Bootstrap 用于格式化,经典 EditFormCheckButton 为我们提供了一个易于设置断点的按钮,以便我们可以检查值和对象。

您可以在表单中看到我们的 MyInput

@page "/"

@using MyNameSpace.Components

<EditForm Model="this.model" OnValidSubmit="this.ValidSubmit">
    <div class="container m-5 p-4 border border-secondary">
        <div class="row mb-2">
            <div class="col-12">
                <h2>Test Editor</h2>
            </div>
        </div>
        <div class="row mb-2">
            <div class="col-4 form-label" for="txtcountry">
                Country
            </div>
            <div class="col-4">
                <MyInput id="txtcountry" @bind-Value="model.Value" class="form-control">
                </MyInput>
            </div>
        </div>
        <div class="row mb-2">
            <div class="col-6">
            </div>
            <div class="col-6 text-right">
                <button class="btn btn-secondary" @onclick="(e) => this.CheckButton()">
                 Check</button>
                <button type="submit" class="btn btn-primary">Submit</button>
            </div>
        </div>
    </div>
</EditForm>

<div class="container">
    <div class="row mb-2">
        <div class="col-4 form-label">
            Test Value
        </div>
        <div class="col-4 form-control">
            @this.model.Value
        </div>
    </div>
    <div class="row mb-2">
        <div class="col-4 form-label">
            Test Index
        </div>
        <div class="col-4 form-control">
            @this.model.index
        </div>
    </div>
</div>
@code {

    Model model = new Model() { Value = "Australia", index = 2 };

    private void CheckButton()
    {
        var x = true;
    }

    private void ValidSubmit()
    {
        var x = true;
    }

    class Model
    {
        public string Value { get; set; } = string.Empty;
        public int index { get; set; } = 0;
    }
}

注意,当您更改 MyInput 中的文本时,值显示会更新。

在底层,Razor 编译器会将包含 MyInput 的部分编译成如下所示的组件代码

__builder2.OpenComponent<TestBlazorServer.Components.MyInput>(12);
__builder2.AddAttribute(13, "id", "txtcountry");
__builder2.AddAttribute(14, "class", "form-control");
__builder2.AddAttribute(15, "Value", 
Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<System.String>
(model.Value));
__builder2.AddAttribute(16, "ValueChanged", 
Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.
TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>
(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>
(this, Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.
CreateInferredEventCallback(this, __value => model.Value = __value, model.Value))));
__builder2.AddAttribute(17, "ValueExpression", 
Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck
<System.Linq.Expressions.Expression<System.Func<System.String>>>(() => model.Value));
__builder2.CloseComponent();

您可以在 obj 文件夹中看到编译后的 C# 文件。在我的项目中,它位于 \obj\Debug\net5.0\RazorDeclaration\Components\FormControls

@bind-value 已转换为对 ValueValueChangedValueExpression 三要素的完整映射。ValueValueExpression 的设置不言自明。ValueChanged 使用代码工厂生成一个运行时方法,该方法映射到 ValueChanged 并将 model.Value 设置为 ValueChanged 返回的值。

这解释了一个常见的误解 - 您可以像这样为 @onchange 附加一个事件处理程序

<input type="text" @bind-value ="model.Value" @onchange="(e) => myonchangehandler()"/>

控件上没有 @onchange 事件,内部控件上的事件已经绑定,因此无法再次绑定。您不会收到错误消息,只是没有触发。

InputBase

让我们继续 InputBase

首先,我们来看 InputText 来了解实现方式

  1. HTML inputvalue 绑定到 CurrentValueonchange 事件绑定到 CurrentValueAsString。值中的任何更改都会调用 CurrentValueASsString 的 setter。
  2. TryParseValueFromString 只是将 value(输入的值)作为 result 传递。无需进行从 string 到其他类型的转换。
public class InputText : InputBase<string?>
{
    [DisallowNull] public ElementReference? Element { get; protected set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "input");
        builder.AddMultipleAttributes(1, AdditionalAttributes);
        builder.AddAttribute(2, "class", CssClass);
        builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue));
        builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string?>
        (this, __value => CurrentValueAsString = __value, CurrentValueAsString));
        builder.AddElementReferenceCapture(5, __inputReference => Element = __inputReference);
        builder.CloseElement();
    }

    protected override bool TryParseValueFromString(string? value, 
    out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
    {
        result = value;
        validationErrorMessage = null;
        return true;
    }
}

让我们深入研究 InputBase

onchange 事件设置 CurrentValueAsString。请注意,它不是虚拟的,因此不能被重写。

protected string? CurrentValueAsString
{
    get => FormatValueAsString(CurrentValue);
    set
    {
        // clear the ValidationMessageStore
        _parsingValidationMessages?.Clear();

        bool parsingFailed;

        // Error if can't be null and value is null.  
        if (_nullableUnderlyingType != null && string.IsNullOrEmpty(value))
        {
            parsingFailed = false;
            CurrentValue = default!;
        }
        // Call TryParseValueFromString.   
        else if (TryParseValueFromString(value, out var parsedValue, 
                 out var validationErrorMessage))
        {
            // If we pass complete and set CurrentValue
            parsingFailed = false;
            CurrentValue = parsedValue!;
        }
        else
        {   
            // We reach here if we fail parsing
            // set flags and make sure we have a ValidationMessageStore
            parsingFailed = true;
            
            if (_parsingValidationMessages == null)
            {
                _parsingValidationMessages = new ValidationMessageStore(EditContext);
            }
            // Add a parsing error message to the store
            _parsingValidationMessages.Add(FieldIdentifier, validationErrorMessage);

            // Since we're not writing to CurrentValue, 
            // we'll need to notify about modification from here
            EditContext.NotifyFieldChanged(FieldIdentifier);
        }

        // skip the validation notification if we were previously valid and still are
        // if we failed this time notify 
        // if we failed last time but are ok now we need notify 
        // to get the validation controls cleared
        if (parsingFailed || _previousParsingAttemptFailed)
        {
            EditContext.NotifyValidationStateChanged();
            _previousParsingAttemptFailed = parsingFailed;
        }
    }
}

输入 value 绑定到 CurrentValue 的 getter,而 CurrentValueAsString 设置它。再次注意,它不是 virtual,因此不能重写。

    protected TValue? CurrentValue
    {
        // straight getter from Value
        get => Value;
        set
        {
            // Checks for equality between submitted value and class Value
            var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
            // and if it's changed
            if (hasChanged)
            {
                // sets the class Value
                Value = value;
                // calls the ValueChanged EventHandler to update the parent value
                _ = ValueChanged.InvokeAsync(Value);
                // Notifies the EditContext that the field has changed 
                // and passes the FieldIdentifier
                EditContext.NotifyFieldChanged(FieldIdentifier);
            }
        }
    }

最后,TryParseValueFromString 是抽象的,因此必须在继承类中实现。它的目的是验证并将提交的 string 转换为正确的 TValue

protected abstract bool TryParseValueFromString(string? value, 
[MaybeNullWhen(false)] out TValue result, 
[NotNullWhen(false)] out string? validationErrorMessage);

构建我们的 DataList 控件

首先,我们需要一个辅助类来获取国家列表。从存储库获取完整的类。

using System.Collections.Generic;

namespace MyNameSpace.Data
{
    public static class Countries
    {
        public static List<KeyValuePair<int, string>> CountryList
        {
            get
            {
                List<KeyValuePair<int, string>> list = new List<KeyValuePair<int, string>>();
                var x = 1;
                foreach (var v in CountryArray)
                {
                    list.Add(new KeyValuePair<int, string>(x, v));
                    x++;
                }
                return list;
            }
        }

        public static SortedDictionary<int, string> CountryDictionary
        {
            get
            {
                SortedDictionary<int, string> list = new SortedDictionary<int, string>();
                var x = 1;
                foreach (var v in CountryArray)
                {
                    list.Add(x, v);
                    x++;
                }
                return list;
            }
        }

        public static string[] CountryArray = new string[]
        {
            "Afghanistan",
            "Albania",
            "Algeria",
.....
            "Zimbabwe",
        };
    }
}

构建控件

这是 partial 类,将 TValue 设置为 string。有内联的解释注释。

public partial class InputDataList : InputBase<string>
{
    // List of values for datalist
    [Parameter] public IEnumerable<string> DataList { get; set; }
        
    // parameter to restrict valid values to the list
    [Parameter] public bool RestrictToList { get; set; }

    // unique id for the datalist based on a guid - we may have more than one in a form
    private string dataListId { get; set; } = Guid.NewGuid().ToString();

    // instruction to CurrentStringValue that we are in RestrictToList mode 
    // and the user has tabbed
    private bool _valueSetByTab = false;
    // current typed value in the input box - kept up to date by UpdateEnteredText
    private string _typedText = string.Empty;

    // New method to parallel CurrentValueAsString
    protected string CurrentStringValue
    {
        get
        {
            // check if we have a match to the datalist and get the value from the list
            if (DataList != null && DataList.Any(item => item == this.Value))
                return DataList.First(item => item == this.Value);
            // if not return an empty string
            else if (RestrictToList)
                return string.Empty;
            else
                return _typedText;
        }
        set
        {
            // Check if we have a ValidationMessageStore
            // Either get one or clear the existing one
            if (_parsingValidationMessages == null)
                _parsingValidationMessages = new ValidationMessageStore(EditContext);
            else
                _parsingValidationMessages?.Clear(FieldIdentifier);

            // Set defaults
            string val = string.Empty;
            var _havevalue = false;
            // check if we have a previous valid value - we'll stick with 
            // this is the current attempt to set the value is invalid
            var _havepreviousvalue = DataList != null && DataList.Contains(value);

            // Set the value by tabbing in Strict mode. 
            // We need to select the first entry in the DataList
            if (_setValueByTab)
            {
                if (!string.IsNullOrWhiteSpace(this._typedText))
                {
                    // Check if we have at least one match in the filtered list
                    _havevalue = DataList != null && DataList.Any
                                 (item => item.Contains(_typedText, 
                                 StringComparison.CurrentCultureIgnoreCase));
                    if (_havevalue)
                    {
                        // the the first value
                        var filteredList = DataList.Where(item => 
                        item.Contains(_typedText, 
                        StringComparison.CurrentCultureIgnoreCase)).ToList();
                        val = filteredList[0];
                    }
                }
            }
            // Normal set
            else if (this.RestrictToList)
            {
                // Check if we have a match and set it if we do
                _havevalue = DataList != null && DataList.Contains(value);
                if (_havevalue)
                    val = DataList.First(item => item.Equals(value));
            }
            else
            {
                _havevalue = true;
                val = value;
            }

            // check if we have a valid value
            if (_havevalue)
            {
                // assign it to current value - this will kick off 
                // a ValueChanged notification on the EditContext
                this.CurrentValue = val;
                // Check if the last entry failed validation. 
                // If so notify the EditContext that validation has changed,
                // i.e., it's now clear
                if (_previousParsingAttemptFailed)
                {
                    EditContext.NotifyValidationStateChanged();
                    _previousParsingAttemptFailed = false;
                }
            }
            // We don't have a valid value
            else
            {
                // check if we're reverting to the last entry. 
                // If we don't have one the generate error message
                if (!_havepreviousvalue)
                {
                    // No match so add a message to the message store
                    _parsingValidationMessages?.Add(FieldIdentifier, 
                    "You must choose a valid selection");
                    // keep track of validation state for the next iteration
                    _previousParsingAttemptFailed = true;
                    // notify the EditContext which will precipitate 
                    // a Validation Message general update
                    EditContext.NotifyValidationStateChanged();
                }
            }
            // Clear the Tab notification flag
            _setValueByTab = false;
        }
    }

    // Keep _typedText up to date with typed entry
    private void UpdateEnteredText(ChangeEventArgs e)
        => _typedText = e.Value.ToString();

    // Detector for Tabbing away from the input
    private void OnKeyDown(KeyboardEventArgs e)
    {
        // Check if we have a Tab with some text already typed 
        // and are in RestrictToList Mode
        _setValueByTab = RestrictToList && (!string.IsNullOrWhiteSpace(e.Key)) && 
        e.Key.Equals("Tab") && !string.IsNullOrWhiteSpace(this._typedText);
    }

    protected override bool TryParseValueFromString(string value, 
    [MaybeNullWhen(false)] out string result, [NotNullWhen(false)] 
    out string validationErrorMessage)
        => throw new NotSupportedException($"This component does not parse string inputs. 
        Bind to the '{nameof(CurrentValue)}' property, 
        not '{nameof(CurrentValueAsString)}'.");
}

以及 Razor

  1. Input 使用控件生成的 CSS。
  2. 绑定到 CurrentValue
  3. 添加附加属性,包括控件生成的 Aria
  4. list 绑定到 datalist
  5. 连接 oninputonkeydown 的事件处理程序。
  6. 从控件 DataList 属性构建 datalist
@namespace MyNameSpace.Components
@inherits InputBase<string>

<input class="@CssClass" type="text" @bind-value="this.CurrentStringValue" 
 @attributes="this.AdditionalAttributes" list="@dataListId" @oninput="UpdateEnteredText" 
 @onkeydown="OnKeyDown" />

<datalist id="@dataListId">
    @foreach (var option in this.DataList)
    {
        <option value="@option" />
    }
</datalist>

在测试页面中测试控件。

<div class="row mb-2">
    <div class="col-4 form-label" for="txtcountry">
        Country (Any Value)
    </div>
    <div class="col-4">
        <InputDataList @bind-Value="model.Value" DataList="Countries.CountryArray" 
         class="form-control" placeholder="Select a country"></InputDataList>
    </div>
</div>
<div class="row mb-2">
    <div class="col-4 form-label" for="txtcountry">
        Country (Strict)
    </div>
    <div class="col-4">
        <InputDataList @bind-Value="model.StrictValue" 
         DataList="Countries.CountryArray" RestrictToList="true" 
         class="form-control" placeholder="Select a country"></InputDataList>
    </div>
    <div class="col-4">
        <ValidationMessage For="(() => model.StrictValue)"></ValidationMessage>
    </div>
</div>
<div class="row mb-2">
    <div class="col-4 form-label">
        Country Value
    </div>
    <div class="col-4 form-control">
        @this.model.Value
    </div>
</div>
<div class="row mb-2">
    <div class="col-4 form-label">
        Country Strict Value
    </div>
    <div class="col-4 form-control">
        @this.model.StrictValue
    </div>
</div>
class Model
{
    public string Value { get; set; } = string.Empty;
    public string StrictValue { get; set; } = string.Empty;
    public int Index { get; set; } = 0;
    public int TIndex { get; set; } = 0;
    public int Opinion { get; set; } = 0;
}

控件不使用 CurrentValueAsStringTryParseValueFromString。相反,我们构建一个并行的 CurrentStringValue,其中包含 CurrentValueAsStringTryParseValueFromString 中的所有逻辑,并将 HTML 输入连接到它。我们不使用 TryParseValueFromString,但由于它是抽象的,我们需要实现一个盲版本。

Input Search Select 控件

控件的 Select 替代版本建立在 InputDataList 的基础上。我们

  1. 转换为键/值对列表,其中键是泛型的。
  2. 在 HTML input 中添加从 TValuestring 再转换回来的额外逻辑。
  3. 在类中添加泛型处理。

复制 InputDataList 并将其重命名为 InputDataListSelect

添加泛型声明。该控件可以与大多数明显的类型一起用作 Key - 例如,intlongstring

public partial class InputDataListSelect<TValue> : InputBase<TValue>

DataList 更改为 SortedDictionary

[Parameter] public SortedDictionary<TValue, string> DataList { get; set; }

额外的 private 属性如下

// the EditContext ValidationMessageStore
private ValidationMessageStore? _parsingValidationMessages;
// field to manage parsing failure
private bool _previousParsingAttemptFailed = false;

CurrentValue 有所变化,以处理 K/V 对并进行 K/V 对查找。同样,内联注释提供了详细信息。

protected string CurrentStringValue
{
    get
    {
        // check if we have a match to the datalist and get the value from the K/V pair
        if (DataList != null && DataList.Any(item => item.Key.Equals(this.Value)))
            return DataList.First(item => item.Key.Equals(this.Value)).Value;
        // if not return an empty string
        return string.Empty;
    }
    set
    {
        // Check if we have a ValidationMessageStore
        // Either get one or clear the existing one
        if (_parsingValidationMessages == null)
            _parsingValidationMessages = new ValidationMessageStore(EditContext);
        else
            _parsingValidationMessages?.Clear(FieldIdentifier);

        // Set defaults
        TValue val = default;
        var _havevalue = false;
        // check if we have a previous valid value - we'll stick with 
        // this is the current attempt to set the value is invalid
        var _havepreviousvalue = DataList != null && DataList.ContainsKey(this.Value);

        // Set the value by tabbing.   We need to select the first entry in the DataList
        if (_setValueByTab)
        {
            if (!string.IsNullOrWhiteSpace(this._typedText))
            {
                // Check if we have at least one K/V match in the filtered list
                _havevalue = DataList != null && DataList.Any
                (item => item.Value.Contains(_typedText, 
                StringComparison.CurrentCultureIgnoreCase));
                if (_havevalue)
                {
                    // the the first K/V pair
                    var filteredList = DataList.Where(item => item.Value.Contains
                    (_typedText, StringComparison.CurrentCultureIgnoreCase)).ToList();
                    val = filteredList[0].Key;
                }
            }
        }
        // Normal set
        else
        {
            // Check if we have a match and set it if we do
            _havevalue = DataList != null && DataList.ContainsValue(value);
            if (_havevalue)
                val = DataList.First(item => item.Value.Equals(value)).Key;
        }

        // check if we have a valid value
        if (_havevalue)
        {
            // assign it to current value - this will kick off 
            // a ValueChanged notification on the EditContext
            this.CurrentValue = val;
            // Check if the last entry failed validation.
            // If so notify the EditContext that validation has changed, i.e., it's now clear
            if (_previousParsingAttemptFailed)
            {
                EditContext.NotifyValidationStateChanged();
                _previousParsingAttemptFailed = false;
            }
        }
        // We don't have a valid value
        else
        {
            // check if we're reverting to the last entry.
            // If we don't have one the generate error message
            if (!_havepreviousvalue)
            {
                // No K/V match so add a message to the message store
                _parsingValidationMessages?.Add(FieldIdentifier, 
                        "You must choose a valid selection");
                // keep track of validation state for the next iteration
                _previousParsingAttemptFailed = true;
                // notify the EditContext whick will precipitate 
                // a Validation Message general update
                EditContext.NotifyValidationStateChanged();
            }
        }
        // Clear the Tab notification flag
        _setValueByTab = false;
    }
}

OnKeyDown 设置 _setValueByTab 标志。

private void UpdateEnteredText(ChangeEventArgs e)
    => _typedText = e.Value?.ToString();

private void OnKeyDown(KeyboardEventArgs e)
{
    // Check if we have a Tab with some text already typed
    _setValueByTab = ((!string.IsNullOrWhiteSpace(e.Key)) && 
    e.Key.Equals("Tab") && !string.IsNullOrWhiteSpace(this._typedText));
}

// set as blind
protected override bool TryParseValueFromString(string? value, 
[MaybeNullWhen(false)] out TValue result, 
[NotNullWhen(false)] out string validationErrorMessage)
    => throw new NotSupportedException
       ($"This component does not parse normal string inputs.
       Bind to the '{nameof(CurrentValue)}' property, not '{nameof(CurrentValueAsString)}'.");

Razor 几乎相同

  1. datalist 更改以适应 K/V 对列表。
  2. 添加 @typeparam
@namespace Blazor.Database.Components
@inherits InputBase<TValue>
@typeparam TValue

<input class="@CssClass" type="text" @bind-value="this.CurrentStringValue" 
 @attributes="this.AdditionalAttributes" list="@dataListId" 
 @oninput="UpdateEnteredText" @onkeydown="OnKeyDown" />

<datalist id="@dataListId">
    @foreach (var kv in this.DataList)
    {
        <option value="@kv.Value" />
    }
</datalist>

通过在测试页面的编辑表中添加一行来测试它。尝试输入无效的 string - 例如“xxxx”。

<div class="row mb-2">
    <div class="col-4 form-label" for="txtcountry">
        Country T Index
    </div>
    <div class="col-4">
        <InputDataListSelect TValue="int" @bind-Value="model.TIndex" 
         DataList="Countries.CountryDictionary" class="form-control" 
         placeholder="Select a country"></InputDataListSelect>
    </div>
    <div class="col-4">
        <ValidationMessage For="(() => model.TIndex)"></ValidationMessage>
    </div>
</div>
<div class="row mb-2">
    <div class="col-4 form-label">
        Country T Index
    </div>
    <div class="col-4 form-control">
        @this.model.TIndex
    </div>
</div>
class Model
{
    public string Value { get; set; } = string.Empty;
    public string StrictValue { get; set; } = string.Empty;
    public int Index { get; set; } = 0;
    public int TIndex { get; set; } = 0;
    public int Opinion { get; set; } = 0;
}

总结

构建编辑组件并非易事,但也不应令人畏惧。

我构建的示例基于 InputBase。如果您开始构建自己的控件,我强烈建议您花点时间熟悉 InputBase 及其同级控件。代码位于 此处

历史

  • 2021 年 4 月 23 日:初始版本
© . All rights reserved.