Уильям Вон (billva@betav.com) — эксперт по Visual Studio, SQL Server, Reporting Services и интерфейсам доступа к данным

. С помощью сотрудников компании Microsoft Стива Ласкера и Бет Масси я создал демонстрационное приложение, которое иллюстрирует большинство проблем, с которыми можно столкнуться при попытке построить похожее приложение. В данной статье я буду использовать данное приложение в качестве примера и покажу, как создавать такое решение, демонстрируя скрытые механизмы, которые заставляют приложение работать.

Для построения такого приложения я использовал среду Visual Studio 2005 (далее VS), потому что в ней автоматически создается много дополнительного кода, который потом не приходится изменять или отлаживать. Кроме того, VS производит связывание компонентов и управляющих элементов пользовательского интерфейса. Хотя генераторы кода могут создавать код для элементов управления и навигации, в котором нет нужды, привести в порядок эти дополнительные элементы пользовательского интерфейса довольно просто. Только не забудьте почаще архивировать свою работу при использовании VS. Мне несколько раз пришлось начинать с чистого листа после того, как мои изменения приводили к неисправимым ошибкам приложения.

Стоит ли игра свеч?

Если говорить упрощенно, то TableAdapter — это сгенерированный класс VS, который формирует набор строк как DataTable со строгим контролем типов, так что его можно модифицировать и обрабатывать так же, как DataAdapter. Многие, вероятно, встречали приложения с заполняющими формы данными, многократно демонстрирующие использование базовых таблиц. Но типовой подход с применением таблиц базы данных для создания прикладных приложений может оказывать сильное влияние на производительность сервера и масштабирование приложений. Если просто создать новый источник данных VS и щелкнуть на таблице в списке, VS построит запрос SELECT * FROM MyTable, который возвратит все строки из таблицы, даже если пользователю, возможно, требуется только несколько строк. Скорее всего, пользователям не понравится, что придется потерять время при перемещении всех пятидесяти тысяч (или пяти миллионов) строк из сервера в память клиентской системы.

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

Хотя можно использовать собственные запросы Fill для подбора данных, они делают приложения тяжелыми в обслуживании, если логика запроса слишком сложна. Если вдруг логика меняется, нужно пересобрать, перепроверить и повторно установить приложения.

Хранимые процедуры позволяют настроить и сохранить запросы к базе данных, что в свою очередь оптимизирует работу приложения и облегчит обслуживание. Одно замечание о построении приложения, использующего метод, который я описываю: хотя можно создать TableAdapter из набора данных, извлеченного из объединения, этот метод сопряжен со многими трудностями. Если просто добавить столбцы к каждой записи, можно написать собственные хранимые процедуры для обновления соответствующих таблиц. Только убедитесь, что метод Update в ADO.NET, который вызывается скрытно, когда вызывается метод TableAdapter Update, возвращает в RowsAffected значение 1. Возможно, даже придется реализовать собственный класс TableAdapter, который мы в этой статье затрагивать не будем.

Если объединение вернет более одной строки для единственного первичного ключа – как при объединении Customers с Addresses в единый набор строк, и при этом клиент имеет несколько адресов, — это может сбить с толку механизм привязки данных на стороне клиента. Возможно, лучше сохранять запросы простыми и использовать другие механизмы для достижения того же самого результата, вроде исполнения на стороне клиента объединения взаимосвязанных столбцов данных.

Прежде чем показать, как построить пример приложения, давайте установим некоторые базовые правила. Первое: можно использовать этот подход как в приложениях Active Server Pages (ASP), так и в Windows Forms (с несколькими исключениями, о которых я упомяну дальше). Мой пример – это приложение Windows Forms, написанное на Visual Basic (далее VB) и предназначенное для доступа к SQL Server. В приложение включено немного ручного кода, и некоторое количество моего собственного кода было извлечено из кода, сгенерированного VS, который я после изменил или дoнастроил. Однако программистам на C# нужно будет конвертировать код VB.

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

