Поделиться через


Реализация оптимистического параллелизма с помощью элемента управления SqlDataSource (C#)

Скотт Митчелл

Загрузить PDF-файл

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

Введение

В предыдущем руководстве мы рассмотрели, как добавить возможности вставки, обновления и удаления в элемент управления SqlDataSource. Короче говоря, чтобы предоставить эти функции, необходимо указать соответствующую INSERTинструкцию , UPDATEили DELETE SQL в свойствах элемента управления s InsertCommand, UpdateCommandили DeleteCommand , а также соответствующие параметры в InsertParametersколлекциях , UpdateParametersи DeleteParameters . Хотя эти свойства и коллекции можно указать вручную, кнопка Дополнительно мастера настройки источника данных предлагает флажок Создать INSERTинструкции , UPDATEи DELETE , который будет автоматически создавать эти инструкции на основе инструкции SELECT .

Наряду с флажком Создать INSERTинструкции , UPDATEи DELETE диалоговое окно Расширенные параметры создания SQL включает параметр Использовать оптимистичный параллелизм (см. рис. 1). Если этот флажок установлен, WHERE предложения в автоматически созданных UPDATE инструкциях и DELETE изменяются для выполнения обновления или удаления, только если базовые данные базы данных не были изменены с момента последней загрузки данных пользователем в сетку.

Вы можете добавить поддержку оптимистичного параллелизма в диалоговом окне Расширенные параметры создания SQL.

Рис. 1. Вы можете добавить поддержку оптимистичного параллелизма из диалогового окна Расширенные параметры создания SQL

В учебнике Реализация оптимистического параллелизма мы рассмотрели основы управления оптимистическим параллелизмом и способы его добавления в ObjectDataSource. В этом руководстве мы ретушируем основы управления оптимистичным параллелизмом, а затем рассмотрим, как реализовать его с помощью SqlDataSource.

Краткое описание оптимистичного параллелизма

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

Представьте, что два пользователя, Jisun и Сэм, посещали страницу в приложении, которая позволяла посетителям обновлять и удалять продукты с помощью элемента управления GridView. Оба нажатия кнопки Изменить для Chai примерно в одно и то же время. Jisun изменяет название продукта на Чай Чай и нажимает кнопку Обновить. Результатом UPDATE является инструкция, которая отправляется в базу данных, которая задает все обновляемые поля продукта (даже если Jisun обновил только одно поле , ProductName). На данный момент времени в базе данных есть значения Чай Чай, категория Напитки, поставщик экзотических жидкостей и т. д. для данного конкретного продукта. Однако gridView на экране Sam по-прежнему отображает название продукта в редактируемой строке GridView как Chai. Через несколько секунд после фиксации изменений Jisun Сэм обновляет категорию на Condiments и нажимает кнопку Обновить. В результате в UPDATE базу данных отправляется инструкция, которая задает для имени продукта значение Chai, для CategoryID — соответствующий идентификатор категории Condiments и т. д. Изменения jisun в названии продукта были перезаписаны.

На рисунке 2 показано это взаимодействие.

При одновременном обновлении записи двумя пользователями возможны изменения для перезаписи других пользователей

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

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

Примечание

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

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

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

Рис. 3. Для обновления или удаления для успешного выполнения исходные значения должны быть равны значениям текущей базы данных (щелкните для просмотра полноразмерного изображения)

Существуют различные подходы к реализации оптимистичного параллелизма (см. раздел Питер А. Бромберг в разделе Оптимистическая логика обновления параллелизма , чтобы кратко ознакомиться с рядом вариантов). Метод, используемый SqlDataSource (а также ADO.NET typed DataSets, используемый в нашем уровне доступа к данным), дополняет WHERE предложение, чтобы включить сравнение всех исходных значений. UPDATE Следующая инструкция, например, обновляет имя и цену продукта, только если текущие значения базы данных равны значениям, которые были получены при обновлении записи в GridView. Параметры @ProductName и @UnitPrice содержат новые значения, введенные пользователем, тогда как @original_ProductName и @original_UnitPrice содержат значения, которые изначально были загружены в GridView при нажатии кнопки Изменить:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Как мы увидим в этом руководстве, включить управление оптимистическим параллелизмом с помощью SqlDataSource так же просто, как установить флажок.

Шаг 1. Создание SqlDataSource, поддерживающего оптимистичный параллелизм

Начните с открытия страницы OptimisticConcurrency.aspx из SqlDataSource папки. Перетащите элемент управления SqlDataSource из панели элементов в Designer, задав для его ID свойства значение ProductsDataSourceWithOptimisticConcurrency. Затем щелкните ссылку Настройка источника данных в смарт-теге элемента управления. На первом экране мастера выберите для работы с и нажмите кнопку NORTHWINDConnectionString Далее.

Выберите для работы с NORTHWINDConnectionString

Рис. 4. Выберите для работы с NORTHWINDConnectionString (Щелкните для просмотра полноразмерного изображения)

В этом примере мы добавим Элемент GridView, который позволяет пользователям редактировать таблицу Products . Поэтому на экране Настройка инструкции select выберите таблицу Products из раскрывающегося списка и столбцы ProductID, ProductName, UnitPriceи Discontinued , как показано на рисунке 5.

Из таблицы Products верните столбцы ProductID, ProductName, UnitPrice и Discontinued.

Рис. 5. Из Products таблицы возвращает ProductIDстолбцы , ProductName, UnitPriceи Discontinued (щелкните для просмотра полноразмерного изображения)

Выбрав столбцы, нажмите кнопку Дополнительно, чтобы открыть диалоговое окно Расширенные параметры создания SQL. Установите флажки Создать INSERTоператоры , UPDATEи DELETE и Использовать оптимистичный параллелизм и нажмите кнопку ОК (см. снимок экрана на рис. 1). Завершите работу мастера, нажав кнопку Далее, а затем — Готово.

После завершения работы мастера настройки источника данных изучите результирующие DeleteCommand свойства и UpdateCommand , а также DeleteParameters коллекции и UpdateParameters . Самый простой способ сделать это — щелкнуть вкладку Источник в левом нижнем углу, чтобы увидеть декларативный синтаксис страницы. Здесь вы найдете значение UpdateCommand :

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

С семью параметрами в UpdateParameters коллекции:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
      ...
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
    ...
</asp:SqlDataSource>

Аналогичным образом свойство DeleteCommand и DeleteParameters коллекция должны выглядеть следующим образом:

DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued
<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        ...
    </UpdateParameters>
    ...
</asp:SqlDataSource>

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

Когда веб-элемент управления данными вызывает метод Или SqlDataSource Update()Delete() , он передает исходные значения. Если свойство SqlDataSource ConflictDetection имеет значение CompareAllValues, эти исходные значения добавляются в команду . Свойство OldValuesParameterFormatString предоставляет шаблон именования, используемый для этих исходных параметров значений. Мастер настройки источника данных использует original_{0} и присваивает имена каждому исходному параметру UpdateCommand в свойствах и DeleteCommand и UpdateParametersDeleteParameters коллекциях соответственно.

Примечание

Так как мы не используем возможности вставки элемента управления SqlDataSource, вы можете удалить InsertCommand свойство и его InsertParameters коллекцию.

Правильная обработкаNULLзначений

К сожалению, дополненные UPDATE инструкции и DELETE , автоматически созданные мастером настройки источника данных при использовании оптимистичного параллелизма , не работают с записями, содержащими NULL значения. Чтобы понять причину, рассмотрим наши sqlDataSource :UpdateCommand

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

Столбец UnitPrice в Products таблице может иметь NULL значения. Если определенная запись имеет NULL значение , UnitPriceWHERE часть [UnitPrice] = @original_UnitPrice предложения всегда будет иметь значение False, так как NULL = NULL всегда возвращает значение False. Таким образом, записи, содержащие NULL значения, не могут быть изменены или удаленыWHERE, так как UPDATE предложения инструкций и DELETE не возвращают строки для обновления или удаления.

Примечание

Эта ошибка была впервые зарегистрирована в корпорации Майкрософт в июне 2004 г. в sqlDataSource Generates Incorrect SQL Statements и, как сообщается, будет исправлена в следующей версии ASP.NET.

Чтобы исправить это, необходимо вручную обновить WHERE предложения в UpdateCommand свойствах и DeleteCommand для всех столбцов, которые могут иметь NULL значения. Как правило, измените на [ColumnName] = @original_ColumnName :

(
   ([ColumnName] IS NULL AND @original_ColumnName IS NULL)
     OR
   ([ColumnName] = @original_ColumnName)
)

Это изменение можно внести непосредственно с помощью декларативной разметки, параметров UpdateQuery или DeleteQuery из окно свойств или на вкладках UPDATE и DELETE в параметре Указать настраиваемую инструкцию SQL или хранимую процедуру в мастере настройки источника данных. Опять же, это изменение необходимо внести для каждого столбца в предложении UpdateCommand и DeleteCommand , WHERE который может содержать NULL значения.

Применение этого к нашему примеру приводит к следующим измененным UpdateCommand значениям и DeleteCommand :

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued
DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

Шаг 2. Добавление GridView с параметрами "Изменить" и "Удалить"

Если sqlDataSource настроен для поддержки оптимистичного параллелизма, остается только добавить веб-элемент управления данными на страницу, которая использует этот элемент управления параллелизмом. В этом руководстве мы добавим Элемент GridView, который предоставляет функции редактирования и удаления. Для этого перетащите Элемент GridView из панели элементов на Designer и задайте для нее ID значение Products. Из смарт-тега GridView привяжите его к элементу ProductsDataSourceWithOptimisticConcurrency управления SqlDataSource, добавленного на шаге 1. Наконец, проверка параметры Включить редактирование и Включить удаление из смарт-тега.

Привязка GridView к SqlDataSource и включение редактирования и удаления

Рис. 6. Привязка GridView к SqlDataSource и включение редактирования и удаления (щелкните для просмотра полноразмерного изображения)

После добавления GridView настройте его внешний вид, удалив ProductID BoundField, изменив ProductName свойство BoundField HeaderText на Product и обновив UnitPrice BoundField, чтобы его HeaderText свойство было просто Price. В идеале мы должны улучшить интерфейс редактирования, включив RequiredFieldValidator для ProductName значения и CompareValidator для UnitPrice значения (чтобы обеспечить правильное форматирование числовых значений). Дополнительные сведения о настройке интерфейса редактирования GridView см. в руководстве По настройке интерфейса изменения данных.

Примечание

Состояние представления GridView должно быть включено, так как исходные значения, передаваемые из GridView в SqlDataSource, хранятся в состоянии представления.

После внесения этих изменений в GridView декларативная разметка GridView и SqlDataSource должна выглядеть примерно так:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ConflictDetection="CompareAllValues"
    ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
    DeleteCommand=
        "DELETE FROM [Products]
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
              OR ([UnitPrice] = @original_UnitPrice))
         AND [Discontinued] = @original_Discontinued"
    OldValuesParameterFormatString=
        "original_{0}"
    SelectCommand=
        "SELECT [ProductID], [ProductName], [UnitPrice], [Discontinued]
         FROM [Products]"
    UpdateCommand=
        "UPDATE [Products]
         SET [ProductName] = @ProductName, [UnitPrice] = @UnitPrice,
            [Discontinued] = @Discontinued
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
            OR ([UnitPrice] = @original_UnitPrice))
        AND [Discontinued] = @original_Discontinued">
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
</asp:SqlDataSource>
<asp:GridView ID="Products" runat="server"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ProductsDataSourceWithOptimisticConcurrency">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" HeaderText="Price"
            SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

