Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / WPF, Silverlight Новый топик    Ответить
 ValidationRule+IValueConverter: сброс source в null при ошибке+отображение ошибки - как?  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 55
Есть вот такой схематичный код:
<Window.DataContext>
  <Binding RelativeSource="{RelativeSource Self}" />
</Window.DataContext>

<Window.Resources>
  <ControlTemplate x:Key="TextBoxErrorTemplate">
    <StackPanel>
      <Border BorderBrush="#FFdc000c" VerticalAlignment="Top">
        <Grid>
          <AdornedElementPlaceholder x:Name="adorner" Margin="-1" />
        </Grid>
      </Border>
      <Border
        x:Name="errorBorder"
        Background="#FFdc000c"
        Margin="0,2,0,0"
        MinHeight="24"
        Width="{Binding ElementName=adorner, Path=AdornedElement.ActualWidth}">
        <TextBlock
          Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
          Foreground="White"
          Margin="5,2,5,3"
          TextWrapping="Wrap"
          VerticalAlignment="Stretch"/>
      </Border>
    </StackPanel>
  </ControlTemplate>
</Window.Resources>

<Grid Margin="5">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition />
  </Grid.RowDefinitions>
  <TextBox
    Margin="5"
    Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}">
    <TextBox.Resources>
      <local:StringToIntConverter x:Key="StringToIntConverter" />
    </TextBox.Resources>
    <TextBox.Text>
      <Binding
        Path="Id"
        Mode="TwoWay"
        UpdateSourceTrigger="PropertyChanged"
        Converter="{StaticResource StringToIntConverter}">
        <Binding.ValidationRules>
          <local:IntegerValidator />
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>
  <Button
    Grid.Row="1"
    VerticalAlignment="Center"
    HorizontalAlignment="Center"
    Content="Test"
    Padding="5"
    Command="{x:Static local:MainWindow.ShowIdCommand}">
    <Button.CommandBindings>
      <CommandBinding
        Command="{x:Static local:MainWindow.ShowIdCommand}"
        Executed="ShowIdCommandHandler" />
    </Button.CommandBindings>
  </Button>
</Grid>


public partial class MainWindow : INotifyPropertyChanged
{
  public MainWindow()
  {
    InitializeComponent();
  }

  int? _id;
  public int? Id
  {
    get => _id;
    set
    {
      _id = value;
      Console.WriteLine("Id={0}", Id?.ToString() ?? "null");
      OnPropertyChanged();
    }
  }

  public static RoutedUICommand ShowIdCommand { get; } = new RoutedUICommand("ShowId", "ShowId", typeof(MainWindow));
  void ShowIdCommandHandler(object sender, ExecutedRoutedEventArgs e)
  {
    MessageBox.Show($"Id=({Id?.ToString() ?? "null"})");
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

public class StringToIntConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return ((int?)value)?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    if (string.IsNullOrWhiteSpace(value as string))
      return (int?)null;
    if (int.TryParse((string)value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n))
      return (int?)n;
    return (int?)null;
  }
}

public class IntegerValidator : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
    if (string.IsNullOrWhiteSpace(value as string))
      return ValidationResult.ValidResult;
    try
    {
      int.Parse((string)value, NumberStyles.Integer, CultureInfo.InvariantCulture);
      return ValidationResult.ValidResult;
    }
    catch (Exception e)
    {
      return new ValidationResult(false, e.Message);
    }
  }
}

Работает он вполне нормально, но есть одно но: если сначала в текстбокс ввести правильное значение (например, 123 - связанное свойство примет значение 123), потом выделить весь текст, и начать вводить неправильное значение (например, qqq), то соответствующая ошибка валидации будет отображена, но связанное свойство по-прежнему будет содержать предыдущее правильное значение (123) - а нужно, чтобы оно при этом сбросилось в null (т.к. в реальности пользователь, как обычно, не обращает внимания на эти сообщения валидации, жмакает на кнопку, и возмущается, что команда отработала с значением, которое он уже вводил).
Пробовал экспериментировать с ValidationStep (по умолчанию оно равно RawProposedValue, т.е. правило отрабатывает до конвертера) - получилось не то, что хотелось: при ValidationStep=CommitedValue или UpdatedValue сначала отрабатывает IValueConverter.ConvertBack, постит null-значение в source, потом отрабатывает IValueConverter.Convert, и target апдейтится значением пустой строки, и только после этого отрабатывает ValidationRule, в который приходит пустая строка (а это значение считается валидным - оно нужно для сброса source в null). В результате невалидный текст просто вообще не набирается (что, конечно, хорошо, но чисто ради научного интереса хотелось бы, чтобы он набирался, и программа писала, что Input string was not in a correct format).
Как сделать задуманное?
16 фев 19, 13:58    [21811549]     Ответить | Цитировать Сообщить модератору
 Re: ValidationRule+IValueConverter: сброс source в null при ошибке+отображение ошибки - как?  [new]
Eld Hasp
Member

Откуда:
Сообщений: 178
WinterGraveyard, сразу оговариваюсь - я не спец.., токо начинаю. Но из того что пришло в голову - Вам валидация не нужна. Смысл валидации, именно в то чтьобы не пропускать не корректные данные. А вывод сообщения о некорректности - это визуальная "красивость". Если Вам не нужна валидация, а нужно просто известить пользователя о некорректном значении, то введите вывод извещения или в обработчик TextChanged, или в сеттер свойство VM? к которому привязан Text.
17 фев 19, 20:05    [21812190]     Ответить | Цитировать Сообщить модератору
 Re: ValidationRule+IValueConverter: сброс source в null при ошибке+отображение ошибки - как?  [new]