Давайте начнем работу с подготовительных шагов, которые необходимы для создания тестового приложения. Прежде всего, запустим VS и начнем новый проект Windows Forms. Я использовал редакцию VS Team System, но можно взять VS Standard Edition или другую. Однако в VS Express Edition отсутствуют некоторые ключевые возможности, которые я использую, так что я не рекомендую применять его для серьезной разработки. В некоторых версиях VS проект фактически не сохраняется, пока вы не щелкните Save All, так что сохраните свой проект сразу и убедитесь, что каталог создан и сохраненные файлы расположены в нужных местах.

Затем откройте окно Data Sources, которое можно запустить посредством выбора Data, Show Data Sources. Нажмите на Add New Data Source и выберите Database из меню Choose a Data Source Type. Выберите или создайте соединение Data Connection, которое указывает на экземпляр SQL Server, содержащий целевую базу данных. В нашем случае я указал на сервер с базой данных Customers, созданной для тестового приложения. Сохраните строку соединения в файле app.config так, чтобы мастер Adapter Configuration Wizard мог использовать ее, когда будет в фоновом режиме строить объекты соединения ADO.NET.

Диалоговое окно Choose Your Database Objects, которое показано на экране 1, позволяет выбрать хранимые процедуры, которые будут возвращать набор строк для созданного объекта TableAdapter. В данном случае выбраны хранимые процедуры GetCustomerbyState, GetItemsbyCustomerOrder и GetOrdersbyCustomer. Независимо от того, что вы делаете, никогда не выбирайте корневые элементы, такие как Tables, Views Stored Procedures. Иначе вы создадите объекты TableAdapter для всех элементов списка, а это совсем не нужно.

 

Выбор хранимых процедур
Экран 1. Выбор хранимых процедур

Мы хотим обратиться к данным через написанные хранимые процедуры с параметрическим управлением для возврата выбранного набора данных. Чтобы посмотреть на эти хранимые процедуры, используйте Server Explorer в VS или SQL Server Management Studio (SSMS), для того чтобы открыть определения хранимых процедур. Обратите внимание, что нельзя использовать Server Explorer, пока Data Source Configuration Wizard не закончит работу. В Листинге 1 показана первая хранимая процедура, которая возвращает клиентов из выбранного штата.

Теперь нажмите на Finish в мастере Data Source Configuration Wizard, хотя мы еще далеки от завершения и работа мастера займет определенное время. В конце этот процесс создает TableAdapter со строгим контролем типов для каждого набора строк, который возвращает хранимые процедуры и добавляет файл CustomerDataSet.xsd к проекту. В окне Data Sources мы также видим новые объекты TableDef.

Вид окна Data Sources изменяется в зависимости от текущего открытого окна редактирования. Например, если открыть окно конструктора формы пользовательского приложения Form1.vb, можно выбрать, как индивидуальные объекты TableAdapter в окне Data Sources используются при генерации элементов управления пользовательского интерфейса. Но пока не перемещайте произвольные объекты, потому что вручную вы не сможете установить взаимосвязи между наборами данных, возвращаемых хранимыми процедурами. Обратите внимание, что если бы объект TableAdapter был создан из таблиц базы данных, VS извлек бы автоматически объекты DataRelation.

Также учтите, что метод TableAdapter.Fill автоматически настроен на то, чтобы захватывать любые входные параметры, определяемые вами. В этом случае метод Fill будет нуждаться в значениях, размещаемых в параметрах @StateWanted и @NameHint.

Перенастройка объектов TableAdapter

Поскольку были выбраны три хранимые процедуры, которые возвращают выбранные наборы строк Customer, Order и Items, требуется перенастроить объекты TableAdapter, чтобы правильно обрабатывать эти наборы строк. Следующие шаги потребуется повторять для каждого из трех объектов TableAdapter, созданных мастером Data Source Configuration Wizard.

