Как узнать, подтверждено ли свойство?

Я пишу страницу настроек, чтобы установить IP-адрес для устройства Ethernet.

Я разместил на странице текстовое поле и кнопку «Применить». После ввода IP-адреса в текстовое поле и нажатия кнопки «Применить» установите IP-адрес устройства.

Теперь мне нужно проверить текст в TextBox, является ли это IP-адресом.

Я нашел учебник в https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/observablevalidator#custom-validation-attributes

Вот мой код:

public partial class EthernetConnection : ObservableObject
{
    string _IP = "";
    [IPAddress]
    public string IP {
        get => _IP;
        set { 
            if (_IP != value)
            {                    
                _IP = value;                    
                OnPropertyChanged();                           
            }
        }
    }    

    public sealed class IPAddressAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {                
            string _pattern = @"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$";
            if (value is string _string && Regex.IsMatch(_string, _pattern))
            {
                return ValidationResult.Success;
            }                
            return new("Failed");
        }            
    }
}   

И это работает хорошо: когда входные данные в TextBox не являются IP-адресом, TextBox предложит вам это сделать.

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

Есть ли способ напрямую определить, было ли свойство проверено?

Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
91
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я бы использовал для этого Maybe<T>, а не атрибуты.

public class Maybe<T>
{
    public class MissingValueException : Exception { }

    public readonly static Maybe<T> Nothing = new Maybe<T>();

    private T _value;
    public T Value
    {
        get
        {
            if (!this.HasValue)
            {
                throw new MissingValueException();
            }
            return _value;
        }
        private set
        {
            _value = value;
        }
    }
    public bool HasValue { get; private set; }

    public Maybe()
    {
        HasValue = false;
    }

    public Maybe(T value)
    {
        Value = value;
        HasValue = true;
    }

    public T ValueOrDefault() => this.HasValue ? this.Value : default(T);
    public T ValueOrDefault(T @default) => this.HasValue ? this.Value : @default;
    public T ValueOrDefault(Func<T> @default) => this.HasValue ? this.Value : @default();

    public static implicit operator Maybe<T>(T v)
    {
        return v.ToMaybe();
    }

    public override string ToString()
    {
        return this.HasValue ? this.Value.ToString() : "()";
    }

    public override bool Equals(object obj)
    {
        if (obj is Maybe<T>)
            return Equals((Maybe<T>)obj);
        return false;
    }

    public bool Equals(Maybe<T> obj)
    {
        if (obj == null) return false;
        if (!EqualityComparer<T>.Default.Equals(_value, obj._value)) return false;
        if (!EqualityComparer<bool>.Default.Equals(this.HasValue, obj.HasValue)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<T>.Default.GetHashCode(_value);
        hash ^= EqualityComparer<bool>.Default.GetHashCode(this.HasValue);
        return hash;
    }

    public static bool operator ==(Maybe<T> left, Maybe<T> right)
    {
        if (object.ReferenceEquals(left, null))
        {
            return object.ReferenceEquals(right, null);
        }

        return left.Equals(right);
    }

    public static bool operator !=(Maybe<T> left, Maybe<T> right)
    {
        return !(left == right);
    }
}

Затем сделайте следующее:

public partial class EthernetConnection : ObservableObject
{
    public Maybe<string> MaybeIP { get; private set; } = "";

    public string IP
    {
        set
        {
            string _pattern = @"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$";
            if (MaybeIP.HasValue && MaybeIP.Value != value)
            {
                MaybeIP = Regex.IsMatch(value, _pattern) ? value : Maybe<string>.Nothing;
                OnPropertyChanged();
            }
        }
    }
}

Это сейчас как Nullable<T>, но для всех типов.

Вот несколько удобных методов расширения, которые сделают это еще лучше:

public static class MaybeEx
{
    public static Maybe<T> ToMaybe<T>(this T value)
    {
        return new Maybe<T>(value);
    }

    public static T GetValue<T>(this Maybe<T> m, T @default) => m.HasValue ? m.Value : @default;
    public static T GetValue<T>(this Maybe<T> m, Func<T> @default) => m.HasValue ? m.Value : @default();

    public static Maybe<U> Select<T, U>(this Maybe<T> m, Func<T, U> k)
    {
        return m.SelectMany(t => k(t).ToMaybe());
    }

    public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k)
    {
        if (!m.HasValue)
        {
            return Maybe<U>.Nothing;
        }
        return k(m.Value);
    }

