Практикум прикладного программирования на C# в среде VS.NET 2008

Разработка приложений ADO.NET для OLE DB

Разбить на страницы
Показывать лекцию целиком

Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.

В этой работе мы последовательно познакомимся с технологией ADO.NET (ActiveX Data Objects) управления базами данных типа OLE DB (Object Linking and Embedding Database), к которым, в частности, относятся и базы файлового типа Microsoft Access с расширением .mdb. Наш выбор основан на том, что не требуется устанавливать дополнительные программы, типа SQL Server, для такого рода баз. Пока мы будем пользоваться готовой учебной базой данных Northwind.mdb, расположенной в прилагаемом к работе каталоге Source. Позднее мы научимся создавать свои базы данных либо с помощью СУБД Microsoft Office Access, либо с помощью COM объектов в оболочке Visual Studio 2008.

Создание базы данных начинается с ее проектирования с помощью диаграмм. Затем создается сам файл (или несколько взаимосвязанных файлов) БД и устанавливается структура БД, определяющая таблицы, поля и их типы, назначение ключей и индексов. Затем проектируется приложение, управляющее данными БД. Мы займемся разработкой приложения, решающего типовые задачи по редактированию и представлению данных БД.

Базы данных называют источниками данных. Microsoft ADO.NET - это часть библиотеки .NET Framework, позволяющая создавать приложения для управления данными в структурированных файловых или серверных хранилищах. Библиотечные средства ADO.NET обеспечивают подключение к источникам данных, выполнение команд SQL, обработку и выборку данных.

