Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - ADO.NET »

Обсудить на форуме Обсудить на форуме

Работа с данными вчера, сегодня, завтра. Typed dataset

Все течет все изменяется.....

Введение

Typed dataset. Как много в этом звуке для сердца нашего слилось. Этими, чуть измененными, словами известного русского классика мне бы хотелось начать описание, наверное, самой крупной и интересной главы в механизме доступа к данным Ado.Net.

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

Чем же я был обеспокоен сейчас, спросите Вы? Я хочу в своих статьях показать, и очень надеюсь, что мне это удается, всю прелесть нового подхода в построении объектов доступа к данным. И обеспокоен я был тем, что в раздел о typed dataset (типизированный dataset) попадает несколько очень весомых «камней», а именно механизм генерации скриптов таблиц, собственно сам класс DataSet который поддерживает XML сериализацию, и описание XML Schema Definition (XSD). Я долго смотрел на то, как мне разделить эти все понятия на отдельные главы и увязать между собой, но я понял, что, к сожалению, мне это не удастся. И решил, что все таки эту статью мне видимо, придется выпустить в несколько этапов по частям...

Я долго искал статьи по данной теме которые могли бы компактно дать введение в данный раздел, и несмотря на сарказм моих друзей, которые говорят что чтение статей с западных порталов влияет на объем моих статей :), хочу сказать, что самая маленькая статья на данную тему и занимала двадцать восемь листов формата А4. И была она написана весьма и весьма сжато.

Я и сейчас не уверен, что взял правильное «начало» в написании данной статьи, но большинство статей и книг начинают рассмотрение typed dataset с введения в XML|XSD, а я бы хотел отложить все-таки рассмотрение этих понятий на следующие разделы. А вот как раз с описанием классов таблиц в типизированных dataset проблемы – ни у одного из авторов я не нашел нормального описания данной темы. И именно потому я буду стараться восполнить этот пробел.

Итак, как говорится, меньше лирики - больше дела.

Обзор того, что имеем.

Давайте взглянем на то, что мы получили после первой и второй статьи. Мы будем развивать и дальше наш проект DataWorking, который мы начали в первой статье. Я предлагаю взглянуть на то, что мы имеем на текущий момент после наших предыдущих этапов. Предлагаю открыть проект и взглянуть на окошко дизайнера нашей единственной формы Form1.

Кроме чистой формы и нескольких объектов в разделе не визуальных компонентов ничего больше на форме нет.

Давайте окинем взглядом окна Class View и Solution Explorer.

На самом деле, когда мы ни о чем, не подозревая, дали команду Generate DataSet, используя в дизайнере контекстное меню экземпляра объекта SqlDataAdpater, в предыдущих наших изысканиях мы как раз неявно сгенерировали тот самый пресловутый типизированный dataset. «Вот это да», скажете Вы, «ничего себе». Нажали одну кнопку и получили кода для нескольких статей! :) Точно – так и есть! И я не скажу что это плохо. Еще раз предлагаю вернуться к нашей среде разработки и окинуть взглядом дерево классов (Class View).

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

Хочется еще взглянуть на дерево Solution Explorer для того, чтобы, как говорится, полностью владеть ситуацией.

В нашем случае Solution Explorer не отображает еще имя файла с кодом dataset - он называется myDataSet.cs. Сделать возможным отображение всех файлов проекта и всей директории можно нажав на иконку в тулбаре окна Solution Explorer.

В файле myDataSet.cs как раз и описывается тот код, который принадлежит классу myDataSet и именно его мы видели на скриншоте с дерева классов (Class View).

Одного беглого взгляда по файлу датасета достаточно, чтобы понять, как постарался визард для Вас. И уж конечно для меня. :)

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

Повторение мать учения.

Мы окинули взглядом наш проект. Так сказать бегло. Но все-таки, для закрепления материала я предлагаю вам сейчас сделать следующее: удалить существующий dataset и создать его заново, так как теперь, как мы уже знаем, нас ожидает долгий путь по глубинам типизированного dataset и мы должны его «прочувствовать» от начала и до конца.

И вместе с тем, мы научимся корректно делать удаление dataset, если вдруг по каким либо причинам он нас не устроил.

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

Зайдем в окно дизайнера формы и выберем три объекта, находящиеся в разделе невизуальных компонентов, а именно connection, adapter и собственно сам dataset, и удалим их, нажав на клавишу delete.

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

Но это еще не все. Данная операция не удаляет файл классов dataset, и мы это сделаем руками. Давайте взглянем в окно Solution Explorer.

Там мы видим специализированный файл dataset, который на самом деле включает в себя группу файлов в проекте. Не будем пока вдаваться в подробности, что это за файлы, мы сделаем это чуть позже. Наша задача, пока состоит в том что бы «вычистить» проект. Выберем файл с названием myDataSet.xsd и нажмем клавишу "Del" или воспользуемся контекстным меню:

Система удалит три файла сразу и, что очень важно, удалит «с концами». Т.е. не просто исключит из проекта, а удалит физически с диска при этом, правда, выдаст предупреждение. Если вы, по каким либо причинам, не хотите удалять файлы dataset вы можете просто исключить их из проекта. Для этого из контекстного меню Вам нужно будет выбрать пункт Exclude From Project. Вы можете его увидеть на скриншоте контекстного меню.

