Реализация оптимистичного параллелизма (C#)
Для веб-приложения, которое позволяет нескольким пользователям изменять данные, существует риск того, что два пользователя могут одновременно редактировать одни и те же данные. В этом руководстве мы реализуем управление оптимистическим параллелизмом для обработки этого риска.
Введение
Для веб-приложений, которые позволяют только пользователям просматривать данные, или для тех, которые включают только одного пользователя, который может изменять данные, нет никакой угрозы случайного перезаписи двумя пользователями изменений друг друга. Однако для веб-приложений, которые позволяют нескольким пользователям обновлять или удалять данные, существует вероятность того, что изменения одного пользователя могут конфликтовать с другими параллельными пользователями. Без политики параллелизма, когда два пользователя одновременно редактируют одну запись, пользователь, который фиксирует последние изменения, переопределяет изменения, внесенные первой.
Например, представьте, что два пользователя, 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
.
Рис. 3. Подключение к той же базе данных Northwind (щелкните для просмотра полноразмерного изображения)
Далее нам будет предложено запросить данные: с помощью нерегламентированной инструкции SQL, новой хранимой процедуры или существующей хранимой процедуры. Так как мы использовали нерегламентированные SQL-запросы в исходном DAL, используйте этот параметр и здесь.
Рис. 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
Рис. 5. Использование того же SQL-запроса из Products
TableAdapter в исходном DAL (Щелкните для просмотра полноразмерного изображения)
Перед переходом на следующий экран нажмите кнопку Дополнительные параметры. Чтобы этот объект TableAdapter использовал элемент управления оптимистическим параллелизмом, просто проверка флажок "Использовать оптимистичный параллелизм".
Рис. 6. Включение управления оптимистичным параллелизмом с помощью флажка "Использовать оптимистичный параллелизм" (щелкните для просмотра полноразмерного изображения)
Наконец, укажите, что TableAdapter должен использовать шаблоны доступа к данным, которые заполняют таблицу DataTable и возвращают таблицу Данных; также указывает, что необходимо создать прямые методы базы данных. Измените имя метода для шаблона Return a DataTable с GetData на GetProducts, чтобы зеркало соглашения об именовании, которые мы использовали в исходном DAL.
Рис. 7. Использование табличного адаптера всех шаблонов доступа к данным (щелкните для просмотра полноразмерного изображения)
После завершения работы мастера Designer DataSet будет включать строго типизированные Products
таблицы DataTable и TableAdapter. Уделите немного времени, чтобы переименовать DataTable с Products
на ProductsOptimisticConcurrency
, что можно сделать, щелкнув правой кнопкой мыши строку заголовка DataTable и выбрав команду Переименовать в контекстном меню.
Рис. 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.
Рис. 10. Добавление WHERE
предложения в предварительно загруженный запрос для возврата определенной записи продукта (щелкните для просмотра полноразмерного изображения)
Наконец, измените созданные имена методов на 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
.
Рис. 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) при использовании шаблона пакетного обновления для обновления сведений о продукте наш код выполняет следующую последовательность событий:
- Чтение текущей информации о продукте базы данных в
ProductRow
экземпляр с помощью метода TableAdapterGetProductByProductID(productID)
- Назначение новых значений экземпляру из
ProductRow
шага 1 - Вызов метода TableAdapter
Update
, передав экземплярProductRow
Однако эта последовательность шагов не будет правильно поддерживать оптимистичный параллелизм, так как ProductRow
заполненный на шаге 1 заполняется непосредственно из базы данных, а это означает, что исходные значения, используемые DataRow, — это те, которые в настоящее время существуют в базе данных, а не те, которые были привязаны к GridView в начале процесса редактирования. Вместо этого при использовании DAL с поддержкой оптимистичного параллелизма необходимо изменить перегрузки UpdateProduct
метода, чтобы выполнить следующие действия:
- Чтение текущей информации о продукте базы данных в
ProductsOptimisticConcurrencyRow
экземпляр с помощью метода TableAdapterGetProductByProductID(productID)
- Назначение исходных значений экземпляру из
ProductsOptimisticConcurrencyRow
шага 1 ProductsOptimisticConcurrencyRow
Вызов метода экземпляраAcceptChanges()
, который указывает DataRow, что его текущие значения являются "исходными"- Назначение новых значений экземпляру
ProductsOptimisticConcurrencyRow
- Вызов метода TableAdapter
Update
, передав экземпляр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 в Designer, задав для его ID
свойства значение ProductsGrid
. В смарт-теге GridView выберите создать объект ObjectDataSource с именем ProductsOptimisticConcurrencyDataSource
. Так как мы хотим, чтобы объект ObjectDataSource использовал DAL, поддерживающий оптимистичный параллелизм, настройте его для использования 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
экземпляр для каждого из десяти входных параметров в методе ProductsOptimisticConcurrencyBLL
DeleteProduct
класса . Аналогичным образом коллекция 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. В этих TemplateFieldsItemTemplates
CategoryName
отображаются поля данных и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 выводит список всех продуктов с помощью кнопки Изменить и Удалить в крайнем левом столбце.
Рис. 14. Продукты перечислены в GridView (щелкните для просмотра полноразмерного изображения)
Если нажать кнопку Удалить для любого продукта, 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") %>
. В обработчике событий GridViewRowDataBound
программным способом получите доступ к веб-элементу управления Метки, в которомUnitPrice
отображается значение, и задайте для егоText
свойства форматированную версию. - Оставьте значение в
UnitPrice
формате валюты. В обработчике событий GridViewRowDeleting
замените существующее исходноеUnitPrice
значение (19,95 долл. США) фактическим десятичным значением с помощьюDecimal.Parse
. Мы узнали, как выполнить нечто подобное вRowUpdating
обработчике событий, в руководстве по обработке исключений BLL и DAL-Level на странице ASP.NET .
В моем примере я решил использовать второй подход, добавив скрытый веб-элемент управления Label, свойство которого Text
является двусторонними данными, привязанными к неформатованному UnitPrice
значению.
После решения этой проблемы попробуйте нажать кнопку Удалить для любого продукта еще раз. На этот раз вы получите , InvalidOperationException
когда ObjectDataSource попытается вызвать метод BLL UpdateProduct
.
Рис. 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". Во втором окне браузера обновите до UnitPrice
25.00
. Без поддержки оптимистичного параллелизма нажатие кнопки "Обновить" во втором экземпляре браузера приведет к изменению названия продукта на "Chai", тем самым перезаписывая изменения, внесенные первым экземпляром браузера. Однако при использовании оптимистичного параллелизма нажатие кнопки Обновить во втором экземпляре браузера приводит к переходу к 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
, я также присвоил свойству CssClass
Warning
значение , что приводит к отображению метки крупным, красным курсивом, полужирным шрифтом. Этот класс 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.