Сначала дважды щелкните на файле CustomerDataSet.xsd в Solution Explorer в VS и откройте TableAdapter Designer. Начните с GetCustomersByState TableAdapter, который возвращает корневой родительский набор строк Customers; щелкните правой кнопкой мыши заголовок окна и выберите Configure. Откроется мастер TableAdapter, в котором можно указать корректные хранимые процедуры Insert, Update и Delete, вызываемые посредством TableAdapter Update. Убедитесь, что хранимая процедура выбрана правильно. Невнимательность может иметь серьезные последствия. Я использовал VS для генерации начальной версии этих процедур. Однажды создав хранимую процедуру в VS (или самостоятельно), можно определять соответствующие входные параметры, выходной набор строк и значения RETURN. С этого места можно начинать по мере необходимости добавлять прикладную логику.

Обратите внимание, как столбцы начального набора строк из хранимой процедуры Get-CustomersByState отображены в исходных столбцах команды Insert. Если столбцы, которые возвращает хранимая процедура, не соответствуют столбцам, указанным в запросе SELECT, будет выдано предупреждение.

Теперь нажмите Next. Поскольку настройка DataTable не используется, лучше ее отключить. Для того чтобы создать код, необходимый для связки Table-Adapter с соответствующей хранимой процедурой, снова нажмите Next, а затем, чтобы завершить операцию, щелкните Finish. Повторите эти шаги для двух дочерних наборов строк Orders и Items.

Определение взаимосвязей наборов строк на стороне клиента

Поскольку SQL Server не определяет взаимосвязи между независимыми наборами строк, на усмотрение пользователя и VS остается определение объектов DataRelation между тремя наборами строк, созданными объектами TableAdapter. Этот процесс довольно прост. Один раз узнайте, как выбрать правильные индексные значения «первичный ключ – внешний ключ» в интерфейсе пользователя. Как я отмечал ранее, эти взаимосвязи создаются автоматически, когда указываются источники данных на основе таблиц базы данных с установленными взаимосвязями на стороне сервера.

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

Большинство администраторов баз данных устанавливают эти ограничивающие условия всякий раз при создании реляционной базы данных. Можно создавать их, используя инструмент диаграмм схемы базы данных в VS на закладке Database Diagrams в Server Explorer. Этот инструмент работает во многом подобно конструктору-помощнику TableAdapter Configuration Wizard, он поможет определять не только индексы, но и взаимосвязи первичный ключ — внешний ключ.

Теперь давайте рассмотрим, как создаются эти ограничивающие условия с помощью обьекта DataSet DataRelation на стороне клиента. Начиная с родительского набора данных (GetCustomersByState), щелкните на столбце первичного ключа (CustID) и перетащите этот столбец налево. Если перетащить вниз, то можно выбрать дополнительные столбцы, необходимые для GetOrdersByCustomer Table-Adapter, потому что есть два столбца, которые определяют первичный ключ. После того, как будут выбраны только первичные ключевые столбцы, перетащите их налево и подождите, пока VS создаст указатель, а затем перетащите указатель на дочернюю таблицу TableAdapter (то есть GetOrdersByCustomer) и оставьте там. Эти действия вызывают открытие диалогового окна Relation, которое связывает два объекта TableAdapter по столбцам первичных и внешних ключей.

В диалоговом окне Relation соотнесите каждый внешний столбец с соответствующим столбцом в списке Key Columns, который должен содержать все первичные ключевые столбцы родительской Table-Adapter. В моем случае названия столбцов переходят от родителя к потомку, но это необязательно. Кроме того, установите настройку Choose what to create в Both Relation and Foreign Key Constraint и определите все правила в Cascade, дабы быть уверенными, что в случае если родительская строка будет удалена, дочерняя строка будет удалена тоже.

Требуется повторить этот процесс для следующих взаимосвязей родитель-потомок. Можно перетаскивать или просто щелкать правой кнопкой мыши по верхней границе родительского окна TableAdapter и выбирать Add, Relation. Будьте очень осторожны: объекты TableAdapter перечислены в алфавитном порядке, так что легко выбрать неправильный TableAdapter для родителя или потомка. В этом случае из-за того, что есть две части в первичном ключе в родительском наборе строк (Orders), два ключевых столбца соединены с двумя внешними ключевыми столбцами в дочерней таблице. Снова убедитесь, что правила установлены в каскадирование изменений.