Итак, мы удалили старый dataset из проекта. Дизайнер удалил свою часть кода, а мы руками удалили то, до чего дизайнер добраться не смог. Если вы сейчас запустите компиляцию проекта, он выдаст вам ряд ошибок так как он не нашел экземпляра myDataSet в Вашем проекте, но это и не мудрено.

Дизайнер «добрался» до кода, который он сгенерировал сам, но к тому коду, который мы писали вручную, он не прикоснулся. Я предлагаю не вычищать тот код, который мы имеем, а создать новый dataset с таким же именем экземпляра объекта DataSet. Зачистку кода оставим на самый конец.

И теперь настала пора проделать путь по созданию нового, «типизированного» dataset.

«Клиент» для форума.

Пока я описывал то, как можно удалять в среде разработки старый dataset мне пришла в голову мысль, что нет смысла создавать очередной тестовый пример. А лучше создать новое готовое приложение, которое мы возможно в дальнейшем сможем использовать для работы с форумом www.aspnetmania.com.

Мы напишем клиента по работе с сообщениями форума, похожего на тот, что используется на www.aspnetmania.com, который позволит нам отправлять и читать сообщения не используя интернет-браузер. По-видимому, его можно сравнить с news reader-ом, чем-то похожим на Outlook Express. Да и решение вполне конкретной задачи, тем более с возможной пользой в дальнейшем, будет куда интересней, чем создание очередной «пустышки».

И в дальнейшем после описания раздела Typed dataset я надеюсь, что мы перейдем к детальному рассмотрению возможностей работы с Web Services из WinForms приложений. Так что, как видите, процесс познания бесконечен :)

Структура БД.

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

Я не буду вдаваться детально в структуру БД, остановимся пока на структуре, которая делалась на основе БД использующейся на самом сервере www.aspnetmania.com для хранения сообщений. Тем более, что скорее всего по ходу разработки она будет меняться.

Для разработки структуры я использую case средство ERStudio, и для наглядности буду приводить скриншоты именно из этого приложения. Вы можете использовать любое средство или не использовать его вовсе, потому что я буду приводить здесь и код по созданию таблиц на T-Sql, который вы можете использовать прямо в Query Analyzer входящий в стандартный набор MS SQL Server 2000. Я думаю, что данные скрипты вполне могут быть использованы и под MS SQL Server 7.0, но, к сожалению, у меня нет возможности проверить их на практике в данный момент.

Итак, начальная структура БД точнее ее первая таблица будет выглядеть следующим образом:

И ниже, я привожу полный скрипт для ее генерации на сервере БД.

/* 
 * TABLE: ForumMessages 
 */

CREATE TABLE ForumMessages(
    ItemID          int              IDENTITY(1,1),
    ForumID         int              NULL,
    ParentItemID    int              NULL,
    ThreadID        int              NOT NULL,
    UserID          int              NULL,
    postDate        smalldatetime    DEFAULT (getdate()) NOT NULL,
    Subject         varchar(255)     NOT NULL,
    Text            text             NOT NULL,
    NameUser        varchar(50)      DEFAULT ('Anonymous') NOT NULL,
    EmailUser       varchar(100)     NULL,
    level           smallint         DEFAULT (0) NOT NULL,
    sendEmail       bit              DEFAULT (0) NOT NULL,
    watched         bit              NULL,
    CONSTRAINT PK_ForumMessages PRIMARY KEY CLUSTERED (ItemID)
) 
go


IF OBJECT_ID('ForumMessages') IS NOT NULL
    PRINT '<<< CREATED TABLE ForumMessages >>>'
ELSE
    PRINT '<<< FAILED CREATING TABLE ForumMessages >>>'
go

/* 
 * TABLE: ForumMessages 
 */

ALTER TABLE ForumMessages ADD CONSTRAINT FK_ForumMessages_ForumMessages 
    FOREIGN KEY (ParentItemID)
    REFERENCES ForumMessages(ItemID)
go

Здесь же я привожу процедуры «бизнес-логики». Они будут использоваться нашими классами Commands для соответствующих операций: вставки, удаления и изменения данных.

/* 
 * PROCEDURE: ForumMessagesInsProc 
 */

CREATE PROCEDURE ForumMessagesInsProc
(
    @ForumID          int                      = NULL,
    @ParentItemID     int                      = NULL,
    @ThreadID         int,
    @UserID           int                      = NULL,
    @postDate         smalldatetime,
    @Subject          varchar(255),
    @Text             text,
    @NameUser         varchar(50),
    @EmailUser        varchar(100)             = NULL,
    @level            smallint,
    @sendEmail        bit,
    @watched          bit                      = NULL)
AS
BEGIN
    BEGIN TRAN

    INSERT INTO ForumMessages(ForumID,
                              ParentItemID,
                              ThreadID,
                              UserID,
                              postDate,
                              Subject,
                              Text,
                              NameUser,
                              EmailUser,
                              level,
                              sendEmail,
                              watched)
    VALUES(@ForumID,
           @ParentItemID,
           @ThreadID,
           @UserID,
           @postDate,
           @Subject,
           @Text,
           @NameUser,
           @EmailUser,
           @level,
           @sendEmail,
           @watched)

    IF (@@error!=0)
    BEGIN
        RAISERROR  20000 'ForumMessagesInsProc: Cannot insert because primary key value not found in ForumMessages '
        ROLLBACK TRAN
        RETURN(1)
    
    END

    COMMIT TRAN