Чтобы увидеть элемент управления оптимистическим параллелизмом в действии, откройте два окна браузера и загрузите страницу OptimisticConcurrency.aspx в обоих. Нажмите кнопки Изменить для первого продукта в обоих браузерах. В одном браузере измените название продукта и нажмите кнопку Обновить. Браузер выполнит обратную передачу, и GridView вернется в режим предварительного редактирования, отображая новое название продукта для только что измененной записи.

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

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

Рис. 7. Изменения во втором окне браузера были потеряны автоматически (щелкните для просмотра полноразмерного изображения)

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

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL) OR
        ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

Когда во втором окне браузера обновляется запись, исходное имя продукта, указанное WHERE в предложении , не совпадает с существующим названием продукта (так как оно было изменено первым браузером). Таким образом, оператор [ProductName] = @original_ProductName возвращает значение False, а UPDATE не влияет на записи.

Примечание

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

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

Шаг 3. Определение того, когда произошло нарушение параллелизма

Так как нарушение параллелизма отклоняет внесенные изменения, было бы неплохо оповещать пользователя о нарушении параллелизма. Чтобы предупредить пользователя, добавьте веб-элемент управления Label в верхнюю часть страницы с именем ConcurrencyViolationMessage , свойство которого Text отображает следующее сообщение: Вы попытались обновить или удалить запись, которая была одновременно обновлена другим пользователем. Просмотрите изменения другого пользователя, а затем повторите обновление или удалите его. Присвойте свойству Label control s CssClass значение Warning— класс CSS, определенный в Styles.css , который отображает текст красным, курсивом, полужирным шрифтом и крупным шрифтом. Наконец, присвойте свойствам Label и VisibleEnableViewState значение false. При этом метка будет скрыта, за исключением только тех обратных передач, для которых мы явно задали его Visible свойству значение true.