ADO.NET представлено набором программных средств, прежде всего классов, которые находятся в библиотечной сборке System.Data.dll. Применительно к формату OLE DB наиболее важными из них являются специализированные классы пространства имен System.Data.OleDb:

  • OleDbConnection - обоспечивает подключение к источнику данных
  • OleDbDataAdapter - обеспечивает связь между источником данных и набором данных в оперативной памяти
  • OleDbCommand - объект команд SQL для работы в источнике данных
  • В совокупности экземпляры этих классов в приложении называются поставщиком данных (Data Provider). Далее идет независимый от формата БД слой для хранения данных в кэше оперативной памяти и просмотра данных из пространства имен System.Data:

  • DataSet - набор кэшированных данных, выборочно загруженных из источника
  • DataView - позволяет создать несколько представлений одной и той же таблицы
  • Упражнение 1. Связывание данных с элементами управления ListBox и TextBox

    Начнем с подготовительных действий и созданию связанного с данными списка, дающего возможность только просматривать данные БД.

  • Создайте новый проект типа Windows Forms Application с именем FormsADO и решением ADO
  • Замечание. Место расположения проекта в файловой системе компьютера выбирайте таким, чтобы путь к нему не содержал пробелов и кириллицы. Иначе могут возникнуть проблемы с работой некоторых мастеров Visual Studio 2008!
  • В палитре компонентов Toolbox откройте вкладку дата и перетащите на форму компонент OleDbDataAdapter. Если в настройках проекта такого компонента не окажется, то щелкните на вкладке Data палитры Toolbox правой кнопкой мыши и подключите искомый компонент командой Choose Items
  • В появившемся окне мастера настройки поставщика щелкните по кнопке New Connection и выберите файл подключаемой БД Northwind.mdb из прилагаемого к работе каталога Source
  • После щелчка на кнопке Next оболочка выдаст сообщение с предложением скопировать присоединенный файл БД в каталог проекта

  • Щелкните на кнопке " Да " и перейдите к следующей вкладке мастера
  • Здесь в текстовом поле предлагается указать SQL -запрос, который поставщик данных должен выполнять по умолчанию при заполнении набора данных DataSet. Мы хотим прочитать содержимое двух столбцов CustomerID и CompanyName из таблицы Customers, поэтому либо руками, либо с помощью мастера нужно в многострочном текстовом поле интерфейса мастера заполнить команду:

  • Щелкните на кнопке Query Builder, в диалоговом окне Add Table выберите из списка таблицу Customers и по щелчку на кнопке Add вкладки Tables (потом на Close ) отметьте в интерфейсе флажки против нужных полей таблицы, чтобы мастер написал для них текст запроса
  • Щелкните на кнопке OK окна Query Builder и текст запроса будет помещен в поле мастера
  • По умолчанию мастер готов создать операторы SQL не только для выборки, но и для вставки, обновления и удаления данных.

  • Нам на данном этапе потребуется только чтение данных и другие возможности нужно отключить щелчком на кнопке Advanced Options, сбросив ненужные флажки
  • Дойдите до последней вкладки мастера настройки поставщика и убедитесь, что включен только режим Select (но не Update, Insert, Delete)
  • Щелкните на кнопке Finish, чтобы завершить создание и настройку объектов поставщика данных (экземпляров OleDbDataAdapter и OleDbConnection ) и сохранить параметры в установках проекта
  • В панели Solution Explorer откройте файл Form1.Designer.cs и убедитесь, что мастер создал необходимый код с настройками поставщика, в состав которого входят экземпляры классов OleDbCommand, OleDbDataAdapter и OleDbConnection
  • Описанную процедуру можно в любом месте прервать щелчком на кнопке Cancel мастера настроек и вернуться к ней в любое время. Для этого достаточно щелкнуть в нижней части рабочей области на экземпляре класса OleDbDataAdapter правой кнопкой мыши и выполнитиь команду Configure Data Adapter (или выделить объект поставщика и выполнить ту же команду через пункт Data главного меню оболочки)

  • Выделите в проекте файл БД и через панель Properties установите его свойство Copy to Output Directory в значение Copy if newer, чтобы он находился вместе с исполняемой сборкой в одном каталоге bin проекта
  • Итак, у нас есть объект соединения с БД и объект-посредник между базой и набором данных (кэшем оперативной памяти), который должен выполнять команды SQL и управлять потоками данных. Теперь нужно создать сам набор данных как экземпляр класса DataSet.

  • Щелкните правой кнопкой мыши на экземпляре oleDbDataAdapter1 в нижней части рабочей области и выполните команду Generate DataSet (то же самое можно выполнить через меню Data оболочки)
  • В появившемся окне мастера Generate DataSet щелкните на кнопке OK
  • Будет создан объект набора данных dataSet11, который будет загружать два столбца таблицы Customers согласно SQL-запроса, указанного в настройках поставщика oleDbDataAdapter1.

    В качестве элемента, отображающего набор данных объекта dataSet11, применим список ListBox.

  • Поместите на форму из вкладки Common Controls палитры компонентов экземпляр компонента ListBox и настройте его в соответствии с таблицей
  • Свойство Значение
    ( Name ) listBox
    DataSource dataSet11
    DisplayMember Customers.CompanyName
    Dock Fill

    Хотя набор данных dataSet11 правильно связан со свойствами элемента управления ListBox, но если выполнить приложение, то список останется пустым, потому, что набор данных не был заполнен данными. Код заполнения поместим в обработчик события Load формы.

  • В панели Properties выделите форму из раскрывающегося списка (или щелкните на заголовке формы) и в режиме Events создайте обработчик события Load, который заполните так
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        }
    }
  • Запустите приложение и вы увидите данные поля Customers.CompanyName, связанного со свойством DisplayMember списка ListBox, подключенного к набору данных dataSet11 через свойство DataSource
  • Фильтрация данных в связанном списке

    Метод Fill() поставщика данных загружает все данные указанных столбцов таблицы в набор данных. Часто не все данные нужны пользователю, к тому же их может быть очень много, что перегружает ресурсы компьютера. В этом разделе будет показано, как создать параметризованный оператор SQL для заполнения набора данных не всеми строками таблицы БД, а пока только одной. Мы продолжим развитие нашего проекта, который ограничивает выборку данных из источника определенным условием, а заодно и установим строку подключения к БД вручную.

    Если выделить объект строки подключения и посмотреть свойство ConnectionString, то можно убедиться, что оно имеет следующее строковое значение:

    Provider=Microsoft.Jet.OLEDB.4.0;
    Data Source=|DataDirectory|\Northwind.mdb;Persist Security Info=True

    Переопределим его программно на другое значение.

  • Вставьте в класс Form1 следующий код
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            String ConnectionString(String fileNameNotExt)
            {
                string JetEngineString = @"Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=";
                string pathToFile = Application.StartupPath.ToString() + "\\Data\\";
                return JetEngineString + pathToFile + fileNameNotExt.Trim() + ".mdb";
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        }
    }
  • В панели Solution Explorer для узла проекта FormsADO вызовите контекстное меню, командой Add/New Folder добавьте каталог Data и курсором перенесите в него БД Northwind.mdb
  • Выделите файл БД и через панель Properties убедитесь, что его свойство Copy to Output Directory установлено в значение Copy if newer
  • Запустите приложение - программа FormsADO.exe работает нормально
  • Теперь установим фильтр на заполнение данными набора данных DataSet через команду SQL -запроса. Если открыть файл Form1.Designer.cs, то мы увидим примерно следующий код в методе InitializeComponent() формы

    private void InitializeComponent()
            {
                this.oleDbSelectCommand1 = new System.Data.OleDb.OleDbCommand();
                this.oleDbConnection1 = new System.Data.OleDb.OleDbConnection();
                this.oleDbDataAdapter1 = new System.Data.OleDb.OleDbDataAdapter();
                this.dataSet11 = new FormsADO.DataSet1();
                this.listBox = new System.Windows.Forms.ListBox();
                ((System.ComponentModel.ISupportInitialize)(this.dataSet11)).BeginInit();
                this.SuspendLayout();
                // 
                // oleDbSelectCommand1
                // 
                this.oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName\r\nFROM  Customers";
                this.oleDbSelectCommand1.Connection = this.oleDbConnection1;
                // 
                // oleDbConnection1
                // 
                this.oleDbConnection1.ConnectionString = 
                  "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\\Northwind.mdb;Persis" +
                  "t Security Info=True";
                // 
                // oleDbDataAdapter1
                // 
                this.oleDbDataAdapter1.SelectCommand = this.oleDbSelectCommand1;
                this.oleDbDataAdapter1.TableMappings.AddRange(new System.Data.Common.DataTableMapping[] {
                new System.Data.Common.DataTableMapping("Table", "Customers", new System.Data.Common.DataColumnMapping[] {
                            new System.Data.Common.DataColumnMapping("CustomerID", "CustomerID"),
                            new System.Data.Common.DataColumnMapping("CompanyName", "CompanyName")})});
                
                ...........................................
            }
            ...............................................
        
            private System.Data.OleDb.OleDbCommand oleDbSelectCommand1;

    Обратите внимание на выделенные строки в приведенном листинге. Объявляется поле-ссылка на объект команд oleDbSelectCommand1, затем создается сам объект, его свойству CommandText присваивается SQL-запрос и, наконец, объект команд подключается к одноименному свойству объекта адаптера данных. Теперь метод Fill() адаптера данных будет выбирать из БД нужные данные и передавать их набору данных DataSet. Если в коде события Load формы поменять команду в свойстве CommandText, то она и будет выполняться адаптером данных.

    Таким образом, изменим для адаптера команду выборки данных из БД и добавим к форме необходимый для этого пользовательский интерфейс.

  • Добавьте в обработчик события Load формы код перенастройки объекта oleDbSelectCommand1
  • private void Form1_Load(object sender, EventArgs e)
            {
                // Настраиваем объект команд 
                oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')";
                oleDbSelectCommand1.Parameters.Add(new System.Data.OleDb.OleDbParameter());
                oleDbSelectCommand1.Parameters[0].Value = "A";
        
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }

    В данном коде пояснения требует только SQL-команда, написанная на диалекте SQL для формата файловых баз данных, принятого в Microsoft Office Access 2003

    SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')
  • \r\n - управляющие символы "возврат каретки - перевод строки"
  • WHERE условие - условие отбора данных из источника
  • LIKE шаблон - проверка на соответствие заданному шаблону, в котором могут использоваться специальные символы, означающие подстановки значений параметров команды. В качестве шаблона поиска могут быть использованы два подстановочных знака
  • ?  - одиночный символ в текущей позиции
  • % - любое количество символов в текущей позиции
  • В нашем случае выражение после ключевого слова LIKE означает в своей совокупности, что поиск данных в источнике будет проведен по любому значению, которое введет пользователь в текстовое поле формы. Следуеть помнить, что если в команде присутствует LIKE   шаблон, то даже если для поля поиска существует индекс, то он не работает, а проверка условия выполняется последовательным перебором всех записей таблицы от начала до конца. Это очень замедляет выборку данных на таблицах с большим количеством записей.

  • Измените свойство Dock экземпляра класса ListBox с Fill на Bottom и освободите место сверху клиентской области формы
  • Поместите на форму, как ее дочерние элементы, экземпляры компонентов Label, TextBox, Button в соответствии со снимком и настройте их как показано в таблице свойств ниже
  • Тип Свойство Значение
    ListBox Dock Bottom
    Label Text Customer
    TextBox (Name) txtCustLimit
      Text A (латинское)
    Button (Name) btnLoadList
      Text Load
  • Создайте обработчик события Click кнопки btnLoadList и заполните его кодом переназначения содержимого параметра SQL-запроса
  • private void btnLoadList_Click(object sender, EventArgs e)
            {
                NewLoad();
            }
        
            void NewLoad()
            {
                // Извлекаем значение текстового поля без пробелов
                String text = txtCustLimit.Text.Trim();
                txtCustLimit.Text = text; // Сохраняем без пробелов для удобства
                oleDbSelectCommand1.Parameters[0].Value = text;// Устанавливаем значение параметра
        
                // Очищаем набор данных
                dataSet11.Clear();
        
                // Заполняем набор данных командой с новым значением параметра
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
  • Создайте обработчик события KeyDown для текстового поля txtCustLimit с тем, чтобы набор данных обновлялся и по нажатию пользователем клавиши Enter при завершении ввода очередного значения параметра, и заполните его следующим кодом
  • private void txtCustLimit_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                    NewLoad();
            }
  • Запустите приложение и поэкспериментируйте с его функциональностью, заложенной нами
  • Применение нескольких поставщиков и нескольких наборов данных

    В данном разделе мы продолжим развитие проекта и добавим еще по одному дополнительному экземпляру классов OleDbDataAdapter и DataSet. Для них мы сформируем SQL-команду, которая будет выбирать из источника одну запись таблицы Customers (клиенты), соответствующую выделенному в списке ListBox конкретному клиенту по его идентификатору. Значения полей этой записи будем отображать в текстовых полях формы, маркерованных именами столбцов таблицы. Структура таблицы Customers файловой БД Northwind.mdb, открытая в Microsoft Office Access 2003, приведена на снимке

    Добавление и настройка поставщика данных

  • Перейдите в режим View Designer и поместите на форму еще один компонент OleDbDataAdapter с автоматически сгенерированным именем oleDbDataAdapter2
  • В мастере настроек адаптера Data Adapter Configuration Wizard перейдите на вкладку Generate the SQL statements, щелкните на кнопке Advanced Options и сбросьте все флажки вкладки Advanced Options, поскольку поставщик будет только читать данные
  • Щелкните на кнопке Query Builder построителя SQL-запроса, в появившемся окне выберите таблицу Customers и щелкните один раз на кнопке Add, а затем - Close
  • В появившемся списке полей таблицы Customers отметьте флажки против каждого поля, из которых должен читать данные БД настраиваемый адаптер данных
  • В поле Filter строки CustomerID второй секции построителя команд введите =?, после чего в третьей секции будет сгенерирована команда
  • SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, 
    City, Region, PostalCode, Country, Phone, Fax
    FROM  Customers
    WHERE (CustomerID = ?)

    показанная на снимке

  • Завершите построение запроса кнопкой OK и указанная команда окажется на текущей вкладке мастера настройки поставщика данных, который по ней будет заполнять будущий набор данных класса DataSet
  • Дойдите до последней вкладки мастера настройки поставщика и убедитесь, что включен только режим Select (но не Update, Insert, Delete )
  • Щелчком на кнопке Finish завершите настройку поставщика данных и обратите внимание, что в нижней части появился объект строки соединения для текущего адаптера
  • Теперь нужно создать связанный с поставщиком объект набора данных DataSet, который будет хранить данные таблицы Customers. Этот набор данных должен быть согласован по структуре с настройками поставщика, поэтому создадим его с помощью мастера. В панели Solution Explorer обратите внимание, что пока в приложении присутствует только один узел DataSet1.xsd, который был создан мастером на предыдущих этапах выполнения работы и который описывает класс набора данных, соответствующий хранению значений только двух полей таблицы Customers. Сейчас создадим еще один такой узел.

  • Щелкните правой кнопкой мыши на объекте oleDbDataAdapter2, выполните команду Generate DataSet контекстного меню (или в Data главного меню проекта) и настройте одноименное окно мастера как показано на снимке
  • После щелчка на кнопке OK будет создан объект набора данных, согласованный с объектом oleDbDataAdapter2 второго поставщика данных, в обязанность которого входит поставка данных в соответствии со строкой запроса.

    Изменение пользовательского интерфейса формы

    Сейчас нужно изменить пользовательский интерфейс формы, разместив в клиентской области справа от списка элементы Label и TextBox с целью просмотра значений полей таблицы Customers для выделенной в списке записи.

  • Измените свойство Dock списка на значение None и оформите интерфейс формы в соответствии с рисунком
  • На форму было помещено 11 элементов Label и 11 элементов TextBox. У текстовых меток Label в свойство текст были скопированы названия полей таблицы Customers из SQL-команды Select, приведенной выше. Свойство AutoSize элементов Label было установлено в false, а свойство TextAlign - в значение MiddleRight. После выравнивания элементов на форме с помощью панели инструментов Layout они были выделены и зафиксированы командой Format/Lock Controls, чтобы случайно не нарушить пользовательский интерфейс формы. Такого же самого эффекта можно достигнуть, если установить свойство Locked всех элементов в значение true.

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

  • Выделите последовательно все добавленные элементы TextBox и присвойте им значение свойства (Name) равное именам столбцов таблицы Customers с префиксом txt, например, первый элемент будет txtCustomerID, второй - txtCompanyName, и т.д.
  • Связывание элементов представления с набором данных

    Обратите внимание, что при выполнении предыдущих разделов мы заполняли набор данных из двух полей таблицы Customers в соответствии с командой Select поставщика oleDbDataAdapter1: поля CustomerID и CompanyName, а использовали только данные поля CompanyName, настроив свойство DisplayMember объекта списка listBox согласно приведенной ранее таблице

    Свойство Значение
    (Name) listBox
    DataSource dataSet11
    DisplayMember Customers.CompanyName

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

  • Выделите на форме объект listBox и установите его свойство ValueMember в значение Customers.CustomerID через раскрывающийся список
  • Выделите объект listBox и через панель Properties в режиме Events создайте для события SelectedIndexChanged обработчик, который заполните так
  • private void listBox_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Очистить второй набор данных
                dataSet21.Clear();
                // Если выбран элемент списка, заполнить второй набор
                if (listBox.SelectedIndex != -1)
                {
                    oleDbSelectCommand2.Parameters[0].Value = listBox.SelectedValue;
                    oleDbDataAdapter2.Fill(dataSet21);
                }
            }
  • Запустите приложение - вы увидите пустые значения текстовых полей
  • Это происходит потому, что хоть второй набор данных и заполняется в обработчике записью, соответствующей переданному в качестве параметра SQL-команды значению поля CustomerID, но эти данные никак не связаны с элементами текстовых полей. Исправим это:

  • Выделите на форме элемент txtCustomerID текстового поля, в панели Properties раскройте его свойство (DataBindings).Text и присоедините к нему поле CustomerID из второго набора данных, как показано на снимке
  • Проделайте то же самое для остальных элементов TextBox формы, связывая их с соответствующими полями второго набора данных
  • Запустите приложение - теперь все в порядке, для выбранной в списке строки в элементах TextBox отображаются значения всех полей текущей записи таблицы Customers
  • Здесь есть одна несогласованность, которую желательно сразу же устранить. Дело в том, что текстовые поля мы используем только для просмотра данных, а они еще и допускают видимость редактирования, которая на данном этапе никак не отразится на самих данных в БД. Поэтому, чтобы не вводить пользователя в заблуждение, редактирование в них нужно запретить. Это можно сделать установкой их свойства ReadOnly в значение true, но тогда они станут "тускло некрасивыми". Но есть другой способ их 'стерилизовать':

  • Поместите на форму над текстовыми полями компонент CheckBox и настройте его так
  • Свойство Значение
    (Name) checkEdit
    CheckAlign MiddleRight
    CheckState Unchecked
    Text Edit
  • Выделите на форме все расположенные справа элементы TextBox и в панели Properties в режиме Events создайте для них общий обработчик события KeyPress, который заполните так
  • private void txtCustomerID_KeyPress(object sender, KeyPressEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }

    Если сейчас запустить приложение, то можно убедиться (при сброшенном флажке), что хоть теперь текстовые поля не реагируют на ввод с клавиатуры символов, но осталась функциональность управляющих редактированием клавиш Delete, Home, End и стрелок. Чтобы окончательно 'добить' текстовые поля, выполните следующее:

  • Обведите блоком курсора все текстовые поля, чтобы сразу опять выделить их, и создайте для них общий обработчик события KeyDown, который заполните так
  • private void txtFax_KeyDown(object sender, KeyEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }

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

  • Запустите приложение - теперь (при сброшенном флажке) поля не реагируют и на управляющие редактированием клавиши, но в то же время остаются светлыми и даже лучезарными
  • Редактирование и обновление полей данных с использованием связанных элементов управления

    Форма, развитая в предыдущем разделе, пригодна только для просмотра данных БД. Даже если пользователь будет пытаться редактировать текстовые поля, это никак не отразится на хранящихся в БД данных. Однако более важной является возможность редактирование данных в текстовых полях и обновление их в БД. В этом разделе мы введем такую функциональность в приложение.

  • Поместите на форму (перед этим выделите форму) над текстовыми полями две кнопки Button, которые настройте в соответствии с таблицей свойств
  • Свойство Значение
    (Name) btnSave
    Text Save
     
    (Name) btnCancel
    Text Cancel

    Кнопка Save будет сохранять содержимое полей редактирования в БД, а кнопка Cancel - восстанавливать поля повторным чтением соответствующей записи из БД. Вначале запрограммируем кнопку Cancel. Обработчик события кнопки Cancel должен выполнить ту же самую работу по заполнению текстовых полей из базы, что у нас уже делает обработчик SelectedIndexChanged списка ListBox, поэтому

  • Выделите на форме кнопку Cancel, зайдите в панель Properties в режиме Events и для события Click через раскрывающийся список его поля присоедините созданный ранее обработчик listBox_SelectedIndexChanged
  • Запустите приложение и убедитесь в правильном функционировании кнопки Cancel
  • Для сбора редактируемой в текстовых полях информации нужно с них ее собрать и обновить кэш набора данных, после чего выполнить метод Update() поставщика данных, чтобы измененния набора данных сохранились в БД. Для сбора данных с редактируемых полей, связанных с данными, мы применим экземпляр библиотечного класса System.Windows.Forms.BindingContext.

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

  • Двойным щелчком на кнопке Save создайте для нее обработчик события Click, который заполните так
  • private void btnSave_Click(object sender, EventArgs e)
            {
                SaveRecord();
            }
        
            private void SaveRecord()
            {
                // Применить свойство формы типа BindingContext 
                // и вытолкнуть изменения со всех дочерних элементов формы, 
                // связанных с данными, в кэш набора данных для заданной таблицы 
                this.BindingContext[dataSet21, "Customers"].EndCurrentEdit();
        
                // Обновить данные в БД из кэша набора данных dataSet21 
                oleDbDataAdapter2.Update(dataSet21, "Customers");
        
                // Принять изменения
                // (лишнее!!! - вызывается автоматически методом Update)
                //dataSet21.AcceptChanges();
        
                // Обновить первый набор данных и список listBox 
                // с сохранением позиции выделенного элемента списка
                // на случай, если вдруг редактировалось поле CompanyName,
                // отображаемое в списке 
                int index = listBox.SelectedIndex;
                NewLoad();
                int count = listBox.Items.Count - 1;
                index = index < count ? index : count; // Однострочный условный оператор
                listBox.SelectedIndex = index;
            }
  • Запустите приложение - будет выброшено исключение, что адаптер данных не поддерживает команду Update()
  • Да, действительно, наш второй поставщик данных был ранее настроен только на чтение данных из базы (как и первый), поэтому нужно его перенастроить, чтобы добавить функциональность обновлять данные в БД.

  • В режиме View Designer формы вызовите контекстное меню для объекта oleDbDataAdapter2 и выполните команду Configure Data Adapter
  • Дойдите до вкладки Generate the SQL statements мастера, щелкните на кнопке Advanced Options и установите все флажки вкладки Advanced Options
  • Дойдите до последней вкладки мастера настройки поставщика, убедитесь, что включены все режимы Select, Insert, Update, Delete и щелкните на кнопке Finish, чтобы завершить настройку
  • В обработчике кнопки Save мы использовали обновление данных в базе в два этапа: вначале вытолкнули изменения из связанных элементов управления в набор данных, и лишь потом дали команду на окончательное внесение изменений из набора в саму базу. Такой подход реализован в среде ADO.NET для обеспечения отсоединенного подключения к базе данных.

    Механизм работы таков: поставщик данных при выполнении запроса SELECT на выборку данных кратковременно подключается к базе данных, загружает в кэш найденные данные и сразу же отключается от базы. Это дает возможность работать с базой данных максимальному количеству пользователей. Поставщик данных после разъединения имитирует теперь собой базу данных в кэше оперативной памяти и пользователь работает с данными через набор данных не замечая, что фактически работает с копией данных.

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

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

    Для многопользовательского режима работы с БД наиболее распространены следующие типы проверки нарушения параллелизма и стратегий обновления данных

  • Изменения заносятся в базу данных в том случае, если значение первичного ключа обновленной строки совпадает со значением первичного ключа существующей строки.
  • Изменения заносятся в базу данных в том случае, если ни один из столбцов, обновленных данным пользователем, не был изменен никем другим с тех пор, как строка была считана данным пользователем.
  • Изменения заносятся в базу данных в том случае, если ни один из столбцов строки не был изменен никем другим с тех пор, как строка была считана данным пользователем.
  • Изменения заносятся в базу данных в том случае, если метка времени последнего обновления строки не изменилась с тех пор, как строка была считана данным пользователем. По своему смыслу этот тип проверки аналогичен предыдущему.
  • Функционирование среды ADO.NET в отсоединенном режиме основано на создании временных данных в файле на языке XML. Чтобы убедиться в этом, достаточно заглянуть в панель Solution Explorer и увидеть файлы с расширением .xsd и с именами классов, созданных оболочкой для наших наборов данных. Если открыть, например, файл DataSet2.xsd через команду контекстного меню Open With/Source Code (Text) Editor, то можно увидеть схему на языке XML, настраивающую работу поставщика.

    Для нас из этой скромной теории важно то, что значение ключевого поля существующей записи редактировать нельзя.

    Ранее мы запрограммировали кнопку Cancel немного нерационально, посадив ее на обработчик события SelectedIndexChanged списка. Это ведет к лишним затратам ресурсов, заставляя приложение при откатах заново считывать данные во второй набор данных dataSet21. Более правильный способ - заставить набор данных вытолкнуть свое неизменившееся содержимое в элементы редактирования.

  • В режиме View Designer формы выделите кнопку Cancel и через панель Properties в режиме Events очистите поле события Click кнопки
  • Двойным щелчком на кнопке Cancel создайте для нее обработчик, который заполните следующим кодом
  • private void btnCancel_Click(object sender, EventArgs e)
            {
                // Вытолкнуть первоначальные данные из 
                // набора данных в поля редактирования
                this.BindingContext[dataSet21, "Customers"].CancelCurrentEdit();
            }
  • Запустите приложение - теперь восстановление текстовых полей редактирования из набора данных происходит мгновенно
  • Добавление и удаление записей с использованием связанных элементов управления

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

  • Добавьте на форму две кнопки и настройти их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Button (Name) btnAdd
      Text Add
    Button (Name) btnDelete
      Text Delete
  • Создайте обработчики события Click для обеих кнопок, которые заполните так
  • private void btnAdd_Click(object sender, EventArgs e)
            {
                // Добавить пустую запись в набор и 
                // показать ее в элементах редактирования 
                this.BindingContext[dataSet21, "Customers"].AddNew();
            }
        
            private void btnDelete_Click(object sender, EventArgs e)
            {
                if (listBox.Items.Count <= 0)// Список пустой, удалять нечего
                    return;
                // Отметить в наборе текущую запись как удаленную 
                int index = this.BindingContext[dataSet21, "Customers"].Position;
                this.BindingContext[dataSet21, "Customers"].RemoveAt(index);
        
                SaveRecord();// Записать изменения в БД
            }

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

  • Выделите флажок редактирования и установите его свойство checkEdit.Checked=true
  • Внешне окно запущенного приложения на данном этапе должно быть примерно таким

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

  • Выделите форму и создайте обработчик для ее события FormClosed, которое возбуждается при закрытии формы. В обработчик вставьте вызов функции сохранения данных
  • private void Form1_FormClosed(object sender, FormClosedEventArgs e)
            {
                SaveRecord();
            }

    Обработка возможных исключений при обращении к БД

    Работа с БД подвержена внешним ошибкам, которые могут возникнуть при обращении к БД. Программист должен заготовить код на такой случай, чтобы необработанные исключения не поставили пользователя в тупик или повредили данные. Попробуйте в нашем приложении удалить какую нибудь родную запись БД Northwind, - будет выброшено необработанное исключение и приложение завершится аварийно.

    Стандартным способом перехвата исключений и их обработки является применение механизма try... catch... finally. Последовательность блоков catch организует цепочку фильтров для отлавливания исключений в сторону увеличения их общего характера. Исключения средой исполнения выдаются в виде объектов-экземпляров классов, порожденных в конечном итоге от класса Exception. Мы воспользуемся перехватом специализированных объектов исключений типа OleDbException, поскольку именно они соответствуют типу возможных ошибок. Другие возможные исключения общего характера будем отлавливать самыми последними в цепочке фильтров.

    В нашем проекте уязвимыми с точки зрения ошибок являются операции чтения данных из БД, сохранение и удаление записей. Ниже приведен полный листинг файла Form1.cs с кодом обработки ошибок

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    using System.Data.OleDb;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            String ConnectionString(String fileNameNotExt)
            {
                string JetEngineString = @"Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=";
                string pathToFile = Application.StartupPath.ToString() + "\\Data\\";
                return JetEngineString + pathToFile + fileNameNotExt.Trim() + ".mdb";
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Настраиваем объект команд 
                oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')";
                oleDbSelectCommand1.Parameters.Add(new System.Data.OleDb.OleDbParameter());
                oleDbSelectCommand1.Parameters[0].Value = "A";
        
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                //oleDbDataAdapter1.Fill(dataSet11);
                SafeFill_1();
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        
            private void btnLoadList_Click(object sender, EventArgs e)
            {
                NewLoad();
            }
        
            void NewLoad()
            {
                // Извлекаем значение текстового поля без пробелов
                String text = txtCustLimit.Text.Trim();
                txtCustLimit.Text = text; // Сохраняем без пробелов для удобства
                oleDbSelectCommand1.Parameters[0].Value = text;// Устанавливаем значение параметра
        
                // Очищаем набор данных
                dataSet11.Clear();
        
                // Заполняем набор данных командой с новым значением параметра
                //oleDbDataAdapter1.Fill(dataSet11);
                SafeFill_1();
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        
            private void txtCustLimit_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                    NewLoad();
            }
        
            private void listBox_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Очистить второй набор данных
                dataSet21.Clear();
                // Если выбран элемент списка, заполнить второй набор
                if (listBox.SelectedIndex != -1)
                {
                    oleDbSelectCommand2.Parameters[0].Value = listBox.SelectedValue;
                    //oleDbDataAdapter2.Fill(dataSet21);
                    SafeFill_2();
                }
            }
        
            private void txtCustomerID_KeyPress(object sender, KeyPressEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }
        
            private void txtFax_KeyDown(object sender, KeyEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }
        
            private void btnSave_Click(object sender, EventArgs e)
            {
                SaveRecord();
            }
        
            private void SaveRecord()
            {
                // Применить свойство формы типа BindingContext 
                // и вытолкнуть изменения со всех дочерних элементов формы, 
                // связанных с данными, в кэш набора данных для заданной таблицы 
                this.BindingContext[dataSet21, "Customers"].EndCurrentEdit();
        
                // Обновить данные в БД из кэша набора данных dataSet21 
                //oleDbDataAdapter2.Update(dataSet21, "Customers");
                SafeUpdate_2();
        
                // Принять изменения
                // (лишнее!!! - вызывается автоматически методом Update)
                //dataSet21.AcceptChanges();
        
                // Обновить первый набор данных и список listBox 
                // с сохранением позиции выделенного элемента списка
                // на случай, если вдруг редактировалось поле CompanyName,
                // отображаемое в списке 
                int index = listBox.SelectedIndex;
                NewLoad();
                int count = listBox.Items.Count - 1;
                index = index < count ? index : count; // Однострочный условный оператор
                listBox.SelectedIndex = index;
            }
        
            private void btnCancel_Click(object sender, EventArgs e)
            {
                // Вытолкнуть первоначальные данные из 
                // набора данных в поля редактирования
                this.BindingContext[dataSet21, "Customers"].CancelCurrentEdit();
            }
        
            private void btnAdd_Click(object sender, EventArgs e)
            {
                // Добавить пустую запись в набор и 
                // показать ее в элементах редактирования 
                this.BindingContext[dataSet21, "Customers"].AddNew();
            }
        
            private void btnDelete_Click(object sender, EventArgs e)
            {
                if (listBox.Items.Count <= 0)// Список пустой, удалять нечего
                    return;
                // Отметить в наборе текущую запись как удаленную 
                int index = this.BindingContext[dataSet21, "Customers"].Position;
                this.BindingContext[dataSet21, "Customers"].RemoveAt(index);
        
                SaveRecord();// Записать изменения в БД
            }
        
            private void Form1_FormClosed(object sender, FormClosedEventArgs e)
            {
                SaveRecord();
            }
        
            private void SafeFill_1()
            {
                try // Обработка ощибок
                {
                    // Заполняем набор данных настроенным адаптером данных 
                    oleDbDataAdapter1.Fill(dataSet11);
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка чтения данных из БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection1.Close();
                }
            }
        
            private void SafeFill_2()
            {
                try // Обработка ощибок
                {
                    // Заполняем набор данных настроенным адаптером данных 
                    oleDbDataAdapter2.Fill(dataSet21);
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка чтения данных из БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection2.Close();
                }
            }
        
            private void SafeUpdate_2()
            {
                try // Обработка ощибок
                {
                    // Обновить данные в БД из кэша набора данных dataSet21 
                    oleDbDataAdapter2.Update(dataSet21, "Customers");
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка записи данных в БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection2.Close();
                }
            }
        }
    }

    Ниже приведены примеры окна сообщений для неверной строки соединения и для несоответствия данных типу поля:

    Конечно, нужно было бы еще защитить методы объекта BindingContext, но это уже механическая работа и ее можно сделать самостоятельно.

    Упражнение 2. Связывание данных с элементами управления ComboBox и DataGrid

    Продолжим работу на примере готовой БД Northwind. В этом упражнении создадим проект, в котором содержимое поля CompanyName таблицы Customers (заказчики) будем загружать в раскрывающийся список ComboBox, а при выборе определенного элемента списка связанные с соответствующей записью данные другой таблицы ( Orders - заказы) будем отображать пользователю в элементе управления DataGrid.

    Отображение данных на одной форме

    Для двух разных таблиц нам понадобятся два поставщика OleDbDataAdapter и два набора данных DataSet. Элементы управления ComboBox и DataGrid свяжем с наборами данных через их свойство DataSource. Для ComboBox нужно дополнительно еще связать свойства DisplayMember и ValueMember с соответствующими полями ( CompanyName и CustomerID ) подключенной таблицы Customers, а для DataGrid указать таблицу Orders в свойстве DataMember.

    При выборе пользователем элемента списка значение CustomerID будет передаваться как параметр в команду поставщика данных, связанного с таблицей Orders, который загрузит из БД в набор нужные записи для отображения их в DataGrid. Если пользователь редактировал данные в DataGrid, то при смене записи в ComboBox эти данные нужно сохранить в БД. Для этого нужно создать соответствующую SQL-команду для поставщика. Итак, приступим.

  • Добавьте к решению командой Add/New Project новый проект типа Windows Forms Application с именем FormsADO2, а прежний проект FormsADO исключите из решения командой Remove
  • Поместите в рабочую область оболочки компонент OleDbDataAdapter, соедините его с файлом Northwind.mdb БД из прилагаемого каталога Source (на предложение оболочки скопировать файл БД в проект ответьте Да ) и настройте его только на чтение полей CustomerID и CompanyName таблицы Customers (заказчики), используя построитель Query Builder, как это мы делали ранее в предыдущем упражнении
  • Выделите поставщик oleDbDataAdapter1 и командой меню Data/Generate Dataset создайте для него класс DataSet1 и объект набора данных с автоматически сгенерированным именем dataSet11
  • Поместите на форму новый поставщик OleDbDataAdapter с именем oleDbDataAdapter2 для таблицы Orders (заказы), в его настройках учтите все возможности взаимодействия с данными, а в команде SELECT предусмотрите передачу значения параметра по полю CustomerID и сортировку по дате выбранных из БД данных. Для правильного формирования команды используйте окно Query Builder
  • Обратите внимание, как в каком порядке мы вызуально формируем в мастере Query Builder условие выборки по параметру CustomerID: вначале отмечаем поле CustomerID в верхней вкладке Orders окна мастера, чтобы это поле появилось во второй вкладке, далее прописываем фильтр =?, а затем в этой же второй вкладке снимаем флажок Output со столбца CustomerID, чтобы не включать его данные поставщиком.

  • Выделите поставщик oleDbDataAdapter2 и командой меню Data/Generate Dataset создайте для него класс DataSet2 и объект набора данных с автоматически сгенерированным именем dataSet21
  • Поместите на форму компоненты Label, ComboBox и DataGrid (если их нет в палитре Toolbox, добавьте к соответствующей вкладке командой контекстного меню Choose Items ), разместите их в соответствии с рисунком и настройте согласно приведенной таблице свойств
  • Компонент Свойство Значение
    Label Text Заказчики
    ComboBox (Name) cbCustomers
      DataSource dataSet11
      DisplayMember Customers.CompanyName
      ValueMember Customers.CustomerID
    DataGrid (Name) dgOrders
      DataSource dataSet21
      DataMember Orders
      CaptionText Заказы для выбранного заказчика
  • Создайте для формы Form1 обработчик события Load, а для ComboBox - обработчик события SelectedIndexChanged, которые заполните так
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO2
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Заполнить dataSet11 и связанный с ним ComboBox 
                // данными таблицы Customers - Заказчики
                oleDbDataAdapter1.Fill(dataSet11);
    
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                RefreshOrders();
            }
        
            private void RefreshOrders()
            {
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                dataSet21.Clear();
                // Если выбран Заказчик в ComboBox
                if (cbCustomers.SelectedIndex != -1)
                {
                    oleDbDataAdapter2.SelectCommand.Parameters[0].Value =
                        cbCustomers.SelectedValue;
                    oleDbDataAdapter2.Fill(dataSet21);
                }
            }
        
            private void cbCustomers_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                RefreshOrders();
            }
        }
    }
  • Запустите проект FormsADO2 и испытайте работу приложения
  • Отображение связанных подробностей на дополнительной форме

    В предыдущем разделе мы просматривали отфильтрованные записи дочерней таблицы Orders (заказы) по записи, выбранной в таблице Customers (заказчики). В этом разделе мы разовьем проект и дадим возможность пользователю отметить в DataGrid конкретный заказ, после чего в дополнительной форме появятся подробности о выбранном заказе.

  • Командой меню Project/Add Windows Form добавьте к проекту новую форму
  • Установите для нее свойства , чтобы размеры формы нельзя было менять и при запуске она бы не отображалась в панели задач, а также убрали системные кнопки свертывания и развертывания окна
  • Из палитры Toolbox перенесите на форму невизуальные пользовательские элементы, указанные в таблице, которые обеспечать только чтение данных из БД, и настройте их через мастера как ранее ( только для Select )
  • Мы будем связывать поставщики с тремя таблицами БД Northwind: Orders, Employees, Order Details и для них в значениях свойств объектов приведены уже готовые SQL-команды, которые попытайтесь получить с помощью мастера Query Builder.

    Компонент Свойство Значение
    OleDbDataAdapter (Name) oleOrders
      CommandText
    SELECT OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate,
          ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion,
          ShipPostalCode, ShipCountry
                FROM Orders
                WHERE (OrderID = ?)
    OleDbDataAdapter (Name)
    oleEmployees
      CommandText
    SELECT EmployeeID, RTRIM(LastName) + ', ' + RTRIM(FirstName) AS FullName
            FROM Employees
            WHERE (EmployeeID = ?)
    OleDbDataAdapter (Name) oleOrderDetails
      CommandText
    SELECT OrderID, ProductID, UnitPrice, Quantity, Discount
            FROM [Order Details]
            WHERE (OrderID = ?)

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

  • Вызовите контекстное меню для поставщика oleOrders, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Переименуйте созданный объект набора данных в dataSetAll
  • Вызовите контекстное меню для поставщика oleEmployees, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Вызовите контекстное меню для поставщика oleOrderDetails, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Что мы сделали? Мы связали каждый поставщик с общим набором данных, где будут храниться три таблицы одновременно, а постащики будут заполнять свои таблицы по команде из кода. Теперь нужно создать пользовательский интерфейс дополнительной формы.

  • Добавьте на форму Form2 и настройте компоненты в соответствии с таблицей свойств, позиционируйте их в соответствии с рисунком
  • Компонент Свойство Значение
    Label Text Номер заказа:
    TextBox BorderStyle FixedSingle
      (DataBindings).Text dataSetAll - Orders.OrderID
    Label Text Дата заказа:
    DateTimePicker (DataBindings).Value dataSetAll - Orders.OrderDate
      Format Long
    Label Text Принял заказ:
    TextBox BorderStyle FixedSingle
      (DataBindings).Text dataSetAll - Employees.FullName
    DataGrid (Name) dgOrderDetails
      CaptionText Подробности заказов
      DataSource dataSetAll
      DataMember Order Details

  • В конструктор класса Form2 файла Form2.cs введите дополнительный параметр для передачи из Form1 номера выбранного заказа
  • public partial class Form2 : Form
        {
            long orderID;// Поле для идентификатора заказа
            // Идентификатор заказа передается 
            // через конструктор вызывающей формы 
            public Form2(long orderID)
            {
                InitializeComponent();
                this.orderID = orderID;
            }
        }
  • Для формы Form2 создайте обработчик события Load, который заполните так
  • private void Form2_Load(object sender, EventArgs e)
            {
                // Найти одну запись по номеру orderID ключевого поля
                oleOrders.SelectCommand.Parameters[0].Value = orderID;
                oleOrders.Fill(dataSetAll, "Orders");
        
                // Найти закрепленного за заказом служащего 
                oleEmployees.SelectCommand.Parameters[0].Value = 
                    dataSetAll.Orders[0].EmployeeID;
                oleEmployees.Fill(dataSetAll, "Employees");
        
                // Найти записи с деталями, соответствующие выбранному заказу
                oleOrderDetails.SelectCommand.Parameters[0].Value = orderID;
                oleOrderDetails.Fill(dataSetAll, "Order Details");
            }
  • Перейдите в режим View Designer формы Form1, выделите объект dgOrders и через панель Properties создайте обработчик события Click, который заполните так
  • private void dgOrders_Click(object sender, EventArgs e)
            {
                // В первом столбце находится OrderID
                long orderID = Convert.ToInt64(dataSet21.Orders.Rows
                    [dgOrders.CurrentCell.RowNumber].ItemArray[0]);
                Form frm2 = new Form2(orderID);// Передаем параметр
                frm2.ShowDialog();
            }
  • Вторичное окно мы показываем в модальном режиме, который перехватывает на себя цикл сообщений приложения, пока не будет закрыто модальное окно
  • Испытайте работу приложения
  • Спроектированное приложение читает всех заказчиков в элемент ComboBox и заполняет элемент DataGrid всеми заказами для выбранного заказчика. Затем по выделенному заказу во вторую форму передается номер заказа и по нему заполняется подробностями заказа объект DataGrid и другие элементы представления данных. Данные, выбранные из разных таблиц, хранятся в одном наборе данных с именем dataSetAll.

    Упражнение 3. Создание простого приложения БД только визуальными средствами IDE

    В данном упражнении мы познакомимся с простыми, но мощными возможностями среды проектирования по созданию СУБД. Нам не придется писать ни строчки кода, все через настройки среда сделает за нас.

  • Добавьте к решению командой File/Add/New Project новый проект с именем FormsADO3 и назначьте его стартовым командой Project/Set as StartUp Project (или первые два проекта исключите из решения командой Remove и единственный проект автоматически станет стартовым)
  • Командой Data/Show Data Sources откройте панель Data Sources
  • Щелкните на ссылке Add New Data Source, чтобы создать новый источник (поставщик + набор) данных, и настройте его в соответствии с приведенной на рисунках последовательностью вкладок мастера:
  • Выберите источник Database
  • Создайте новое соединение с файловой БД, расположенной в прилагаемом к работе каталоге Source
  • На вопрос, скопировать ли БД в каталог проекта, ответьте Да
  • Отметьте флажок напротив таблицы Categories
  • После щелчка на кнопке Finish в панели Data Sources в раскрывающемся списке с именем таблицы Categories выберите элемент DataGridView, с помощью которого будет отображаться таблица Categories на форме, а после этого схватите в панели Data Sources курсором пиктограмму таблицы и перенесите ее на форму
  • Выделите на форме объект DataGridView и в панели Properties настройте его так
  • Компонент Свойство Подсвойство Значение
    DataGridView AutoSizeColumnsMode   Fill
      Dock   Fill
      Columns CategoryID.HeaderText Номер
        CategoryName.HeaderText Название
        Description.HeaderText Пояснения
        Picture.HeaderText Рисунки
  • Запустите приложение - получится примерно такой результат
  • Сравните таблицу в нашем приложении с той же таблицей, открытой в Microsoft Office Access - разницы нет
  • Откройте в текущем проекте файлы Form1.cs, Form1.Designer.cs, NorthwindDataSet.Designer.cs и поудивляйтесь, сколько оболочка нагенерировала за нас кода, а мы сами не написали ни строчки!!! Вот как раз такое программирование называется визуальным (для ленивых)
  • Страницы:

    Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.

    В этой работе мы последовательно познакомимся с технологией ADO.NET (ActiveX Data Objects) управления базами данных типа OLE DB (Object Linking and Embedding Database), к которым, в частности, относятся и базы файлового типа Microsoft Access с расширением .mdb. Наш выбор основан на том, что не требуется устанавливать дополнительные программы, типа SQL Server, для такого рода баз. Пока мы будем пользоваться готовой учебной базой данных Northwind.mdb, расположенной в прилагаемом к работе каталоге Source. Позднее мы научимся создавать свои базы данных либо с помощью СУБД Microsoft Office Access, либо с помощью COM объектов в оболочке Visual Studio 2008.

    Создание базы данных начинается с ее проектирования с помощью диаграмм. Затем создается сам файл (или несколько взаимосвязанных файлов) БД и устанавливается структура БД, определяющая таблицы, поля и их типы, назначение ключей и индексов. Затем проектируется приложение, управляющее данными БД. Мы займемся разработкой приложения, решающего типовые задачи по редактированию и представлению данных БД.

    Базы данных называют источниками данных. Microsoft ADO.NET - это часть библиотеки .NET Framework, позволяющая создавать приложения для управления данными в структурированных файловых или серверных хранилищах. Библиотечные средства ADO.NET обеспечивают подключение к источникам данных, выполнение команд SQL, обработку и выборку данных.

    ADO.NET представлено набором программных средств, прежде всего классов, которые находятся в библиотечной сборке System.Data.dll. Применительно к формату OLE DB наиболее важными из них являются специализированные классы пространства имен System.Data.OleDb:

  • OleDbConnection - обоспечивает подключение к источнику данных
  • OleDbDataAdapter - обеспечивает связь между источником данных и набором данных в оперативной памяти
  • OleDbCommand - объект команд SQL для работы в источнике данных
  • В совокупности экземпляры этих классов в приложении называются поставщиком данных (Data Provider). Далее идет независимый от формата БД слой для хранения данных в кэше оперативной памяти и просмотра данных из пространства имен System.Data:

  • DataSet - набор кэшированных данных, выборочно загруженных из источника
  • DataView - позволяет создать несколько представлений одной и той же таблицы
  • Упражнение 1. Связывание данных с элементами управления ListBox и TextBox

    Начнем с подготовительных действий и созданию связанного с данными списка, дающего возможность только просматривать данные БД.

  • Создайте новый проект типа Windows Forms Application с именем FormsADO и решением ADO
  • Замечание. Место расположения проекта в файловой системе компьютера выбирайте таким, чтобы путь к нему не содержал пробелов и кириллицы. Иначе могут возникнуть проблемы с работой некоторых мастеров Visual Studio 2008!
  • В палитре компонентов Toolbox откройте вкладку дата и перетащите на форму компонент OleDbDataAdapter. Если в настройках проекта такого компонента не окажется, то щелкните на вкладке Data палитры Toolbox правой кнопкой мыши и подключите искомый компонент командой Choose Items
  • В появившемся окне мастера настройки поставщика щелкните по кнопке New Connection и выберите файл подключаемой БД Northwind.mdb из прилагаемого к работе каталога Source
  • После щелчка на кнопке Next оболочка выдаст сообщение с предложением скопировать присоединенный файл БД в каталог проекта

  • Щелкните на кнопке " Да " и перейдите к следующей вкладке мастера
  • Здесь в текстовом поле предлагается указать SQL -запрос, который поставщик данных должен выполнять по умолчанию при заполнении набора данных DataSet. Мы хотим прочитать содержимое двух столбцов CustomerID и CompanyName из таблицы Customers, поэтому либо руками, либо с помощью мастера нужно в многострочном текстовом поле интерфейса мастера заполнить команду:

  • Щелкните на кнопке Query Builder, в диалоговом окне Add Table выберите из списка таблицу Customers и по щелчку на кнопке Add вкладки Tables (потом на Close ) отметьте в интерфейсе флажки против нужных полей таблицы, чтобы мастер написал для них текст запроса
  • Щелкните на кнопке OK окна Query Builder и текст запроса будет помещен в поле мастера
  • По умолчанию мастер готов создать операторы SQL не только для выборки, но и для вставки, обновления и удаления данных.

  • Нам на данном этапе потребуется только чтение данных и другие возможности нужно отключить щелчком на кнопке Advanced Options, сбросив ненужные флажки
  • Дойдите до последней вкладки мастера настройки поставщика и убедитесь, что включен только режим Select (но не Update, Insert, Delete)
  • Щелкните на кнопке Finish, чтобы завершить создание и настройку объектов поставщика данных (экземпляров OleDbDataAdapter и OleDbConnection ) и сохранить параметры в установках проекта
  • В панели Solution Explorer откройте файл Form1.Designer.cs и убедитесь, что мастер создал необходимый код с настройками поставщика, в состав которого входят экземпляры классов OleDbCommand, OleDbDataAdapter и OleDbConnection
  • Описанную процедуру можно в любом месте прервать щелчком на кнопке Cancel мастера настроек и вернуться к ней в любое время. Для этого достаточно щелкнуть в нижней части рабочей области на экземпляре класса OleDbDataAdapter правой кнопкой мыши и выполнитиь команду Configure Data Adapter (или выделить объект поставщика и выполнить ту же команду через пункт Data главного меню оболочки)

  • Выделите в проекте файл БД и через панель Properties установите его свойство Copy to Output Directory в значение Copy if newer, чтобы он находился вместе с исполняемой сборкой в одном каталоге bin проекта
  • Итак, у нас есть объект соединения с БД и объект-посредник между базой и набором данных (кэшем оперативной памяти), который должен выполнять команды SQL и управлять потоками данных. Теперь нужно создать сам набор данных как экземпляр класса DataSet.

  • Щелкните правой кнопкой мыши на экземпляре oleDbDataAdapter1 в нижней части рабочей области и выполните команду Generate DataSet (то же самое можно выполнить через меню Data оболочки)
  • В появившемся окне мастера Generate DataSet щелкните на кнопке OK
  • Будет создан объект набора данных dataSet11, который будет загружать два столбца таблицы Customers согласно SQL-запроса, указанного в настройках поставщика oleDbDataAdapter1.

    В качестве элемента, отображающего набор данных объекта dataSet11, применим список ListBox.

  • Поместите на форму из вкладки Common Controls палитры компонентов экземпляр компонента ListBox и настройте его в соответствии с таблицей
  • Свойство Значение
    ( Name ) listBox
    DataSource dataSet11
    DisplayMember Customers.CompanyName
    Dock Fill

    Хотя набор данных dataSet11 правильно связан со свойствами элемента управления ListBox, но если выполнить приложение, то список останется пустым, потому, что набор данных не был заполнен данными. Код заполнения поместим в обработчик события Load формы.

  • В панели Properties выделите форму из раскрывающегося списка (или щелкните на заголовке формы) и в режиме Events создайте обработчик события Load, который заполните так
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        }
    }
  • Запустите приложение и вы увидите данные поля Customers.CompanyName, связанного со свойством DisplayMember списка ListBox, подключенного к набору данных dataSet11 через свойство DataSource
  • Фильтрация данных в связанном списке

    Метод Fill() поставщика данных загружает все данные указанных столбцов таблицы в набор данных. Часто не все данные нужны пользователю, к тому же их может быть очень много, что перегружает ресурсы компьютера. В этом разделе будет показано, как создать параметризованный оператор SQL для заполнения набора данных не всеми строками таблицы БД, а пока только одной. Мы продолжим развитие нашего проекта, который ограничивает выборку данных из источника определенным условием, а заодно и установим строку подключения к БД вручную.

    Если выделить объект строки подключения и посмотреть свойство ConnectionString, то можно убедиться, что оно имеет следующее строковое значение:

    Provider=Microsoft.Jet.OLEDB.4.0;
    Data Source=|DataDirectory|\Northwind.mdb;Persist Security Info=True

    Переопределим его программно на другое значение.

  • Вставьте в класс Form1 следующий код
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            String ConnectionString(String fileNameNotExt)
            {
                string JetEngineString = @"Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=";
                string pathToFile = Application.StartupPath.ToString() + "\\Data\\";
                return JetEngineString + pathToFile + fileNameNotExt.Trim() + ".mdb";
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        }
    }
  • В панели Solution Explorer для узла проекта FormsADO вызовите контекстное меню, командой Add/New Folder добавьте каталог Data и курсором перенесите в него БД Northwind.mdb
  • Выделите файл БД и через панель Properties убедитесь, что его свойство Copy to Output Directory установлено в значение Copy if newer
  • Запустите приложение - программа FormsADO.exe работает нормально
  • Теперь установим фильтр на заполнение данными набора данных DataSet через команду SQL -запроса. Если открыть файл Form1.Designer.cs, то мы увидим примерно следующий код в методе InitializeComponent() формы

    private void InitializeComponent()
            {
                this.oleDbSelectCommand1 = new System.Data.OleDb.OleDbCommand();
                this.oleDbConnection1 = new System.Data.OleDb.OleDbConnection();
                this.oleDbDataAdapter1 = new System.Data.OleDb.OleDbDataAdapter();
                this.dataSet11 = new FormsADO.DataSet1();
                this.listBox = new System.Windows.Forms.ListBox();
                ((System.ComponentModel.ISupportInitialize)(this.dataSet11)).BeginInit();
                this.SuspendLayout();
                // 
                // oleDbSelectCommand1
                // 
                this.oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName\r\nFROM  Customers";
                this.oleDbSelectCommand1.Connection = this.oleDbConnection1;
                // 
                // oleDbConnection1
                // 
                this.oleDbConnection1.ConnectionString = 
                  "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\\Northwind.mdb;Persis" +
                  "t Security Info=True";
                // 
                // oleDbDataAdapter1
                // 
                this.oleDbDataAdapter1.SelectCommand = this.oleDbSelectCommand1;
                this.oleDbDataAdapter1.TableMappings.AddRange(new System.Data.Common.DataTableMapping[] {
                new System.Data.Common.DataTableMapping("Table", "Customers", new System.Data.Common.DataColumnMapping[] {
                            new System.Data.Common.DataColumnMapping("CustomerID", "CustomerID"),
                            new System.Data.Common.DataColumnMapping("CompanyName", "CompanyName")})});
                
                ...........................................
            }
            ...............................................
        
            private System.Data.OleDb.OleDbCommand oleDbSelectCommand1;

    Обратите внимание на выделенные строки в приведенном листинге. Объявляется поле-ссылка на объект команд oleDbSelectCommand1, затем создается сам объект, его свойству CommandText присваивается SQL-запрос и, наконец, объект команд подключается к одноименному свойству объекта адаптера данных. Теперь метод Fill() адаптера данных будет выбирать из БД нужные данные и передавать их набору данных DataSet. Если в коде события Load формы поменять команду в свойстве CommandText, то она и будет выполняться адаптером данных.

    Таким образом, изменим для адаптера команду выборки данных из БД и добавим к форме необходимый для этого пользовательский интерфейс.

  • Добавьте в обработчик события Load формы код перенастройки объекта oleDbSelectCommand1
  • private void Form1_Load(object sender, EventArgs e)
            {
                // Настраиваем объект команд 
                oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')";
                oleDbSelectCommand1.Parameters.Add(new System.Data.OleDb.OleDbParameter());
                oleDbSelectCommand1.Parameters[0].Value = "A";
        
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }

    В данном коде пояснения требует только SQL-команда, написанная на диалекте SQL для формата файловых баз данных, принятого в Microsoft Office Access 2003

    SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')
  • \r\n - управляющие символы "возврат каретки - перевод строки"
  • WHERE условие - условие отбора данных из источника
  • LIKE шаблон - проверка на соответствие заданному шаблону, в котором могут использоваться специальные символы, означающие подстановки значений параметров команды. В качестве шаблона поиска могут быть использованы два подстановочных знака
  • ?  - одиночный символ в текущей позиции
  • % - любое количество символов в текущей позиции
  • В нашем случае выражение после ключевого слова LIKE означает в своей совокупности, что поиск данных в источнике будет проведен по любому значению, которое введет пользователь в текстовое поле формы. Следуеть помнить, что если в команде присутствует LIKE   шаблон, то даже если для поля поиска существует индекс, то он не работает, а проверка условия выполняется последовательным перебором всех записей таблицы от начала до конца. Это очень замедляет выборку данных на таблицах с большим количеством записей.

  • Измените свойство Dock экземпляра класса ListBox с Fill на Bottom и освободите место сверху клиентской области формы
  • Поместите на форму, как ее дочерние элементы, экземпляры компонентов Label, TextBox, Button в соответствии со снимком и настройте их как показано в таблице свойств ниже
  • Тип Свойство Значение
    ListBox Dock Bottom
    Label Text Customer
    TextBox (Name) txtCustLimit
      Text A (латинское)
    Button (Name) btnLoadList
      Text Load
  • Создайте обработчик события Click кнопки btnLoadList и заполните его кодом переназначения содержимого параметра SQL-запроса
  • private void btnLoadList_Click(object sender, EventArgs e)
            {
                NewLoad();
            }
        
            void NewLoad()
            {
                // Извлекаем значение текстового поля без пробелов
                String text = txtCustLimit.Text.Trim();
                txtCustLimit.Text = text; // Сохраняем без пробелов для удобства
                oleDbSelectCommand1.Parameters[0].Value = text;// Устанавливаем значение параметра
        
                // Очищаем набор данных
                dataSet11.Clear();
        
                // Заполняем набор данных командой с новым значением параметра
                oleDbDataAdapter1.Fill(dataSet11);
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
  • Создайте обработчик события KeyDown для текстового поля txtCustLimit с тем, чтобы набор данных обновлялся и по нажатию пользователем клавиши Enter при завершении ввода очередного значения параметра, и заполните его следующим кодом
  • private void txtCustLimit_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                    NewLoad();
            }
  • Запустите приложение и поэкспериментируйте с его функциональностью, заложенной нами
  • Применение нескольких поставщиков и нескольких наборов данных

    В данном разделе мы продолжим развитие проекта и добавим еще по одному дополнительному экземпляру классов OleDbDataAdapter и DataSet. Для них мы сформируем SQL-команду, которая будет выбирать из источника одну запись таблицы Customers (клиенты), соответствующую выделенному в списке ListBox конкретному клиенту по его идентификатору. Значения полей этой записи будем отображать в текстовых полях формы, маркерованных именами столбцов таблицы. Структура таблицы Customers файловой БД Northwind.mdb, открытая в Microsoft Office Access 2003, приведена на снимке

    Добавление и настройка поставщика данных

  • Перейдите в режим View Designer и поместите на форму еще один компонент OleDbDataAdapter с автоматически сгенерированным именем oleDbDataAdapter2
  • В мастере настроек адаптера Data Adapter Configuration Wizard перейдите на вкладку Generate the SQL statements, щелкните на кнопке Advanced Options и сбросьте все флажки вкладки Advanced Options, поскольку поставщик будет только читать данные
  • Щелкните на кнопке Query Builder построителя SQL-запроса, в появившемся окне выберите таблицу Customers и щелкните один раз на кнопке Add, а затем - Close
  • В появившемся списке полей таблицы Customers отметьте флажки против каждого поля, из которых должен читать данные БД настраиваемый адаптер данных
  • В поле Filter строки CustomerID второй секции построителя команд введите =?, после чего в третьей секции будет сгенерирована команда
  • SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, 
    City, Region, PostalCode, Country, Phone, Fax
    FROM  Customers
    WHERE (CustomerID = ?)

    показанная на снимке

  • Завершите построение запроса кнопкой OK и указанная команда окажется на текущей вкладке мастера настройки поставщика данных, который по ней будет заполнять будущий набор данных класса DataSet
  • Дойдите до последней вкладки мастера настройки поставщика и убедитесь, что включен только режим Select (но не Update, Insert, Delete )
  • Щелчком на кнопке Finish завершите настройку поставщика данных и обратите внимание, что в нижней части появился объект строки соединения для текущего адаптера
  • Теперь нужно создать связанный с поставщиком объект набора данных DataSet, который будет хранить данные таблицы Customers. Этот набор данных должен быть согласован по структуре с настройками поставщика, поэтому создадим его с помощью мастера. В панели Solution Explorer обратите внимание, что пока в приложении присутствует только один узел DataSet1.xsd, который был создан мастером на предыдущих этапах выполнения работы и который описывает класс набора данных, соответствующий хранению значений только двух полей таблицы Customers. Сейчас создадим еще один такой узел.

  • Щелкните правой кнопкой мыши на объекте oleDbDataAdapter2, выполните команду Generate DataSet контекстного меню (или в Data главного меню проекта) и настройте одноименное окно мастера как показано на снимке
  • После щелчка на кнопке OK будет создан объект набора данных, согласованный с объектом oleDbDataAdapter2 второго поставщика данных, в обязанность которого входит поставка данных в соответствии со строкой запроса.

    Изменение пользовательского интерфейса формы

    Сейчас нужно изменить пользовательский интерфейс формы, разместив в клиентской области справа от списка элементы Label и TextBox с целью просмотра значений полей таблицы Customers для выделенной в списке записи.

  • Измените свойство Dock списка на значение None и оформите интерфейс формы в соответствии с рисунком
  • На форму было помещено 11 элементов Label и 11 элементов TextBox. У текстовых меток Label в свойство текст были скопированы названия полей таблицы Customers из SQL-команды Select, приведенной выше. Свойство AutoSize элементов Label было установлено в false, а свойство TextAlign - в значение MiddleRight. После выравнивания элементов на форме с помощью панели инструментов Layout они были выделены и зафиксированы командой Format/Lock Controls, чтобы случайно не нарушить пользовательский интерфейс формы. Такого же самого эффекта можно достигнуть, если установить свойство Locked всех элементов в значение true.

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

  • Выделите последовательно все добавленные элементы TextBox и присвойте им значение свойства (Name) равное именам столбцов таблицы Customers с префиксом txt, например, первый элемент будет txtCustomerID, второй - txtCompanyName, и т.д.
  • Связывание элементов представления с набором данных

    Обратите внимание, что при выполнении предыдущих разделов мы заполняли набор данных из двух полей таблицы Customers в соответствии с командой Select поставщика oleDbDataAdapter1: поля CustomerID и CompanyName, а использовали только данные поля CompanyName, настроив свойство DisplayMember объекта списка listBox согласно приведенной ранее таблице

    Свойство Значение
    (Name) listBox
    DataSource dataSet11
    DisplayMember Customers.CompanyName

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

  • Выделите на форме объект listBox и установите его свойство ValueMember в значение Customers.CustomerID через раскрывающийся список
  • Выделите объект listBox и через панель Properties в режиме Events создайте для события SelectedIndexChanged обработчик, который заполните так
  • private void listBox_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Очистить второй набор данных
                dataSet21.Clear();
                // Если выбран элемент списка, заполнить второй набор
                if (listBox.SelectedIndex != -1)
                {
                    oleDbSelectCommand2.Parameters[0].Value = listBox.SelectedValue;
                    oleDbDataAdapter2.Fill(dataSet21);
                }
            }
  • Запустите приложение - вы увидите пустые значения текстовых полей
  • Это происходит потому, что хоть второй набор данных и заполняется в обработчике записью, соответствующей переданному в качестве параметра SQL-команды значению поля CustomerID, но эти данные никак не связаны с элементами текстовых полей. Исправим это:

  • Выделите на форме элемент txtCustomerID текстового поля, в панели Properties раскройте его свойство (DataBindings).Text и присоедините к нему поле CustomerID из второго набора данных, как показано на снимке
  • Проделайте то же самое для остальных элементов TextBox формы, связывая их с соответствующими полями второго набора данных
  • Запустите приложение - теперь все в порядке, для выбранной в списке строки в элементах TextBox отображаются значения всех полей текущей записи таблицы Customers
  • Здесь есть одна несогласованность, которую желательно сразу же устранить. Дело в том, что текстовые поля мы используем только для просмотра данных, а они еще и допускают видимость редактирования, которая на данном этапе никак не отразится на самих данных в БД. Поэтому, чтобы не вводить пользователя в заблуждение, редактирование в них нужно запретить. Это можно сделать установкой их свойства ReadOnly в значение true, но тогда они станут "тускло некрасивыми". Но есть другой способ их 'стерилизовать':

  • Поместите на форму над текстовыми полями компонент CheckBox и настройте его так
  • Свойство Значение
    (Name) checkEdit
    CheckAlign MiddleRight
    CheckState Unchecked
    Text Edit
  • Выделите на форме все расположенные справа элементы TextBox и в панели Properties в режиме Events создайте для них общий обработчик события KeyPress, который заполните так
  • private void txtCustomerID_KeyPress(object sender, KeyPressEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }

    Если сейчас запустить приложение, то можно убедиться (при сброшенном флажке), что хоть теперь текстовые поля не реагируют на ввод с клавиатуры символов, но осталась функциональность управляющих редактированием клавиш Delete, Home, End и стрелок. Чтобы окончательно 'добить' текстовые поля, выполните следующее:

  • Обведите блоком курсора все текстовые поля, чтобы сразу опять выделить их, и создайте для них общий обработчик события KeyDown, который заполните так
  • private void txtFax_KeyDown(object sender, KeyEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }

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

  • Запустите приложение - теперь (при сброшенном флажке) поля не реагируют и на управляющие редактированием клавиши, но в то же время остаются светлыми и даже лучезарными
  • Редактирование и обновление полей данных с использованием связанных элементов управления

    Форма, развитая в предыдущем разделе, пригодна только для просмотра данных БД. Даже если пользователь будет пытаться редактировать текстовые поля, это никак не отразится на хранящихся в БД данных. Однако более важной является возможность редактирование данных в текстовых полях и обновление их в БД. В этом разделе мы введем такую функциональность в приложение.

  • Поместите на форму (перед этим выделите форму) над текстовыми полями две кнопки Button, которые настройте в соответствии с таблицей свойств
  • Свойство Значение
    (Name) btnSave
    Text Save
     
    (Name) btnCancel
    Text Cancel

    Кнопка Save будет сохранять содержимое полей редактирования в БД, а кнопка Cancel - восстанавливать поля повторным чтением соответствующей записи из БД. Вначале запрограммируем кнопку Cancel. Обработчик события кнопки Cancel должен выполнить ту же самую работу по заполнению текстовых полей из базы, что у нас уже делает обработчик SelectedIndexChanged списка ListBox, поэтому

  • Выделите на форме кнопку Cancel, зайдите в панель Properties в режиме Events и для события Click через раскрывающийся список его поля присоедините созданный ранее обработчик listBox_SelectedIndexChanged
  • Запустите приложение и убедитесь в правильном функционировании кнопки Cancel
  • Для сбора редактируемой в текстовых полях информации нужно с них ее собрать и обновить кэш набора данных, после чего выполнить метод Update() поставщика данных, чтобы измененния набора данных сохранились в БД. Для сбора данных с редактируемых полей, связанных с данными, мы применим экземпляр библиотечного класса System.Windows.Forms.BindingContext.

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

  • Двойным щелчком на кнопке Save создайте для нее обработчик события Click, который заполните так
  • private void btnSave_Click(object sender, EventArgs e)
            {
                SaveRecord();
            }
        
            private void SaveRecord()
            {
                // Применить свойство формы типа BindingContext 
                // и вытолкнуть изменения со всех дочерних элементов формы, 
                // связанных с данными, в кэш набора данных для заданной таблицы 
                this.BindingContext[dataSet21, "Customers"].EndCurrentEdit();
        
                // Обновить данные в БД из кэша набора данных dataSet21 
                oleDbDataAdapter2.Update(dataSet21, "Customers");
        
                // Принять изменения
                // (лишнее!!! - вызывается автоматически методом Update)
                //dataSet21.AcceptChanges();
        
                // Обновить первый набор данных и список listBox 
                // с сохранением позиции выделенного элемента списка
                // на случай, если вдруг редактировалось поле CompanyName,
                // отображаемое в списке 
                int index = listBox.SelectedIndex;
                NewLoad();
                int count = listBox.Items.Count - 1;
                index = index < count ? index : count; // Однострочный условный оператор
                listBox.SelectedIndex = index;
            }
  • Запустите приложение - будет выброшено исключение, что адаптер данных не поддерживает команду Update()
  • Да, действительно, наш второй поставщик данных был ранее настроен только на чтение данных из базы (как и первый), поэтому нужно его перенастроить, чтобы добавить функциональность обновлять данные в БД.

  • В режиме View Designer формы вызовите контекстное меню для объекта oleDbDataAdapter2 и выполните команду Configure Data Adapter
  • Дойдите до вкладки Generate the SQL statements мастера, щелкните на кнопке Advanced Options и установите все флажки вкладки Advanced Options
  • Дойдите до последней вкладки мастера настройки поставщика, убедитесь, что включены все режимы Select, Insert, Update, Delete и щелкните на кнопке Finish, чтобы завершить настройку
  • В обработчике кнопки Save мы использовали обновление данных в базе в два этапа: вначале вытолкнули изменения из связанных элементов управления в набор данных, и лишь потом дали команду на окончательное внесение изменений из набора в саму базу. Такой подход реализован в среде ADO.NET для обеспечения отсоединенного подключения к базе данных.

    Механизм работы таков: поставщик данных при выполнении запроса SELECT на выборку данных кратковременно подключается к базе данных, загружает в кэш найденные данные и сразу же отключается от базы. Это дает возможность работать с базой данных максимальному количеству пользователей. Поставщик данных после разъединения имитирует теперь собой базу данных в кэше оперативной памяти и пользователь работает с данными через набор данных не замечая, что фактически работает с копией данных.

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

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

    Для многопользовательского режима работы с БД наиболее распространены следующие типы проверки нарушения параллелизма и стратегий обновления данных

  • Изменения заносятся в базу данных в том случае, если значение первичного ключа обновленной строки совпадает со значением первичного ключа существующей строки.
  • Изменения заносятся в базу данных в том случае, если ни один из столбцов, обновленных данным пользователем, не был изменен никем другим с тех пор, как строка была считана данным пользователем.
  • Изменения заносятся в базу данных в том случае, если ни один из столбцов строки не был изменен никем другим с тех пор, как строка была считана данным пользователем.
  • Изменения заносятся в базу данных в том случае, если метка времени последнего обновления строки не изменилась с тех пор, как строка была считана данным пользователем. По своему смыслу этот тип проверки аналогичен предыдущему.
  • Функционирование среды ADO.NET в отсоединенном режиме основано на создании временных данных в файле на языке XML. Чтобы убедиться в этом, достаточно заглянуть в панель Solution Explorer и увидеть файлы с расширением .xsd и с именами классов, созданных оболочкой для наших наборов данных. Если открыть, например, файл DataSet2.xsd через команду контекстного меню Open With/Source Code (Text) Editor, то можно увидеть схему на языке XML, настраивающую работу поставщика.

    Для нас из этой скромной теории важно то, что значение ключевого поля существующей записи редактировать нельзя.

    Ранее мы запрограммировали кнопку Cancel немного нерационально, посадив ее на обработчик события SelectedIndexChanged списка. Это ведет к лишним затратам ресурсов, заставляя приложение при откатах заново считывать данные во второй набор данных dataSet21. Более правильный способ - заставить набор данных вытолкнуть свое неизменившееся содержимое в элементы редактирования.

  • В режиме View Designer формы выделите кнопку Cancel и через панель Properties в режиме Events очистите поле события Click кнопки
  • Двойным щелчком на кнопке Cancel создайте для нее обработчик, который заполните следующим кодом
  • private void btnCancel_Click(object sender, EventArgs e)
            {
                // Вытолкнуть первоначальные данные из 
                // набора данных в поля редактирования
                this.BindingContext[dataSet21, "Customers"].CancelCurrentEdit();
            }
  • Запустите приложение - теперь восстановление текстовых полей редактирования из набора данных происходит мгновенно
  • Добавление и удаление записей с использованием связанных элементов управления

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

  • Добавьте на форму две кнопки и настройти их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Button (Name) btnAdd
      Text Add
    Button (Name) btnDelete
      Text Delete
  • Создайте обработчики события Click для обеих кнопок, которые заполните так
  • private void btnAdd_Click(object sender, EventArgs e)
            {
                // Добавить пустую запись в набор и 
                // показать ее в элементах редактирования 
                this.BindingContext[dataSet21, "Customers"].AddNew();
            }
        
            private void btnDelete_Click(object sender, EventArgs e)
            {
                if (listBox.Items.Count <= 0)// Список пустой, удалять нечего
                    return;
                // Отметить в наборе текущую запись как удаленную 
                int index = this.BindingContext[dataSet21, "Customers"].Position;
                this.BindingContext[dataSet21, "Customers"].RemoveAt(index);
        
                SaveRecord();// Записать изменения в БД
            }

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

  • Выделите флажок редактирования и установите его свойство checkEdit.Checked=true
  • Внешне окно запущенного приложения на данном этапе должно быть примерно таким

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

  • Выделите форму и создайте обработчик для ее события FormClosed, которое возбуждается при закрытии формы. В обработчик вставьте вызов функции сохранения данных
  • private void Form1_FormClosed(object sender, FormClosedEventArgs e)
            {
                SaveRecord();
            }

    Обработка возможных исключений при обращении к БД

    Работа с БД подвержена внешним ошибкам, которые могут возникнуть при обращении к БД. Программист должен заготовить код на такой случай, чтобы необработанные исключения не поставили пользователя в тупик или повредили данные. Попробуйте в нашем приложении удалить какую нибудь родную запись БД Northwind, - будет выброшено необработанное исключение и приложение завершится аварийно.

    Стандартным способом перехвата исключений и их обработки является применение механизма try... catch... finally. Последовательность блоков catch организует цепочку фильтров для отлавливания исключений в сторону увеличения их общего характера. Исключения средой исполнения выдаются в виде объектов-экземпляров классов, порожденных в конечном итоге от класса Exception. Мы воспользуемся перехватом специализированных объектов исключений типа OleDbException, поскольку именно они соответствуют типу возможных ошибок. Другие возможные исключения общего характера будем отлавливать самыми последними в цепочке фильтров.

    В нашем проекте уязвимыми с точки зрения ошибок являются операции чтения данных из БД, сохранение и удаление записей. Ниже приведен полный листинг файла Form1.cs с кодом обработки ошибок

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    using System.Data.OleDb;
        
    namespace FormsADO
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            String ConnectionString(String fileNameNotExt)
            {
                string JetEngineString = @"Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=";
                string pathToFile = Application.StartupPath.ToString() + "\\Data\\";
                return JetEngineString + pathToFile + fileNameNotExt.Trim() + ".mdb";
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Настраиваем объект команд 
                oleDbSelectCommand1.CommandText = "SELECT CustomerID, CompanyName FROM  Customers\r\n"
                    + "WHERE (CompanyName LIKE ? + '%')";
                oleDbSelectCommand1.Parameters.Add(new System.Data.OleDb.OleDbParameter());
                oleDbSelectCommand1.Parameters[0].Value = "A";
        
                // Задаем строку соединения
                oleDbConnection1.ConnectionString = ConnectionString("Northwind");
        
                // Заполняем набор данных настроенным адаптером данных 
                //oleDbDataAdapter1.Fill(dataSet11);
                SafeFill_1();
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        
            private void btnLoadList_Click(object sender, EventArgs e)
            {
                NewLoad();
            }
        
            void NewLoad()
            {
                // Извлекаем значение текстового поля без пробелов
                String text = txtCustLimit.Text.Trim();
                txtCustLimit.Text = text; // Сохраняем без пробелов для удобства
                oleDbSelectCommand1.Parameters[0].Value = text;// Устанавливаем значение параметра
        
                // Очищаем набор данных
                dataSet11.Clear();
        
                // Заполняем набор данных командой с новым значением параметра
                //oleDbDataAdapter1.Fill(dataSet11);
                SafeFill_1();
        
                // Устанавливаем курсор списка в конец
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }
        
            private void txtCustLimit_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                    NewLoad();
            }
        
            private void listBox_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Очистить второй набор данных
                dataSet21.Clear();
                // Если выбран элемент списка, заполнить второй набор
                if (listBox.SelectedIndex != -1)
                {
                    oleDbSelectCommand2.Parameters[0].Value = listBox.SelectedValue;
                    //oleDbDataAdapter2.Fill(dataSet21);
                    SafeFill_2();
                }
            }
        
            private void txtCustomerID_KeyPress(object sender, KeyPressEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }
        
            private void txtFax_KeyDown(object sender, KeyEventArgs e)
            {
                if(!checkEdit.Checked)
                    e.Handled = true;
            }
        
            private void btnSave_Click(object sender, EventArgs e)
            {
                SaveRecord();
            }
        
            private void SaveRecord()
            {
                // Применить свойство формы типа BindingContext 
                // и вытолкнуть изменения со всех дочерних элементов формы, 
                // связанных с данными, в кэш набора данных для заданной таблицы 
                this.BindingContext[dataSet21, "Customers"].EndCurrentEdit();
        
                // Обновить данные в БД из кэша набора данных dataSet21 
                //oleDbDataAdapter2.Update(dataSet21, "Customers");
                SafeUpdate_2();
        
                // Принять изменения
                // (лишнее!!! - вызывается автоматически методом Update)
                //dataSet21.AcceptChanges();
        
                // Обновить первый набор данных и список listBox 
                // с сохранением позиции выделенного элемента списка
                // на случай, если вдруг редактировалось поле CompanyName,
                // отображаемое в списке 
                int index = listBox.SelectedIndex;
                NewLoad();
                int count = listBox.Items.Count - 1;
                index = index < count ? index : count; // Однострочный условный оператор
                listBox.SelectedIndex = index;
            }
        
            private void btnCancel_Click(object sender, EventArgs e)
            {
                // Вытолкнуть первоначальные данные из 
                // набора данных в поля редактирования
                this.BindingContext[dataSet21, "Customers"].CancelCurrentEdit();
            }
        
            private void btnAdd_Click(object sender, EventArgs e)
            {
                // Добавить пустую запись в набор и 
                // показать ее в элементах редактирования 
                this.BindingContext[dataSet21, "Customers"].AddNew();
            }
        
            private void btnDelete_Click(object sender, EventArgs e)
            {
                if (listBox.Items.Count <= 0)// Список пустой, удалять нечего
                    return;
                // Отметить в наборе текущую запись как удаленную 
                int index = this.BindingContext[dataSet21, "Customers"].Position;
                this.BindingContext[dataSet21, "Customers"].RemoveAt(index);
        
                SaveRecord();// Записать изменения в БД
            }
        
            private void Form1_FormClosed(object sender, FormClosedEventArgs e)
            {
                SaveRecord();
            }
        
            private void SafeFill_1()
            {
                try // Обработка ощибок
                {
                    // Заполняем набор данных настроенным адаптером данных 
                    oleDbDataAdapter1.Fill(dataSet11);
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка чтения данных из БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection1.Close();
                }
            }
        
            private void SafeFill_2()
            {
                try // Обработка ощибок
                {
                    // Заполняем набор данных настроенным адаптером данных 
                    oleDbDataAdapter2.Fill(dataSet21);
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка чтения данных из БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection2.Close();
                }
            }
        
            private void SafeUpdate_2()
            {
                try // Обработка ощибок
                {
                    // Обновить данные в БД из кэша набора данных dataSet21 
                    oleDbDataAdapter2.Update(dataSet21, "Customers");
                }
                catch (OleDbException ext)
                {
                    string message = "Ошибка записи данных в БД\n";
                    string caption = "Ошибка обращения к БД";
                    MessageBox.Show(String.Format(message + "Подробности: {0}", ext.Message),
                        caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    oleDbConnection2.Close();
                }
            }
        }
    }

    Ниже приведены примеры окна сообщений для неверной строки соединения и для несоответствия данных типу поля:

    Конечно, нужно было бы еще защитить методы объекта BindingContext, но это уже механическая работа и ее можно сделать самостоятельно.

    Упражнение 2. Связывание данных с элементами управления ComboBox и DataGrid

    Продолжим работу на примере готовой БД Northwind. В этом упражнении создадим проект, в котором содержимое поля CompanyName таблицы Customers (заказчики) будем загружать в раскрывающийся список ComboBox, а при выборе определенного элемента списка связанные с соответствующей записью данные другой таблицы ( Orders - заказы) будем отображать пользователю в элементе управления DataGrid.

    Отображение данных на одной форме

    Для двух разных таблиц нам понадобятся два поставщика OleDbDataAdapter и два набора данных DataSet. Элементы управления ComboBox и DataGrid свяжем с наборами данных через их свойство DataSource. Для ComboBox нужно дополнительно еще связать свойства DisplayMember и ValueMember с соответствующими полями ( CompanyName и CustomerID ) подключенной таблицы Customers, а для DataGrid указать таблицу Orders в свойстве DataMember.

    При выборе пользователем элемента списка значение CustomerID будет передаваться как параметр в команду поставщика данных, связанного с таблицей Orders, который загрузит из БД в набор нужные записи для отображения их в DataGrid. Если пользователь редактировал данные в DataGrid, то при смене записи в ComboBox эти данные нужно сохранить в БД. Для этого нужно создать соответствующую SQL-команду для поставщика. Итак, приступим.

  • Добавьте к решению командой Add/New Project новый проект типа Windows Forms Application с именем FormsADO2, а прежний проект FormsADO исключите из решения командой Remove
  • Поместите в рабочую область оболочки компонент OleDbDataAdapter, соедините его с файлом Northwind.mdb БД из прилагаемого каталога Source (на предложение оболочки скопировать файл БД в проект ответьте Да ) и настройте его только на чтение полей CustomerID и CompanyName таблицы Customers (заказчики), используя построитель Query Builder, как это мы делали ранее в предыдущем упражнении
  • Выделите поставщик oleDbDataAdapter1 и командой меню Data/Generate Dataset создайте для него класс DataSet1 и объект набора данных с автоматически сгенерированным именем dataSet11
  • Поместите на форму новый поставщик OleDbDataAdapter с именем oleDbDataAdapter2 для таблицы Orders (заказы), в его настройках учтите все возможности взаимодействия с данными, а в команде SELECT предусмотрите передачу значения параметра по полю CustomerID и сортировку по дате выбранных из БД данных. Для правильного формирования команды используйте окно Query Builder
  • Обратите внимание, как в каком порядке мы вызуально формируем в мастере Query Builder условие выборки по параметру CustomerID: вначале отмечаем поле CustomerID в верхней вкладке Orders окна мастера, чтобы это поле появилось во второй вкладке, далее прописываем фильтр =?, а затем в этой же второй вкладке снимаем флажок Output со столбца CustomerID, чтобы не включать его данные поставщиком.

  • Выделите поставщик oleDbDataAdapter2 и командой меню Data/Generate Dataset создайте для него класс DataSet2 и объект набора данных с автоматически сгенерированным именем dataSet21
  • Поместите на форму компоненты Label, ComboBox и DataGrid (если их нет в палитре Toolbox, добавьте к соответствующей вкладке командой контекстного меню Choose Items ), разместите их в соответствии с рисунком и настройте согласно приведенной таблице свойств
  • Компонент Свойство Значение
    Label Text Заказчики
    ComboBox (Name) cbCustomers
      DataSource dataSet11
      DisplayMember Customers.CompanyName
      ValueMember Customers.CustomerID
    DataGrid (Name) dgOrders
      DataSource dataSet21
      DataMember Orders
      CaptionText Заказы для выбранного заказчика
  • Создайте для формы Form1 обработчик события Load, а для ComboBox - обработчик события SelectedIndexChanged, которые заполните так
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace FormsADO2
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
        
            private void Form1_Load(object sender, EventArgs e)
            {
                // Заполнить dataSet11 и связанный с ним ComboBox 
                // данными таблицы Customers - Заказчики
                oleDbDataAdapter1.Fill(dataSet11);
    
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                RefreshOrders();
            }
        
            private void RefreshOrders()
            {
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                dataSet21.Clear();
                // Если выбран Заказчик в ComboBox
                if (cbCustomers.SelectedIndex != -1)
                {
                    oleDbDataAdapter2.SelectCommand.Parameters[0].Value =
                        cbCustomers.SelectedValue;
                    oleDbDataAdapter2.Fill(dataSet21);
                }
            }
        
            private void cbCustomers_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Заполнить dataSet21 и связанный с ним DataGrid данными
                // таблицы Orders - Заказы для выбранного Заказчика
                RefreshOrders();
            }
        }
    }
  • Запустите проект FormsADO2 и испытайте работу приложения
  • Отображение связанных подробностей на дополнительной форме

    В предыдущем разделе мы просматривали отфильтрованные записи дочерней таблицы Orders (заказы) по записи, выбранной в таблице Customers (заказчики). В этом разделе мы разовьем проект и дадим возможность пользователю отметить в DataGrid конкретный заказ, после чего в дополнительной форме появятся подробности о выбранном заказе.

  • Командой меню Project/Add Windows Form добавьте к проекту новую форму
  • Установите для нее свойства , чтобы размеры формы нельзя было менять и при запуске она бы не отображалась в панели задач, а также убрали системные кнопки свертывания и развертывания окна
  • Из палитры Toolbox перенесите на форму невизуальные пользовательские элементы, указанные в таблице, которые обеспечать только чтение данных из БД, и настройте их через мастера как ранее ( только для Select )
  • Мы будем связывать поставщики с тремя таблицами БД Northwind: Orders, Employees, Order Details и для них в значениях свойств объектов приведены уже готовые SQL-команды, которые попытайтесь получить с помощью мастера Query Builder.

    Компонент Свойство Значение
    OleDbDataAdapter (Name) oleOrders
      CommandText
    SELECT OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate,
          ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion,
          ShipPostalCode, ShipCountry
                FROM Orders
                WHERE (OrderID = ?)
    OleDbDataAdapter (Name)
    oleEmployees
      CommandText
    SELECT EmployeeID, RTRIM(LastName) + ', ' + RTRIM(FirstName) AS FullName
            FROM Employees
            WHERE (EmployeeID = ?)
    OleDbDataAdapter (Name) oleOrderDetails
      CommandText
    SELECT OrderID, ProductID, UnitPrice, Quantity, Discount
            FROM [Order Details]
            WHERE (OrderID = ?)

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

  • Вызовите контекстное меню для поставщика oleOrders, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Переименуйте созданный объект набора данных в dataSetAll
  • Вызовите контекстное меню для поставщика oleEmployees, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Вызовите контекстное меню для поставщика oleOrderDetails, выполните команду Generate DataSet и заполните окно мастера в соответствии с рисунком
  • Что мы сделали? Мы связали каждый поставщик с общим набором данных, где будут храниться три таблицы одновременно, а постащики будут заполнять свои таблицы по команде из кода. Теперь нужно создать пользовательский интерфейс дополнительной формы.

  • Добавьте на форму Form2 и настройте компоненты в соответствии с таблицей свойств, позиционируйте их в соответствии с рисунком
  • Компонент Свойство Значение
    Label Text Номер заказа:
    TextBox BorderStyle FixedSingle
      (DataBindings).Text dataSetAll - Orders.OrderID
    Label Text Дата заказа:
    DateTimePicker (DataBindings).Value dataSetAll - Orders.OrderDate
      Format Long
    Label Text Принял заказ:
    TextBox BorderStyle FixedSingle
      (DataBindings).Text dataSetAll - Employees.FullName
    DataGrid (Name) dgOrderDetails
      CaptionText Подробности заказов
      DataSource dataSetAll
      DataMember Order Details

  • В конструктор класса Form2 файла Form2.cs введите дополнительный параметр для передачи из Form1 номера выбранного заказа
  • public partial class Form2 : Form
        {
            long orderID;// Поле для идентификатора заказа
            // Идентификатор заказа передается 
            // через конструктор вызывающей формы 
            public Form2(long orderID)
            {
                InitializeComponent();
                this.orderID = orderID;
            }
        }
  • Для формы Form2 создайте обработчик события Load, который заполните так
  • private void Form2_Load(object sender, EventArgs e)
            {
                // Найти одну запись по номеру orderID ключевого поля
                oleOrders.SelectCommand.Parameters[0].Value = orderID;
                oleOrders.Fill(dataSetAll, "Orders");
        
                // Найти закрепленного за заказом служащего 
                oleEmployees.SelectCommand.Parameters[0].Value = 
                    dataSetAll.Orders[0].EmployeeID;
                oleEmployees.Fill(dataSetAll, "Employees");
        
                // Найти записи с деталями, соответствующие выбранному заказу
                oleOrderDetails.SelectCommand.Parameters[0].Value = orderID;
                oleOrderDetails.Fill(dataSetAll, "Order Details");
            }
  • Перейдите в режим View Designer формы Form1, выделите объект dgOrders и через панель Properties создайте обработчик события Click, который заполните так
  • private void dgOrders_Click(object sender, EventArgs e)
            {
                // В первом столбце находится OrderID
                long orderID = Convert.ToInt64(dataSet21.Orders.Rows
                    [dgOrders.CurrentCell.RowNumber].ItemArray[0]);
                Form frm2 = new Form2(orderID);// Передаем параметр
                frm2.ShowDialog();
            }
  • Вторичное окно мы показываем в модальном режиме, который перехватывает на себя цикл сообщений приложения, пока не будет закрыто модальное окно
  • Испытайте работу приложения
  • Спроектированное приложение читает всех заказчиков в элемент ComboBox и заполняет элемент DataGrid всеми заказами для выбранного заказчика. Затем по выделенному заказу во вторую форму передается номер заказа и по нему заполняется подробностями заказа объект DataGrid и другие элементы представления данных. Данные, выбранные из разных таблиц, хранятся в одном наборе данных с именем dataSetAll.

    Упражнение 3. Создание простого приложения БД только визуальными средствами IDE

    В данном упражнении мы познакомимся с простыми, но мощными возможностями среды проектирования по созданию СУБД. Нам не придется писать ни строчки кода, все через настройки среда сделает за нас.

  • Добавьте к решению командой File/Add/New Project новый проект с именем FormsADO3 и назначьте его стартовым командой Project/Set as StartUp Project (или первые два проекта исключите из решения командой Remove и единственный проект автоматически станет стартовым)
  • Командой Data/Show Data Sources откройте панель Data Sources
  • Щелкните на ссылке Add New Data Source, чтобы создать новый источник (поставщик + набор) данных, и настройте его в соответствии с приведенной на рисунках последовательностью вкладок мастера:
  • Выберите источник Database
  • Создайте новое соединение с файловой БД, расположенной в прилагаемом к работе каталоге Source
  • На вопрос, скопировать ли БД в каталог проекта, ответьте Да
  • Отметьте флажок напротив таблицы Categories
  • После щелчка на кнопке Finish в панели Data Sources в раскрывающемся списке с именем таблицы Categories выберите элемент DataGridView, с помощью которого будет отображаться таблица Categories на форме, а после этого схватите в панели Data Sources курсором пиктограмму таблицы и перенесите ее на форму
  • Выделите на форме объект DataGridView и в панели Properties настройте его так
  • Компонент Свойство Подсвойство Значение
    DataGridView AutoSizeColumnsMode   Fill
      Dock   Fill
      Columns CategoryID.HeaderText Номер
        CategoryName.HeaderText Название
        Description.HeaderText Пояснения
        Picture.HeaderText Рисунки
  • Запустите приложение - получится примерно такой результат
  • Сравните таблицу в нашем приложении с той же таблицей, открытой в Microsoft Office Access - разницы нет
  • Откройте в текущем проекте файлы Form1.cs, Form1.Designer.cs, NorthwindDataSet.Designer.cs и поудивляйтесь, сколько оболочка нагенерировала за нас кода, а мы сами не написали ни строчки!!! Вот как раз такое программирование называется визуальным (для ленивых)
  • Вернуться к учебному плану