Сон Веры Павловны
Member

Откуда:
Сообщений: 4604
WinterGraveyard
Как сделать задуманное?

В озвученной постановке - никак, т.к. налицо взаимоисключающие требования: если ввод неволиден, то источник нужно сбросить в null, а null, как написано выше, валиден как с т.з. типа данных, так и с т.з бизнес-логики (логики валидатора): сброс источника в валидное значение будет протранслирован в target, сработает валидация по валидному значению, и ошибка пропадет (контрол Validation.ErrorTemplate скроется).
Что здесь можно сделать:
1. Слушать событие валидации Validation.Error. Не очень удобно - code-behind не всегда доступен, и скрещивать его с вьюмоделью тоже не особенно удобно. Можно использовать паттерн event to command (attached command behavior, Interaction.Triggers из System.Windows.Interactivity), чтобы оповещать модель о невалидном вводе, и вручную синхронизировать сообщения о невалидном вводе, и состояние свойств. Если таких наблюдаемых свойств будет несколько, получится не особенно изящно.
2. Оповещать модель о невалидном вводе прямо из ValidationRule - соответственно, ValidationRule должен иметь ссылку на вьюмодель. Можно её предоставлять с помощь синглтона, сервис-локатора, можно у ValidationRule задать public-свойство на чтение/запись, и в него из xaml-разметки передавать как StaticResource ссылку на вьюмодель, если вьюмодель инициализурется в xaml. Дальше по этой ссылке делать, что нужно - выставлять признаки некорректного ввода, обнулять backing fields свойств (без возбуждения PropertyChanged), итд. Минусы всё те же, что и выше.
3. Держать для свойства 2 поля: одно - текстовое, используемое в биндинге контрола, и валидируемое на шаге CommitedValue. Второе - только для чтения, вычисляемое на лету по содержимому первого свойства.
В этом случае при невалидном значении первого свойства (которое будет сохранено в VM, т.к. шаг валидации CommitedValue), второе свойство может вернуть null (по результату парсинга значения первого свойства), и при этом будет отображаться контрол Validation.ErrorTemplate, т.к. валидация отработает с ValidationResult.IsValid = false:
<Grid Margin="5">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition />
  </Grid.RowDefinitions>
  <TextBox
    Margin="5"
    Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}">
    <TextBox.Text>
      <Binding
        Path="IdStr"
        Mode="TwoWay"
        UpdateSourceTrigger="PropertyChanged">
        <Binding.ValidationRules >
          <local:IntegerValidator ValidationStep="CommittedValue"/>
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>
  <TextBox
    Grid.Row="1"
    Margin="5,30,5,5"
    IsReadOnly="True"
    Text="{Binding Id, Mode=OneWay}" />
  <Button
    Grid.Row="2"
    VerticalAlignment="Center"
    HorizontalAlignment="Center"
    Content="Test"
    Padding="5"
    Command="{Binding ShowIdCommand}" />
</Grid>


public partial class MainWindow
{
  public MainWindow()
  {
    InitializeComponent();
    DataContext = new MainModel();
  }
}

public class MainModel : INotifyPropertyChanged
{
  public MainModel()
  {
    ShowIdCommand = new RelayCommand(ShowId);
  }

  string _idStr;
  public string IdStr
  {
    get => _idStr;
    set
    {
      _idStr = value;
      OnPropertyChanged();
      OnPropertyChanged(nameof(Id));
      Console.WriteLine("IdStr={0}, Id={1}", IdStr ?? "null", Id?.ToString() ?? "null");
    }
  }

  public int? Id =>
    int.TryParse(IdStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n)
      ? (int?)n
      : null;

  public RelayCommand ShowIdCommand { get; }
  void ShowId()
  {
    MessageBox.Show($"Id=({Id?.ToString() ?? "null"})");
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

public class IntegerValidator : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
    var idStr = ((MainModel) ((BindingExpression) value).ResolvedSource).IdStr;
    if (string.IsNullOrWhiteSpace(idStr))
      return ValidationResult.ValidResult;
    try
    {
      int.Parse(idStr, NumberStyles.Integer, CultureInfo.InvariantCulture);
      return ValidationResult.ValidResult;
    }
    catch (Exception e)
    {
      return new ValidationResult(false, e.Message);
    }
  }
}


Eld Hasp
Смысл валидации, именно в то чтьобы не пропускать не корректные данные. А вывод сообщения о некорректности - это визуальная "красивость".

Ну неправильно же. ValidationRule для ValidationStep=CommitedValue и UpdatedValue отрабатывает тогда, когда валидируемое значение уже передано в источник. Это заложено в дизайн системы валидации. И ТС об этом в курсе, т.к. экспериментировал с этим.
Ну, и насчет красивости я бы поспорил. При разборе полетов [якобы] некорректной работы приложения, одним из аргументов обвинения может стать отсутствие нотификации о некорректном вводе.
18 фев 19, 08:21    [21812494]     Ответить | Цитировать Сообщить модератору
 Re: ValidationRule+IValueConverter: сброс source в null при ошибке+отображение ошибки - как?  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 55
Сон Веры Павловны,

Спасибо. Последний вариант с двумя свойствами вполне устраивает.
18 фев 19, 17:52    [21813492]     Ответить | Цитировать Сообщить модератору
Все форумы / WPF, Silverlight Ответить