Как создать свойство Nullable<T> в универсальном классе, если T не имеет значения NULL?

У меня есть следующий класс, который может принимать обычно числовые типы или типы string:

public class MdSlider<TValue> where TValue: IConvertible, IEquatable<TValue>
{
    TValue? Value { get; set; }
    TValue? Min { get; set; }
    TValue? Max { get; set; }

    // ...
}

Однако я только что понял, что если пользователь создает, например, MdSlider<double>, все эти три свойства будут double, а не double?, и по умолчанию они имеют значения 0 вместо null. Моему приложению требуется нулевое значение, если пользователь его не устанавливает.

  • Если я добавлю ограничение struct, string больше не будет приниматься.

  • Я не могу объявить отдельный MdSlider для string, потому что это в Blazor, и имя компонента должно быть уникальным (MdSlider<TValue> и MdSlider считается одинаковым), и я не хочу, например, делать отдельный MdSliderString.

Можно ли объявить параметр универсального типа и принудительно установить Nullable на свойство, если оно не имеет значения NULL? Любое другое решение, которое я могу использовать для компонентов Blazor?

Короче говоря - нет, без родового ограничения. Смотрите дубликат. В качестве обходного пути вы можете создать две версии — одну для структур и одну для ссылочных типов.

Guru Stron 23.09.2023 04:55

Вы, вероятно, правы. Я проверил официальный проект Microsoft, чтобы узнать, как они с этим справляются (у них также есть общие элементы управления Blazor). Таким образом, у них есть все свойства, допускающие значение NULL (Min, Max и т. д.), чтобы быть string вместо T, и только Value (который всегда присутствует) является T.

Luke Vo 23.09.2023 08:36

Ваш вопрос предполагает, что MdSlider — это компонент слайдера Blazor, а Value, Min и Max на самом деле являются параметрами для этого компонента. Если да, то откройте вопрос, и я попытаюсь продемонстрировать, как и почему это можно заставить работать в контексте компонента. Мне нужно больше места, чем комментарий!

MrC aka Shaun Curtis 24.09.2023 10:01

Сделанный. См. ответ ниже, который может дать вам ответ, который вы ищете.

MrC aka Shaun Curtis 24.09.2023 22:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В этом ответе показано, как это работает с компонентами Blazor:

Вот моя демо Slider.razor

@typeparam TValue

<input type = "range" @bind:get = "Value" @bind:set = "SetValue" class = "form-range" min = "@Min" max = "@Max" id = "customRange2">


@code {
    [Parameter] public TValue? Value { get; set; }
    [Parameter] public EventCallback<TValue?> ValueChanged { get; set; }
    [Parameter] public TValue? Max { get; set; }
    [Parameter] public TValue? Min { get; set; }

    private async Task SetValue(TValue? value)
    {
        await this.ValueChanged.InvokeAsync(value);
    }
}

И демо Index использует как строку с нулевым значением, так и [не обнуляемое] двойное значение.

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title = "How is Blazor working for you?" />

<div class = "mb-3">
    <Slider Max = "_stringMax" Min = "_stringMin" @bind-Value = "_stringValue" />
</div>

<div class = "mb-3">
    <Slider Max = "_max" Min = "_min" @bind-Value = "_value" />
</div>



<div class = "bg-dark text-white m-2 p-2">
    <pre>String Value : @_stringValue</pre>
    <pre>Value : @_value</pre>
</div>

@code {
    string? _stringMax = "10";
    string? _stringMin = "-10";
    string? _stringValue;

    double _max = 10;
    double _min = -10;
    double _value;
}

Причина, по которой это работает, заключается в том, что между Value в дочернем элементе и значением, указанным в определении компонента, нет жесткой связи. Значение, указанное в родительском компоненте, определяется в ParameterView, передаваемом дочернему компоненту в ParametersSetAsync.

То, как это применяется к дочернему компоненту, зависит от дочернего компонента: обычный способ использует Reflection и корректно обрабатывает преобразование типов.

    public virtual Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);

Вы можете сделать это вручную — см. https://learn.microsoft.com/en-us/aspnet/core/blazor/ Performance?view=aspnetcore-7.0#implement-setparametersasync-manually.

Дополнительная версия компонента слайдера

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

SetParametersAsync выполняет всю работу по обработке входящего контента и преобразует все в строки для использования во входных данных.

SetValue выполняет преобразование строки, возвращенной из ввода, в TValue с помощью утилиты BindConverter.

BuildRenderTree использует построитель дерева рендеринга для построения контента с помощью логики для управления тем, что необходимо включить.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System.Globalization;

namespace SO77161463.Pages;