Добавление элемента управления

Рис. 8. Добавление элемента управления "Метка" на страницу для отображения предупреждения (щелкните для просмотра полноразмерного изображения)

При выполнении обновления или удаления обработчики событий GridView RowUpdated и RowDeleted срабит после того, как его элемент управления источником данных выполнил запрошенное обновление или удаление. Мы можем определить, сколько строк было затронуто операцией, из этих обработчиков событий. Если были затронуты нулевые строки, необходимо отобразить метку ConcurrencyViolationMessage .

Создайте обработчик событий и для RowUpdated событий и RowDeleted и добавьте следующий код:

protected void Products_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.AffectedRows == 0)
    {
        ConcurrencyViolationMessage.Visible = true;
        e.KeepInEditMode = true;
        // Rebind the data to the GridView to show the latest changes
        Products.DataBind();
    }
}
protected void Products_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    if (e.AffectedRows == 0)
        ConcurrencyViolationMessage.Visible = true;
}

В обоих обработчиках событий мы проверка e.AffectedRows свойство и, если оно равно 0, присвойте свойству ConcurrencyViolationMessage Label s Visible значение true. В обработчике RowUpdated событий мы также предписываем GridView оставаться в режиме KeepInEditMode редактирования, задав для свойства значение true. При этом необходимо повторно привязать данные к сетке, чтобы данные другого пользователя загружались в интерфейс редактирования. Это достигается путем вызова метода GridView.DataBind()

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

Сообщение отображается перед лицом нарушения параллелизма

Рис. 9. Сообщение отображается в лице нарушения параллелизма (щелкните для просмотра полноразмерного изображения)

Сводка

При создании веб-приложения, в котором несколько одновременных пользователей могут редактировать одни и те же данные, важно учитывать параметры управления параллелизмом. По умолчанию ASP.NET веб-элементы управления данными и элементы управления источником данных не используют управление параллелизмом. Как мы видели в этом руководстве, реализация управления оптимистическим параллелизмом с помощью SqlDataSource является относительно быстрой и простой. SqlDataSource обрабатывает большую часть работы по добавлению дополненных WHERE предложений в автоматически созданные UPDATE операторы и и DELETE , но есть несколько тонкостей в обработке NULL столбцов значений, как описано в разделе Правильная обработка NULL значений.

В этом руководстве мы завершаем изучение SqlDataSource. Оставшиеся руководства вернутся к работе с данными с использованием ObjectDataSource и многоуровневой архитектуры.

Счастливое программирование!

Об авторе

Скотт Митчелл (Scott Mitchell), автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с Веб-технологиями Майкрософт с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Sams Teach Yourself ASP.NET 2.0 в 24 часа. Его можно связать по адресу mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.