Сообщений 24    Оценка 510 [+2/-0]         Оценить  
Система Orphus

Привязка данных в Windows Forms

Основные принципы

Автор: Сергей Тепляков
ООО НПП Кронос

Источник: RSDN Magazine #3-2008
Опубликовано: 28.12.2008
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
1. Мир без привязки
2. Создание класса, управляющего привязкой данных
3. Привязка данных средствами Windows Forms
3.1 Простая привязка данных (Simple data binding)
3.2 Простая привязка к списку объектов
3.3 Сложная привязка данных
Выводы
Источники

Введение

В приложении, работающем с данными, существует логическая взаимосвязь между уровнем данных и представлением, хотим мы этого или нет. Вопрос в том, сколько рутинной работы ложится на наши плечи. Механизм привязки данных в Windows Forms в значительной степени упрощает создание приложений, работающих с данными. Понимание основных концепций, на которых строится привязка данных, может в значительной степени упростить построение таких приложений, а также помочь в решении различных проблем, которые неизменно преследуют каждого разработчика.

1. Мир без привязки

Привязка данных – это логическая ассоциация между свойствами элементами управления и свойствами некоторого объекта. И вполне можно реализовать эту ассоциацию вручную.

Рассмотрим класс BookInfo, предназначенный для описания информации о книге.

      class BookInfo
{
  publicstring Author { get; set; }
  publicstring Title { get; set; }
  publicstring Isbn { get; set; }
  publicint PageCount { get; set; }
  publicstring Publisher { get; set; }
}

И класс формы, предназначенный для отображения данных класса BookInfo.

      public partial class BindinglessForm : Form
{
  public BindinglessForm()
  {
    InitializeComponent();
    bookInfo = CreateBookInfo();
    FillControls();
    AdviseToControlEvents();
  }

  private BookInfo CreateBookInfo()
  {
    BookInfo bookInfo = new BookInfo();
    bookInfo.Author = "Том Клэнси";
    bookInfo.Title = "Игры патриотов";
    bookInfo.ISBN = "5-699-18175-Х";
    bookInfo.PageCount = 706;
    bookInfo.Publisher = "ЭКСМО";
    return bookInfo;
  }

  privatevoid FillControls()
  {
    authorTextBox.Text = bookInfo.Author;
    titleTextBox.Text = bookInfo.Title;
    isbnTextBox.Text = bookInfo.ISBN;
    pageCountTextBox.Text = bookInfo.PageCount.ToString();
    publisherTextBox.Text = bookInfo.Publisher;
  }

  privatevoid AdviseToControlEvents()
  {
    authorTextBox.TextChanged += (o, e) => 
    { 
      bookInfo.Author = authorTextBox.Text; 
    };
    titleTextBox.TextChanged += (o, e) => 
    {
      bookInfo.Title = titleTextBox.Text; 
    };
    isbnTextBox.TextChanged += (o, e) => 
    {
      bookInfo.ISBN = isbnTextBox.Text; 
    };
    pageCountTextBox.TextChanged += (o, e) => 
    {
      bookInfo.PageCount = int.Parse(pageCountTextBox.Text);
    };
    publisherTextBox.TextChanged += (o, e) => 
    {
      bookInfo.Publisher = publisherTextBox.Text; 
    };
  }

  private BookInfo bookInfo;
}


Рисунок 1. Отображение свойств объекта BookInfo.

Так, отлично. Данные отображаются и изменяются, но есть одно замечание. Что, если объект класса BookInfo будет изменен без использования этой формы? В данный момент форма не может узнать об этих изменениях. Это и понятно, сейчас класс BookInfo не поддерживает такую функциональность.

Добавим генерацию событий при изменении свойств класса BookInfo:

      class BookInfo
{
  public BookInfo()
  {
  }
  publicevent EventHandler<EventArgs> AuthorChanged;
  publicstring Author
  {
    get { return author; }
    set
    {
      if (author != value)
      {
        author = value;
        if (AuthorChanged != null)
          AuthorChanged(this, EventArgs.Empty);
      }
    }

  }
  //Остальная часть класса изменяется аналогичным образом
}

И немного изменим класс формы:

      public partial class BindinglessForm : Form
{
  public BindinglessForm()
  {
    InitializeComponent();
    bookInfo = CreateBookInfo();
    FillControls();
    AdviseToBookEvents();
    AdviseToControlEvents();
  }
  privatevoid AdviseToBookEvents()
  {
    bookInfo.AuthorChanged += (s, e) => 
    {
      authorTextBox.Text = ((BookInfo)s).Author;
    };
    bookInfo.TitleChanged += (s, e) => 
    {
      titleTextBox.Text = ((BookInfo)s).Title; 
    };
    bookInfo.ISBNChanged += (s, e) => 
    {
      isbnTextBox.Text = ((BookInfo)s).ISBN;
    };
    bookInfo.PageCountChanged += (s, e) => 
    {
      pageCountTextBox.Text = ((BookInfo)s).PageCount.ToString(); 
    };
    bookInfo.PublisherChanged += (s, e) =>
    {
      publisherTextBox.Text = ((BookInfo)s).Publisher; 
    };
  }
}

Ну что же, благодаря нововведениям C# 3.0 мы смогли сэкономить немало строк кода и в результате получили работоспособное приложение, которое осуществляет двустороннюю «привязку» данных объекта BookInfo к элементам управления. Такой поход применялся длительное время в огромном количестве приложений и, как это ни странно, по сей день активно используется в приложениях Windows Forms.

Недостатком этого подхода является обилие рутинных операций, которые необходимо выполнять при каждой синхронизации данных. Причем эти операции повторяются от приложения к приложению и не зависят от бизнес-логики, вследствие чего напрашивается обобщенное решение привязки данных к элементам управления.

2. Создание класса, управляющего привязкой данных

Разработаем класс, решающий задачу синхронизации свойства объекта (иначе говоря, источника данных) и свойства элемента управления.

Идея достаточно проста. Нужно получить PropertyDescriptor для свойства источника данных и элемента управления. Подписаться на событие изменение этих свойств и в обработчике этих событий изменять соответствующее свойство противоположной стороны.

      public
      class CustomBinder
{
  public CustomBinder(Control control, string controlPropertyName,
                 object dataSource, string dataSourcePropertyName)
  {
    this.control = control;
    this.dataSource = dataSource;
    
    // Получаем экземпляр класса PropertyDescriptor 
    // для управления свойством элемента управления
    controlPropertyDescriptor =
      TypeDescriptor.GetProperties(control).Find(controlPropertyName, true);
    if (controlPropertyDescriptor == null)
      thrownew ArgumentException(
String.Format(
          "Не удалось найти свойство элемента управления с именем {0}",
          controlPropertyName), "controlPropertyName");
    if (controlPropertyDescriptor.SupportsChangeEvents)
      controlPropertyDescriptor.AddValueChanged(
        control, ControlPropertyChanged);

    // Получаем экземпляр класса PropertyDescriptor 
    // для управления свойством источника данных
    dataSourcePropertyDescriptor = 
      TypeDescriptor.GetProperties(dataSource).Find(
        dataSourcePropertyName, true);
    if (dataSourcePropertyDescriptor == null)
      thrownew ArgumentException(
String.Format("
Не удалось найти свойство источника данных с именем {0}", 
dataSourcePropertyName, "dataSourcePropertyName"));

    if (dataSourcePropertyDescriptor.SupportsChangeEvents)
      dataSourcePropertyDescriptor.AddValueChanged(
        dataSource, DataSourcePropertyChanged);
    
    // Генерация события приведет к установке 
    // значения свойства элемента управления
    DataSourcePropertyChanged(this, EventArgs.Empty);
  }
  
  // Обработчик события изменения свойства элемента управленияprivatevoid ControlPropertyChanged(object sender, EventArgs e)
  {
    // получаем новое значение свойства элемента управленияobject controlPropertyValue = controlPropertyDescriptor.GetValue(control);
    //сразу присвоить свойству источника данных новое значение нельзя, //т.к. типы свойств могут не совпадать.//для этого воспользуемся TypeConverter-ами, которые являются частью //класса PropertyDescriptorif(
controlPropertyDescriptor.Converter.CanConvertTo(
        dataSourcePropertyDescriptor.PropertyType))
    {
      object convertedValue = 
controlPropertyDescriptor.Converter.ConvertTo(
          controlPropertyValue, 
          dataSourcePropertyDescriptor.PropertyType);
      //изменяем значение свойства источника данных
      dataSourcePropertyDescriptor.SetValue(dataSource, convertedValue);
    }
    elseif(
dataSourcePropertyDescriptor.Converter.CanConvertFrom(
        controlPropertyDescriptor.PropertyType))
    {
      object convertedValue = 
dataSourcePropertyDescriptor.Converter.ConvertFrom(
          controlPropertyValue);
      //изменяем значение свойства источника данных
      dataSourcePropertyDescriptor.SetValue(dataSource, convertedValue);
    }
  }

  //Обработчик события изменения свойства источника данныхprivatevoid DataSourcePropertyChanged(object sender, EventArgs e)
  {
    //получаем новое значение свойства источника данныхobject dataSourceValue = 
dataSourcePropertyDescriptor.GetValue(dataSource);
    // сразу присвоить свойству элемента управление полученное значение // нельзя, т.к. типы свойств могут не совпадать.// Воспользуемся TypeConverter-ами, которые являются частью// класса PropertyDescriptorif(
dataSourcePropertyDescriptor.Converter.CanConvertTo(
        controlPropertyDescriptor.PropertyType))
    {
      object convertedValue =
 dataSourcePropertyDescriptor.Converter.ConvertTo(
          dataSourceValue, 
          controlPropertyDescriptor.PropertyType);
      // object value = 
      //   controlPropertyDescriptor.Converter.ConvertFrom(dataSourceValue);//изменяем значение свойства элемента управления
      controlPropertyDescriptor.SetValue(control, convertedValue);
    }
    elseif(
controlPropertyDescriptor.Converter.CanConvertFrom(
        dataSourcePropertyDescriptor.PropertyType))
    {
      object ConvertedValue = 
controlPropertyDescriptor.Converter.ConvertFrom(
          dataSourceValue);
      controlPropertyDescriptor.SetValue(control, ConvertedValue);
    }
  }
  //закрытые поля
}

Тогда класс формы будет выглядеть следующим образом:

      public partial class CustomBindingForm : Form
{
  public CustomBindingForm()
  {
    InitializeComponent();
    bookInfo = CreateBookInfo();
    Bind();
  }

  privatevoid Bind()
  {
    CustomBinder authorPropertyManager = 
      new CustomBinder(authorTextBox, "Text", bookInfo, "Author");
    CustomBinder titlePropertyManager = 
      new CustomBinder(titleTextBox, "Text", bookInfo, "Title");
    CustomBinder isbnPropertyManager = 
      new CustomBinder(isbnTextBox, "Text", bookInfo, "ISBN");
    CustomBinder pageCountPropertyManager = 
      new CustomBinder(pageCountTextBox, "Text", bookInfo, "PageCount");
    CustomBinder publisherPropertyManager = 
      new CustomBinder(publisherTextBox, "Text", bookInfo, "Publisher");
  }
}

Весь остальной код, предназначенный для подписки на события изменения элемента управления и источника данных, больше не нужен. Теперь всем этим занимается CustomBinder. Уже неплохо.

Чтобы лучше понять внутренности реализации CustomBinder и стандартного механизма привязки в Windows Forms, рассмотрим следующий фрагмент кода более подробно.

      // Получаем экземпляр класса PropertyDescriptor 
// для управления свойством элемента управления
controlPropertyDescriptor = 
  TypeDescriptor.GetProperties(control).Find(controlPropertyName, true);
if (controlPropertyDescriptor.SupportsChangeEvents)
  controlPropertyDescriptor.AddValueChanged(control, ControlPropertyChanged);

В первой строке с использованием рефлексии мы получили ссылку на PropertyDescriptor. PropertyDescriptor – это абстрактный класс, который описывает свойство (property) некоторого класса и позволяет манипулировать им. Реально нам возвращается ссылка на экземпляр конкретного (не абстрактного), но недокументированного класса ReflectPropertyDescriptor.

Во второй строчке осуществляется подписка на событие изменения свойства (если объект поддерживает механизм уведомления об изменении конкретно этого свойства). В этом методе ReflectPropertyDescriptor делает следующее. Осуществляется поиск события с именем PropertyNameChanged, если класс реализует это событие, то ReflectPropertyManager подписывается именно на него. В противном случае ReflectPropertyManager пробует найти событие с именем PropertyChanged, объявленное в интерфейсе INotifyPropertyChanged и преобразует его к типу PropertyChangedEventHandler.

Таким образом, для того, чтобы объект PropertyDescriptor мог уведомить об изменении свойства объекта, нужно, чтобы объект отвечал одному из следующих критериев:

  1. Объект реализует событие PropertyNameChanged и генерирует его при изменении этого свойства. Причем обработчик этого события должен обязательно иметь тип EventHandler. В случае любого другого типа (включая EventHandler<EventArgs>) при попытке подписаться на изменение этого свойства будет сгенерировано исключение.
  2. Объект реализует интерфейс INotifyPropertyChanged с его событием PropertyChanged и генерирует это событие, передавая имя свойства в качестве параметра (вместо имени свойства можно передать пустую строку или null - это будет означать, что обновились все свойства объекта).

Теперь, я думаю, понятно, почему класс PropertyDescriptor не содержит событие с именем ValueChanged, а содержит набор функций для регистрации и дерегистрации обработчиков этого события.

Еще нужно обратить внимание на обработчики событий изменения свойств источника данных и элемента управления.

      private
      void ControlPropertyChanged(object sender, EventArgs e)
{
  //получаем новое значение свойства элемента управленияobject controlPropertyValue = controlPropertyDescriptor.GetValue(control);
  //сразу присвоить свойству источника данных новое значение нельзя, //т.к. типы свойств могут не совпадать.//для этого воспользуемся TypeConverter-ами, которые являются частью//класса PropertyDescriptorif(
controlPropertyDescriptor.Converter.CanConvertTo(
      dataSourcePropertyDescriptor.PropertyType))
  {
    object convertedValue =
 controlPropertyDescriptor.Converter.ConvertTo(
        controlPropertyValue, dataSourcePropertyDescriptor.PropertyType);
    //изменяем значение свойства источника данных
    dataSourcePropertyDescriptor.SetValue(dataSource, convertedValue);
  }
  elseif(
dataSourcePropertyDescriptor.Converter.CanConvertFrom(
      controlPropertyDescriptor.PropertyType))
  {
    object convertedValue =
 dataSourcePropertyDescriptor.Converter.ConvertFrom(
        controlPropertyValue);
    //изменяем значение свойства источника данных
    dataSourcePropertyDescriptor.SetValue(dataSource, convertedValue);
  }
}

При изменении свойства элемента управления нельзя напрямую изменить свойство источника данных, т.к. возможно банальное несоответствие типов данных (как в случае привязки BookInfo.PageCount к pageCountTextBox.Text, которые имеют тип Int32 и String соответственно). Именно для этих целей используется объект класса TypeConverter, доступ к которому можно получить через PropertyDescriptor.

В результате мы получили класс, существенно упрощающий привязку данных. Это решение не претендует на полноту и далеко от совершенства, это всего лишь пример того, как с помощью метаданных и механизма рефлексии можно создать обобщенное средства привязки свойств источника данных и элемента управления. Реализация привязки в Windows Forms значительно сложнее и обладает большей функциональностью, но идея, я думаю, понятна.

3. Привязка данных средствами Windows Forms

3.1 Простая привязка данных (Simple data binding)

Фундаментальным строительным блоком, на основе которого строится привязка данных в Windows Forms, является класс Binding. Основная задача этого класса – обеспечивать синхронизацию между свойством элемента управления и свойством источника данных.

Рассмотрим следующий пример.

        public partial class SimpleBindingForm : Form
{
  public SimpleBindingForm()
  {
    InitializeComponent();
    bookInfo = CreateBookInfo();
    Bind();
  }

  privatevoid Bind()
  {
    Binding authorBinding = new Binding("Text11", bookInfo, "Author", true);
    authorTextBox.DataBindings.Add(authorBinding);
    titleTextBox.DataBindings.Add("Text", bookInfo, "Title");
    iSBNTextBox.DataBindings.Add("Text", bookInfo, "ISBN");
    pageCountTextBox.DataBindings.Add("Text", bookInfo, "PageCount");
    publisherTextBox.DataBindings.Add("Text", bookInfo, "Publisher");
  }
  ...
}

В этом фрагменте кода показано два варианта добавления привязки. В первом случае объект класса Binding создается явным образом, а затем добавляется в набор объектов Binding. Во втором случае используется перегруженная версия функции Add класса BindingCollections. Конструктор класса Binding (и аналогичная перегруженная версия функции Add) принимает четыре параметра: имя свойства элемента управления, объект источника данных, имя свойства в источнике данных и булев флаг, определяющий, нужно ли использовать автоматическое форматирование (вопросы форматирования выходят за рамки данной статьи).

Какой бы вариант добавления объекта класса Binding не использовался, выполняются следующие дополнительные действия. Во-первых, автоматически устанавливается свойство элемента управления, как если бы был выполнен следующий фрагмент кода:

authorTextBox.Text = bookInfo.Author;

Во-вторых, механизм привязки данных подписывается на события изменения свойств элемента управления и источника данных, и заботится об их синхронизации (с использованием класса PropertyDescriptor).

Рассмотрим подробнее, как работает механизм передачи данных из элемента управления в источник данных, какие условия должны выполняться для этого, и как может повлиять на это пользователь (рисунок 2).


Рисунок 2. Механизм передачи данных из элемента управления в источник данных.

При изменении свойства элемента управления проверяется значение свойства DataSourceUpdateMode класса Binding, которое определяет, как элемент управления обновляет источник данных. В качестве значения ему присваивается один из членов перечисления DataSourceUpdateMode: OnPropertyChanged, OnValidation или Never. Значение по умолчанию, OnValidation, указывает на необходимость генерации событий Validating и Validated класса Control. Они генерируются, когда элемент управления теряет фокус ввода. В программе они служат для проверки правильности введенных в элемент управления данных. Таким способом создатели Windows Forms подсказывают программисту, что желательно проверить корректность значения элемента управления до того, как оно попадет в источник данных. Если проверка не нужна, то значению DataSourceUpdateMode нужно присвоить значение OnPropertyChanged, если же вообще нет необходимости в обновлении источника данных при изменении свойства элемента управления – нужно присвоить значение Never.

После проверки значения свойства DataSourceUpdateMode проверяется необходимость форматирования данных перед тем, как они будут отправлены в источник.

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

Теперь рассмотрим механизм обновления элемента управления при изменении источника данных (рисунок 3).


Рисунок 3. Механизм обновления элемента управления.

Основным условием возможности обновления элемента управления при изменении источника данных является поддержка определенного набора событий со стороны источника данных. Источник данных должен содержать события PropertyNameChanged для каждого свойства, либо реализовывать интерфейс INotifyPropertyChanged (более подробно об этом я рассказывал в разделе «Создание класса, управляющего привязкой данных»).

Даже при наличии одного из вышеперечисленных событий, существует возможность включать или отключать обновление элемента управления. Для этого служит свойство ControlUpdateMode класса Binding, которое может принимать одно из двух значений: OnPropertyChanged или Never.

Кром того, программист может вмешаться в процесс обновления элемента управления данными из источника, подписавшись на событие Parse класса Binding.

Рассмотренная выше привязка одного свойства элемента управления к одному свойству источника данных называется простой привязкой (simple binding), а источник данных (в нашем случае объект класса BookInfo) называется одиночным элементом источника данных (item data source).

А что, если нужно работать с коллекцией объектов BookInfo? Для этих целей служит привязка данных к списочным источникам (list data source).

3.2 Простая привязка к списку объектов

Списочный источник данных (list data source) – это коллекция объектов, предназначенная для привязки к элементу управления. Минимальным требованием, которое предъявляет механизм привязки данных Windows Forms к списочному источнику данных, является реализация интерфейса IList. Хотя, с появлением обобщенных коллекций, использование List<T> выглядит более предпочтительным.

Рассмотрим следующий пример.

        public partial class SimpleBindingAndListDataSourceForm : Form
{
  public SimpleBindingAndListDataSourceForm()
  {
    InitializeComponent();
    bookInfoList = CreateBookInfoList();
    Bind();
    BindingManager.PositionChanged += 
      new EventHandler(BindingManager_PositionChanged);
    RefreshData();
  }

  privatevoid Bind()
  {
    authorTextBox.DataBindings.Add("Text", bookInfoList, "Author", true);
    titleTextBox.DataBindings.Add("Text", bookInfoList, "Title");
    isbnTextBox.DataBindings.Add("Text", bookInfoList, "ISBN");
    pageCountTextBox.DataBindings.Add("Text", bookInfoList, "PageCount");
    publisherTextBox.DataBindings.Add("Text", bookInfoList, "Publisher");
  }
  //дополнительный код
}

Этот код практически не отличается от кода, приведенного в предыдущем разделе, за исключением того, что привязка осуществляется не к объекту класса BookInfo, а к объекту List<BookInfo> (который создается и заполняется в методе CreateBookInfoList).

Здесь возникает проблема, так как элемент управления TextBox не умеет просматривать более одного значения. Необходимо добавить возможность навигации по списку объектов BookInfo.

Для решения этой задачи элемент управления для каждого источника данных содержит объект класса BindingManagerBase, для доступа к которому используется BindingContext. Проблема в том, что BindingManagerBase – это абстрактный базовый класс, экземпляры которого не могут быть созданы. На самом деле BindingContext содержит экземпляры одного из двух классов PropertyManager или CurrencyManager, в зависимости от типа привязки. Так, для одиночного источника данных (item data source) используется PropertyManager (рисунок 4), а для списочного источника данных (list data source) используется CurrencyManager (рисунок 5).

Класс BindingManagerBase содержит свойство Position, которое определяет текущий объект в источнике данных. Для объектов класса PropertyManager значение свойства Position всегда равно 0, т.к. он отвечает только за один объект в источнике данных.


Рисунок 4. PropertyManager и одиночный источник данных.

Для объектов CurrencyManager значение свойства Position равно индексу в источнике данных.


Рисунок 5. CurrencyManager и списочный источник данных.

Для реализации навигации по источнику данных необходимо получить объект BindingManagerBase, который соответствует этому источнику.

        public partial class SimpleBindingAndListDataSourceForm : Form
{
  public SimpleBindingAndListDataSourceForm()
  {
    InitializeComponent();
    bookInfoList = CreateBookInfoList();
    Bind();
    BindingManager.PositionChanged += 
      new EventHandler(BindingManager_PositionChanged);
    RefreshData();
  }
  //...private BindingManagerBase BindingManager
  {
    get
    {
      return BindingContext[bookInfoList];
    }
  }

  privatevoid BindingManager_PositionChanged(object sender, EventArgs e)
  {
    RefreshData();
  }

  privatevoid moveFirstButton_Click(object sender, EventArgs e)
  {
    BindingManager.Position = 0;
    RefreshData();
  }

  privatevoid movePrevButton_Click(object sender, EventArgs e)
  {
    BindingManager.Position--;
    RefreshData();
  }

  privatevoid moveNextButton_Click(object sender, EventArgs e)
  {
    BindingManager.Position++;
    RefreshData();
  }

  privatevoid moveLastButton_Click(object sender, EventArgs e)
  {
    BindingManager.Position = BindingManager.Count - 1;
    RefreshData();
  }

  privatevoid RefreshData()
  {
    int count = BindingManager.Count;
    int position = BindingManager.Position + 1;

    countLabel.Text = count.ToString();
    positionLabel.Text = position.ToString();

    moveFirstButton.Enabled = position > 1;
    movePrevButton.Enabled = position > 1;
    moveNextButton.Enabled = position < count;
    moveLastButton.Enabled = position < count;
  }

  private List<BookInfo> bookInfoList;
}

Результат запуска программы показан на рисунке 6.


Рисунок 6. Простая привязка данных к списочному источнику.

Хотя простая привязка (simple binding) отлично работает со списочными источниками данных (list data source), все же для этих целей лучше подойдут элементы управления, способные отображать более одного элемента за раз, такие как ListView или DataGridView. Для этих целей используется сложная привязка данных (complex data binding).

3.3 Сложная привязка данных

Сложная привязка (complex data binding) – это привязка списочного источника данных целиком к элементу управления, в отличие от простой привязки, в которой к элементу управления привязан только текущий элемент источника данных.

Термин «сложная привязка» не означает, что использование этого типа связывания сложнее, чем использование простого связывания. Название всего-навсего отражает тот факт, что элементы управления поддерживают дополнительную функциональность при отображении данных и работе с ними.

Наиболее простым и распространенным примером сложной привязки является использование элемента управления DataGridView.

Рассмотрим пример.

        public partial class ComplexBindingForm : Form
{
  private List<BookInfo> bookInfoList;

  public ComplexBindingForm()
  {
    InitializeComponent();
    bookInfoList = CreateBookInfoList();

    bookInfoListDataGridView.DataSource = bookInfoList;
    RefreshData();
  }
  //Код управления кнопками навигации
privatevoid addButton_Click(object sender, EventArgs e)
  {
    bookInfoList.Add(new BookInfo());
    // Выбираю вновь добавленный элементthis.BindingManager.Position = this.BindingManager.Count - 1;
  }

  privatevoid removeButton_Click(object sender, EventArgs e)
  {
    if (BindingManager.Count != 0)
    {
      bookInfoList.Remove((BookInfo)BindingManager.Current);
    }
  }
}


Рисунок 7. Привязка данных к элементу управления DataGridView.

В случае привязки данных к элементу управления DataGridView происходит следующее. DataGridView автоматически определяет публичные (public) свойства источника данных, создает соответствующие колонки, и позволяет редактировать данные непосредственно в таблице (включая добавление и удаление записей).

К сожалению, при изменении количества записей в источнике данных DataGridView автоматически не обновляется. Это связано с тем, что DataGridView никак не может узнать об изменении в источнике данных.

Для реализации автоматического обновления элементов управления при использовании простой привязки источник данных должен реализовывать определенный интерфейс (событие с именем PropertyNameChanged или интерфейс INotifyPropertyChanged). Аналогично, для автоматического обновления элемента управления при использовании сложной привязки данных, источник данных должен следовать определенным соглашениям, в частности, он должен реализовывать интерфейс IBindingList.

Интерфейс IBindingList

Краткое описание интерфейса IBindingList:

          namespace System.ComponentModel 
{
  interface IBindingList : IList, ... 
{

    // Управление редактированием спискаbool AllowEdit   { get; }
    bool AllowNew    { get; }
    bool AllowRemove { get; }

    object AddNew();

    // Оповещение об изменении спискаbool SupportsChangeNotification { get; }
    event ListChangedEventHandler ListChanged;

    // Поддержка сортировкиbool SupportsSorting { get; }
    ... // Rest of sorting members elided// Поддержка поискаbool SupportsSearching { get; }
    ... // Rest of searching members elided
  }
}

Интерфейс IBindingList предназначен для поддержки таких дополнительных операций над источником данных, как добавление, обновление, удаление элементов списка, а также добавление нового элемента с помощью функции AddNew. Кроме этого, класс, реализующий интерфейс IBindingList, может поддерживать сортировку, поиск и уведомление при изменении элемента списка. Все эти операции являются не обязательными и определяются свойствами SupportSorting, SupportSerching и SupportChangeNotification соответственно.

Если источник данных, реализующий интерфейс IBindingList, поддерживает уведомление об изменении (и сигнализирует об этом, возвращая true в свойстве SupportsChangeNotification), тогда элемент управления может подписаться на событие ListChanged и получать уведомления о добавлении, изменении и удалении элементов коллекции. Если источник данных поддерживает сортировку, то такие элементы управления, как DataGridView, могут использовать эту функциональность.

Для реализации интерфейса IBindingList вручную, помимо логики уведомления, вам придется реализовать IEnumerable, ICollection и IList, что является достаточно утомительным. К счастью, в большинстве случаев вполне подойдет использование класса BindingList<T>, реализующего интерфейс IBindingList. Вы также можете создать класс, производный от BindingList<YourClass>, и переопределить либо добавить определенную функциональность.

Класс BindingList<T>

Класс BindingList<T> – это обобщенная реализация интерфейса IBindingList. Он реализует управление списком объектов (через AllowEdit, AllowNew, AllowRemove и AddNew), уведомления при изменении коллекции (SupportChangeNotification возвращает true, генерируется событие ListChanged), а также реализует транзакционность добавления новых элементов путем реализации интерфейса ICancelAddNew.

Рассмотрим пример использование класса BindingList<T>.

          public partial class ComplexBindingForm1 : Form
{
  public ComplexBindingForm1()
  {
    InitializeComponent();
    bookInfoBindingList = CreateDataSource();
    bookInfoDataGridView.DataSource = bookInfoBindingList;
  }
  privatestatic BindingList<BookInfo> CreateDataSource()
  {
    var bookInfoList = new List<BookInfo>();

    bookInfoList.Add(new BookInfo("Том Клэнси", "Игры патриотов", 
"5-699-18175-Х", 576, "ЭКСМО"));

    bookInfoList.Add(new BookInfo("Том Клэнси", "Красный кролик", 
"5-699-15113-3", 768, "ЭКСМО"));

    bookInfoList.Add(new BookInfo("Том Клэнси", "Кремлевский кардинал", 
"5-699-15113-3", 800, "ЭКСМО"));

    returnnew BindingList<BookInfo>(bookInfoList);
  }