public class MySlider<TValue> : ComponentBase
{
    [Parameter] public TValue? Value { get; set; }
    [Parameter] public EventCallback<TValue?> ValueChanged { get; set; }
    [Parameter] public TValue? Max { get; set; }
    [Parameter] public TValue? Min { get; set; }
    [Parameter] public string? CssClass { get; set; } = "form-range";

    private string? _value;
    private string? _max;
    private string? _min;

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(Value):
                    if (parameter.Value is null)
                    {
                        _value = null;
                        this.Value = default;
                        break;
                    }
                    _value = parameter.Value.ToString();
                    this.Value = (TValue)parameter.Value;
                    break;
 
                case nameof(Max):
                    if (parameter.Value is null)
                    {
                        _max = null;
                        this.Max = default;
                        break;
                    }
                    _max = parameter.Value.ToString();
                    this.Max = (TValue)parameter.Value;
                    break;

                case nameof(Min):
                    if (parameter.Value is null)
                    {
                        _min = null;
                        this.Min = default;
                        break;
                    }
                    this.Min = (TValue)parameter.Value;
                    _min = parameter.Value.ToString();
                    break;

                case nameof(CssClass):
                    this.CssClass = parameter.Value?.ToString() ?? null;
                    break;

                case nameof(ValueChanged):
                    if (parameter.Value is not null)
                        ValueChanged = (EventCallback<TValue?>)parameter.Value;
                    break;

                default:
                    throw new ArgumentException($"Unknown parameter: {parameter.Name}");
            }
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }

    private async Task SetValue(ChangeEventArgs e)
    {
        if (BindConverter.TryConvertTo<TValue>(e.Value, CultureInfo.InvariantCulture, out TValue? result))
            await ValueChanged.InvokeAsync(result);
        else
            await ValueChanged.InvokeAsync(default(TValue));
    }


    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(1, "input");
        builder.AddAttribute(2, "type", "range");
        builder.AddAttribute(3, "value", _value);
        builder.AddAttribute(4, "class", this.CssClass);

        if (_max is not null)
            builder.AddAttribute(5, "max", _max);

        if (_min is not null)
            builder.AddAttribute(6, "min", _min);

        builder.AddAttribute(7, "onchange", EventCallback.Factory.Create<ChangeEventArgs>(this, SetValue));

        builder.CloseElement();
    }
}

Обратите внимание, что Parameters используются просто как служба обмена сообщениями для передачи значений в компонент.

Привет, извините, я пока не смогу протестировать его в ближайшие несколько дней, но не могли бы вы уточнить, что если пользователи не предоставляют значение Min или Max, этот атрибут не печатается (это важно)? Если TValue равно double, то Max по умолчанию будет равен 0 и не может быть нулевым и, следовательно, будет распечатан, когда пользователь его не предоставит?

Luke Vo 25.09.2023 03:51

Поясните, пожалуйста, что вы подразумеваете под словом «напечатать».

MrC aka Shaun Curtis 25.09.2023 09:45

См. исправленный ответ и комментарий.

MrC aka Shaun Curtis 25.09.2023 16:15

Привет, еще раз извините, что в ближайшие несколько дней у меня не будет доступа к IDE для тестирования. Под «печатью» я подразумевал, что полученный HTML-код не должен иметь этого атрибута, если он равен нулю. Например: <MdSlider Value = "10" /> должен печатать только <md-slider value = "10"></md-slider> и НЕ <md-slider value = "10" max = "0" />, если разработчик Blazor, который использует MdSlider, не дает Max значения (это проблема, с которой я сталкиваюсь, потому что это приводит к неисправности <md-slider>, <md-slider> эквивалентно input в вашем случае). Опять же, это просто объяснение, я не знаю, работает ли ваш ответ или нет.

Luke Vo 25.09.2023 18:15

Я предполагаю, что это Material Design. Второй компонент показывает, как добавлять или не добавлять атрибуты в зависимости от их значений. Вы можете сделать это в Razor, но здесь очень много дублирований разметки, поэтому в данном случае метод RenderTreeBuilder кажется более подходящим. В любом случае, надеюсь, есть какие-то подсказки о том, как закодировать настоящий.

MrC aka Shaun Curtis 25.09.2023 18:22

Спасибо, сейчас я следую подходу команды MS в Fluent Blazor. Их Value (который в каком-то смысле является обязательным) относится к типу TValue?, но их необязательные Max, Min и т. д. относятся к string?. Таким образом, когда значение равно null (пользователь его не предоставляет), Blazor не распечатывает этот атрибут. Однако это имеет смысл, потому что вам на самом деле не нужно его привязывать, поэтому отсутствие TValue на самом деле не является проблемой.

Luke Vo 25.09.2023 21:57

Другие вопросы по теме