    public static Maybe<V> SelectMany<T, U, V>(this Maybe<T> @this, Func<T, Maybe<U>> k, Func<T, U, V> s)
    {
        return @this.SelectMany(x => k(x).SelectMany(y => s(x, y).ToMaybe()));
    }

    public static Maybe<V> Maybe<K, V>(this IDictionary<K, V> @this, K key) => (@this.ContainsKey(key)) ? @this[key].ToMaybe() : Maybe<V>.Nothing;
}

Рекомендуемый способ реализации проверки данных — реализовать INotifyDataErrorInfo. Это позволяет удобно предоставлять пользователю визуальную обратную связь с помощью встроенной функции проверки механизма привязки. Например, если проверка свойства не удалась, цель привязки (обычно TextBox) показывает красную рамку и может даже отображать пользователю сообщение об ошибке.

Обычно я рекомендую использовать статические методы IPAddress.TryParse или IPAddress.Parse для анализа/преобразования входных данных. Это быстрее, чем использование регулярного выражения. Кроме того, в результате вы получаете объект IPAddress, который более эффективен в обработке, чем строки, и который можно напрямую использовать с сетевым API .NET. IPAdress также может предоставлять другие методы API, полезные для вашего сценария.

Я также рекомендую разделить ваш одиночный TextBox на 4 TextBox элемента (IPv4) или 8 TextBlock элементов (IPv6). Позвольте пользователю переключаться между IPv4 и IPv6 и показывать правильное количество элементов TextBox соответственно. Я рекомендую создать Ip4TextBox и Ip6TextBox для инкапсуляции элементов TextBox и их различной проверки ввода. Таким образом, переключение между двумя вариантами становится проще.

Ключевым моментом является помочь пользователю избежать незаконных вводов, например. слишком много цифр или нечисловых символов и т. д. В этом контексте вам следует реализовать двухэтапную проверку:
одна проверка необработанных входных данных на уровне управления, которая просто проверяет, находятся ли входные данные в правильном формате (например, для Ipv4 разрешены только числа в диапазоне 1–255).
Вторая (необязательная) проверка происходит в вашей модели данных, где вы реализуете вышеупомянутое INotifyDataErrorInfo, чтобы убедиться, что адрес семантически правильный (например, правильное адресное пространство).
Как добавить проверку для просмотра свойств модели или как реализовать INotifyDataErrorInfo

Пример

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

Источник привязки по-прежнему может реализовать INotifyDataErrorInfo для дальнейшей проверки входных данных на основе более конкретных бизнес-правил.

Пример использования

<Window>
  <TextBlock Text = "{Binding ElementName=Ip4TextBox, Path=IpAddress}" />
  <Ip4TextBox x:Name = "Ip4TextBox" />
</Window>

Выполнение

IP4TextBox.cs

public class Ip4TextBox : Control
{
  public IPAddress IpAddress
  {
    get => (IPAddress)GetValue(IpAddressProperty);
    set => SetValue(IpAddressProperty, value);
  }

  public static readonly DependencyProperty IpAddressProperty = DependencyProperty.Register(
    "IpAddress",
    typeof(IPAddress),
    typeof(Ip4TextBox),
    new PropertyMetadata(default));

  public int Octet1Text
  {
    get => (int)GetValue(Octet1TextProperty);
    set => SetValue(Octet1TextProperty, value);
  }

  public static readonly DependencyProperty Octet1TextProperty = DependencyProperty.Register(
    "Octet1Text",
    typeof(int),
    typeof(Ip4TextBox),
    new FrameworkPropertyMetadata(
      default(int),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      null,
      null,
      false,
      UpdateSourceTrigger.LostFocus));

  public int Octet2Text
  {
    get => (int)GetValue(Octet2TextProperty);
    set => SetValue(Octet2TextProperty, value);
  }

  public static readonly DependencyProperty Octet2TextProperty = DependencyProperty.Register(
    "Octet2Text",
    typeof(int),
    typeof(Ip4TextBox),
    new FrameworkPropertyMetadata(
      default(int),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      null,
      null,
      false,
      UpdateSourceTrigger.LostFocus));

  public int Octet3Text
  {
    get => (int)GetValue(Octet3TextProperty);
    set => SetValue(Octet3TextProperty, value);
  }

  public static readonly DependencyProperty Octet3TextProperty = DependencyProperty.Register(
    "Octet3Text",
    typeof(int),
    typeof(Ip4TextBox),
    new FrameworkPropertyMetadata(
      default(int),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      null,
      null,
      false,
      UpdateSourceTrigger.LostFocus));