END
go
IF OBJECT_ID('ForumMessagesInsProc') IS NOT NULL
    PRINT '<<< CREATED PROCEDURE ForumMessagesInsProc >>>'
ELSE
    PRINT '<<< FAILED CREATING PROCEDURE ForumMessagesInsProc >>>'
go


/* 
 * PROCEDURE: ForumMessagesUpdProc 
 */

CREATE PROCEDURE ForumMessagesUpdProc
(
    @ItemID           int,
    @ForumID          int                      = NULL,
    @ParentItemID     int                      = NULL,
    @ThreadID         int,
    @UserID           int                      = NULL,
    @postDate         smalldatetime,
    @Subject          varchar(255),
    @Text             text,
    @NameUser         varchar(50),
    @EmailUser        varchar(100)             = NULL,
    @level            smallint,
    @sendEmail        bit,
    @watched          bit                      = NULL)
AS
BEGIN
    BEGIN TRAN

    UPDATE ForumMessages
       SET ForumID           = @ForumID,
           ParentItemID      = @ParentItemID,
           ThreadID          = @ThreadID,
           UserID            = @UserID,
           postDate          = @postDate,
           Subject           = @Subject,
           Text              = @Text,
           NameUser          = @NameUser,
           EmailUser         = @EmailUser,
           level             = @level,
           sendEmail         = @sendEmail,
           watched           = @watched
     WHERE ItemID = @ItemID

    IF (@@error!=0)
    BEGIN
        RAISERROR  20001 'ForumMessagesUpdProc: Cannot update  in ForumMessages '
        ROLLBACK TRAN
        RETURN(1)
    
    END

    COMMIT TRAN

    RETURN(0)
END
go
IF OBJECT_ID('ForumMessagesUpdProc') IS NOT NULL
    PRINT '<<< CREATED PROCEDURE ForumMessagesUpdProc >>>'
ELSE
    PRINT '<<< FAILED CREATING PROCEDURE ForumMessagesUpdProc >>>'
go


/* 
 * PROCEDURE: ForumMessagesDelProc 
 */

CREATE PROCEDURE ForumMessagesDelProc
(
    @ItemID           int)
AS
BEGIN
    BEGIN TRAN

    DELETE
      FROM ForumMessages
     WHERE ItemID = @ItemID

    IF (@@error!=0)
    BEGIN
        RAISERROR  20002 'ForumMessagesDelProc: Cannot delete because foreign keys still exist in ForumMessages '
        ROLLBACK TRAN
        RETURN(1)
    
    END

    COMMIT TRAN

    RETURN(0)
END
go
IF OBJECT_ID('ForumMessagesDelProc') IS NOT NULL
    PRINT '<<< CREATED PROCEDURE ForumMessagesDelProc >>>'
ELSE
    PRINT '<<< FAILED CREATING PROCEDURE ForumMessagesDelProc >>>'
go


/* 
 * PROCEDURE: ForumMessagesSelProc 
 */

CREATE PROCEDURE ForumMessagesSelProc
(
    @ItemID           int)
AS
BEGIN
    SELECT ItemID,
           ForumID,
           ParentItemID,
           ThreadID,
           UserID,
           postDate,
           Subject,
           Text,
           NameUser,
           EmailUser,
           level,
           sendEmail,
           watched
      FROM ForumMessages
     WHERE ItemID = @ItemID

    RETURN(0)
END
go
IF OBJECT_ID('ForumMessagesSelProc') IS NOT NULL
    PRINT '<<< CREATED PROCEDURE ForumMessagesSelProc >>>'
ELSE
    PRINT '<<< FAILED CREATING PROCEDURE ForumMessagesSelProc >>>'
go

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

Создание dataset.

Теперь у нас есть структура, на основе которой мы можем создать наш первый не экспериментальный dataset.

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

Давайте посмотрим, что у нас есть. У нас есть скрипт БД, и теперь настала пора создать саму БД. Я намеренно не вносил в скрипт код создания новой БД так как она зависит от системы.

Да и в принципе, поэкспериментировать со средой IDE Visual Studio и конкретно с Server Explorer нам тоже не повредит.

Открываем проект DataWorking (если он у вас еще не открыт), и, вызвав Server Explorer вызываем следующее контекстное меню:

Создадим новую БД под названием forum:

БД создана.

Теперь задача состоит в том, чтобы запустить наш скрипт по генерации таблицы и процедур.

Открываем Query Analyzer из состава MS SQL Server 2000, загружаем в него наш скрипт и дописываем в начале скрипта строку: use forum - теперь мы уже знаем как называется наша БД. Запускаем скрипт на выполнение:

Все в порядке. Мы видим, что скрипт не вызвал ошибок при генерации. Давайте вернемся в IDE VS и взглянем теперь на окно Server Explorer. Вызвав пункт контекстного меню Refresh для обновления данных о БД,

мы получим следующий результат:

Как мы видим наша БД «наполнилась» таблицей и четырьмя хранимыми процедурами. Это как раз то, что нам нужно. Теперь мы можем перейти к генерации dataset на основе сконструированной БД.

Перетащим таблицу ForumMessages на наш «дизайнерский» стол. После перетаскивания таблицы из окна Server Explorer на форму мы получили экземпляры connection и adapter:

Далее как всегда нам нужно сконфигурировать наши экземпляры перед созданием dataset.

