Как стать автором
Обновить

Валидация данных в DataGrid по «столбцам»

Время на прочтение7 мин
Количество просмотров4K

Вступление



В своих проектах на WPF для отображения данных я использую в основном DataGrid. Этот элемент управления очень удобный, прост в использовании и к тому же с выходом Visual Studio 2010 является частью 4-ого фрэймвёрка.

Так вот, при необходимости изменить данные в таблице (DataGrid) я предлагал пользователю модальное окно, в котором отображались данные маркированного в таблице объекта. И пользователь изменял этот самый объект в зависимости от потребности. Валидация данных происходила до того, как пользователь закроет окно. Всё работало гладко.

Но как-то раз возникла необходимость предоставить возможность пользователю изменять данные напрямую в таблице (как в Excel), без вызова модального окна. Надо – сделаем.

Но при реализации этого самого действия я столкнулся с одной проблемой: валидация данных. А если конкретней необходимо избежать ввода одинаковых данных в таблицу. Надо сказать, что DataGrid содержит поддержку валидации данных реализованный объектом ValidationRule. Но дело в том, что валидация данных происходила в пределах актуального объекта. То есть, валидация осуществлялась по “строке” DataGrid  а не по “столбцу”. Google в этом помочь не смог. Поэтому пришлось немного покумекать.


Реализация



Итак, попробуем осуществить валидацию в DataGrid по “столбцам”.

Для начала определимся с объектом, который будем изменениям. Это будет объект Person. Но прежде чем приступить к реализации этого самого объекта создадим вспомогательный класс WPFBase, имплементирующий INotifyPropertyChanged для сообщения об изменении данных в объекте Person:

  1. public class WPFBase : INotifyPropertyChanged
  2.   {
  3.     public event PropertyChangedEventHandler PropertyChanged;
  4.  
  5.     public void OnPropertyChanged(string PropertyName)
  6.     {
  7.       if (PropertyChanged != null)
  8.       {
  9.         this.PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
  10.       }
  11.     }
  12.  
  13.  
  14.     public void OnPropertyChanged(MethodBase MethodeBase)
  15.     {
  16.       this.OnPropertyChanged(MethodeBase.Name.Substring(4));
  17.     }
  18.   }

* This source code was highlighted with Source Code Highlighter.


Теперь создадим класс Person, содержащий два свойства: Name и Vorname. И наследуем его от WPFBase:

  1. public partial class Person : WPFBase
  2.   {
  3.     private string _Name;
  4.     private string _Vorname;
  5.  
  6.     public string Vorname
  7.     {
  8.       get { return _Vorname; }
  9.       set { _Vorname = value; OnPropertyChanged(MethodInfo.GetCurrentMethod()); }
  10.     }
  11.  
  12.  
  13.     public string Name
  14.     {
  15.       get { return _Name; }
  16.       set { _Name = value; OnPropertyChanged(MethodInfo.GetCurrentMethod()); }
  17.     }
  18.  
  19.  
  20.   }

* This source code was highlighted with Source Code Highlighter.


Вы наверно заметили, что класс Person обозначен как partial. Это сделанно для удобства, так как мы реализуем ещё пару интерфейсов и для удобочитаемости разделим класс на три части.

Итак реализуем интерфейс IDataErrorInfo. В объектно-ориентированном программирование каждый объект сам отвечает за самого себя и за те данные, которые он содержит. Поэтому пусть объект Person сам и переживает о том чем его “пичкают”. Приступим:

  1. public partial class Person : IDataErrorInfo
  2.   {
  3.     public string Error
  4.     {
  5.       get
  6.       {
  7.         string error;
  8.         StringBuilder sb = new StringBuilder();
  9.  
  10.         error=this["Name"];
  11.         if (error != string.Empty)
  12.           sb.AppendLine(error);
  13.  
  14.         error=this["Vorname"];
  15.         if (error != string.Empty)
  16.           sb.AppendLine(error);
  17.  
  18.         if (!string.IsNullOrEmpty(sb.ToString()))
  19.           return sb.ToString();
  20.  
  21.         return "";
  22.       }
  23.     }
  24.  
  25.     public string this[string columnName]
  26.     {
  27.       get
  28.       {
  29.         switch (columnName)
  30.         {
  31.           case "Name":
  32.             if (string.IsNullOrEmpty(this.Name))
  33.               return "Введите имя!!!";
  34.             break;
  35.           case "Vorname":
  36.             if (string.IsNullOrEmpty(this.Vorname))
  37.               return "Введите фамилию!!!";
  38.             break;
  39.           default:
  40.             break;
  41.         }
  42.         return "";
  43.       }
  44.     }
  45.   }