Построение пользовательского интерфейса и привязывание к набору строк

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

Сначала вернемся к Solution Explorer в VS и выберем в проектировщике Form1. Увеличим размер формы, приспосабливая ее для нескольких больших элементов. Я не буду последовательно описывать процесс совершенствования формы, наверняка читатели знают, как это сделать. Открываем окно Data Sources и замечаем, что объекты TableAdapter (выделенные как DataTables в Customer- DataSet) теперь показаны в иерархическом виде так, как определено объектом DataRelation.

Если в иерархии не видно дочерней DataTables, как показано на экране 2, значит, что-то сделано не так. В любом случае помните, что VS не сможет установить правильную привязку к данным, если не перетащить объекты из иерархической диаграммы в окне Data Sources. И пока мы еще не готовы делать любое перетаскивание. Поскольку нам необходимо, чтобы часть Customers пользовательского интерфейса отображалась как индивидуальные элементы управления, нужно выполнить некоторые корректировки в TableAdapter, подобные сделанным в окне Data Sources, прежде чем перетаскивать их в форму.

 

Дочерняя таблица
Экран 2. Дочерняя таблица

Сначала щелкните на родительской DataTable GetCustomersByState. Если окно Form Designer активно, то в результате щелчка на любом из столбцов DataTable будет выведен раскрывающийся список, который позволяет указать, как поле будет выводиться в форме (в определенных пределах). По умолчанию система выводит столбцы в управляющем элементе DataGridView. Для использования индивидуального элемента управления выберите из раскрывающегося списка Details.

Возможно, есть необходимость скрыть от пользователя поле TimeStamp. Поскольку выбран вариант Details, можно работать из окна Data Sources. Щелкните на поле TimeStamp и выберите None из списка, если это значение не было выбрано по умолчанию.

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

Теперь вы можете перетащить GetCustomersByState DataTable в форму. Щелкните, перенесите и оставьте DataTable в верхнем левом углу формы, при этом сохраните немного места, чтобы позже создать BindingNavigator и FillToolStrip. До настоящего времени VS не добавлял к форме ни один из классов TableAdapter, а добавлял их только к проекту. Однако перетаскивание DataTable в форму сгенерирует пять новых элементов управления и классов и добавит их в форму (см. экран 3).

 

Добавление к форме
Экран 3. Добавление к форме

Эти добавленные элементы включают в себя:

— CustomerDataSet — DataSet со строгим контролем типов, содержащим объект DataTable, который обрабатывается классами TableAdapter, произведенными VS из запросов Select;

— GetCustomersByStateTableAdapter, созданный из DataSources TableAdapter;

— GetCustomersByStateBindingSource, который сопоставляет DataSource и GetCustomersByStateTableAdapter с взаимосвязанными элементами управления;

— GetCustomersbyStateBindingNavigator, который выводит элемент пользовательского интерфейса, позволяющий пользователям с помощь прокрутки просматривать набор строк, возвращенный запросом Select; включенные в данную панель инструменты — это дополнительные элементы управления, которые можно использовать для добавления, удаления и изменения записей в связанном наборе строк;

— управляющий элемент FillToolStrip, который обеспечивает элементы пользовательского интерфейса для захвата входных параметров и кнопку для вызова метода Fill, выполняющего запрос Select.

Теперь можно добавить первую дочернюю DataTable к форме. В этом случае необходимо вывести дочернюю DataTable в управляющем элементе DataGridView, так что не придется ничего изменять в раскрывающемся списке DataTable.

Теперь приходит черед трюкачества — перетаскивания GetOrdersByCustomer дочерней DataTable из окна Data Sources. Только убедитесь, что вы перетаскиваете DataTable, которая указана на вершине иерархии, а не ту, что из нижнего списка объектов DataTable в CustomerDataSet. Перетаскивание дочерней DataTable в форму добавляет еще одну группу элементов управления и классов, включая BindingSource, TableAdapter, BindingNavigator и FillToolStrip.

