пятница, 3 октября 2014 г.

Ввод данных и их проверка на уровне Binding

Очередной вопрос на MSDN. Стоит задача при редактировании записи сначала проверить данные, а только потом применить изменения к объекту модели. Я всю конструкцию MVVM воспроизводить не буду и покажу на примере в котором будет только один объект модели, а все остальное будет в лоб. Начнем.

Создаем пустой проект и добавляем в него два класса. Первый класс для модели:

class Person : DependencyObject
{
    public string LastName
    {
        get { return (string)GetValue(LastNameProperty); }
        set { SetValue(LastNameProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for LastName.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty LastNameProperty =
        DependencyProperty.Register("LastName"typeof(string), typeof(Person), new PropertyMetadata(""));
 
    public string FirstName
    {
        get { return (string)GetValue(FirstNameProperty); }
        set { SetValue(FirstNameProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for FirstName.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty FirstNameProperty =
        DependencyProperty.Register("FirstName"typeof(string), typeof(Person), new PropertyMetadata(""));
        
}
Для проверки добавляем класс чекающий строку на пустоту:
class NotEmptyValidation : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        var result = new ValidationResult(false"Не допустима пустая строка");
        if (value is string && !string.IsNullOrWhiteSpace(value.ToString()))
        {
            result = new ValidationResult(truenull);
        }
        return result;
    }
}
Разметка формы:
<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication3"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBox Text="{Binding Person.LastName}" />
        <TextBox Text="{Binding Person.FirstName}" Grid.Row="1" />
        <TextBox Grid.Column="2" x:Name="tbLastName">
            <TextBox.Text>
                <Binding Path="Person.LastName" UpdateSourceTrigger="Explicit">
                    <Binding.ValidationRules>
                        <local:NotEmptyValidation />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBox Grid.Row="1" Grid.Column="2" x:Name="tbFirstName">
            <TextBox.Text>
                <Binding Path="Person.FirstName" UpdateSourceTrigger="Explicit">
                    <Binding.ValidationRules>
                        <local:NotEmptyValidation />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <Button Content="Принять" Click="Button_Click" Grid.Column="2" Grid.Row="2" />
    </Grid>
</Window>
Код формы:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }
 
    public Person Person
    {
        get { return (Person)GetValue(PersonProperty); }
        set { SetValue(PersonProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for Person.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty PersonProperty =
        DependencyProperty.Register("Person"typeof(Person), typeof(MainWindow), new PropertyMetadata(null));
 
 
    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Person = new Person() { LastName = "Иванов" };
        this.DataContext = this;
    }
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        BindingExpression beLastName = tbLastName.GetBindingExpression(TextBox.TextProperty);
        BindingExpression beFirstName = tbFirstName.GetBindingExpression(TextBox.TextProperty);
        if (beLastName.ValidateWithoutUpdate() && beFirstName.ValidateWithoutUpdate())
        {
            beLastName.UpdateSource();
            beFirstName.UpdateSource();
            // Вот здесь можно закрывать View, не забыв уведомить ViewModel
        }
    }
}
Несколько комментариев:
1. Не забываем на форме подключить пространство имен где описан класс валидатора:
xmlns:local="clr-namespace:WpfApplication3"
2. Для того, чтобы Binding не срабатывал сам, а только по кнопке, прописываем в нем:
UpdateSourceTrigger="Explicit"
3. Перед принудительным применением Binding проверяем, а все ли нормально:
if (beLastName.ValidateWithoutUpdate() && beFirstName.ValidateWithoutUpdate())
4. Работает вот так. Запускаем:
Вводим слева значение и переводим фокус ввода, значение отображается справа:
Теперь вводим значение в правый TextBox и меняем фокус ввода:
Как видим, значение слева не обновилось. Нажимаем Принять:
Значение обновилось. Теперь значение меняем на некорректное (пустую строку) и нажимаем принять:
Видим подсветку ошибки и то, что в модель пустое значение не скопировалось.



 

3 комментария:

  1. А почему бы просто не реализовать INotifyPropertyChanged вместо DependencyObject. код получится проще, а результат, насколько я понимаю, тот же. Здесь http://channel9.msdn.com/Shows/Visual-Studio-Toolbox/MVVM-Best-Practices подобную реализацию называют антипаттерном.

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      У INotifyPropertyChanged есть ряд преимуществ, например, если ваш класс уже имеет предков, то альтернатив нет, вы будите использовать именно его. Также, использование INotifyPropertyChanged немного экономит память. Но вот максимальное быстродействие достигается при использовании DependencyObject. Смотрите табличку сравнения здесь: http://msdn.microsoft.com/ru-ru/library/vstudio/bb613546(v=vs.100).aspx
      Поэтому, я не очень понимаю, в чем заключается антипаттерн?

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить