Использование Generic::List в BindingSource
Использую привязку данных с помощью DataGridView и BindingSource. Для BindingSource в качестве DataSource использую Generic::List. Пролема в том, что при запуске приложения получаю вот такое исключение: «System.IndexOutOfRangeException: «Индексу -1 не присвоено значение.» (на фото прикрепленном подробности). Понять не могу куда копать. Список этот точно не нулевой. При этом если вместо Generic::List использовать array, то все норм работает.
Ниже приведу внутренности формы и описание класса источника данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
void InitializeComponent(void) { this->dgvSystemStatus = (gcnew System::Windows::Forms::DataGridView()); this->bsSystemStatus = (gcnew System::Windows::Forms::BindingSource(this->components)); // // dgvSystemStatus // this->dgvSystemStatus->AllowUserToAddRows = false; this->dgvSystemStatus->AllowUserToDeleteRows = false; this->dgvSystemStatus->BackgroundColor = System::Drawing::SystemColors::ControlLightLight; this->dgvSystemStatus->ColumnHeadersHeightSizeMode = System::Windows::Forms::DataGridViewColumnHeadersHeightSizeMode::AutoSize; this->dgvSystemStatus->Location = System::Drawing::Point(22, 181); this->dgvSystemStatus->Name = L"dgvSystemStatus"; this->dgvSystemStatus->ReadOnly = true; this->dgvSystemStatus->Size = System::Drawing::Size(339, 327); this->dgvSystemStatus->TabIndex = 0; } private: Generic::ListWrapperSystemStatus^>^ systemStatus = gcnew Generic::ListWrapperSystemStatus^>(); private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) { /* Initialise bindings */ bsSystemStatus->DataSource = systemStatus; systemStatus->Add( gcnew WrapperSystemStatus("Closed", &ds_ProtStatus.Closed)); dgvSystemStatus->DataSource = bsSystemStatus; } ref class WrapperSystemStatus { public: WrapperSystemStatus (String^ name, UInt16* pointerValue) { _name = name; _pointerValue = pointerValue; } ~WrapperSystemStatus () { this->!WrapperSystemStatus(); } /** * \brief Аналог финализатора. Предназначен для уничтожения unmanaged ресурсов */ !WrapperSystemStatus() { } property UInt16 Value { UInt16 get() { return *_pointerValue; } void set(UInt16 value) { *_pointerValue = value; } } property String^ Name { String^ get() { return _name; } void set(String^ name) { _name = name; } } private: UInt16 * _pointerValue; String^ _name; };
Ткните носом в чем может быть проблема?
Привязка данных в Windows Forms
В приложении, работающем с данными, существует логическая взаимосвязь между уровнем данных и представлением, хотим мы этого или нет. Вопрос в том, сколько рутинной работы ложится на наши плечи. Механизм привязки данных в 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 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( "Не удалось найти свойство элемента управления с именем ", controlPropertyName), "controlPropertyName"); if (controlPropertyDescriptor.SupportsChangeEvents) controlPropertyDescriptor.AddValueChanged( control, ControlPropertyChanged); // Получаем экземпляр класса PropertyDescriptor // для управления свойством источника данных dataSourcePropertyDescriptor = TypeDescriptor.GetProperties(dataSource).Find( dataSourcePropertyName, true); if (dataSourcePropertyDescriptor == null) thrownew ArgumentException( String.Format(" Не удалось найти свойство источника данных с именем ", 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 делает следующее. Осуществляется поиск события с именем PropertyName Changed , если класс реализует это событие, то ReflectPropertyManager подписывается именно на него. В противном случае ReflectPropertyManager пробует найти событие с именем PropertyChanged, объявленное в интерфейсе INotifyPropertyChanged и преобразует его к типу PropertyChangedEventHandler.
Таким образом, для того, чтобы объект PropertyDescriptor мог уведомить об изменении свойства объекта, нужно, чтобы объект отвечал одному из следующих критериев:
- Объект реализует событие PropertyName Changed и генерирует его при изменении этого свойства. Причем обработчик этого события должен обязательно иметь тип EventHandler. В случае любого другого типа (включая EventHandler) при попытке подписаться на изменение этого свойства будет сгенерировано исключение.
- Объект реализует интерфейс 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. Механизм обновления элемента управления.
Основным условием возможности обновления элемента управления при изменении источника данных является поддержка определенного набора событий со стороны источника данных. Источник данных должен содержать события 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 выглядит более предпочтительным.
Рассмотрим следующий пример.
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 (который создается и заполняется в методе 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 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 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, реализующего интерфейс IBindingList. Вы также можете создать класс, производный от BindingList, и переопределить либо добавить определенную функциональность.
Класс BindingList
Класс BindingList – это обобщенная реализация интерфейса IBindingList. Он реализует управление списком объектов (через AllowEdit, AllowNew, AllowRemove и AddNew), уведомления при изменении коллекции (SupportChangeNotification возвращает true, генерируется событие ListChanged), а также реализует транзакционность добавления новых элементов путем реализации интерфейса ICancelAddNew.
Рассмотрим пример использование класса BindingList.
public partial class ComplexBindingForm1 : Form < public ComplexBindingForm1() < InitializeComponent(); bookInfoBindingList = CreateDataSource(); bookInfoDataGridView.DataSource = bookInfoBindingList; >privatestatic BindingList CreateDataSource() < var bookInfoList = new List(); 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(bookInfoList); > privatevoid updateButton_Click(object sender, EventArgs e) < if (bookInfoBindingList.Count != 0) bookInfoBindingList[BindingManager.Position].PageCount++; > //Функции добавления и удаления записей >
Рисунок 8. Сложная привязка данных с использованием BindingList.
Теперь, в отличие от примера из предыдущего раздела, DataGridView нормально реагирует на добавление и удаление элементов, но никак не реагирует на нажатие реализованной мной кнопки «Update Current» (обновляющую текущий элемент списка). Для того чтобы BindingList генерировал событие ListChanged при изменении объекта T, необходимо, чтобы класс T реализовывал интерфейс INotifyPropertyChanged.
Хочу напомнить, что для автоматического обновления элемента при использовании простой привязки данных необходимо выполнение одного из двух условий: реализация интерфейса INotifyPropertyChanged или реализация событий в вида PropertyNameChanged. А при использовании BindingSource наличие событий вида PropertyName Changed никак не приведет к автоматическому обновлению элемента управления.
А что, если объекты коллекции уже содержат события, уведомляющие об изменении состояния объекта, но не реализуют интерфейс INotifyPropertyChanged?
В таком случае нужно создать класс, производный от BindingList, и переопределить функции InsertItem и RemoveItem.
Создадим обобщенный класс AdvancedBindingList, который будет генерировать событие ListChanged в случае изменения свойства элемента коллекции, для которого реализовано событие вида PropertyName Changed .
public class AdvancedBindingList : BindingList < 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 BookInfoChanged; >
Тогда можно создать класс BookInfoBindingList следующим образом:
class BookInfoBindingList : BindingList < 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 позволяет добавлять новые объекты «атомарным» способом. Интерфейс 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.
public partial class ComplexBindingForm4 : Form < public ComplexBindingForm4() < InitializeComponent(); bookInfoList = CreateBookInfoList(); bookInfoBindingSource.DataSource = bookInfoList; this.bookInfoDataGridView.DataSource = bookInfoBindingSource; > private List CreateBookInfoList() < var list = new List(); 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 bookInfoList; >
Поскольку компонент BindingSource построен на основе BindingList, для него справедливо все, о чем было сказано в предыдущем разделе. Единственное, что нужно помнить при работе с BindingSource: операции добавления/удаления элементов коллекции должны осуществляться через BindingSource, а не напрямую.
Компонент BindingSource можно рассматривать как типизированную коллекцию элементов, при этом тип источника данных может быть задан различными способами.
BookInfo bookInfo = new BookInfo(); List bookInfoList = new List(); //. 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], вероятно, наиболее полному источнику данных по этой теме.
Источники
- Brian Noyes. Data Binding with Windows Forms 2.0: Programming Smart Client Data Applications with .NET. AW. 2006
- Chris Sells, Michael Weinhardt. Windows Forms 2.0 Programming. AW. 2006
- Matthew MacDonald. Pro .Net 2.0 Windows Forms and Custom Controls. Apress. 2005
- Чарльз Петцольд. Программирование с использованием Microsoft Windows Forms. Издательский дом «Питер», 2006
- MSDN
Эта статья опубликована в журнале RSDN Magazine #3-2008. Информацию о журнале можно найти здесь
Что такое BindingSource
Не могу понять, что такое BindingSource . Через адаптеры мы можем манипулировать данные. Тогда зачем нам BindingSource ?
Отслеживать
14k 13 13 золотых знаков 38 38 серебряных знаков 63 63 бронзовых знака
задан 19 дек 2017 в 20:29
user265018 user265018
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Основная задача компонента BindingSource — обеспечить унифицированный доступ к источнику данных. Но он также помогает использовать несколько возможностей:
- В случае привязки DataTable к BindingSource появляется возможность сортировки, фильтрации и поиска.
- BindingSource может являться источником данных не только для элементов управления, но и для самого BindingSource.
- Вы можете работать с одним источником данных, привязанным ко многим элементам управления.
Справочная информация по вопросу: Компонент BindingSource.
Связывание элементов управления Windows Forms с компонентом BindingSource с помощью конструктора
После добавления элементов управления на форму и определения пользовательского интерфейса приложения можно связать элементы управления с источниками данных, чтобы при выполнении приложения пользователи могли изменять и сохранять данные.
Для привязки одного или нескольких элементов управления в Windows Forms проще всего использовать элемент управления BindingSource в качестве связующего средства между элементами управления в форме и источником данных.
Один или несколько элементов управления в форме могут быть привязаны к данным. В следующей процедуре элемент управления TextBox привязан к источнику данных.
Для привязки элемента управления при разработке выполните следующие действия.
- Перетащите элемент управления TextBox в форму.
- В окне Свойства:
- Разверните узел (DataBindings).
- Щелкните стрелку рядом со свойством Text.
Откроется редактор Источник данных.

Если источник данных ранее был настроен для проекта или формы, он будет показан.

- Щелкните Добавить источник данных проекта для подключения к данным и создания источника данных.
- На странице Выберите тип источника данных выберите База данных.
- На странице Выберите подключение к данным выберите подключение в списке доступных подключений. Если нужное подключения к базе данных недоступно, выберите Новое подключение для создания нового подключения к базе данных.
- Выберите Да, сохранить подключение для сохранения строки подключения в файле конфигурации приложения.
- Выберите объекты базы данных для использования в приложении. В данном случае выберите поле в таблице, где следует отображать TextBox.
- При необходимости замените имя набора данных по умолчанию.
- Нажмите кнопку Готово.
- В окне Свойства снова щелкните стрелку рядом со свойством Text. В редакторе источника данных выберите имя поля для привязки TextBox.
Редактор источника данных закроется, а набор данных BindingSource и адаптер таблицы для этого подключения к данным будут добавлены в форму.
Переход между строками с помощью элемента управления BindingNavigator
Элемент управления BindingNavigator является элементом управления ToolStrip с кнопками, предварительно настроенными для перехода к первой, последней, следующей и предыдущей записям набора данных, а также для добавления и удаления записей. Добавление кнопок к элементу управления BindingNavigator — достаточно простая процедура, так как это является элементом управления ToolStrip.
Каждой кнопке элемента управления BindingNavigator соответствует член компонента BindingSource, обеспечивающий ту же функциональность программным путем. Например, кнопка MoveFirstItem соответствует методу MoveFirst компонента BindingSource, кнопка DeleteItem соответствует методу RemoveCurrent и т.д.
Изменение положения навигатора
По умолчанию навигатор располагается вдоль верхней границы формы. Если таблиц на форме несколько, то расположение всех навигаторов в верхней части формы неудобно для работы пользователя.
Перенести навигатор в другое место можно несколькими способами.
Чтобы перенести навигатор выполните одно из следующих действий:
- В панели задач навигатора выберите в свойстве Dock из раскрывающегося списка None или щелкните по нижней полоске эскиза формы со словом None.
- В окне Свойства для навигатора измените значение свойства Dock на значение None.