Повторите процедуру перетаскивания для GetItems- ByCustomerOrder DataTable, причем опять же убедитесь, что перемещение произошло из DataTable по иерархии прямо под ее родителем, которым является GetOrdersByCustomer DataTable. Так как в нашем примере не требуются элементы управления Orders или Items FillToolStrip, удалите их из формы.

Теперь нужно внести некоторые изменения в элементы управления DataGridView, потому что они не получают «не представленные» параметры настройки. Если не изменять элементы управления DataGridView, столбцы таблицы TimeStamp появятся в пользовательском интерфейсе, что вызовет проблемы, поскольку код таблицы попробует сформировать двоичную величину, которую пользователь не должен видеть. Щелкните правой кнопкой мыши по элементу управления Orders DataGridView и выберите редактирование столбцов. Выберите столбец TimeStamp в списке с левой стороны и установите свойство Visible в False в находящемся справа диалоговом окне. Повторите эту процедуру для таблицы данных Items. Обратите внимание, что можно использовать это диалоговое окно, чтобы переупорядочить столбцы и установить собственное форматирование и изменить поведение.

Затем нужно внести небольшое, но важное изменение для того, чтобы создать элемент управления для помощи в обработке взаимосвязей «первичный ключ-внешний ключ» и заданного по умолчанию механизма привязки к данным. Выберите GetOrders- ByCustomerTableAdapter и GetItemsByCustomer- OrderTableAdapter и используйте диалоговое окно Properties, чтобы установить свойство ClearBeforeFill в значение False. При этом оставьте свойство ClearBeforeFill объекта GetCustomersBy- StateTableAdapter установленным в True, потому что необходимо очищать все предыдущие записи всякий раз, когда пользователь нажимает кнопку Fill в FillToolStrip.

Настройка и расширение кода

Несмотря на то, что VS создает код для поддержки некоторых операций «перетаскивания», появляются утраченные ссылки и куски кода, которые нужно добавить, чтобы получить ожидаемый работающий проект. Частично проблема возникает из-за использования в примере хранимых процедур и удаления двух элементов управления FillToolStrip, которые предусматривают другой способ перемещения между строками в родительской DataTable и связанных дочерних DataTables. Я удалил элементы управления, поэтому приложение извлекает только соответствующих потомков для выбранной родительской строки. Давайте пройдемся по функциональным основам приложения, чтобы увидеть, где нужно заполнить пробелы.

Реализация Form_Load. Я не всегда соглашаюсь с разработчиками Microsoft в вопросах стратегии соединений, но в данном случае, думаю, могу четко обосновать свой подход. Я открываю объект Connection, который использует объекты TableAdapter и оставляет их открытыми во время работы приложения. Это показано в Листинге 2. Делая так, вы не будете ждать доступного соединения, когда потребуется перейти к следующему клиенту, выполнить методы Fill или отправить исправление в базу данных. По умолчанию методам Fill и Update предписано открывать объект Connection непосредственно перед тем, как запрос (или операция Data Manipulation Language, DML) будет выполнен. При таком подходе пул Connection должен определить подходящее соединение, и SQL Server должен сбрасывать и перенастраивать соединение каждый раз при обращении к нему. Мало того, что этот процесс занимает время, он еще и усложняет отладку операций через использование SQL Server Profiler, потому что поток трассировки заполняется мусором, ненужным для приложений Windows Forms, особенно тех, которые не масштабируются за пределы применения несколькими сотнями пользователей.

Реализация кнопки Fill. Управляющий элемент FillToolStrip принимает два входных параметра (State Wanted, Name Hint) и подсвечивает кнопку Fill, которую пользователь может нажать, чтобы заполнить список клиентов. Затем событие FillToolStripButton_Click выполнит метод Get- CustomersByStateTableAdapter.Fill, проходя по параметрам управляющего элемента FillToolStrip TextBox. Тем не менее, эта операция не будет автоматически заполнять дочерний набор строк, так что необходим вызов методов Orders и Items Fill. Каждый метод — это сфокусированный запрос, который возвращает заказы только определенного клиента и только элементы из конкретного заказа. Также нужно добавить обработчик исключений для случаев, когда клиенты в пределах данного интервала отсутствуют. Чтобы убедиться, что пользователь не будет пытаться добавлять записи до начального заполнения набора строк, я отключил BindingNavigator и повторно разблокирую его, если запрос возвратит, по крайней мере, одну строку. Код показан в Листинге 3.

Реализация события DataError. Вы по неосторожности можете включить столбцы в DataGridView, с которыми представление не сможет справиться, в том числе нераспознанное двоичное значение вроде TimeStamp. Если вы не скрываете этот столбец или если ваш код (или пользователь) генерирует значение, с которым DataGridView не сможет справиться, необходимо реализовать событие DataError. Его код сможет делать все, что вы захотите, включая отмену операции, порождающей проблему. В Листинге 4 показана моя подпрограмма, которая обрабатывает событие DataError для обоих элементов управления DataGridView, собирая исключения в окно Debug, так что я могу просмотреть их позже.

Заполнение наборов строк дочерней DataTables. Когда пользователь выбирает определенного клиента для просмотра и список клиентов изначально заполнен, возникает потребность в заполнении списков соответствующих дочерних заказов и элементов. Управляют этим процессом два обработчика событий, которые вызываются по триггеру событием BindingSource PositionChanged. Эти события срабатывают, когда пользователь выбирает другого клиента или другой заказ выбранного клиента. Каждая из операций обработчика события вызывает соответствующий метод TableAdapter Fill, передавая ID текущего клиента и ID заказа для формирования запроса по строкам, имеющим отношение только к этому клиенту, как показано в Листинге 5.

Сохранение данных в базе. До того момента, как пользователь (или написанный код) захочет сохранить изменения, нужно реализовать событие SaveItem_Click, как показано в управляющем элементе BindingNavigator ToolStrip. Часть этого кода реализована при операциях перетаскивания, но эти операции имеют дело только с верхним родительским управляющим элементом. Сохранение данных в базе данных выполняется в две фазы. Первая фаза проверяет достоверность и фиксирует любые изменения в связанной с элементом управления строке в базе данных DataTable посредством методов Validate and EndEdit (см. Листинг 6).

Вторая фаза операции модификации проходит по иерархии родитель/дочь/внук (например: Customer/Order/Item) и вносит любые изменения в базу данных. Эти операции должны выполняться в правильном порядке, и удовлетворять ограничениям первичный ключ — внешний ключ, которые были определены в TableAdapter Designer. Есть еще ограничивающие условия, введенные классами ADO.NET Framework, которые предотвращают удаление родительских записей, которые все еще имеют дочерние и добавление дочерних записей без родительских. Реализуются ограничения путем программирования объекта DataRelation, который определяет, как применяются взаимосвязи. Не помните, как программировать эти объекты? А это и не нужно, TableAdapter Configuration Wizard сделал это за вас, когда вы устанавливали соответствующие взаимосвязи первичных и внешних ключей. Только откройте файл CustomerDataSet. Designer.vb и просмотрите определения.

Установка ограничивающих условий на сервере. Конечно, следует также реализовать подобные ограничения на SQL Server, устанавливая ограничения на связь первичный ключ — внешний ключ и каскадное исполнение операций, где это подходит. В большинстве баз данных администратор базы данных уверен, что эти ограничения реализованы так, чтобы случайно нельзя было нарушить ссылочную целостность базы данных. Инструменты для определения этих взаимосвязей включены в утилиту Server Explorer в VS (через инструмент схематического изображения базы данных) или в Object Explorer в SSMS. Я задал эти ограничивающее условия, когда создавал базу данных Customer так, что если бы мною написанный код на VS провоцировал ошибку, на базу данных она бы не влияла.

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

Использование транзакций для защиты ссылочной целостности данных. Связывание этих разделенных операций для решения критически важных задач в единую атомарную транзакцию имеет большое значение. Написано немало статей об использовании транзакций, но я думаю, что самый простой подход состоит в использовании пространства имен System.Transactions, которые позволяют. NET Framework управлять транзакциями и уменьшать количество кода, который требуется написать. В Листинге 7 показан код, который проходит по иерархии и отправляет изменения в базу данных. Он также иллюстрирует использование класса System.Transactions.TransactionScope.

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