* This source code was highlighted with Source Code Highlighter.

Хорошо, мы добились того, что при отсутствии данных в свойствах Name  и Vorname объекта Person возвращается сообщение о необходимости ввести имя или фамилию.

Ну и последний штрих: реализуем интерфейс IDataEditObject. Он необходим для того, чтобы в случае ввода недействительных данных в объект Person при отмене операции ввода данные вернулись в так сказать исходное положение:

  1. public partial class Person : IEditableObject
  2.   {
  3.     bool inEdit = false;
  4.     Person tempPerson;
  5.  
  6.     public void BeginEdit()
  7.     {
  8.       if (inEdit)
  9.         return;
  10.  
  11.       inEdit = true;
  12.       tempPerson = new Person();
  13.       tempPerson.Name = this.Name;
  14.       tempPerson.Vorname = this.Vorname;
  15.     }
  16.  
  17.     public void CancelEdit()
  18.     {
  19.       if (!inEdit)
  20.         return;
  21.  
  22.       inEdit = false;
  23.       this.Name = tempPerson.Name;
  24.       this.Vorname = tempPerson.Vorname;
  25.       tempPerson = null;
  26.     }
  27.  
  28.     public void EndEdit()
  29.     {
  30.  
  31.       if (!inEdit)
  32.         return;
  33.  
  34.       inEdit = false;
  35.       tempPerson = null;
  36.     }
  37.   }

* This source code was highlighted with Source Code Highlighter.

Создадим ещё один класс ListPerson наследующий от ObservableCollection<Person>. Объект Personen и будет испльзоваться как контейнер для объектов Person. Ну и для простоты тестирования сразу добавим в коллекцию ListPerson несколько объектов:

  1. public class ListPerson:ObservableCollection<Person>
  2.   {
  3.     public ListPerson()
  4.     {
  5.       this.Add(new Person()
  6.       {
  7.         Name="Иванов",
  8.         Vorname="Сергей"
  9.       });
  10.       this.Add(new Person()
  11.       {
  12.         Name="Петров",
  13.         Vorname="Андрей"
  14.       });
  15.     }
  16.   }

* This source code was highlighted with Source Code Highlighter.

Добовляем коллекцию ListPerson в ресурсы окна:

  1. <Window.Resources>
  2.     <dt:ListPerson x:Key="ListPerson"/>
  3. </Windows.Resources>

* This source code was highlighted with Source Code Highlighter.

И сообщаем нашей таблице, где находятся данные:

  1. <DataGrid ItemsSource="{Binding Source={StaticResource ListPerson}}" />

* This source code was highlighted with Source Code Highlighter.

Теперь добавим в DataGrid нижестоящий код:

  1. <DataGrid.RowValidationRules>
  2.    <DataErrorValidationRule ValidationStep="UpdatedValue"/>
  3. </DataGrid.RowValidationRules>

* This source code was highlighted with Source Code Highlighter.

Что это значит: мы добавили в коллекцию правил DataGrid новое правило, которое сообщает о том, что после изменения данных в объекте Person необходимо проверить, а не сообщает ли этот самый объект о том, что данные не соответствуют правилам, указанных нами при имплементиции интерфейса IDataErrorInfo. Если это так, DataGrid среагирует на ошибку.

Чего нам не хватает, так это того, ради чего мы собственно всё это и затеяли: не допустить ввод одинаковых данных в таблице.

Для этого создадим новый класс ValidationRule но уже с нашими собственными критериями проверки на валидность данных. Кроме того нам необходимо будет создать дополнительное свойство ссылающееся на тот же контейнер объектов, что и сам DataGrid. Для того, чтобы в XAML можно было привязать данные к этому свойству небходимо декларировать его как DependencyProperty. Проблема в том, что для этого объект должен быть унаследован от класса DependencyObject. Наше же новое правило наследует от ValidationRule. Поэтому просто напросто создадим дополнительный класс ListPersonHelper  и наследуем его от DependencyObject. Ладно, хватит болтать. Посмотрим лучше на код:

  1.   class ColumnValidation : ValidationRule
  2.   {
  3.     private ListPersonHelper _ListPerson;
  4.  
  5.     //Данное свойство является вспомогательным, служит для реализации DependencyProperty
  6.     public ListPersonHelper ListPerson
  7.     {
  8.       get { return _ListPerson; }
  9.       set { _ListPerson = value; }
  10.     }
  11.  
  12.  
  13.     //Проверка на валидность данных
  14.     public override ValidationResult Validate
  15.       (object value, System.Globalization.CultureInfo cultureInfo)
  16.     {
  17.       BindingGroup bg = value as BindingGroup;
  18.       Person person = bg.Items[0] as Person;
  19.  
  20.       //Если в коллекции находится объект с таким же именем или фамилией сообщаем об ошибке!
  21.       var result = from p in ListPerson.Items
  22.              where p != person &&
  23.              (p.Name == person.Name
  24.              || p.Vorname == person.Vorname)
  25.              select p;
  26.       if (result.Any())
  27.       {
  28.         return new ValidationResult(false, "Имя или Фамилия уже находятся в базе данных");
  29.       }
  30.  
  31.       return ValidationResult.ValidResult;
  32.     }
  33.   }
  34.  
  35.   //Вспомогательный класс для реализации DependencyProperty
  36.   public class ListPersonHelper : DependencyObject
  37.   {
  38.     //К свойству Items можно привиязать данные в XAML, так как оно объявленно как DependencyProperty
  39.     public ListPerson Items
  40.     {
  41.       get { return (ListPerson)GetValue(ItemsProperty); }
  42.       set { SetValue(ItemsProperty, value); }
  43.     }
  44.  
  45.     public static readonly DependencyProperty ItemsProperty =
  46.       DependencyProperty.Register
  47.       ("Items",
  48.       typeof(ListPerson),
  49.       typeof(ListPersonHelper),
  50.       new UIPropertyMetadata(null));
  51.  
  52.  
  53.   }

* This source code was highlighted with Source Code Highlighter.

Добовляем ListPersonHelper в ресурсы окна. Свойству Items укажем тот же источник данных, что и для DataGrid. В ValidationRules таблицы добавим наше новое правило ColumnValidation. Свойству ListPerson укажем на ресурс ListPerson. Вот и всё:

  1. <Window x:Class="DPTest.MainWindow"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:dt="clr-namespace:DPTest"
  5.     Title="MainWindow" Height="437" Width="684">
  6.   <Window.Resources>
  7.     <dt:ListPerson x:Key="ListPerson"/>
  8.     <dt:ListPersonHelper x:Key="ListPersonHelper"
  9.              Items="{Binding Source={StaticResource ListPerson}}"/>
  10.     <ControlTemplate x:Key="GridError">
  11.       <Grid Margin="0,-2,0,-2"
  12.          ToolTip="{Binding RelativeSource={RelativeSource

             FindAncestor, AncestorType={x:Type DataGridRow}},

             Path=(Validation.Errors)[0].ErrorContent}"
    >
  13.         <Ellipse StrokeThickness="0" Fill="Red"
  14.               Width="{TemplateBinding FontSize}"
  15.               Height="{TemplateBinding FontSize}" />
  16.         <TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
  17.               FontWeight="Bold" Foreground="White"
  18.               HorizontalAlignment="Center" />
  19.       </Grid>
  20.     </ControlTemplate>
  21.   </Window.Resources>
  22.   <Grid>
  23.     <DataGrid ItemsSource="{Binding Source={StaticResource ListPerson}}"
  24.          AutoGenerateColumns="False"
  25.          RowValidationErrorTemplate="{StaticResource GridError}">
  26.       <DataGrid.Columns>
  27.         <DataGridTextColumn
  28.           Binding="{Binding Path=Vorname}"
  29.           Header="Vorname" />
  30.         <DataGridTextColumn
  31.           Binding="{Binding Path=Name}"
  32.           Header="Name" />
  33.       </DataGrid.Columns>
  34.       <DataGrid.RowValidationRules>
  35.         <DataErrorValidationRule
  36.           ValidationStep="UpdatedValue"/>
  37.         <dt:ColumnValidation
  38.           ValidationStep="UpdatedValue">
  39.           <dt:ColumnValidation.ListPerson>
  40.             <dt:ListPersonHelper
  41.               Items="{Binding Source={StaticResource ListPerson}}"/>
  42.           </dt:ColumnValidation.ListPerson>
  43.         </dt:ColumnValidation>
  44.       </DataGrid.RowValidationRules>
  45.     </DataGrid>
  46.   </Grid>
  47. </Window>

* This source code was highlighted with Source Code Highlighter.


Пример можно скачать здесь: DataGridColumnValidation.zip (71,37 kb)
Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+2
Комментарии4

Публикации