  public int Octet4Text
  {
    get => (int)GetValue(Octet4TextProperty);
    set => SetValue(Octet4TextProperty, value);
  }

  public static readonly DependencyProperty Octet4TextProperty = DependencyProperty.Register(
    "Octet4Text",
    typeof(int),
    typeof(Ip4TextBox),
    new FrameworkPropertyMetadata(
      default(int),
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
      null,
      null,
      false,
      UpdateSourceTrigger.LostFocus));

  private HashSet<ValidationError> ValidationErrors { get; }
  private bool HasValidationErrors => this.ValidationErrors.Any();

  static Ip4TextBox() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Ip4TextBox), new FrameworkPropertyMetadata(typeof(Ip4TextBox)));

  public Ip4TextBox()
  {
    this.ValidationErrors = new HashSet<ValidationError>();
    Validation.AddErrorHandler(this, OnValidationError);
  }

  // Commit input when the Ip4TextBox loses focus
  // and the child TextBox elements don't report any validation errors
  protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e)
  {
    base.OnIsKeyboardFocusWithinChanged(e);
    if (!this.IsKeyboardFocusWithin
      && !this.HasValidationErrors
      && Keyboard.FocusedElement is UIElement focusedUiElement)
    {
      // Defer invocation using the dispatcher to allow the TextBox to complete event handling
      _ = Application.Current.Dispatcher.InvokeAsync(UpdateIpAddress, System.Windows.Threading.DispatcherPriority.Background);
    }
  }

  private void UpdateIpAddress()
  {
    string ipAddressText = string.Join('.', this.Octet1Text, this.Octet2Text, this.Octet3Text, this.Octet4Text);
    var ipAddress = IPAddress.Parse(ipAddressText);
    this.IpAddress = ipAddress;
  }

  // Select all text
  protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
  {
    base.OnGotKeyboardFocus(e);

    if (e.OriginalSource is TextBox textBox)
    {
      // Defer invocation using the dispatcher to allow the TextBox to complete event handling
      _ = Application.Current.Dispatcher.InvokeAsync(textBox.SelectAll, System.Windows.Threading.DispatcherPriority.Background);
    }
  }

  private void OnValidationError(object? sender, ValidationErrorEventArgs e)
  {
    switch (e.Action)
    {
      case ValidationErrorEventAction.Added:
        _ = this.ValidationErrors.Add(e.Error);
        break;
      case ValidationErrorEventAction.Removed:
        _ = this.ValidationErrors.Remove(e.Error);
        break;
    }
  }
}

Ip4ValidationRule.cs
Правило проверки, которое применяется к входным данным с помощью Ip4TextBox.

public class Ip4ValidationRule : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    => value is string inputText
      && int.TryParse(inputText, out int inputNumber)
      && inputNumber is > -1 and < 256
        ? ValidationResult.ValidResult
        : new ValidationResult(false, "Number must be within range [1-255]");
}

Generic.xaml
Значение Style по умолчанию, которое необходимо добавить в файл /Themes/Gernic.xaml.