  privatevoid updateButton_Click(object sender, EventArgs e)
  {
    if (bookInfoBindingList.Count != 0)
      bookInfoBindingList[BindingManager.Position].PageCount++;
  }

  //Функции добавления и удаления записей
}


Рисунок 8. Сложная привязка данных с использованием BindingList<T>.

Теперь, в отличие от примера из предыдущего раздела, DataGridView нормально реагирует на добавление и удаление элементов, но никак не реагирует на нажатие реализованной мной кнопки «Update Current» (обновляющую текущий элемент списка). Для того чтобы BindingList<T> генерировал событие ListChanged при изменении объекта T, необходимо, чтобы класс T реализовывал интерфейс INotifyPropertyChanged.

Хочу напомнить, что для автоматического обновления элемента при использовании простой привязки данных необходимо выполнение одного из двух условий: реализация интерфейса INotifyPropertyChanged или реализация событий в вида PropertyNameChanged. А при использовании BindingSource<T> наличие событий вида PropertyNameChanged никак не приведет к автоматическому обновлению элемента управления.

А что, если объекты коллекции уже содержат события, уведомляющие об изменении состояния объекта, но не реализуют интерфейс INotifyPropertyChanged?

В таком случае нужно создать класс, производный от BindingList<T>, и переопределить функции InsertItem и RemoveItem.

Создадим обобщенный класс AdvancedBindingList<T>, который будет генерировать событие ListChanged в случае изменения свойства элемента коллекции, для которого реализовано событие вида PropertyNameChanged.

          public
          class AdvancedBindingList<T> : BindingList<T>
{
  protectedoverridevoid InsertItem(int index, T item)
  {
    foreach (PropertyDescriptor prDesc in TypeDescriptor.GetProperties(item))
      if (prDesc.SupportsChangeEvents)
        prDesc.AddValueChanged(item, OnItemChanged);

    base.InsertItem(index, item);
  }

  protectedoverridevoid RemoveItem(int index)
  {
    T item = Items[index];

    var propDescs = TypeDescriptor.GetProperties(item);

    foreach (PropertyDescriptor propDesc in propDescs)
      if (propDesc.SupportsChangeEvents)
        propDesc.RemoveValueChanged(item, OnItemChanged);

    base.RemoveItem(index);
  }

  void OnItemChanged(object sender, EventArgs args)
  {
    int index = Items.IndexOf((T)sender);

    OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged,
                                           index));
  }
}

Теперь предположим, что класс BookInfo реализует событие BookInfoChanged, которое срабатывает при изменении свойств объекта.

          class BookInfo
{
  publicint PageCount
  {
    get { return pageCount; }
    set
    {
      pageCount = value;
      if (BookInfoChanged != null)
        BookInfoChanged(this, EventArgs.Empty);
    }
  }
  // аналогичные изменения всех свойствpublicevent EventHandler<EventArgs> BookInfoChanged;
}

Тогда можно создать класс BookInfoBindingList следующим образом:

          class BookInfoBindingList : BindingList<BookInfo>
{
  protectedoverridevoid InsertItem(int index, BookInfo item)
  {
    item.BookInfoChanged += item_BookInfoChanged;
    base.InsertItem(index, item);
  }

  protectedoverridevoid RemoveItem(int index)
  {
    BookInfo item = Items[index];
    item.BookInfoChanged -= item_BookInfoChanged;
    base.RemoveItem(index);
  }
  
  privatevoid item_BookInfoChanged(object sender, EventArgs e)
  {
    OnItemChanged(sender, e);
  }

  privatevoid OnItemChanged(object sender, EventArgs args)
  {
    int index = Items.IndexOf((BookInfo)sender);
    OnListChanged(new ListChangedEventArgs(
       ListChangedType.ItemChanged, index));
  }
}

Реализация интерфейса ICancelAddNew классом BindingList<T> позволяет добавлять новые объекты «атомарным» способом. Интерфейс ICancelAddNew содержит два метода: EndNew и CancelNew. При вызове метода AddNew класса BindingList<T> в коллекцию добавляется новый объект . Если после этого будет вызван метод CancelNew, то этот объект сразу же будет удален. Если в процессе инициализации (после вызова AddNew и до вызова EndNew) произойдет ошибка, то откат будет выполнен автоматически. После вызова метода EndNew процесс добавления элемента в коллекцию завершается, и новый элемент может быть удален с помощью метода Remove.

Класс BindingList<T> не реализует поиск и сортировку, то есть свойства SupportSearching и SupportSorting возвращают false, а при попытке вызова методов ApplySort или Find будет сгенерировано исключение NotSupportedException.

Для реализации сортировки и поиска достаточно создать производный класс от BindingList<YourClass> и переопределить несколько методов, в то время как основная работа по управлению привязкой будет выполняться базовым классом.

