Реализация оптимистичного параллелизма (C#)

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

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

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

Введение

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

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

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

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

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

Существует три доступных стратегии управления параллелизмом :

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

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

Примечание

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

Шаг 1. Просмотр принципов реализации оптимистичного параллелизма

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

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

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

Существуют различные подходы к реализации оптимистичного параллелизма (см. раздел Питер А. Бромберг в разделе Оптимистическая логика обновления параллелизма , чтобы кратко ознакомиться с рядом вариантов). Типизированный набор данных ADO.NET предоставляет одну реализацию, которую можно настроить с помощью флажка. Включение оптимистичного параллелизма для Объекта TableAdapter в typed DataSet дополняет операторы TableAdapter UPDATE и , DELETE чтобы включить сравнение всех исходных значений в предложении 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

Примечание

Эта UPDATE инструкция упрощена для удобства чтения. На практике проверка в предложении WHERE будет более активной, так как UnitPrice может содержать NULL s и проверять, UnitPrice если NULL = NULL всегда возвращает значение False (вместо этого необходимо использовать IS NULL).

Помимо использования другой базовой UPDATE инструкции, настройка TableAdapter для использования оптимистичного параллелизма также изменяет сигнатуру прямых методов базы данных. В нашем первом учебнике Создание уровня доступа к данным мы узнали, что прямые методы базы данных — это методы, принимающие список скалярных значений в качестве входных параметров (а не строго типизированный экземпляр DataRow или DataTable). При использовании оптимистичного параллелизма прямые Update() методы базы данных и Delete() включают входные параметры для исходных значений. Кроме того, необходимо изменить код в BLL для использования шаблона пакетного обновления ( Update() перегрузки методов, которые принимают DataRows и DataTables, а не скалярные значения).

Вместо того, чтобы расширять существующие табличные адаптеры DAL для использования оптимистичного параллелизма (что потребует изменения BLL в соответствии с потребностями), давайте создадим новый типизированный набор данных с именем NorthwindOptimisticConcurrency, в который мы добавим Products TableAdapter, использующий оптимистичный параллелизм. После этого мы создадим ProductsOptimisticConcurrencyBLL класс уровня бизнес-логики, который имеет соответствующие изменения для поддержки DAL оптимистичного параллелизма. После того как эта основа будет создана, мы будем готовы к созданию страницы ASP.NET.

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

Чтобы создать типизированный набор данных, щелкните правой кнопкой мыши папку DAL в папке App_Code и добавьте новый набор данных с именем NorthwindOptimisticConcurrency. Как мы видели в первом руководстве, это приведет к добавлению нового объекта TableAdapter в typed DataSet и автоматическому запуску мастера настройки TableAdapter. На первом экране нам будет предложено указать базу данных для подключения — подключиться к той же базе данных Northwind с помощью NORTHWNDConnectionString параметра из Web.config.

Подключение к той же базе данных Northwind

Рис. 3. Подключение к той же базе данных Northwind (щелкните для просмотра полноразмерного изображения)

Далее нам будет предложено запросить данные: с помощью нерегламентированной инструкции SQL, новой хранимой процедуры или существующей хранимой процедуры. Так как мы использовали нерегламентированные SQL-запросы в исходном DAL, используйте этот параметр и здесь.

Указание данных для извлечения с помощью нерегламентированной инструкции SQL

Рис. 4. Указание данных для извлечения с помощью нерегламентированной инструкции SQL (щелкните для просмотра полноразмерного изображения)

На следующем экране введите SQL-запрос, который будет использоваться для получения сведений о продукте. Давайте воспользуемся тем же SQL-запросом, который используется для Products TableAdapter из исходного Product DAL, который возвращает все столбцы, а также имена поставщиков и категорий продукта:

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

Использование того же SQL-запроса из адаптера таблицы Products в исходном DAL

Рис. 5. Использование того же SQL-запроса из Products TableAdapter в исходном DAL (Щелкните для просмотра полноразмерного изображения)

Перед переходом на следующий экран нажмите кнопку Дополнительные параметры. Чтобы этот объект TableAdapter использовал элемент управления оптимистическим параллелизмом, просто проверка флажок "Использовать оптимистичный параллелизм".

Включите управление оптимистичным параллелизмом, установив флажок

Рис. 6. Включение управления оптимистичным параллелизмом с помощью флажка "Использовать оптимистичный параллелизм" (щелкните для просмотра полноразмерного изображения)

Наконец, укажите, что TableAdapter должен использовать шаблоны доступа к данным, которые заполняют таблицу DataTable и возвращают таблицу Данных; также указывает, что необходимо создать прямые методы базы данных. Измените имя метода для шаблона Return a DataTable с GetData на GetProducts, чтобы зеркало соглашения об именовании, которые мы использовали в исходном DAL.

Использование всех шаблонов доступа к данным в tableadapter

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

После завершения работы мастера Designer DataSet будет включать строго типизированные Products таблицы DataTable и TableAdapter. Уделите немного времени, чтобы переименовать DataTable с Products на ProductsOptimisticConcurrency, что можно сделать, щелкнув правой кнопкой мыши строку заголовка DataTable и выбрав команду Переименовать в контекстном меню.

DataTable и TableAdapter добавлены в типизированный набор данных.

Рис. 8. Таблицы Данных и TableAdapter добавлены в типизированный набор данных (щелкните для просмотра полноразмерного изображения)

Чтобы увидеть различия между UPDATE запросами и DELETE между ProductsOptimisticConcurrency TableAdapter (который использует оптимистичный параллелизм) и Products TableAdapter (который не используется), щелкните TableAdapter и перейдите к окно свойств. DeleteCommand В подсвойствах свойств CommandText и UpdateCommand можно увидеть фактический синтаксис SQL, который отправляется в базу данных при вызове методов, связанных с обновлением или удалением DAL. ProductsOptimisticConcurrency Для TableAdapter используется следующая DELETE инструкция:

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

DELETE В то время как инструкция для Product TableAdapter в исходном DAL гораздо проще:

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

Как видите, WHERE предложение в DELETE инструкции для TableAdapter, использующего оптимистичный параллелизм, включает сравнение всех Product существующих значений столбцов таблицы и исходных значений на момент последнего заполнения GridView (или DetailsView или FormView). Так как все поля, кроме ProductID, ProductNameи Discontinued могут иметь NULL значения, для правильного сравнения NULL значений в предложении WHERE включаются дополнительные параметры и проверки.

Мы не будем добавлять дополнительные данные DataTable в набор данных с поддержкой оптимистичного параллелизма для этого руководства, так как наша ASP.NET страница будет содержать только обновление и удаление сведений о продукте. Однако нам по-прежнему GetProductByProductID(productID) нужно добавить метод в ProductsOptimisticConcurrency TableAdapter.

Для этого щелкните правой кнопкой мыши строку заголовка TableAdapter (область над Fill именами методов и GetProducts ) и выберите в контекстном меню пункт Добавить запрос. Запустится мастер настройки запросов TableAdapter. Как и в случае с начальной конфигурацией tableAdapter, создайте GetProductByProductID(productID) метод с помощью нерегламентированной инструкции SQL (см. рис. 4). GetProductByProductID(productID) Так как метод возвращает сведения о конкретном продукте, укажите, что этот запрос является типом SELECT запроса, возвращающего строки.

Пометьте тип запроса как

Рис. 9. Пометка типа запроса как "SELECT , возвращающего строки" (щелкните для просмотра полноразмерного изображения)

На следующем экране нам будет предложено использовать SQL-запрос с предварительно загруженным запросом по умолчанию TableAdapter. Дополнить существующий запрос, включив предложение WHERE ProductID = @ProductID, как показано на рисунке 10.

Добавление предложения WHERE в предварительно загруженный запрос для возврата определенной записи продукта

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

Наконец, измените созданные имена методов на FillByProductID и GetProductByProductID.

Переименуйте методы в FillByProductID и GetProductByProductID.

Рис. 11. Переименование методов в FillByProductID и GetProductByProductID (щелкните для просмотра полноразмерного изображения)

После завершения работы мастера TableAdapter теперь содержит два метода для получения данных: GetProducts(), который возвращает все продукты; и GetProductByProductID(productID), который возвращает указанный продукт.

Шаг 3. Создание уровня бизнес-логики для оптимистичного Concurrency-Enabled DAL

Наш существующий ProductsBLL класс содержит примеры использования как шаблонов пакетного обновления, так и прямых шаблонов базы данных. Метод AddProduct и UpdateProduct перегрузки используют шаблон пакетного обновления, передавая ProductRow экземпляр методу Update TableAdapter. Метод DeleteProduct , с другой стороны, использует прямой шаблон базы данных, вызывая метод TableAdapter Delete(productID) .

В новом ProductsOptimisticConcurrency TableAdapter прямые методы базы данных теперь требуют, чтобы исходные значения также передавались. Например, Delete метод теперь ожидает десять входных параметров: исходные ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, , UnitsInStock, UnitsOnOrder, ReorderLevelи Discontinued. Он использует значения этих дополнительных входных параметров в предложении инструкцииDELETE, отправленной в WHERE базу данных, и удаляет указанную запись только в том случае, если текущие значения базы данных сопоставляют с исходными.

Хотя сигнатура Update метода для метода TableAdapter, используемого в шаблоне пакетного обновления, не изменилась, код, необходимый для записи исходных и новых значений, имеет значение . Поэтому вместо того, чтобы пытаться использовать DAL с поддержкой оптимистичного параллелизма с существующим ProductsBLL классом, давайте создадим класс уровня бизнес-логики для работы с новым DAL.

Добавьте класс с именем ProductsOptimisticConcurrencyBLL в папку BLL в папке App_Code .

Добавление класса ProductsOptimisticConcurrencyBLL в папку BLL

Рис. 12. Добавление класса в ProductsOptimisticConcurrencyBLL папку BLL

Затем добавьте следующий код в ProductsOptimisticConcurrencyBLL класс :

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

Обратите внимание на инструкцию using NorthwindOptimisticConcurrencyTableAdapters над началом объявления класса. Пространство NorthwindOptimisticConcurrencyTableAdapters имен содержит ProductsOptimisticConcurrencyTableAdapter класс , который предоставляет методы DAL. Кроме того, перед объявлением класса вы найдете System.ComponentModel.DataObject атрибут , который указывает Visual Studio включить этот класс в раскрывающийся список мастера ObjectDataSource.

Свойство ProductsOptimisticConcurrencyBLLпредоставляет Adapter быстрый доступ к экземпляру ProductsOptimisticConcurrencyTableAdapter класса и соответствует шаблону, используемому в наших исходных классах BLL (ProductsBLL, CategoriesBLLи т. д.). Наконец, GetProducts() метод просто вызывает метод DAL GetProducts() и возвращает ProductsOptimisticConcurrencyDataTable объект, заполненный экземпляром для каждой ProductsOptimisticConcurrencyRow записи продукта в базе данных.

Удаление продукта с помощью прямого шаблона базы данных с оптимистическим параллелизмом

При использовании прямого шаблона базы данных для DAL, использующего оптимистичный параллелизм, методы должны передавать новые и исходные значения. Для удаления новые значения отсутствуют, поэтому необходимо передать только исходные значения. В нашем BLL мы должны принять все исходные параметры в качестве входных параметров. Давайте сделаем так, DeleteProduct чтобы метод в ProductsOptimisticConcurrencyBLL классе использовал прямой метод базы данных. Это означает, что этот метод должен принимать все десять полей данных продукта в качестве входных параметров и передавать их в DAL, как показано в следующем коде:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

Если исходные значения ( те, которые были в последний раз загружены в GridView или DetailsView или FormView) - отличаются от значений в базе данных, когда пользователь нажимает кнопку WHERE Удалить, предложение не будет соответствовать ни одной записи базы данных, и записи не будут затронуты. Таким образом, метод TableAdapter Delete возвращает 0 , а метод BLL DeleteProduct возвращает false.

Обновление продукта с помощью шаблона пакетного обновления с оптимистическим параллелизмом

Как отмечалось ранее, метод TableAdapter для шаблона пакетного обновления имеет одинаковую сигнатуру Update метода независимо от того, используется ли оптимистичный параллелизм. А именно, Update метод ожидает DataRow, массив DataRows, DataTable или Typed DataSet. Дополнительные входные параметры для указания исходных значений отсутствуют. Это возможно, так как DataTable отслеживает исходные и измененные значения для своих объектов DataRow. Когда DAL выдает свою UPDATE инструкцию @original_ColumnName , параметры заполняются исходными значениями DataRow, а @ColumnName параметры заполняются измененными значениями DataRow.

ProductsBLL В классе (который использует исходный, неоптимический параллелизм DAL) при использовании шаблона пакетного обновления для обновления сведений о продукте наш код выполняет следующую последовательность событий:

  1. Чтение текущей информации о продукте базы данных в ProductRow экземпляр с помощью метода TableAdapter GetProductByProductID(productID)
  2. Назначение новых значений экземпляру из ProductRow шага 1
  3. Вызов метода TableAdapterUpdate, передав экземпляр ProductRow

Однако эта последовательность шагов не будет правильно поддерживать оптимистичный параллелизм, так как ProductRow заполненный на шаге 1 заполняется непосредственно из базы данных, а это означает, что исходные значения, используемые DataRow, — это те, которые в настоящее время существуют в базе данных, а не те, которые были привязаны к GridView в начале процесса редактирования. Вместо этого при использовании DAL с поддержкой оптимистичного параллелизма необходимо изменить перегрузки UpdateProduct метода, чтобы выполнить следующие действия:

  1. Чтение текущей информации о продукте базы данных в ProductsOptimisticConcurrencyRow экземпляр с помощью метода TableAdapter GetProductByProductID(productID)
  2. Назначение исходных значений экземпляру из ProductsOptimisticConcurrencyRow шага 1
  3. ProductsOptimisticConcurrencyRow Вызов метода экземпляраAcceptChanges(), который указывает DataRow, что его текущие значения являются "исходными"
  4. Назначение новых значений экземпляру ProductsOptimisticConcurrencyRow
  5. Вызов метода TableAdapterUpdate, передав экземпляр ProductsOptimisticConcurrencyRow

Шаг 1 считывает все текущие значения базы данных для указанной записи продукта. Этот шаг является излишним в UpdateProduct перегрузке, которая обновляет все столбцы продукта (так как эти значения перезаписываются на шаге 2), но имеет важное значение для тех перегрузок, где в качестве входных параметров передается только подмножество значений столбцов. После назначения исходных значений экземпляру ProductsOptimisticConcurrencyRowAcceptChanges() вызывается метод , который помечает текущие значения DataRow как исходные значения для использования в @original_ColumnName параметрах в инструкции UPDATE . Затем новые значения параметров назначаются объекту ProductsOptimisticConcurrencyRow и, наконец, Update вызывается метод , передавая dataRow.

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

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
    // STEP 2: Assign the original values to the product instance
    AssignAllProductValues(product, original_productName, original_supplierID,
        original_categoryID, original_quantityPerUnit, original_unitPrice,
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
        original_discontinued);
    // STEP 3: Accept the changes
    product.AcceptChanges();
    // STEP 4: Assign the new values to the product instance
    AssignAllProductValues(product, productName, supplierID, categoryID,
        quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
        discontinued);
    // STEP 5: Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

Шаг 4. Передача исходных и новых значений со страницы ASP.NET в методы BLL

После завершения DAL и BLL остается только создать ASP.NET страницу, которая может использовать логику оптимистического параллелизма, встроенную в систему. В частности, веб-элемент управления данными (GridView, DetailsView или FormView) должен помнить свои исходные значения, а ObjectDataSource должен передавать оба набора значений уровню бизнес-логики. Кроме того, страница ASP.NET должна быть настроена для корректной обработки нарушений параллелизма.

Для начала откройте страницу OptimisticConcurrency.aspx в папке EditInsertDelete и добавьте GridView в Designer, задав для его ID свойства значение ProductsGrid. В смарт-теге GridView выберите создать объект ObjectDataSource с именем ProductsOptimisticConcurrencyDataSource. Так как мы хотим, чтобы объект ObjectDataSource использовал DAL, поддерживающий оптимистичный параллелизм, настройте его для использования ProductsOptimisticConcurrencyBLL объекта .

Использование Объекта ObjectDataSource объекта ProductsOptimisticConcurrencyBLL

Рис. 13. Использование объекта ObjectDataSource ProductsOptimisticConcurrencyBLL (щелкните для просмотра полноразмерного изображения)

Выберите методы GetProducts, UpdateProductи DeleteProduct из раскрывающихся списков в мастере. Для метода UpdateProduct используйте перегрузку, которая принимает все поля данных продукта.

Настройка свойств элемента управления ObjectDataSource

После завершения работы мастера декларативная разметка ObjectDataSource должна выглядеть следующим образом:

<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

Как видите, DeleteParameters коллекция содержит Parameter экземпляр для каждого из десяти входных параметров в методе ProductsOptimisticConcurrencyBLLDeleteProduct класса . Аналогичным образом коллекция UpdateParameters содержит Parameter экземпляр для каждого входного параметра в UpdateProduct.

В предыдущих руководствах, которые включали изменение данных, на этом этапе мы удаляем OldValuesParameterFormatString свойство ObjectDataSource, так как это свойство указывает, что метод BLL ожидает передачи старых (или исходных) значений, а также новых значений. Кроме того, это значение свойства указывает имена входных параметров для исходных значений. Так как мы передаем исходные значения в BLL, не удаляйте это свойство.

Примечание

Значение OldValuesParameterFormatString свойства должно сопоставляться с именами входных параметров в BLL, которые ожидают исходные значения. Так как мы назвали эти параметры original_productName, original_supplierIDи т. д., можно оставить OldValuesParameterFormatString значение свойства как original_{0}. Однако если входные параметры методов BLL имеют такие имена, как old_productName, old_supplierIDи т. д., необходимо обновить OldValuesParameterFormatString свойство на old_{0}.

Существует одно окончательное значение свойства, которое необходимо сделать, чтобы ObjectDataSource правильно передавал исходные значения в методы BLL. ObjectDataSource имеет свойство ConflictDetection , которое может быть назначено одному из двух значений:

  • OverwriteChanges — значение по умолчанию; не отправляет исходные значения в исходные входные параметры методов BLL.
  • CompareAllValues — отправляет исходные значения в методы BLL; Выбор этого параметра при использовании оптимистичного параллелизма

Уделите некоторое время, чтобы задать ConflictDetection для свойства значение CompareAllValues.

Настройка свойств и полей GridView

После правильной настройки свойств ObjectDataSource давайте переключим наше внимание на настройку GridView. Во-первых, так как мы хотим, чтобы GridView поддерживал редактирование и удаление, установите флажки Включить редактирование и Включить удаление из смарт-тега GridView. При этом будет добавлен commandField, для которого ShowEditButton для ShowDeleteButton обоих задано значение true.

При привязке ProductsOptimisticConcurrencyDataSource к ObjectDataSource GridView содержит поле для каждого поля данных продукта. Хотя такой GridView можно изменить, пользовательский интерфейс является любым, кроме приемлемым. SupplierID И CategoryID BoundFields будут отображаться как TextBoxes, требуя, чтобы пользователь ввел соответствующую категорию, а поставщик — в качестве идентификаторов. Для числовых полей не будет форматирования и элементов управления проверкой, чтобы убедиться, что указано название продукта и что цена за единицу, единицы на складе, единицы в заказе и значения уровня изменения порядка являются правильными числовыми значениями и больше или равны нулю.

Как мы обсуждали в руководствах Добавление элементов управления проверкой в интерфейсы редактирования и вставки и Настройка интерфейса изменения данных , пользовательский интерфейс можно настроить, заменив BoundFields на TemplateFields. Я изменил этот GridView и его интерфейс редактирования следующими способами:

  • Удалены ProductIDполя , SupplierNameи CategoryName BoundFields.
  • Преобразование BoundField в ProductName TemplateField и добавление элемента управления RequiredFieldValidation.
  • Преобразовали CategoryID и SupplierID BoundFields в TemplateFields и настроили интерфейс редактирования, чтобы использовать DropDownLists, а не TextBoxes. В этих TemplateFields ItemTemplatesCategoryName отображаются поля данных и SupplierName .
  • Преобразованы UnitPriceполя , UnitsInStock, UnitsOnOrderи ReorderLevel BoundFields в TemplateFields и добавлены элементы управления CompareValidator.

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

<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

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

Примечание

Чтобы веб-элемент управления данными правильно передавал исходные значения в ObjectDataSource (которые затем передаются в BLL), очень важно, чтобы свойству GridView EnableViewState было присвоено значение true (значение по умолчанию). При отключении состояния представления исходные значения теряются при обратной отправке.

Передача правильных исходных значений в ObjectDataSource

Существует несколько проблем с настройкой GridView. Если свойству ObjectDataSource ConflictDetection присвоено значение CompareAllValues (как и в нашем), то при вызове методов Или Delete() ObjectDataSource Update() в GridView (или DetailsView или FormView), ObjectDataSource пытается скопировать исходные значения GridView в соответствующие Parameter экземпляры. Графическое представление этого процесса см. на рисунке 2.

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

Чтобы узнать, почему это важно, посетите нашу страницу в браузере. Как и ожидалось, GridView выводит список всех продуктов с помощью кнопки Изменить и Удалить в крайнем левом столбце.

Продукты перечислены в GridView

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

Если нажать кнопку Удалить для любого продукта, FormatException возникает исключение .

Попытка удалить любой продукт приводит к formatException

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

Вызывается FormatException , когда ObjectDataSource пытается прочитать исходное UnitPrice значение. ItemTemplate Так как имеет UnitPrice формат валюты (<%# Bind("UnitPrice", "{0:C}") %>), он включает символ валюты, например $ 19,95. Происходит FormatException при попытке ObjectDataSource преобразовать эту строку в decimal. Чтобы обойти эту проблему, у нас есть несколько вариантов:

  • Удалите форматирование валюты из ItemTemplate. То есть вместо <%# Bind("UnitPrice", "{0:C}") %>использования просто используйте <%# Bind("UnitPrice") %>. Недостатком этого является то, что цена больше не форматируется.
  • Отображается UnitPrice в виде валюты в ItemTemplate, но для этого используйте Eval ключевое слово. Напомним, что Eval выполняет односторонние привязки данных. Нам по-прежнему нужно указать UnitPrice значение для исходных значений, поэтому нам по-прежнему потребуется оператор двусторонней привязки данных в ItemTemplate, но его можно поместить в элемент управления Label Web, свойству которого Visible присвоено значение false. В ItemTemplate можно использовать следующую разметку:
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • Удалите форматирование валюты из ItemTemplate, используя <%# Bind("UnitPrice") %>. В обработчике событий GridView RowDataBound программным способом получите доступ к веб-элементу управления Метки, в котором UnitPrice отображается значение, и задайте для его Text свойства форматированную версию.
  • Оставьте значение в UnitPrice формате валюты. В обработчике событий GridView RowDeleting замените существующее исходное UnitPrice значение (19,95 долл. США) фактическим десятичным значением с помощью Decimal.Parse. Мы узнали, как выполнить нечто подобное в RowUpdating обработчике событий, в руководстве по обработке исключений BLL и DAL-Level на странице ASP.NET .

В моем примере я решил использовать второй подход, добавив скрытый веб-элемент управления Label, свойство которого Text является двусторонними данными, привязанными к неформатованному UnitPrice значению.

После решения этой проблемы попробуйте нажать кнопку Удалить для любого продукта еще раз. На этот раз вы получите , InvalidOperationException когда ObjectDataSource попытается вызвать метод BLL UpdateProduct .

Объекту ObjectDataSource не удается найти метод с входными параметрами, которые он хочет отправить

Рис. 16. Объекту ObjectDataSource не удается найти метод с входными параметрами, которые он хочет отправить (щелкните для просмотра полноразмерного изображения)

При просмотре сообщения исключения ясно, что ObjectDataSource хочет вызвать метод BLL DeleteProduct , включающий original_CategoryName входные параметры и original_SupplierName . Это связано с тем, что ItemTemplate элементы для CategoryID и SupplierID TemplateFields в настоящее время содержат двусторонние инструкции Bind с полями CategoryName данных и SupplierName . Вместо этого необходимо включить Bind инструкции в CategoryID поля данных и SupplierID . Для этого замените существующие инструкции Eval Bind на операторы , а затем добавьте скрытые элементы управления Label, свойства которых Text привязаны к CategoryID полям данных и SupplierID с помощью двусторонней привязки данных, как показано ниже:

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

Благодаря этим изменениям мы теперь можем успешно удалять и изменять сведения о продукте! На шаге 5 мы рассмотрим, как убедиться, что обнаружены нарушения параллелизма. Но пока попробуйте обновить и удалить несколько записей, чтобы убедиться, что обновление и удаление для одного пользователя работает должным образом.

Шаг 5. Тестирование поддержки оптимистичного параллелизма

Чтобы убедиться, что нарушения параллелизма обнаруживаются (а не приводят к слепой перезаписи данных), необходимо открыть на этой странице два окна браузера. В обоих экземплярах браузера нажмите кнопку Изменить для Chai. Затем в одном из браузеров измените имя на "Чай Чай" и нажмите кнопку Обновить. Обновление должно завершиться успешно и вернуть GridView в состояние предварительного редактирования с именем нового продукта "Чай Чай".

Однако в другом экземпляре окна браузера имя продукта TextBox по-прежнему отображается как "Chai". Во втором окне браузера обновите до UnitPrice25.00. Без поддержки оптимистичного параллелизма нажатие кнопки "Обновить" во втором экземпляре браузера приведет к изменению названия продукта на "Chai", тем самым перезаписывая изменения, внесенные первым экземпляром браузера. Однако при использовании оптимистичного параллелизма нажатие кнопки Обновить во втором экземпляре браузера приводит к переходу к dbConcurrencyException.

При обнаружении нарушения параллелизма возникает исключение DBConcurrencyException.

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

Возникает DBConcurrencyException только при использовании шаблона пакетного обновления DAL. Прямой шаблон базы данных не вызывает исключения, он просто указывает, что строки не были затронуты. Чтобы проиллюстрировать это, верните GridView обоих экземпляров браузера в состояние предварительного редактирования. Затем в первом экземпляре браузера нажмите кнопку Изменить и измените название продукта с Chai Tea на Chai и нажмите кнопку Обновить. Во втором окне браузера нажмите кнопку Удалить для Chai.

После нажатия кнопки Удалить страница выполняет обратную запись, GridView вызывает метод ObjectDataSource Delete() , а ObjectDataSource вызывает ProductsOptimisticConcurrencyBLL метод класса DeleteProduct , передавая исходные значения. Исходное ProductName значение для второго экземпляра браузера — "Чай Чай", которое не соответствует текущему ProductName значению в базе данных. Поэтому инструкция, DELETE выдаваемая для базы данных, влияет на нулевые строки, так как в базе данных нет записей, которым WHERE соответствует предложение . Метод DeleteProduct возвращает, false а данные ObjectDataSource возвращаются в GridView.

С точки зрения конечного пользователя нажатие кнопки "Удалить чай Чай Чай" во втором окне браузера приводило к тому, что экран мигает, и после возврата продукт остается там, хотя теперь он указан как "Chai" (название продукта изменено первым экземпляром браузера). Если пользователь снова нажмет кнопку Удалить, удаление завершится успешно, так как исходное ProductName значение GridView ("Chai") теперь совпадает со значением в базе данных.

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

Чтобы устранить эти две проблемы, мы можем создать веб-элементы управления меток на странице, которые предоставляют объяснение причины сбоя обновления или удаления. Для шаблона пакетного обновления можно определить, произошло ли DBConcurrencyException исключение в обработчике событий после уровня GridView, отображая метку предупреждения при необходимости. Для прямого метода базы данных мы можем проверить возвращаемое значение метода BLL (то есть true , если была затронута одна строка, false в противном случае) и отобразить информационное сообщение по мере необходимости.

Шаг 6. Добавление информационных сообщений и их отображение при нарушении параллелизма

При нарушении параллелизма поведение зависит от того, использовалось ли пакетное обновление DAL или прямой шаблон базы данных. В нашем руководстве используются оба шаблона: шаблон пакетного обновления используется для обновления и прямой шаблон базы данных, используемый для удаления. Чтобы приступить к работе, давайте добавим на страницу два элемента управления Label Web, объясняющие нарушение параллелизма при попытке удаления или обновления данных. Присвойте свойствам falseи EnableViewState свойств элемента управления Visible Метка значение . Это приведет к скрытию их при каждом посещении страницы, за исключением тех конкретных посещений страницы, где их Visible свойству присвоено trueзначение .

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

Помимо задания Visibleсвойств , EnabledViewStateи Text , я также присвоил свойству CssClassWarningзначение , что приводит к отображению метки крупным, красным курсивом, полужирным шрифтом. Этот класс CSS Warning был определен и добавлен в Styles.css в учебнике Изучение событий, связанных с вставкой, обновлением и удалением .

После добавления этих меток Designer в Visual Studio должны выглядеть примерно так, как на рисунке 18.

На страницу добавлены два элемента управления

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

С помощью этих элементов управления Label Web мы готовы изучить, как определить, когда произошло нарушение параллелизма, после чего для соответствующего свойства Label Visible можно задать значение true, отображая информационное сообщение.

Обработка нарушений параллелизма при обновлении

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

Как мы видели в руководстве по обработке исключений BLL- и DAL-Level в ASP.NET Page , такие исключения могут обнаруживаться и подавляться в обработчиках событий постуровневого веб-элемента управления данными. Поэтому необходимо создать обработчик событий для события GridView RowUpdated , который проверяет, было ли DBConcurrencyException создано исключение. Этому обработчику событий передается ссылка на любое исключение, которое было создано в процессе обновления, как показано в приведенном ниже коде обработчика событий:

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

Перед лицом DBConcurrencyException исключения этот обработчик событий отображает UpdateConflictMessage элемент управления Метка и указывает, что исключение обработано. При наличии этого кода при нарушении параллелизма при обновлении записи изменения пользователя теряются, так как они одновременно перезаписываются изменения другого пользователя. В частности, GridView возвращается в состояние предварительного редактирования и привязывается к текущим данным базы данных. Это приведет к обновлению строки GridView с учетом изменений другого пользователя, которые ранее не были видны. Кроме того, UpdateConflictMessage элемент управления Метка объяснит пользователю, что только что произошло. Эта последовательность событий подробно описана на рис. 19.

Обновления пользователя теряются при нарушении параллелизма

Рис. 19. Пользователь Обновления теряется при нарушении параллелизма (щелкните для просмотра полноразмерного изображения)

Примечание

Кроме того, вместо того, чтобы возвращать GridView в состояние предварительного редактирования, можно оставить GridView в состоянии редактирования, присвоив свойству KeepInEditMode переданного GridViewUpdatedEventArgs объекта значение true. Однако если вы используете этот подход, обязательно привязыте данные к GridView (путем вызова его DataBind() метода), чтобы значения других пользователей загружались в интерфейс редактирования. Код, доступный для скачивания в этом руководстве, содержит эти две строки кода в RowUpdated обработчике событий, закомментированных. Просто раскомментируйте эти строки кода, чтобы GridView оставался в режиме редактирования после нарушения параллелизма.

Реагирование на нарушения параллелизма при удалении

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

Возвращаемое значение для метода BLL можно проверить в обработчиках событий post-level ObjectDataSource с помощью ReturnValue свойства объекта , ObjectDataSourceStatusEventArgs переданного в обработчик событий. Так как мы заинтересованы в определении возвращаемого DeleteProduct значения из метода, необходимо создать обработчик событий для события ObjectDataSource Deleted . Свойство ReturnValue имеет тип object и может иметь значение , null если возникло исключение и метод был прерван, прежде чем он мог вернуть значение. Поэтому сначала необходимо убедиться, что ReturnValue свойство не null является и является логическим значением. Если эта проверка пройдена, мы показываем DeleteConflictMessage элемент управления Метка, если ReturnValue имеет значение false. Это можно сделать с помощью следующего кода:

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

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

Удаление пользователя отменено при нарушении параллелизма

Рис. 20. Удаление пользователя отменено в аспекте нарушения параллелизма (щелкните, чтобы просмотреть полноразмерное изображение)

Сводка

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

Типизированный набор данных в .NET предоставляет функциональные возможности для поддержки управления оптимистическим параллелизмом. В частности, инструкции UPDATE и DELETE , выданные для базы данных, включают все столбцы таблицы, что гарантирует, что обновление или удаление произойдет только в том случае, если текущие данные записи совпадают с исходными данными пользователя при обновлении или удалении. После настройки DAL для поддержки оптимистического параллелизма необходимо обновить методы BLL. Кроме того, страница ASP.NET, которая вызывает BLL, должна быть настроена таким образом, чтобы ObjectDataSource извлекает исходные значения из своего веб-элемента управления данными и передает их в BLL.

Как мы видели в этом руководстве, реализация управления оптимистическим параллелизмом в веб-приложении ASP.NET включает обновление DAL и BLL и добавление поддержки на странице ASP.NET. Является ли эта добавленная работа разумным вложением времени и усилий, зависит от вашего приложения. Если пользователи редко обновляют данные одновременно или данные, которые они обновляют, отличаются друг от друга, то управление параллелизмом не является ключевой проблемой. Однако если на вашем сайте обычно работают несколько пользователей, работающих с одними и теми же данными, управление параллелизмом может помочь предотвратить непреднамерее обновления или удаления одного пользователя от невольной перезаписи другого.

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

Об авторе

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