<Style TargetType = "local:Ip4TextBox">
  <Style.Resources>
    <local:Ip4ValidationRule x:Key = "Ip4ValidationRule" />

    <!-- Customizable error template that defines how the error is displayed -->
    <ControlTemplate x:Key = "ValidationErrorTemplate">
      <StackPanel>
        <Border BorderBrush = "Red"
                BorderThickness = "1"
                HorizontalAlignment = "Left">

          <!-- Placeholder for the octet TextBox itself -->
          <AdornedElementPlaceholder x:Name = "AdornedElement" />
        </Border>

        <Border Background = "White"
                BorderBrush = "Red"
                Padding = "4"
                BorderThickness = "1"
                HorizontalAlignment = "Left">
          <ItemsControl ItemsSource = "{Binding}"
                        HorizontalAlignment = "Left">
            <ItemsControl.ItemTemplate>
              <DataTemplate>
                <TextBlock Text = "{Binding ErrorContent}"
                            Foreground = "Red" />
              </DataTemplate>
            </ItemsControl.ItemTemplate>
          </ItemsControl>
        </Border>
      </StackPanel>
    </ControlTemplate>

    <!-- Input field style -->
    <Style TargetType = "TextBox">
      <Setter Property = "Width"
              Value = "50" />
      <Setter Property = "Validation.ErrorTemplate"
              Value = "{StaticResource ValidationErrorTemplate}" />
    </Style>

    <!-- Seperaetor TextBlock style -->
    <Style TargetType = "TextBlock">
      <Setter Property = "Text"
              Value = "." />
      <Setter Property = "FontSize"
              Value = "16" />
      <Setter Property = "FontWeight"
              Value = "Bold" />
      <Setter Property = "VerticalAlignment"
              Value = "Bottom" />
      <Setter Property = "Margin"
              Value = "4,0" />
    </Style>
    <local:ValidationMultiErrorConverter x:Key = "ValidationMultiErrorConverter" />
  </Style.Resources>

  <Setter Property = "BorderBrush"
          Value = "{x:Static SystemColors.ActiveBorderBrush}" />
  <Setter Property = "BorderThickness"
          Value = "1" />
  <Setter Property = "Template">
    <Setter.Value>
      <ControlTemplate TargetType = "local:Ip4TextBox">
        <Border Background = "{TemplateBinding Background}"
                BorderBrush = "{TemplateBinding BorderBrush}"
                BorderThickness = "{TemplateBinding BorderThickness}"
                Padding = "{TemplateBinding Padding}">
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
              <ColumnDefinition Width = "Auto" />
            </Grid.ColumnDefinitions>

            <TextBox Grid.Column = "0">
              <TextBox.Text>
                <Binding RelativeSource = "{RelativeSource TemplatedParent}"
                          Path = "Octet1Text"
                          NotifyOnValidationError = "True">
                  <Binding.ValidationRules>
                    <StaticResource ResourceKey = "Ip4ValidationRule" />
                  </Binding.ValidationRules>
                </Binding>
              </TextBox.Text>
            </TextBox>
            <TextBlock Grid.Column = "1" />

            <TextBox Grid.Column = "2">
              <TextBox.Text>
                <Binding RelativeSource = "{RelativeSource TemplatedParent}"
                          Path = "Octet2Text"
                          NotifyOnValidationError = "True">
                  <Binding.ValidationRules>
                    <StaticResource ResourceKey = "Ip4ValidationRule" />
                  </Binding.ValidationRules>
                </Binding>
              </TextBox.Text>
            </TextBox>
            <TextBlock Grid.Column = "3" />

            <TextBox Grid.Column = "4">
              <TextBox.Text>
                <Binding RelativeSource = "{RelativeSource TemplatedParent}"
                          Path = "Octet3Text"
                          NotifyOnValidationError = "True">
                  <Binding.ValidationRules>
                    <StaticResource ResourceKey = "Ip4ValidationRule" />
                  </Binding.ValidationRules>
                </Binding>
              </TextBox.Text>
            </TextBox>
            <TextBlock Grid.Column = "5" />

            <TextBox Grid.Column = "6">
              <TextBox.Text>
                <Binding RelativeSource = "{RelativeSource TemplatedParent}"
                          Path = "Octet4Text"
                          NotifyOnValidationError = "True">
                  <Binding.ValidationRules>
                    <StaticResource ResourceKey = "Ip4ValidationRule" />
                  </Binding.ValidationRules>
                </Binding>
              </TextBox.Text>
            </TextBox>
          </Grid>
        </Border>

        <ControlTemplate.Triggers>
          <Trigger Property = "IsEnabled"
                    Value = "False">
            <Setter Property = "Background"
                    Value = "{x:Static SystemColors.ControlLightBrush}" />
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
Ответ принят как подходящий

Вскоре я нашел другой способ легко решить эту проблему: https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/observablevalidator

ObservableValidator наследуется от интерфейса INotifyDataErrorInfo: https://learn.microsoft.com/en-us/dotnet/api/system.comComponentmodel.inotifydataerrorinfo?view=net-8.0

И в INotifyDataErrorInfo есть свойство HasErrors, которое я могу связать со свойством IsEnabled ApplyButton.

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

Похожие вопросы

Есть ли способ использовать определяемое системой удостоверение для подключения основного веб-API asp.net к учетной записи хранения Azure с использованием управляемого удостоверения и RBAC?
Веб-API .NET 8 возвращает пустой список
Несколько экземпляров с использованием делегатов в единстве С#
В C# DirectoryEntry возвращает пустую коллекцию AuditRules, даже если правила аудита существуют
Компоненты представления ASP.NET Core возвращают пустые страницы
Каков наиболее эффективный способ объединить два больших списка на основе метки времени в С#?
C# Сопоставляет сериализацию и десериализацию JSON со ссылкой на объект с использованием свойства объекта
Используйте запрос UPDATE внутри запроса SELECT
Как реализовать потребитель параллельных задач для приложения Kafka?
Странная проблема с полосами прокрутки и... текстовыми полями? (ВертикальноеСодержимоеВыравнивание)