Для этого внесем следующие изменения:

  • изменим имя sqlConnection1 на sqlConnection;
  • изменим имя sqlDataAdapter1 на adapterForumMessages;
  • настроим Commands для adapterForumMessages.
  • sqlSelectCommand1 – переименовываем в selectCommand;
  • по тому же принципу переименовываем Commands для Insert, Update и Delete; (соответственно insertCommand, updateCommand и deleteCommand).

Далее. Мы имеем после генерации с помощью нашего скрипта четыре хранимые процедуры для каждой из операций давайте переопределим наши Commands у dataAdapter-а.

Выполним следующие операции. Откроем окно Properties у dataAdapter и выберем для начала свойства объекта selectCommands. Изменим тип команды с Text на StoredPrecedure.

К сожалению, после того как мы поменяем свойство CommandType на StoredProcedure, визард не даст нам возможности выбрать ту процедуру, которую нужно, поэтому её имя придется ввести вручную.

Давайте впишем туда название: ForumMessagesSelProc. После введения имени система предложит Вам обновить список параметров: согласитесь сделать это.

После этого вы получите обновленный список параметров для вызова процедуры. На текущий момент она имеет два параметра по умолчанию @RETURN_VALUE и @ItemID. Но зачем нам выборка значений с параметром @ItemID – нам нужно обычное заполнение таблицы. Я предлагаю исправить «огреху», сделанную нашим case средством.

Я хотел бы отметить еще одну очень важную и приятную возможность IDE Visual Studio по возможности работы с «хранимыми процедурами». Она позволяет визуально и гораздо более удобно редактировать их, а также проходить по ним в режиме отладки, но об этом, как ни будь в другой раз.

Давайте внесем некоторые изменения в код хранимой процедуры для загрузки данных - ForumMessagesSelProc. Для этого откройте в окне Server Explorer ветку Stored Procedures и, сфокусировав курсор на названии нужной нам процедуры, выберите режим редактирования из контекстного меню, предварительно нажав правую клавишу мышки.

Вы увидите окно с исходным текстом хранимой процедуры. Приведенный рисунок показывает, как должна выглядеть эта процедура после редактирования:

Из него был удален входящий параметр @ItemID процедуры и поставлен в комментарий критерий выборки в зависимости от параметра. Это те изменения, которые я хотел внести в процедуру. Не забудьте нажать на иконку Save, и закройте окно редактирования хранимой процедуры.

Теперь вернемся в редактор, и проделаем те же операции и с другими экземплярами Commands (update, insert и delete).

В результате чего мы получим четыре Commands, завязанных на хранимые процедуры сервера.

Многие, конечно, могут сказать, зачем мы создаём себе искусственные сложности. В данном примере действительно было бы проще использовать значение свойства CommandText, сгенерированное визардом, но, во-первых, «тяжело в учении легко в бою» :), а, во-вторых, простая привычка комплексного подхода к составлению бизнес логики и бизнес объектов, приводит меня к способу генерации объектов через хранимые процедуры сервера.

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

Для этого мы должны сделать активным элемент dataAdapter в окне дизайнера, и нажать правую кнопку мышки вызвав тем самым контекстное меню:

После выбора пункта меню «Generate Dataset…» вы получите диалог для ввода наименования создаваемого dataSet. Я предлагаю изменить имя по умолчанию на myDataSet, т.е. то которое мы использовали ранее:

Нам будет предложено, также выбрать те таблицы (с соответствующим dataAdapeter) которые будут добавлены в создаваемый dataSet. В нашем случае это пока единственный объект.

После нажатия на кнопку «Ok» мы получаем экземпляр уже созданного объекта myDataSet с названием экземпляра myDataSet1 в окне дизайнера.

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

Типизированные таблицы.

Сейчас мы можем перейти конкретно к коду. Я подозреваю, что нам не нужно будет много писать, и исправлять код в данной статье, скорее всего, нужно будет сосредоточиться на коде и пытаться внимательно его изучить. Потому, что вся «изюминка» как раз внутри него и находится.

Я полностью согласен с утверждением, что лучшее понимание и запоминание приходит все-таки, когда мы пишем весь код руками, но в данном случае даже моя «болезнь» подробного написания статей не позволит мне заставить написать вас весь тот код, который сгенерировал визард. В моем случае это 26 килобайт и 649 строка текста. И это только для одной таблицы. А в дальнейшем мы будем использовать как минимум две таблицы, а возможно и более.

Выбросить не основное из него, по моему мнению, будет не правильно, потому что хочется все-таки видеть весь код, который нам сделал «дружелюбный» робот.:) Боюсь что сегодня, вместо того чтобы писать код, для лучшего запоминания мы будем писать комментарии, что, кстати, весьма полезно и не является последним для нашей практики и привычки. И данная привычка весьма и весьма облегчит участь будущих поколений, которые будут использовать Ваш код.

И все-таки, перейдем к коду.

Пока оставим основной класс dataset (myDataSet) на потом. А коснемся, так сказать, его глубин.

Что же мы видим внутри? А внутри мы видим как минимум три очень интересных класса, а именно: класс таблицы (DataTable), класс строки (Row) таблицы и класс поведения или событий (EventHandler).

Я в разговоре с коллегами употребляю термин «типизированная таблица». Этот термин как раз и включает в себя именно перечисленных выше три класса. Не знаю, на сколько это вписывается в идеологию, предоставленную от Microsoft, но мне он значительно облегчает общение.

