Я пишу страницу настроек, чтобы установить 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-адресом, что очень затруднительно.
Есть ли способ напрямую определить, было ли свойство проверено?





Я бы использовал для этого 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.