После операций добавления и изменения выполняем операции удаления. В каждом случае используйте для фильтрации метод Select из ADO.NET и только для строк, необходимость в которых основана на свойстве RowState. Когда все операции завершены, позволим. NET Framework убедиться, что транзакция завершена посредством вызова события Complete. Если сделать не так, транзакция автоматически прокручивается назад.

Путь минимальных заблуждений

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

Еще хочу сказать, что я имел возможность поработать с VS 2008, который включает класс TableAdapterManager, обещающий упростить процесс разработки. Данный класс предназначен для того, чтобы заменить большое количество кода, который мне пришлось бы написать для стандартной программы UpdateHierarchy с единым методом вызова.

Листинг 1. GetCustomersByState Stored Procedure

/****** Object: StoredProcedure [dbo].[GetCustomersByState]
Script Date: 09/19/2007 11:54:56 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[GetCustomersByState]
(
@StateWanted char(2),
@NameHint NVarChar(20) = '%'
)
AS
IF CHARINDEX('%',@NameHint)=0 SET @NameHint=@NameHint + '%'
SELECT DISTINCT CustID, TID, CustName, Discount, DateAdded,
TimeStamp, LastName, Photo, Notes
FROM Customers AS C
WHERE (Lastname LIKE @NameHint)
AND (SUBSTRING(@StateWanted, 1, 1) = '*') OR
(CustID IN (SELECT CustID
FROM Biblio.dbo.Addresses AS Addresses_1
WHERE (StateCode = @StateWanted)))
ORDER BY LastName
RETURN
GO

 

Листинг 2. Implementing Form Load

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Me.GetCustomersByStateTableAdapter.Connection.Open()
Me.GetOrdersByCustomerTableAdapter.Connection.Open()
Me.GetItemsByCustomerOrderTableAdapter.Connection.Open()
Me.AddressesTableAdapter.Connection.Open()
End Sub

 

Листинг 3. The FillToolStripButton_Click Event Handler

Imports System.IO
Imports System.Transactions
Public Class Form1
Dim intCustID, intOrderID As Integer
Private Sub FillToolStripButton_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles FillToolStripButton.Click
Try
GetCustomersByStateBindingNavigator.Enabled = True
' Me.CustomerDataSet.EnforceConstraints = False
Me.GetCustomersByStateTableAdapter.Fill( _
Me.CustomerDataSet.GetCustomersByState, _
StateWantedToolStripTextBox.Text, _
NameHintToolStripTextBox.Text)
With Me.CustomerDataSet
intCustID =. GetCustomersByState(0).CustID
Me.GetOrdersByCustomerTableAdapter.Fill( _
Me.CustomerDataSet.GetOrdersByCustomer, intCustID)
intOrderID =. GetOrdersByCustomer(0).OrderID
Me.GetItemsByCustomerOrderTableAdapter.Fill( _
Me.CustomerDataSet.GetItemsByCustomerOrder, _
intOrderID, intCustID)
End With
GroupBox1.Enabled = True
Catch IOREx As IndexOutOfRangeException
MsgBox(«No customers were found using these criteria.», _
MsgBoxStyle.Exclamation, «Fill request»)
Catch ex As System.Exception
System.Windows.Forms.MessageBox.Show(ex.Message)
GetCustomersByStateBindingNavigator.Enabled = False
End Try
End Sub

 

Листинг 4. Handling DataGridView DataError Events

Private Sub GetCustomersByStateDataGridView_DataError( _
ByVal sender As System.Object, _
ByVal e As _
System.Windows.Forms.DataGridViewDataErrorEventArgs) _
Handles GetOrdersByCustomerDataGridView.DataError, _
GetItemsByCustomerOrderDataGridView.DataError
Debug.Print(String.Format(«Row {1}, Column {0}, Exception:{2}», _
e.ColumnIndex, e.RowIndex, e.Exception))
' Basically, do nothing but log the error
End Sub

 

Листинг 5. Handling PositionChanged Events to Populate Child Rowsets

Private Sub GetCustomersByStateBindingSource_position( _
ByVal sender As Object, ByVal e As System.EventArgs) _
Handles GetCustomersByStateBindingSource.PositionChanged
Dim drvCust As DataRowView = _
GetCustomersByStateBindingSource.Current
If drvCust Is Nothing Then
Else
Dim intCID As Integer = CInt(drvCust(«CustID»))
' Test to see if CustID has changed
If Not intCID = intCustID Then
intCustID = intCID 'Returns 0 until populated
Me.AddressesTableAdapter.Fill( _
Me.CustomerDataSet.Addresses, intCustID)
Me.GetOrdersByCustomerTableAdapter.Fill( _
Me.CustomerDataSet.GetOrdersByCustomer, intCustID)
End If
End If
Debug.Print( _
String.Format(«Current Customer changed to: {0}», intCustID))
End Sub
Private Sub GetOrdersByCustomerBindingSource_PositionChanged( _
ByVal sender As Object, ByVal e As System.EventArgs) _
Handles GetOrdersByCustomerBindingSource.PositionChanged
Dim drvOrder As DataRowView = _
GetOrdersByCustomerBindingSource.Current
If drvOrder Is Nothing Then
Else
Dim intOID As Integer = drvOrder(«OrderID»)
If Not intOrderID = intOID Then 'Returns 0 until populated
intOrderID = intOID 'Returns 0 until populated
Me.GetItemsByCustomerOrderTableAdapter.Fill( _
Me.CustomerDataSet.GetItemsByCustomerOrder, _
intOrderID, intCustID)
End If
End If
Debug.Print( _
String.Format(«Current Order changed to: {0}», intOrderID))
End Sub

 

Листинг 6. Calling Validate and EndEdit

Private Sub GetCustomersByStateBindingNavigatorSaveItem_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles GetCustomersByStateBindingNavigatorSaveItem.Click
Me.Validate()
Me.GetCustomersByStateBindingSource.EndEdit()
Me.GetOrdersByCustomerBindingSource.EndEdit()
Me.GetItemsByCustomerOrderBindingSource.EndEdit()
UpdateHierarchy()
btnSetPicture.Enabled = True
End Sub

 

Листинг 7. Updating the Hierarchical DataSet

Private Sub UpdateHierarchy()
Using TransScope As New System.Transactions.TransactionScope
Try
' Add parents first, then children
' Delete children first, then parents
' Use the Select method to return an array
' of rows to be updated or added
Dim dtCust As CustomerDataSet.GetCustomersByStateDataTable = _
Me.CustomerDataSet.Tables(«GetCustomersByState»)
' Add or change first tier parent (Customers)
Me.GetCustomersByStateTableAdapter.Update(dtCust.Select(«", "», _
DataViewRowState.Added Or _
DataViewRowState.ModifiedCurrent))
' Add or change second tier parent (Orders)
Dim dtOrder As _
CustomerDataSet.GetOrdersByCustomerDataTable = _
CustomerDataSet.Tables(«GetOrdersByCustomer»)
Me.GetOrdersByCustomerTableAdapter.Update(dtOrder.Select(«", "», _
DataViewRowState.Added Or _
DataViewRowState.ModifiedCurrent))
' Now Add, Change or Delete third-tier children
Dim dtItems As _
CustomerDataSet.GetItemsByCustomerOrderDataTable = _
Me.CustomerDataSet.Tables(«GetItemsByCustomerOrder»)
Me.GetItemsByCustomerOrderTableAdapter.Update(dtItems)
' Now it's safe to delete the second-tier children (parents to third tier) (Orders)
Me.GetOrdersByCustomerTableAdapter.Update(dtOrder.Select(«", "», _
DataViewRowState.Deleted))
' Now it's safe to delete the parent (Customers)
Me.GetCustomersByStateTableAdapter.Update(dtCust.Select(«", "», _
DataViewRowState.Deleted))
TransScope.Complete()
MsgBox(«Update succeeded...»)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
End Using
End Sub