И что же у нас внутри? Предлагаю начать с класса строки (Row):

...
[System.Diagnostics.DebuggerStepThrough()]
public class ForumMessagesRow : DataRow {
...

Атрибут DebuggerStepThrough() отвечает за то, чтобы Ваш код, точнее данный класс, поддерживал пошаговую обработку в режиме отладки. Я буду вписывать комментарий в код и вам рекомендую делать то же самое.

Класс наследуется от стандартного класса DataRow, что сразу открывает нам богатые возможности биндинга (связывания данных).

Далее мы видим internal («внутренний») конструктор класса, что дает нам область видимости данного класса в рамках проекта. Причем стоит обратить внимание на то, как строится наследование от DataRow. Вы не сможете, унаследовав DataRow, создать класс с «пустым» конструктором. Вам обязательно нужно использовать строку создания с использованием DataRowBuilder класса. Т.е. строка конструктора класса унаследованного от DataRow всегда должна быть вида: <видимость> myDataRow(DataRowBuilder <var>) : base(<var>)

...
public int ItemID {
    get {
        return ((int)(this[this.tableForumMessages.ItemIDColumn]));
    }
    set {
        this[this.tableForumMessages.ItemIDColumn] = value;
    }
}
...

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

...
public int ItemID {
    get {return ((int)(this[this.tableForumMessages.ItemIDColumn]));}
    set {this[this.tableForumMessages.ItemIDColumn] = value;}
}
...

В таком варианте они легче читаются. Но это так сказать дело вкуса. Итак, двинемся потихоньку далее, не забывая писать комментарии.

Что мы видим ниже?

А ниже мы видим пары методов для возможности проверок значений колонок содержащих null, а также для установок значений колонок в null.

«Зачем они нужны?» - спросите Вы. Ввиду того, что значения свойств описывающих колонки не могут в нашем случае иметь значения null (тип int не может быть null согласно синтаксису языка). И мы будем вынуждены в случае необходимости пользоваться данными методами, дабы избежать ошибок.

Как вы видите, визард позаботился даже и о такой приятной мелочи, избавляя нас от написания лишнего кода.

Все. На это описание класса строки окончилось. Не правда ли весьма оптимально написанный класс? В принципе, он не содержит ничего лишнего, достаточно компактен, понятен и расширяем.

Давайте посмотрим на следующий, и быть может, наиболее важный для нас класс в этой иерархии. Это коллекция наших строк DataRow и логично предположить, что коллекцией строк является таблица – DataTable. И снова логика нас не подвела. :)

Давайте пройдемся и по нему.

/*
 * Это класс повторяющий структуру таблицы в качестве строки в нем используется
 * описанный ниже класс строки ForumMessagesRow : DataRow
 */ 
public class ForumMessagesDataTable : DataTable, System.Collections.IEnumerable

Что мы видим: наш класс вполне логично наследуется от базового класса DataTable со всеми соответствующими методами для таблицы, а также реализует интерфейс IEnumerable.

Для чего нужна реализация данного интерфейса? Какие именно методы мы должны реализовать. В этом как всегда нам поможет «наш соратник и друг» MSDN!:)

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

Давайте посмотрим что идет следующим в определении класса таблицы – конечно же объявление колонок которые будут присутствовать в нашей таблице.

...
public class ForumMessagesDataTable : DataTable, System.Collections.IEnumerable 
{
    // Колонки представленные в таблице
    private DataColumn columnItemID;
    private DataColumn columnForumID;
    private DataColumn columnParentItemID;
    private DataColumn columnThreadID;
    private DataColumn columnUserID;
    private DataColumn columnpostDate;
    private DataColumn columnSubject;
    private DataColumn columnText;
    private DataColumn columnNameUser;
    private DataColumn columnEmailUser;
    private DataColumn columnlevel;
    private DataColumn columnsendEmail;
    private DataColumn columnwatched;
...

Как видите мы имеем экземпляры объектов DataColumn которые представляют колонки в нашем объекте таблицы. Названия для объектов «колонок» визард «оформил» в лучшем виде – перепутать практически невозможно. :)

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

Далее. Мы видим блок конструкторов.

И если с конструктором по умолчанию все более или менее ясно:

// Несколько вариантов конструкторов для создания экземпляра таблицы
// Конструктор по умолчанию
internal ForumMessagesDataTable() : base("ForumMessages") 
{
    this.InitClass();
}

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

//Конструктор с параметром в виде подготовленной таблицы.
internal ForumMessagesDataTable(DataTable table) : base(table.TableName) 
{
    if ((table.CaseSensitive != table.DataSet.CaseSensitive)) {
        this.CaseSensitive = table.CaseSensitive;
    }
    if ((table.Locale.ToString() != table.DataSet.Locale.ToString())) {
        this.Locale = table.Locale;
    }
    if ((table.Namespace != table.DataSet.Namespace)) {
        this.Namespace = table.Namespace;
    }
    this.Prefix = table.Prefix;
    this.MinimumCapacity = table.MinimumCapacity;
    this.DisplayExpression = table.DisplayExpression;
}

Для того чтобы понять смысл данного конструктора я предлагаю рассмотреть внимательно метод (this.InitClass()), вызываемый конструктором по умолчанию.

И метод этот, я вам скажу, крайне любопытен и весьма нуждается в подробном рассмотрении.

...
private void InitClass() {
    this.columnItemID = new DataColumn("ItemID", typeof(int), null, System.Data.MappingType.Element);
    this.Columns.Add(this.columnItemID);
...

    this.columnsendEmail = new DataColumn("sendEmail", typeof(bool), null, System.Data.MappingType.Element);
    this.Columns.Add(this.columnsendEmail);

    this.columnwatched = new DataColumn("watched", typeof(bool), null, System.Data.MappingType.Element);
    this.Columns.Add(this.columnwatched);

    this.Constraints.Add(new UniqueConstraint("Constraint1", new DataColumn[] { this.columnItemID}, true));

    this.columnItemID.AutoIncrement = true;
    this.columnItemID.AllowDBNull = false;
    this.columnItemID.ReadOnly = true;
    this.columnItemID.Unique = true;
    this.columnThreadID.AllowDBNull = false;
    this.columnpostDate.AllowDBNull = false;
    this.columnSubject.AllowDBNull = false;
    this.columnText.AllowDBNull = false;
    this.columnNameUser.AllowDBNull = false;
    this.columnlevel.AllowDBNull = false;
    this.columnsendEmail.AllowDBNull = false;
}

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

Взглянем поподробнее на определение колонки «ItemID».

...
this.columnItemID = new DataColumn("ItemID", typeof(int), null, System.Data.MappingType.Element);
...

В данной строке происходит инициализация объекта колонки объявленного выше. Что визард передал в качестве параметров для конструктора? А передал он собственно название колонки, тип ее, значение при создании и так называемый мэппинг - тип, который характеризует нашу колонку при сериализации в XML.

Я думаю, подробней мы коснемся мэппинга в следующей статье, где будет рассматриваться именно XML сериализация dataset и его графическое представление. А в данной ситуации доверимся тому, что сделал за нас «дружелюбный робот».

Теперь подробнее коснемся свойств колонок. Я, например, добавляю всегда инициализацию еще как минимум одного свойства - Caption. Для чего я это делаю? По логике, например тот же графический элемент DataGrid должен был бы автоматически подхватывать Caption для колонки и выводить его в название хидера (планка, которая озаглавливает колонку в гриде) и почему это сейчас не реализовано для меня, честно говоря, остается загадкой. Но даже в первом попавшемся мне для использования «гриде» от стороннего vendor-а, такого как Developer Express это замечательно заработало. И у меня не болела голова с тем, что я могу забыть прописать где-то название колонки и пользователь потом будет меня спрашивать о том, и что это за загадочная колонка такая с именем ItemID. Для этого я предлагаю и вам определять все свойства колонок именно в методе InitClass(). Итак давайте напишем Caption для всех колонок.

Для этого введем в наш код следующий текст:

И тут же, определяя свойства колонки, вы можете увидеть сколько «полезных вещей» предлагается Вам определить для каждой колонки персонально. Это очень важный момент – прошу обратить внимание на то, что «грид» лишь отображает колонки, но никак не придает им каких либо «физических» свойств. «Грид» это всего лишь «графическое» отображение того, что вы определили на «физическом» уровне.

Это очень важный вопрос и большинство наших коллег задают мне именно его на различных форумах. Например: «А как сделать в «гриде» чтобы данная колонка была доступна только для чтения?». Так вот ответ нужно искать именно здесь. На уровне «физического» определения свойств колонок. Как вы будете управлять свойством колонок это Ваше дело, я лишь хочу продемонстрировать то, что это можно и нужно делать именно здесь. Здесь Вы имеете возможность определить для колонки такие полезные свойства как уникальность значений, автоикремент, начальное значение и прирост автоинкремента, поддержку значений null для конкретного поля и массу других возможностей.

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

И что же завершает процесс определения свойств колонки? Вполне логичная команда добавления в коллекцию колонок таблицы:

...
this.Columns.Add(this.columnItemID);
...

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

Далее после всех наших манипуляций со свойствами колонок, мы видим еще одну строку:

...
// Добавление условий на колонки.
this.Constraints.Add(new UniqueConstraint("Constraint1", new DataColumn[] {
this.columnItemID}, true));
...

И это не что иное, как constraint (условие) на нашу с Вами таблицу, а точнее на поле ItemID. И говорит оно о том, что поле ItemID обязано быть уникальным. А что касается синтаксиса, то здесь все довольно просто. В качестве параметра передается имя констрейна (условия) и коллекция колонок. Вот так, достаточно последовательно и добросовестно визард выстроил нам инициализацию таблицы.

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

А передается таблица, повторяющая структуру той же самой таблицы, которая инициализируется нами в методе InitClass(). Сделано это для удобства. Если Вам вдруг понадобится определить свойства колонок для таблицы снаружи dataset, а потом сконструировать ее на основе каркаса, тогда вам незачем будет вызывать метод InitClass(), что собственно данный конструктор и делает. Нужен ли данный конструктор или нет, решать Вам. Я, честно говоря, его ни разу в свой практике не использовал и считаю его излишним.

Есть предложение пройтись по действиям, которые буде совершать конструктор с параметром предопределенной таблицы.

Итак:

... 
if ((table.CaseSensitive != table.DataSet.CaseSensitive)) {
    this.CaseSensitive = table.CaseSensitive;
}
...

Первое условие, определяет, является ли свойство механизма сравнения таблиц связанным с регистром идентичным в предопределенной таблице и в dataset к которому она принадлежит. Весьма правильная проверка, иначе возможно расхождение в методах выборки данных.

Посмотрим далее:

...
if ((table.Locale.ToString() != table.DataSet.Locale.ToString())) {
    this.Locale = table.Locale;
}
...

В общем-то, то же, что и в первом случае только теперь это относится к локализации и что тоже, естественно, влияет на алгоритмы поиска и сортировки.

И третье условие:

...
if ((table.Namespace != table.DataSet.Namespace)) {
    this.Namespace = table.Namespace;
}
...

Проверяет принадлежность предопределенной таблицы одному и тому же неймспейсу (namespace).

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

...
this.Prefix = table.Prefix;
this.MinimumCapacity = table.MinimumCapacity;
this.DisplayExpression = table.DisplayExpression;
...

Первое отвечает за определение неймспейса для предоставления в XML интерпретации, второе отвечает за минимальный размер инициализации таблицы ну а третий отвечает за предоставление вывода возвращаемого значения для данной таблицы.

Ну что же, с конструкторами можно сказать разобрались.

Поехали дальше. А дальше мы имеем набор свойств, для получения значений колонок. Но в начале мы имеем свойство Count с загадочным атрибутом во главе:

[System.ComponentModel.Browsable(false)]

которое возвращает количество строк в таблице. Что же это за атрибут? Данный атрибут определяет нашему свойству «невидимость», если мы вдруг решим определить наш класс в дизайн режим. Дизайн-тайм для компонентов это, возможно, отдельная тема для статьи и потому в настоящий момент мы не будем ее рассматривать в данном контексте. Смиримся с тем, что сделал генератор, хотя бы, потому, что ничего плохого он нам, пока что, не подсовывал.

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

...
internal DataColumn ItemIDColumn { get { return this.columnItemID; } }
internal DataColumn ForumIDColumn { get { return this.columnForumID; } }
internal DataColumn ParentItemIDColumn { get { return this.columnParentItemID; } }
internal DataColumn ThreadIDColumn { get { return this.columnThreadID; } }
internal DataColumn UserIDColumn { get { return this.columnUserID; } }
internal DataColumn postDateColumn { get { return this.columnpostDate; } }
internal DataColumn SubjectColumn { get { return this.columnSubject; } }
internal DataColumn TextColumn { get { return this.columnText; } }
internal DataColumn NameUserColumn { get { return this.columnNameUser; } }
internal DataColumn EmailUserColumn { get { return this.columnEmailUser; } }
internal DataColumn levelColumn { get { return this.columnlevel; } }
internal DataColumn sendEmailColumn { get { return this.columnsendEmail; } }
internal DataColumn watchedColumn { get { return this.columnwatched; } }
...

Зачем это делается, я думаю, Вам понятно – принцип инкапсуляции налицо.

Далее идет свойство индексатора, которое возвращает Вам строку из таблицы тогда, когда Вы делаете обращение в коде, ну скажем вот такого формата: myTable[12].

...
public ForumMessagesRow this[int index] { get { return ((ForumMessagesRow)(this.Rows[index])); } }
...

В этом случае как раз и будет вызван данный индексатор, и он вернет вам строку наследника от DataRow в данном случае экземпляр объекта ForumMessagesRow, который я уже описывал выше.

Далее мы касаемся последнего нашего класса – класса событий.

Перед нашим классом таблицы, стояла совсем не приметная строка объявления делегата. Давайте взглянем на нее.

...
public delegate void ForumMessagesRowChangeEventHandler(object sender, ForumMessagesRowChangeEvent e);
...

Это объявление делегата позволяет визарду сгенерировать в тексте четыре события которые имеют шаблоном вызова методов обработчиков функции, одним из параметров которых является объект нашего третьего класса: public class ForumMessagesRowChangeEvent, являющегося наследником от EventArgs. Это класс событий. Я сейчас не хочу, и не буду вдаваться в механизм работы делегатов потому, что данная тема не освещается в данном контексте и возможно, о делегатах будет рассказано позже. На текущий момент существует уже не мало статей разного уровня описывающих принципы работы делегатов. Одна из самых удачных, по моему мнению, это статья Александра Нестеренко (dotSite virtual team). Прочитать ее можно, сделав переход по ссылке: http://www.dotsite.spb.ru/Publications/PublicationDetails.aspx?ID=55.

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

Add Данная запись была добавлена к таблице
Change Данная строка была изменена
Commit Изменения в строке были подтверждены
Delete Запись была удалена из таблицы
Nothing С записью ничего не происходило
Rollback Все изменения, внесенные в запись, были уничтожены в результате отката, для восстановления первоначальных значений

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

... 
public event ForumMessagesRowChangeEventHandler ForumMessagesRowChanged;
...

Позволит нам определить возможности обработки в тот момент, когда запись уже изменена;

...
public event ForumMessagesRowChangeEventHandler ForumMessagesRowChanging;
...

Обработать ситуацию перед изменением строки;

И такая же ситуация с удалением. Первое событие, когда запись уже помечена на удаление, и то, когда запись начинает только помечаться для удаления.

...
public event ForumMessagesRowChangeEventHandler ForumMessagesRowDeleted;
public event ForumMessagesRowChangeEventHandler ForumMessagesRowDeleting;
...

Теперь переместимся ниже по коду и обратим внимание на четыре перегруженных метода базового класса DataTable:

...
// --  четыре перегруженных метода для определения событий класса
protected override void OnRowChanged(DataRowChangeEventArgs e) 
{
    base.OnRowChanged(e);
    if ((this.ForumMessagesRowChanged != null)) 
    {
        this.ForumMessagesRowChanged(this, new ForumMessagesRowChangeEvent(((ForumMessagesRow)(e.Row)), e.Action));
    }
}

protected override void OnRowChanging(DataRшwChangeEventArgs e) 
{
    base.OnRowChanging(e);
    if ((this.ForumMessagesRowChanging != null)) 
    {
        this.ForumMessagesRowChanging(this, new ForumMessagesRowChangeEvent(((ForumMessagesRow)(e.Row)), e.Action));
    }
}
            
protected override void OnRowDeleted(DataRowChangeEventArgs e) 
{
    base.OnRowDeleted(e);
    if ((this.ForumMessagesRowDeleted != null)) 
    {
       	this.ForumMessagesRowDeleting(this, new ForumMessagesRowChangeEvent(((ForumMessagesRow)(e.Row)), e.Action));
    }
}
            
protected override void OnRowDeleting(DataRowChangeEventArgs e) 
{
    base.OnRowDeleting(e);
    if ((this.ForumMessagesRowDeleting != null)) 
    {
        this.ForumMessagesRowDeleted (this, new ForumMessagesRowChangeEvent(((ForumMessagesRow)(e.Row)), e.Action));
    }
}
// --------------------  
... 

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

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

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

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

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

...
// метод для добавления новой записи в таблицу.
public void AddForumMessagesRow(ForumMessagesRow row)
{
    this.Rows.Add(row);
}
...

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

...
public ForumMessagesRow AddForumMessagesRow(int ForumID, int ParentItemID, int ThreadID, int UserID, System.DateTime postDate, 
    string Subject, string Text, string NameUser, string EmailUser, short level, bool sendEmail, bool watched) 
{
    ForumMessagesRow rowForumMessagesRow = ((ForumMessagesRow)(this.NewRow()));
    rowForumMessagesRow.ItemArray = new object[] 
    {
        null,
        ForumID,
        ParentItemID,
        ThreadID,
        UserID,
        postDate,
        Subject,
        Text,
        NameUser,
        EmailUser,
        level,
        sendEmail,
        watched
    };
    this.Rows.Add(rowForumMessage	Row);
    return rowForumMessagesRow;
}
...

Наверное резонно предположить что Вам захочется проделать и операцию удаления записи – пожалуйста, «генератор» постарался и тут:

...
public void RemoveForumMessagesRow(ForumMessagesRow row) 
{
    this.Rows.Remove(row);
}
...

Идем далее по тексту. Если у вас были определенны Primary ключи для Вашей таблицы – «дружелюбный генератор» построит Вам методы типа FindBy и добавит к ним одну или несколько названий колонок для быстрого поиска в таблице.

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

Также как при удалении и добавлении, возникает вопрос о методах новых строк, и эти вещи оказывается тоже сделаны!

...
// метод добавления новой строки
public ForumMessagesRow NewForumMessagesRow() 
{
    return ((ForumMessagesRow)(this.NewRow()));
}
			
// перегруженный базовый метод, который может 
// быть использован для построения новой строки
protected override DataRow NewRowFromBuilder(DataRowBuilder builder) 
{
    return new ForumMessagesRow(builder);
}
...

Двигаемся далее. Имеем метод Clone() который придется весьма кстати для создания копии таблицы:

...
// Клонирование экземпляра таблицы
public override DataTable Clone() 
{
    ForumMessagesDataTable cln = ((ForumMessagesDataTable)(base.Clone()));
    cln.InitVars();
    return cln;
}
...

Здесь, как Вы видите, вызывается базовый метод Clone() и далее метод, который проводит инициализацию данных из текущей таблицы в копию. Метод InitVars() также создан визардом.

...
// метод инициализации колонок данными при 
// создании например клона таблицы
internal void InitVars() 
{
    this.columnItemID = this.Columns["ItemID"];
    this.columnForumID = this.Columns["ForumID"];
    this.columnParentItemID = this.Columns["ParentItemID"];
    this.columnThreadID = this.Columns["ThreadID"];
    this.columnUserID = this.Columns["UserID"];
    this.columnpostDate = this.Columns["postDate"];
    this.columnSubject = this.Columns["Subject"];
    this.columnText = this.Columns["Text"];
    this.columnNameUser = this.Columns["NameUser"];
    this.columnEmailUser = this.Columns["EmailUser"];
    this.columnlevel = this.Columns["level"];
    this.columnsendEmail = this.Columns["sendEmail"];
    this.columnwatched = this.Columns["watched"];
}
...

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

И я все-таки надеюсь, что мне это удалось. Дело в том, что я не видел ни одной статьи именно с описанием «типизированной» таблицы и я считаю это серьезным упущением.

Ввиду этого я очень часто вижу, как сторонние компании предлагают свои layer-а доступа к БД и, как правило, используют либо идентичную, либо более тяжеловесную логику описания таблиц, чем это уже реализовано в Microsoft. Вот я и попытался заполнить этот пробел, а насколько мне это удалось – судить Вам.

В дальнейших частях «типизированных» мы рассмотрим сериализацию во всей красе XML. А также дизайнер dataset и способы работы с ним.

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


Текст примеров данной статьи можно выкачать здесь

Может пригодится:


Автор: VoRoN
Прочитано: 11251
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру