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

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

Скачивание примера приложения или Загрузка PDF-файла

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

Введение

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

Например, предположим, что два пользователя, Цзисунь и SAM, посещали страницу в нашем приложении, которая позволяла посетителям обновлять и удалять продукты с помощью элемента управления GridView. Одновременно нажмите кнопку изменить в элементе управления GridView. Цзисунь изменяет название продукта на «чай Chai» и нажимает кнопку «Обновить». Результатом является UPDATEная инструкция, которая отправляется в базу данных, которая задает все обновляемые поля продукта (несмотря на то, что цзисунь только обновил одно поле, ProductName). На данный момент база данных имеет значения «Чай Chai», «напитки категории», «поставщик Exotic Liquids» и т. д. для этого конкретного продукта. Однако на экране GridView в SAM по-прежнему отображается название продукта в редактируемой строке GridView в виде "Chai". Через несколько секунд после фиксации изменений Цзисунь SAM обновляет категорию до «специи» и нажимает кнопку Обновить. В результате инструкция UPDATE отправляется в базу данных, которая задает имя продукта «Chai», CategoryID с соответствующим ИДЕНТИФИКАТОРом категории «напитки» и т. д. Изменения в имени продукта цзисунь были перезаписаны. На рис. 1 изображена эта серия событий.

, когда два пользователя одновременно обновляют запись a, существует вероятность того, что один пользователь s может перезаписать другие

Рис. 1. когда два пользователя одновременно обновляют запись a, существует вероятность того, что один пользователь s может перезаписать другие (щелкните, чтобы просмотреть изображение с полным размером).

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

Доступны три стратегии управления параллелизмом :

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

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

Note

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

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

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

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

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

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

Note

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

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

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

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

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

подключиться к той же базе данных Northwind

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

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

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

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

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

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 из TableAdapter Products в исходном DAL

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

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

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

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

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

, что TableAdapter использует все шаблоны доступа к данным

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

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

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

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

Чтобы увидеть различия между запросами UPDATE и DELETE между ProductsOptimisticConcurrency TableAdapter (который использует оптимистичный параллелизм) и TableAdapter Products (который не), щелкните TableAdapter и перейдите к окно свойств. В подсвойствах DeleteCommand и UpdateCommand свойства "CommandText можно увидеть фактический синтаксис 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 для 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, необходимо создать GetProductByProductID(productID) метод с помощью специального оператора SQL (см. рис. 4). Поскольку метод GetProductByProductID(productID) возвращает сведения о конкретном продукте, укажите, что этот запрос является SELECT типом запроса, возвращающим строки.

отметить тип запроса "SELECT, который возвращает строки"

Рис. 9. Пометка типа запроса как «SELECT который возвращает строки» (щелкните, чтобы просмотреть изображение с полным размером)

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

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

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

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

переименование методов в Филлбипродуктид и Жетпродуктбипродуктид

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

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

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

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

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

Хотя сигнатура метода для метода 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.

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

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

При использовании прямого шаблона базы данных для DAL, использующего оптимистичный параллелизм, методам должны быть переданы новые и исходные значения. Для удаления нет новых значений, поэтому необходимо передать только исходные значения. В нашем BLL мы должны принять все исходные параметры в качестве входных параметров. Давайте разберем метод DeleteProduct в классе ProductsOptimisticConcurrencyBLL, используя прямой метод DB. Это означает, что этот метод должен взять все десять полей данных продукта в качестве входных параметров и передать их в 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 не будет соответствовать ни одной записи базы данных, а записи не будут затронуты. Таким образом, метод Delete TableAdapter вернет 0, а метод DeleteProduct BLL возвратит false.

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

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

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

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

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

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

Шаг 1 считывает все значения текущей базы данных для указанной записи продукта. Этот шаг является избыточным в перегрузке UpdateProduct, которая обновляет все столбцы продукта (так как эти значения перезаписываются на шаге 2), но это важно для этих перегрузок, где только подмножество значений столбца передается в качестве входных параметров. После назначения исходных значений экземпляру ProductsOptimisticConcurrencyRow вызывается метод AcceptChanges(), который помечает текущие значения 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 в конструктор, установив для свойства 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 для каждого из десяти входных параметров в методе DeleteProduct класса ProductsOptimisticConcurrencyBLL. Аналогичным образом коллекция UpdateParameters содержит экземпляр Parameter для каждого входного параметра в UpdateProduct.

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

Note

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

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

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

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

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

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

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

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

  • Удалены ProductID, SupplierNameи CategoryName BoundFields
  • Преобразование ProductName BoundField в TemplateField и Добавление элемента управления Рекуиредфиелдвалидатион.
  • Преобразовал CategoryID и SupplierID BoundFields в полей TemplateField и настроил интерфейс редактирования на использование элементов управления DropDownList, а не текстовых полей. В этих полей TemplateField "ItemTemplatesотображаются поля данных CategoryName и SupplierName.
  • Преобразованы UnitPrice, UnitsInStock, UnitsOnOrderи ReorderLevel BoundFields в полей TemplateField и добавлены элементы управления 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>

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

Note

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

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

Существует несколько проблем, связанных с тем, как был настроен элемент управления GridView. Если свойство ConflictDetection ObjectDataSource имеет значение CompareAllValues (как и в нашей ситуации), то, когда методы Update() или Delete() ObjectDataSource вызываются элементом 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, свойство 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") %>. В обработчике событий RowDataBound GridView программный доступ к веб-элементу управления Label, в котором отображается значение UnitPrice, и присвойте его свойству Text форматированную версию.
  • Оставьте UnitPrice в формате денежной единицы. В обработчике событий RowDeleting GridView замените существующее исходное значение UnitPrice ($19,95) фактическим десятичным значением с помощью Decimal.Parse. Мы увидели, как выполнить что-то подобное в обработчике событий RowUpdating в учебнике обработка исключений BLL и DAL на странице ASP.NET .

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

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

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

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

Глядя на сообщение об исключении, ясно, что ObjectDataSource хочет вызвать метод BLL DeleteProduct, включающий original_CategoryName и original_SupplierName входные параметры. Это происходит потому, что ItemTemplate s для CategoryID и SupplierID полей TemplateField в настоящее время содержат двусторонние инструкции BIND с полями данных CategoryName и SupplierName. Вместо этого необходимо включить Bindные инструкции с полями данных CategoryID и SupplierID. Для этого замените существующие инструкции BIND инструкциями Eval, а затем добавьте скрытые элементы управления 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. Затем в одном из браузеров измените имя на «чай Chai» и нажмите кнопку Обновить. Обновление должно выполняться, и возврат GridView к состоянию предварительного редактирования с именем "Чай Chai" в качестве нового названия продукта.

Однако в другом экземпляре окна браузера в текстовом поле Product Name по-прежнему отображается «Chai». В этом втором окне браузера обновите UnitPrice для 25.00. Без поддержки оптимистичного параллелизма при нажатии кнопки Обновить во втором экземпляре браузера будет изменено имя продукта на «Chai», что приведет к перезаписи изменений, внесенных первым экземпляром браузера. Однако при использовании оптимистичного параллелизма нажатие кнопки Обновить во втором экземпляре браузера приводит к DBConcurrencyException.

при обнаружении одновременных нарушений возникает исключение DBConcurrencyException

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

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

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

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

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

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

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

При возникновении одновременного нарушения поведение зависит от того, использовалось ли пакетное обновление DAL или непосредственный шаблон базы данных. В этом учебнике используются оба шаблона с шаблоном пакетного обновления, который используется для обновления, и непосредственный шаблон базы данных, используемый для удаления. Чтобы приступить к работе, давайте добавим на нашу страницу две веб-элемента управления Label, объясняющие, что произошло нарушение параллелизма при попытке удаления или обновления данных. Задайте для свойств Visible и EnableViewState элемента управления Label значение false; Это приведет к тому, что они будут скрыты на каждом посещении страницы, за исключением конкретных посещений страниц, в которых для свойства 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, я также установил для свойства CssClass значение Warning, в результате чего метка будет отображаться в большом, красном, курсивном полужирном шрифте. Этот класс CSS Warning был определен и добавлен в Styles. CSS обратно в ходе изучения событий, связанных с учебником по вставке, обновлению и удалению .

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

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

Рис. 18. Добавление на страницу двух элементов управления Label (щелкните, чтобы просмотреть изображение с полным размером)

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

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

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

Как мы видели при обработке исключений уровня BLL и DAL в учебнике по страницам ASP.NET , такие исключения могут быть обнаружены и подавлены в обработчиках событий постороннего уровня веб-элемента управления данными. Поэтому необходимо создать обработчик событий для события RowUpdated GridView, которое проверяет, вызвано ли исключение 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 этот обработчик событий отображает элемент управления Label UpdateConflictMessage и указывает, что исключение было обработано. При использовании этого кода при возникновении одновременного нарушения при обновлении записи изменения пользователя теряются, так как они будут перезаписаны изменения другого пользователя одновременно. В частности, GridView возвращается в состояние предварительного редактирования и привязывается к текущим данным базы данных. Это приведет к обновлению строки GridView другими изменениями другого пользователя, которые ранее не были видны. Кроме того, элемент управления "метка" UpdateConflictMessage поясняет, что именно произошло. Эта последовательность событий подробно описана на рис. 19.

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

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

Note

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

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

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

Возвращаемое значение для метода BLL можно проверить в обработчиках событий последующей операции на уровне ObjectDataSource с помощью свойства ReturnValue объекта ObjectDataSourceStatusEventArgs, переданного в обработчик событий. Поскольку мы заинтересованы в определении возвращаемого значения метода DeleteProduct, необходимо создать обработчик событий для события Deleted ObjectDataSource. Свойство 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. Отмена удаления пользователя в случае одновременного нарушения (щелкните, чтобы просмотреть изображение с полным размером)

Сводка

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

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

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

Поздравляем с программированием!

Об авторе

Скотт Митчелл, автор семи книг по ASP/ASP. NET и основатель 4GuysFromRolla.com, работал с веб-технологиями Майкрософт с 1998. Скотт работает как независимый консультант, преподаватель и модуль записи. Его последняя книга — Sams обучать себя ASP.NET 2,0 за 24 часа. Он доступен по адресу mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.