пятница, 16 января 2015 г.

WinForms, простая привязка данных и наследование.

 В моем проекте имеется достаточно большое количество объектов данных по различной аппаратуре, с которыми нужно обеспечить работу. Данные мои хранятся в иерархической структуре: родительский класс содержит общую для всех типов аппаратуры информацию, а наследники добавляют свою специфику.

Вот такая вот картинка... получилась у меня, после полутора дней шаманства и чтения форумов. Фотография сделана до чашки чая и зачистки решения от излишества в интерфейсах.


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

Шаг 1. Прямая привязка дело не хитрое.

textbox1.DataBindings.Add("Text", CurrentFotoConf, "ConfName");
- предсказуемо приводит к отображению в текстовом поле данных.

Шаг 2. На уровень глубже

textbox1.DataBindings.Add("Text", CurrentFotoConf, "AppObj.ConfName");
- так наша привязка делать, как выяснилось не умеет. Зато умеет так:
textbox1.DataBindings.Add("Text", CurrentFotoConf.AppObj, "ConfName");
- в самом деле - объекту источнику довольно все равно, из каких глубин вы его извлечете. Главное, чтобы был доступ на чтение.

Шаг 3. Обновление отображения при изменении источника.

Приведенная выше привязка работает только в одну сторону: изменяем значение в интерфейсе, изменяется объект. А как обновить отображение автоматически, при изменении источника?
Для этого нам понадобятся две составляющих:
  • Класс, который мы привязываем, должен поддерживать интерфейс INotifyPropertyChanged. Т.е. у него есть событие, например PropertyChanged, на которое сможет подписаться наш биндинг, и в случае изменения полей это событие вызывается. Подробнее про интерфейс и как выглядит его интеграция можно найти в конце поста. Using для волшебного интерфейса: System.ComponentModel .

  • Надо подписать наш биндинг на это событие. Например, так:
textbox1.DataBindings.Add("Text", CurrentFotoConf.AppObj, "Alpha", true, DataSourceUpdateMode.OnPropertyChanged)
- или другим, более симпатичным вам образом.

Шаг 4. Опаньки: подводный камень.

textbox1.DataBindings.Add("Text", CurrentFotoConf.AppObj, "Alpha", true, DataSourceUpdateMode.OnPropertyChanged)
- работает
textbox1.DataBindings.Add("Text", CurrentFotoConf, "ConfName", true, DataSourceUpdateMode.OnPropertyChanged)
- не работает
textbox1.DataBindings.Add("Text", CurrentFotoConf.AppObj, "ConfName", true, DataSourceUpdateMode.OnPropertyChanged)
- не работает.

Как так?...

Очень просто. Свойство "ConfName" - унаследованное. C# считает, что оно принадлежит не классу Objective (или FotoConf), откуда я его беру для привязки, а классу родителю, где оно объявлено - BDModelClass.
Поэтому, конструктор новой привязки, т.е. биндинга, берет свойство, лезет в класс, с намерением привязать к нему свой обработчик событий изменения... и не находит там этого события. И интерфейса тоже не находит.

Поэтому...
... Если вы привязываете унаследованное свойство - класс родитель, где это свойство объявлено, тоже должен поддерживать интерфейс INotifyPropertyChanged. А метод set нашего свойства, должен инициировать событие после изменения.

Ура!

Все работает. Задача решена, теперь я могу выносить свои обработки, сообщения и ограничения на значения в одно место: в методы set для соответствующего поля.

Шаг 5. Зачистка.

Да, оно работает. Но если вернуться к картинке, то,  между нами, все это не очень красиво. Почему бы просто не унаследовать интерфейс?.. Потому, что в самых первых моих опытах компилятор предупредил меня (в ультимативной форме), что иниировать событие я могу только в классе хозяина. Наследники таковыми не являются. Но есть выход: обернуть инициацию события в защищенный метод, и использовать его в наследниках. И не надо дублировать описание интерфейса, события и вообще.

class BDModelClass : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   protected void OnPropertyChanged(string update_args)
   {
      PropertyChangedEventHandler TmpThreadHandl = PropertyChanged;
      if (TmpThreadHandl != null)
         TmpThreadHandl(this, new PropertyChangedEventArgs(update_args));
   }

   public string ConfName
   {
      set
      {
         _ConfName = value;
         OnPropertyChanged("ConfName");
      }
      get { return _ConfName; }
   }  

}

class FotoConf : BDModelClass
{
   public FotoConf(...)
   {
      ....
      OnPropertyChanged(string.Empty);
   }
}


Примечание.

Этого нет здесь, потому что я узнала об этом раньше, но это частые нюансы, поэтому я их упомяну.
  1. Привязывать можно только свойства. Не поля. Публичные, открытые, хотя бы на чтение, свойства.
  2. Это относится к сложным привязкам, т.е. привязкам списков ко всяким DataGridView - но я оставлю это тут: не все списки одинаково съедобны. Т.е. не все поддерживают интерфейс INotifyPropertyChanged при изменении коллекции.

    List<T>, кстати, не поддерживает.
    System.Collections.ObjectModel.ObservableCollection<T> - точно поддерживает. Обычно я использую его, хотя никак не могу это мотивировать.
    BindingList<T> - не пробовала. Но я бы очень надеялась, что класс с таким названием все-таки это делает. 
  3. Подробнее про интерфейс, способы привязки и вообще, например, здесь:  http://rsdn.ru/article/dotnet/Data_Binding_Basics.xml .

Комментариев нет:

Отправить комментарий