System windows forms bindingsource

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


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


Автор: Сергей Тепляков
ООО НПП Кронос
Источник: RSDN Magazine #3-2008

Опубликовано: 28.12.2008
Исправлено: 10.12.2016
Версия текста: 1.0

Введение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

Основным условием возможности обновления элемента управления при изменении источника данных является поддержка определенного набора событий со стороны источника данных. Источник данных должен содержать события PropertyName Changed для каждого свойства, либо реализовывать интерфейс 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 выглядит более предпочтительным.

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

Этот код практически не отличается от кода, приведенного в предыдущем разделе, за исключением того, что привязка осуществляется не к объекту класса BookInfo, а к объекту List (который создается и заполняется в методе 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, который соответствует этому источнику.

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

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

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

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

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

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

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

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

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

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

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

Интерфейс IBindingList

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

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

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

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

Класс BindingList

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Компонент BindingSource

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

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

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

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

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

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

Выводы

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

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

Читайте также:  Гибридная графика amd linux
Оцените статью