Как правильно включить синтаксис bind* в Blazor для пользовательских компонентов?

Я разрабатываю библиотеку пользовательского интерфейса поверх Blazor, и мне нравится включать синтаксис bind* для моих компонентов, чтобы потребители тоже могли его использовать.

Контекст

Я читал и видел множество примеров, поэтому на самом базовом уровне предположим, что у нас есть следующий пользовательский компонент с именем RawCustomInput.razor:

<input type = "text" value = "@Value" @onchange = "OnValueChanged" style = "width: 5rem" />

@code {

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

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

    private async Task OnValueChanged(ChangeEventArgs args) 
        => await ValueChanged.InvokeAsync(args.Value as string);
}

И другой компонент, который его потребляет, называется InputPage.razor вот так:

@page "/input"

<PageTitle>InputPage</PageTitle>

<RawCustomInput
    @bind-Value = "@_name" />

@if (!string.IsNullOrEmpty(_name))
{
    <p>@_name</p>
}

@code {
    private string? _name = null;
}

Проблема

Теперь вышеприведенное работает, но скажем, я хочу изменить событие с onchange на oninput на стороне потребителя? поэтому я попытался сделать что-то подобное @bind-Value:event = "oninput", но затем я получаю следующую ошибку:

не имеет свойства, соответствующего имени oninput

Поэтому я подумал, что смогу решить эту проблему, представив @oninput = "OnValueChanged" в RawCustomInput.razor, что выглядит так:

<input type = "text" value = "@Value" @onchange = "OnValueChanged" @oninput = "OnValueChanged" style = "width: 5rem" />

@code {

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

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

    private async Task OnValueChanged(ChangeEventArgs args)
        => await ValueChanged.InvokeAsync(args.Value as string);

}

Но это не сработало, поэтому я снова прочитал ошибку и понял, что что-то вроде @bind-Value:event = "ValueChanged" может работать, и это сработало! поэтому я добавил еще одно свойство, которое выглядит так:

[Parameter]
public EventCallback<string> ValueChanging { get; set; }

Итак, теперь мой RawCustomInput.razor выглядит так:

<input type = "text" value = "@Value" @onchange = "OnChange" @oninput = "OnInput" style = "width: 5rem" />

@code {

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

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

    [Parameter]
    public EventCallback<string> ValueChanging { get; set; }

    private async Task OnChange(ChangeEventArgs args)
        => await ValueChanged.InvokeAsync(args.Value as string);

    private async Task OnInput(ChangeEventArgs args)
        => await ValueChanging.InvokeAsync(args.Value as string);

}

И, наконец, я мог использовать приведенный ниже синтаксис, который тоже работал, что здорово, но вызывает мало вопросов.

<RawCustomInput
    @bind-Value = "@_name"
    @bind-Value:event = "ValueChanging" />

Вопросы

  1. Можно использовать синтаксис bind-{Property} = "{Expression}" для привязки, но можно ли включить этот синтаксис bind = "{Expression}" для пользовательских компонентов? потому что, когда я пытаюсь сделать что-то подобное <RawCustomInput @bind = "@_name" />, выдает следующую ошибку does not have a property matching the name '@bind', но это <input type = "text" @bind = "@_name" /> работает, почему?
  2. Можно сделать <input type = "text" @bind = "@_name" @bind:event = "oninput" /> но выполнение <RawCustomInput @bind-Value = "@_name" @bind-Value:event = "oninput" /> выдает следующую ошибку does not have a property matching the name 'oninput', почему? чтобы добиться аналогичного результата, мне нужно ввести дополнительное свойство, как я указал выше, но тогда кажется немного странным, что для bind:event мне нужно указать имя события, тогда как для bind-{Property}:event мне нужно передать свойство, которое может иметь смысл, но я' м не уверен, что понимаю причину.

🤔 А знаете ли вы, что...
C# обладает сборщиком мусора, который автоматически управляет памятью, что снижает риск утечек памяти.


1
284
1

Ответ:

Решено

Для начала нужно понять разницу между компонентами и элементами.

  1. <input type = "text" value = "@Value" .. — это объявление элемента. Это html-код.

  2. <RawCustomInput @bind-Value = "@_name" /> — это объявление компонента.

Во-вторых, вы определяете вещи на языке Razor, а не на настоящем C#. @bind и @bind-value — это директивы компилятора Razor, которые обрабатываются по-разному.

@bind используется для связывания элементов. Он компилируется компилятором Razor в код C# следующим образом:

__builder.AddAttribute(19, "value", BindConverter.FormatValue(this.surName));
__builder.AddAttribute(20, "onchange", EventCallback.Factory.CreateBinder(this, __value => this.surName = __value, this.surName));

Обратите внимание, что он использует различные фабричные классы для создания «геттера», чтобы обеспечить правильно отформатированную строку на входе value, и «сеттера» для обработки сгенерированного события onchange из входа для обновления переменной.

Для сравнения, это код компонента @bind-value на компоненте

__builder.AddAttribute(11, "Value", RuntimeHelpers.TypeCheck<String>(this.firstName));
__builder.AddAttribute(12, "ValueChanged", RuntimeHelpers.TypeCheck<EventCallback<String>>(EventCallback.Factory.Create<String>(this, RuntimeHelpers.CreateInferredEventCallback(this, __value => this.firstName = __value, this.firstName))));

Он устанавливает значение параметра Value и создает анонимную функцию для установки значения из обратного вызова события ValueChanged. Нет прямой связи с базовым элементом ввода, объявленным внутри компонента. Он устанавливает параметр компонента и погружает событие компонента. Как они подключены, зависит от вас, дизайнера.

Чтобы продемонстрировать, вот совсем другое подключение компонентов для «флажка»:

<button class = "@this.ButtonCss" @onclick=this.OnValueChanged>@this.ButtonText</button>

@code {
    [Parameter] public bool Value { get; set; }
    [Parameter] public EventCallback<bool> ValueChanged { get; set; }

    private string ButtonCss => Value ? "btn btn-success": "btn btn-danger";
    private string ButtonText => Value ? "Enabled" : "Disabled";

    private async Task OnValueChanged()
        => await ValueChanged.InvokeAsync(!this.Value);
}

Когда вы объявляете свои компоненты как компоненты Razor, вы ограничены языком Razor. Для 99% компонентов это нормально. Но если вы хотите делать более экзотические вещи, вам нужно вернуться к написанию компонента непосредственно в виде класса C#.

Это показывает один из способов обработки onchange/oninput.

@if (this.ChangeOnInput)
{
    <input type = "text" value = "@Value" @oninput = "OnValueChanged" class = "form-control" />
}
else
{
    <input type = "text" value = "@Value" @onchange = "OnValueChanged" class = "form-control" />
}

@code {
    [Parameter] public string? Value { get; set; }
    [Parameter] public bool ChangeOnInput { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    private async Task OnValueChanged(ChangeEventArgs args)
        => await ValueChanged.InvokeAsync(args.Value as string);
}