Класс BindingList<T> прекрасно подходит, если нужно создать контейнер элементов с нуля. Но бывают случаи, когда контейнер уже есть, но нужно добавить двустороннюю связь между контейнером и элементом управления. Для решения этой задачи (а также многих других) предназначен компонент BindingSource.

Компонент BindingSource

Компонент BindingSource является универсальным связующим звеном в Windows Forms. Он существенно облегчает задачу привязки данных. В частности, он может значительно упростить создание двусторонней привязки данных к коллекции объектов, отличной от BindingList<T>.

Рассмотрим применение компонента BindingSource для работы со списком объектов BookInfo.

          public partial class ComplexBindingForm4 : Form
{
  public ComplexBindingForm4()
  {
    InitializeComponent();
    bookInfoList = CreateBookInfoList();
    bookInfoBindingSource.DataSource = bookInfoList;
    this.bookInfoDataGridView.DataSource = bookInfoBindingSource;
    
  }
  private List<BookInfo> CreateBookInfoList()
  {
    var list = new List<BookInfo>();

    list.Add(new BookInfo("Том Клэнси", "Игры патриотов", "5-699-18175-Х",
                          707, "ЭКСМО"));
    list.Add(new BookInfo("Том Клэнси", "Красный кролик", "5-699-18175-Х",
                          810, "ЭКСМО"));
    list.Add(new BookInfo("Том Клэнси", "Кремлевский кардинал", 
                          "5-699-18175-Х", 841, "ЭКСМО"));
    return list;
  }

  privatevoid addButton_Click(object sender, EventArgs e)
  {
    // DataGridView автоматически обновит свое содержимое
    bookInfoBindingSource.Add(new BookInfo());
  }

  privatevoid deleteButton_Click(object sender, EventArgs e)
  {
    //DataGridView автоматически обновит свое содержимое
    bookInfoBindingSource.RemoveCurrent();
  }

  private BindingSource bookInfoBindingSource = new BindingSource();
  private List<BookInfo> bookInfoList;
}

Поскольку компонент BindingSource построен на основе BindingList<T>, для него справедливо все, о чем было сказано в предыдущем разделе. Единственное, что нужно помнить при работе с BindingSource: операции добавления/удаления элементов коллекции должны осуществляться через BindingSource, а не напрямую.

Компонент BindingSource можно рассматривать как типизированную коллекцию элементов, при этом тип источника данных может быть задан различными способами.

BookInfo bookInfo = new BookInfo();
List<BookInfo> bookInfoList = new List<BookInfo>();
//...
bindingSource.Add(bookInfo);                 //1
bindingSource.DataSource = bookInfo;         //2
bindingSource.DataSource = typeof(BookInfo); //3
bindingSource.DataSource = bookInfoList;     //4

После того как компонент BindingSource определил тип хранимых объектов, он будет обеспечивать типобезопасность (метод Add класса BindingSource принимает object). При попытке добавить объект несоответствующего типа будет сгенерировано исключение InvlidOperationException.

Компонент BindingSource достаточно сложен и предназначен для решения различных задач. Он может применяться как для простой, так и для сложной привязки данных, в качестве одиночного или списочного источника данных, поддерживает фильтрацию, уведомления при изменении списка объектов, транзакционное добавление новых элементов, расширенную поддержку во время разработки и многое другое. Для более подробной информации по этому поводу обращайтесь к [1].

Выводы

Привязка данных является важной частью разработки приложений Windows Forms. От правильного понимания принципов, на которых она построена, может зависеть, сколько усилий вам потребуется для реализации функциональности вашего приложения, и сколько придется потратить времени на анализ кода, который сгенерировал за вас дизайнер Visual Studio.

В данной статье я рассмотрел основные принципы и наиболее важные строительные блоки, из которых построена привязка данных в Windows Forms. Естественно, этот материал нельзя рассматривать как исчерпывающий, есть темы, которые затронуты вскользь (компонент BindingSource является очень сложным, и здесь он рассмотрен в очень ограниченном контексте), есть вопросы, которых я сознательно не касался (связанные с форматированием и парсингом данных, поддержкой фильтрации, сортировки и поиска и т.д.). По многим из этих вопросов стоит обратиться к [1], вероятно, наиболее полному источнику данных по этой теме.

Источники

  1. Brian Noyes. Data Binding with Windows Forms 2.0: Programming Smart Client Data Applications with .NET. AW. 2006
  2. Chris Sells, Michael Weinhardt. Windows Forms 2.0 Programming. AW. 2006
  3. Matthew MacDonald. Pro .Net 2.0 Windows Forms and Custom Controls. Apress. 2005
  4. Чарльз Петцольд. Программирование с использованием Microsoft Windows Forms. Издательский дом «Питер», 2006
  5. MSDN

Эта статья опубликована в журнале RSDN Magazine #3-2008. Информацию о журнале можно найти здесь
    Сообщений 24    Оценка 510 [+2/-0]         Оценить