Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Для веб-приложения, позволяющего нескольким пользователям изменять данные, существует риск того, что два пользователя могут редактировать одни и те же данные одновременно. В этом руководстве мы реализуем контроль оптимистического параллелизма для обработки этого риска.
Введение
Для веб-приложений, которые позволяют только пользователям просматривать данные или для тех, кто включает только одного пользователя, который может изменять данные, не существует угрозы случайного перезаписи изменений двух одновременных пользователей. Однако для веб-приложений, которые позволяют нескольким пользователям обновлять или удалять данные, существует вероятность того, что изменения одного пользователя могут конфликтовать с изменениями другого одновременного пользователя. Без какой-либо политики конкурентности, когда два пользователя одновременно редактируют одну запись, пользователь, который сохраняет свои изменения последним, заменит изменения, внесенные первым.
Например, представьте, что два пользователя Jisun и Sam посетили страницу в нашем приложении, которая позволила посетителям обновлять и удалять продукты с помощью элемента управления GridView. Оба нажимают кнопку "Изменить" в GridView примерно в одно и то же время. Jisun изменяет имя продукта на "Chai Tea" и нажимает кнопку "Обновить". Чистый UPDATE
результат — это инструкция, которая отправляется в базу данных, которая задает все обновляемые поля продукта (несмотря на то, что Jisun обновил только одно поле, ProductName
). На данный момент времени база данных имеет значения "Chai Tea", категория "Напитки", поставщик Экзотические жидкости и т. д. для этого конкретного продукта. Тем не менее, GridView на экране Сэма по-прежнему отображает имя продукта в редактируемой строке GridView как "Chai". Через несколько секунд после фиксации изменений Jisun Сэм обновляет категорию на Condiments и нажимает кнопку "Обновить". Это приводит к отправке UPDATE
инструкции в базу данных, которая задает имя продукта "Chai", соответствующий идентификатор категории "Напитки" CategoryID
и т. д. Изменения, внесенные Jisun в имя продукта, были перезаписаны. На рисунке 1 графически показана эта серия событий.
Рисунок 1: Когда два пользователя одновременно обновляют запись, есть вероятность того, что изменения одного пользователя перезапишут изменения другого (нажмите, чтобы просмотреть изображение в полном размере)
Аналогичным образом, когда два пользователя посещают страницу, один пользователь может находиться в процессе обновления записи, пока другой пользователь её удаляет. Кроме того, между тем, когда пользователь загружает страницу и когда он нажимает кнопку "Удалить", другой пользователь может изменить содержимое этой записи.
Доступны три стратегии управления параллелизмом :
- Do Nothing -if одновременных пользователей изменяют одну и ту же запись, пусть победит последний из них (поведение по умолчанию)
- Оптимистическая конкуренция - предполагается, что хотя конфликт может возникать время от времени, подавляющее большинство времени такие конфликты не возникают; поэтому, если конфликт возникает, просто сообщите пользователю, что их изменения не могут быть сохранены, так как другой пользователь изменил те же данные.
- Pessimistic Concurrency — предполагается, что конфликты параллелизма являются обычными, и что пользователи не будут терпеть, что их изменения не были сохранены из-за параллельного действия другого пользователя; таким образом, когда один пользователь начнет обновлять запись, заблокируйте ее, тем самым предотвращая редактирование или удаление этой записи другими пользователями до тех пор, пока пользователь не зафиксирует свои изменения.
Все наши учебные пособия до сих пор использовали стратегию разрешения параллелизма по умолчанию — а именно, мы принимали последнюю запись как окончательную. В этом руководстве мы рассмотрим, как реализовать управление оптимистическим параллелизмом.
Замечание
Мы не будем рассматривать примеры пессимистичной синхронизации в этой серии руководств. Пессимистичный параллелизм редко используется, так как такие блокировки, если не будут должным образом сняты, могут помешать другим пользователям обновлять данные. Например, если пользователь блокирует запись для редактирования, а затем покидает ее до разблокировки, другой пользователь не сможет обновить запись до тех пор, пока исходный пользователь не вернется и завершит его обновление. Поэтому в ситуациях, когда используется пессимистическая блокировка, обычно устанавливается время ожидания, которое при достижении отменяет блокировку. Веб-сайты продаж билетов, которые блокируют определенное место для сидения в течение короткого периода, пока пользователь завершает процесс заказа, является примером пессимистического управления параллелизмом.
Шаг 1: Изучение того, как реализуется оптимистическая конкуренция
Управление оптимистичным параллелизмом работает, проверяя, что обновляемая или удаляемая запись сохраняет те же значения, что и в момент старта процесса обновления или удаления. Например, при нажатии кнопки "Изменить" в редактируемом GridView значения записи считываются из базы данных и отображаются в TextBoxes и других веб-элементах управления. Эти исходные значения сохраняются в GridView. Позже после внесения изменений и нажатия кнопки "Обновить" исходные значения и новые значения отправляются на уровень бизнес-логики, а затем до уровня доступа к данным. Уровень доступа к данным должен выдавать инструкцию SQL, которая обновляет запись только в том случае, если исходные значения, которые пользователь начал редактировать, идентичны значениям, которые все еще находятся в базе данных. На рисунке 2 показана эта последовательность событий.
Рис. 2. Для успешного обновления или удаления исходные значения должны быть равны текущим значениям базы данных (щелкните, чтобы просмотреть изображение полного размера)
Существуют различные подходы к реализации оптимистического параллелизма (см. Питер А. Бромберглогика обновления при оптимистическом параллелизме для краткого ознакомления с рядом вариантов). Набор данных типа ADO.NET предоставляет одну реализацию, которую можно настроить только с помощью галочки флажка. Включение оптимистического параллелизма для TableAdapter в типизированном наборе данных приводит к расширению инструкций 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
инструкция была упрощена для удобства чтения. На практике проверка в UnitPrice
условии WHERE
будет более сложной, поскольку UnitPrice
может содержать NULL
, и проверка, всегда ли NULL = NULL
возвращает значение False, не будет работать (вместо этого необходимо использовать IS NULL
).
Помимо использования другой базовой UPDATE
инструкции, настройка TableAdapter для использования оптимистического параллелизма также изменяет сигнатуру прямых методов базы данных. Помните из нашего первого руководства, создав уровень доступа к данным, прямые методы базы данных были теми, которые принимают список скалярных значений в качестве входных параметров (а не строго типизированный экземпляр DataRow или DataTable). При использовании оптимистического параллелизма прямые Update()
и Delete()
методы базы данных включают входные параметры для исходных значений. Кроме того, необходимо изменить код в BLL для использования шаблона пакетного обновления (перегрузки методов Update()
, которые принимают DataRows и DataTables, а не скалярные значения).
Вместо расширения существующих адаптеров таблиц DAL для использования оптимистического параллелизма (что потребует изменения BLL для учета), давайте создадим новый типизированный набор данных с именем NorthwindOptimisticConcurrency
, в который мы добавим TableAdapter Products
, использующий оптимистический параллелизм. После этого мы создадим ProductsOptimisticConcurrencyBLL
класс уровня бизнес-логики, который имеет соответствующие изменения для поддержки оптимистического параллелизма DAL. После того как эта основа будет создана, мы будем готовы создать страницу ASP.NET.
Шаг 2. Создание уровня доступа к данным, поддерживающего оптимистическое параллелизм
Чтобы создать новый Typed DataSet, щелкните правой кнопкой мыши папку DAL
, находящуюся в папке App_Code
, и добавьте новый набор данных с именем NorthwindOptimisticConcurrency
. Как мы видели в первом руководстве, это добавит новый TableAdapter в типизированный набор данных, автоматически запуская мастер настройки TableAdapter. На первом экране нам предлагается указать базу данных для подключения — используйте настройку NORTHWNDConnectionString
из Web.config
для подключения к той же базе данных Northwind.
Рис. 3. Подключение к той же базе данных Northwind (щелкните, чтобы просмотреть изображение полного размера)
Далее нам будет предложено запросить данные: с помощью нерегламентированной инструкции SQL, новой хранимой процедуры или существующей хранимой процедуры. Поскольку мы использовали произвольные запросы SQL в исходном DAL, используйте этот параметр здесь также.
Рис. 4. Указание данных для извлечения с помощью инструкции AD-Hoc 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
Рис. 5. Использование того же SQL-запроса из Products
TableAdapter в исходном DAL (нажмите, чтобы просмотреть полноразмерное изображение)
Перед переходом на следующий экран нажмите кнопку "Дополнительные параметры". Чтобы этот TableAdapter использовал управление оптимистическим параллелизмом, просто установите флажок "Использовать оптимистическое управление параллелизмом".
Рис. 6.: Включите оптимистический контроль параллелизма, установив флажок "Использовать оптимистический контроль параллелизма" (щелкните, чтобы просмотреть изображение в полном размере)
Наконец, укажите, что TableAdapter должен использовать шаблоны доступа к данным, которые заполняют DataTable и возвращают DataTable; также укажите, что следует создать методы прямого доступа к базе данных. Измените имя метода для шаблона Return a DataTable из GetData на GetProducts, в соответствии с соглашениями об именованиях исходного DAL.
Рис. 7. Использование шаблонов доступа ко всем данным (щелкните, чтобы просмотреть изображение полного размера)
После завершения работы мастера конструктор наборов данных будет включать строго типизированный Products
DataTable и TableAdapter. Переименуйте DataTable из Products
на ProductsOptimisticConcurrency
, что можно сделать, щелкнув правой кнопкой мыши по заголовку DataTable и выбрав "Переименовать" в контекстном меню.
Рис. 8. Таблица данных и TableAdapter добавлены в типизированный набор данных (щелкните, чтобы просмотреть изображение полного размера)
Чтобы увидеть различия между запросами UPDATE
и DELETE
для ProductsOptimisticConcurrency
TableAdapter (который использует оптимистическую согласованность) и Products TableAdapter (который её не использует), щелкните на TableAdapter и перейдите в окно свойств.
DeleteCommand
В подсвойствах свойств UpdateCommand
CommandText
можно увидеть фактический синтаксис SQL, который отправляется в базу данных при вызове методов обновления или удаления DAL. Для TableAdapter используется инструкция: ProductsOptimisticConcurrency
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))
"Заявление для 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
. Он использует эти дополнительные значения входных параметров в WHERE
условии предложения инструкции DELETE
, отправленной в базу данных, и удаляет указанную запись только в том случае, если текущие значения базы данных совпадают с исходными.
Хотя сигнатура метода для метода TableAdapter Update
, используемого в шаблоне пакетного обновления, не изменилась, код, необходимый для записи исходных и новых значений, изменился. Таким образом, вместо того, чтобы попытаться использовать DAL с поддержкой оптимистического параллелизма с существующим ProductsBLL
классом, давайте создадим новый класс уровня бизнес-логики для работы с нашим новым DAL.
Добавьте класс с именем ProductsOptimisticConcurrencyBLL
в папку BLL
в папке App_Code
.
Рис. 12. Добавление ProductsOptimisticConcurrencyBLL
класса в папку BLL
Затем добавьте следующий код в ProductsOptimisticConcurrencyBLL
класс:
Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
Get
If _productsAdapter Is Nothing Then
_productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
End If
Return _productsAdapter
End Get
End Property
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Select, True)> _
Public Function GetProducts() As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
Return Adapter.GetProducts()
End Function
End Class
Обратите внимание на инструкцию using NorthwindOptimisticConcurrencyTableAdapters
, расположенную перед началом объявления класса. Пространство имен NorthwindOptimisticConcurrencyTableAdapters
содержит класс ProductsOptimisticConcurrencyTableAdapter
, который предоставляет методы DAL. Кроме того, перед объявлением класса вы найдете System.ComponentModel.DataObject
атрибут, который указывает Visual Studio включить этот класс в раскрывающийся список мастера ObjectDataSource.
ProductsOptimisticConcurrencyBLL
Свойство Adapter
предоставляет быстрый доступ к экземпляру ProductsOptimisticConcurrencyTableAdapter
класса и следует шаблону, используемому в наших исходных классах BLL (ProductsBLL
и CategoriesBLL
т. д.). Наконец, GetProducts()
метод вызывает метод GetProducts()
в DAL и возвращает объект ProductsOptimisticConcurrencyDataTable
, заполняя его экземплярами ProductsOptimisticConcurrencyRow
для каждой записи продукта в базе данных.
Удаление продукта с помощью прямого шаблона базы данных с оптимистическим параллелизмом
При использовании прямого шаблона работы с базой данных в DAL, который использует оптимистичную конкурентность, методы должны получить новые и исходные значения. Для удаления нет новых значений, поэтому необходимо передать только исходные значения. В нашем BLL мы должны принять все исходные параметры в качестве входных параметров. Давайте настроим DeleteProduct
метод в ProductsOptimisticConcurrencyBLL
классе для использования прямого метода работы с базой данных. Это означает, что этот метод должен принимать все десять полей данных продукта в качестве входных параметров и передавать их в DAL, как показано в следующем коде:
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
ByVal original_productID As Integer, ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean) _
As Boolean
Dim rowsAffected As Integer = 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
End Function
Если исходные значения, то есть те значения, которые были загружены в GridView (или DetailsView или FormView), отличаются от значений в базе данных, когда пользователь нажимает кнопку WHERE
"Удалить", это условие не будет соответствовать ни одной записи в базе данных, и никакие записи не будут затронуты. Поэтому метод TableAdapter Delete
возвращается 0
, а метод BLL DeleteProduct
возвращается false
.
Обновление продукта с помощью шаблона пакетного обновления с оптимистическим параллелизмом
Как отмечалось ранее, метод TableAdapter Update
для шаблона пакетного обновления имеет одинаковую сигнатуру метода независимо от того, используется ли оптимистическая согласованность. А именно, Update
метод ожидает DataRow, массив DataRows, DataTable или типизированный набор данных. Для указания исходных значений нет дополнительных входных параметров. Это возможно, так как DataTable отслеживает исходные и измененные значения для своих dataRow(s). Когда 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 Sub AssignAllProductValues( _
ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean)
product.ProductName = productName
If Not supplierID.HasValue Then
product.SetSupplierIDNull()
Else
product.SupplierID = supplierID.Value
End If
If Not categoryID.HasValue Then
product.SetCategoryIDNull()
Else
product.CategoryID = categoryID.Value
End If
If quantityPerUnit Is Nothing Then
product.SetQuantityPerUnitNull()
Else
product.QuantityPerUnit = quantityPerUnit
End If
If Not unitPrice.HasValue Then
product.SetUnitPriceNull()
Else
product.UnitPrice = unitPrice.Value
End If
If Not unitsInStock.HasValue Then
product.SetUnitsInStockNull()
Else
product.UnitsInStock = unitsInStock.Value
End If
If Not unitsOnOrder.HasValue Then
product.SetUnitsOnOrderNull()
Else
product.UnitsOnOrder = unitsOnOrder.Value
End If
If Not reorderLevel.HasValue Then
product.SetReorderLevelNull()
Else
product.ReorderLevel = reorderLevel.Value
End If
product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean, ByVal productID As Integer, _
_
ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean, _
ByVal original_productID As Integer) _
As Boolean
'STEP 1: Read in the current database product information
Dim products As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
Adapter.GetProductByProductID(original_productID)
If products.Count = 0 Then
' no matching record found, return false
Return False
End If
Dim product As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = 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
Dim rowsAffected As Integer = Adapter.Update(product)
' Return true if precisely one row was updated, otherwise false
Return rowsAffected = 1
End Function
Шаг 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
объекта.
Рис. 13. Использование ProductsOptimisticConcurrencyBLL
объекта ObjectDataSource (щелкните, чтобы просмотреть изображение полного размера)
Выберите методы 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
.
Для этих предыдущих руководств, которые включали изменение данных, мы удалим свойство ObjectDataSource OldValuesParameterFormatString
на данный момент, так как это свойство указывает, что метод BLL ожидает передачи старых (или исходных) значений, а также новых значений. Кроме того, это значение свойства указывает имена входных параметров для исходных значений. Так как мы передаваем исходные значения в BLL, не удаляйте это свойство.
Замечание
Значение OldValuesParameterFormatString
свойства должно сопоставляться с именами входных параметров в BLL, которые ожидают исходных значений. Так как мы назвали эти параметры original_productName
, original_supplierID
и т. д., можно оставить OldValuesParameterFormatString
значение свойства как original_{0}
. Однако если входные параметры методов BLL имеют такие имена, как old_productName
, old_supplierID
и т. д., необходимо обновить OldValuesParameterFormatString
свойство до old_{0}
.
Есть один окончательный параметр свойства, который необходимо установить для правильной передачи исходных значений в методы BLL. ObjectDataSource имеет свойство ConflictDetection , которое можно назначить одному из двух значений:
-
OverwriteChanges
— значение по умолчанию; не отправляет исходные значения в исходные входные параметры методов BLL -
CompareAllValues
— отправляет исходные значения методам BLL; Выберите этот параметр при использовании оптимистического параллелизма
Уделите немного времени, чтобы установить свойство ConflictDetection
в CompareAllValues
.
Настройка свойств и полей GridView
Теперь, когда свойства ObjectDataSource правильно настроены, обратимся к настройке GridView. Во-первых, так как мы хотим, чтобы GridView поддерживал редактирование и удаление, щелкните флажки "Включить редактирование" и "Включить удаление" из смарт-тега GridView. Это добавит CommandField, для которого и ShowEditButton
, и ShowDeleteButton
установлены в true
.
При привязке к ProductsOptimisticConcurrencyDataSource
ObjectDataSource GridView содержит поле для каждого поля данных продукта. Хотя такое представление GridView может быть изменено, пользовательский опыт оставляет желать лучшего. Поля CategoryID
SupplierID
и BoundFields будут отображаться как текстовые поля, требуя от пользователя ввести соответствующую категорию и поставщика в качестве идентификаторов. Для числовых полей не будет ни форматирования, ни элементов управления проверкой для обеспечения наличия имени продукта, а также для проверки, что цена за единицу, количество единиц на складе, заказанные единицы и значения уровня переупорядочения являются корректными числовыми значениями и больше или равны нулю.
Как мы говорили в руководстве по добавлению элементов управления проверки в интерфейсы редактирования и вставки инастройке интерфейса изменения данных , пользовательский интерфейс можно настроить, заменив 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
(как и для нашего случая), то, когда методы ObjectDataSource CompareAllValues
или Update()
вызываются GridView (или DetailsView или FormView), ObjectDataSource пытается скопировать исходные значения GridView в соответствующие экземпляры Delete()
. Вернитесь к рис. 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, у которого свойство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
программно обращаются к элементу веб-управления Label, в котором отображается значениеUnitPrice
, и устанавливают для его свойстваText
отформатированную версию. - Оставьте
UnitPrice
в формате валюты. В обработчике событий GridViewRowDeleting
замените существующее исходноеUnitPrice
значение ($19,95) фактическим десятичным значением.Decimal.Parse
Мы узнали, как сделать нечто подобное в обработчикеRowUpdating
событий в руководстве по обработке BLL и DAL-Level исключений на странице ASP.NET.
В моем примере я решил использовать второй подход, добавив скрытый веб-элемент управления Label, свойство которого Text
является двусторонним данным, привязанным к неформатированного UnitPrice
значению.
После решения этой проблемы попробуйте снова нажать кнопку "Удалить" для любого продукта. На этот раз вы получите InvalidOperationException
, когда ObjectDataSource попытается вызвать метод BLL UpdateProduct
.
Рис. 16. ОбъектDataSource не может найти метод с входными параметрами, которые он хочет отправить (щелкните, чтобы просмотреть изображение полного размера)
Глядя на сообщение исключения, ясно, что ObjectDataSource хочет вызвать метод BLL DeleteProduct
, включающий original_CategoryName
и original_SupplierName
входные параметры. Это связано с тем, что в ItemTemplate
и CategoryID
TemplateFields в настоящее время содержатся двусторонние инструкции Bind с полями данных SupplierID
и CategoryName
. Вместо этого необходимо включить 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 Tea и нажмите кнопку "Обновить". Обновление должно завершиться успешно и вернуть GridView в его состояние до редактирования с именем нового продукта «Chai Tea».
Однако в другом экземпляре окна браузера название продукта в TextBox всё ещё отображает "Chai". Во втором окне браузера измените UnitPrice
на 25.00
. Без поддержки оптимистического параллелизма нажатие кнопки "Обновить" во втором экземпляре браузера изменит название продукта на "Chai", тем самым перезаписав изменения, внесенные первым экземпляром браузера. Однако при использовании оптимистического параллелизма нажатие кнопки "Обновить" во втором экземпляре браузера приводит к ошибке DBConcurrencyException.
Рис. 17. При обнаружении DBConcurrencyException
нарушения параллелизма генерируется исключение (нажмите, чтобы просмотреть изображение в полном размере)
Возникает DBConcurrencyException
только при использовании шаблона пакетного обновления DAL. Прямой шаблон базы данных не вызывает исключения, он просто указывает, что ни одной строки не были затронуты. Чтобы проиллюстрировать это, верните оба экземпляра браузера GridView в состояние предварительного редактирования. Затем в первом экземпляре браузера нажмите кнопку "Изменить" и измените имя продукта с "Chai Tea" обратно на "Chai" и нажмите кнопку "Обновить". Во втором окне браузера нажмите кнопку "Удалить" для Chai.
При нажатии кнопки "Удалить" страница обновляется, GridView вызывает метод Delete()
объекта ObjectDataSource, а ObjectDataSource вызывает метод ProductsOptimisticConcurrencyBLL
класса DeleteProduct
, передавая исходные значения. Исходное ProductName
значение для второго экземпляра браузера — Chai Tea, которое не соответствует текущему ProductName
значению в базе данных. Поэтому инструкция DELETE
, выданная базе данных, не затрагивает ни одной строки, так как в ней нет записи, которая WHERE
соответствует условию. Метод DeleteProduct
возвращает false
, а данные ObjectDataSource повторно привязываются к GridView.
С точки зрения пользователя, нажатие кнопки "Удалить" для "Чай латте" во втором окне браузера вызвало мерцание экрана, и после его восстановления продукт все еще присутствует, хотя теперь он указан как "Chai" (изменение названия продукта, внесённое первым окном браузера). Если пользователь снова нажимает кнопку "Удалить", удаление будет выполнено успешно, так как исходное ProductName
значение GridView ("Chai") теперь совпадает со значением в базе данных.
В обоих случаях взаимодействие с пользователем далеко не идеально. Очевидно, что мы не хотим показывать пользователю подробные сведения об DBConcurrencyException
исключении при использовании шаблона пакетного обновления. И поведение при использовании прямого шаблона базы данных несколько запутано, поскольку команда для пользователей не удалась, но не было точного объяснения причины.
Чтобы устранить эти две проблемы, мы можем создать элементы управления Label на странице, которые предоставляют объяснение, почему обновление или удаление завершилось сбоем. Для шаблона пакетного обновления можно определить, произошло ли DBConcurrencyException
исключение в обработчике событий после уровня GridView, отображая метку предупреждения по мере необходимости. Для простого метода работы с базой данных мы можем проверить возвращаемое значение метода BLL (которое равно true
, если была затронута одна строка, false
в противном случае) и при необходимости отобразить информационное сообщение.
Шаг 6. Добавление информационных сообщений и их отображение перед лицом нарушения параллелизма
При возникновении нарушения параллелизма поведение, которое отображается, зависит от того, использовался ли пакетное обновление DAL или прямой шаблон базы данных. В нашем руководстве используются оба шаблона: шаблон пакетного обновления используется для обновления, а шаблон прямого доступа к БД используется для удаления. Чтобы приступить к работе, давайте добавим два веб-элемента управления Label Web на страницу, объясняющую, что нарушение параллелизма произошло при попытке удалить или обновить данные. Установите у элемента управления Label свойства Visible
и EnableViewState
в значение 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.
Рис. 18. На страницу добавлены два элемента управления метками (щелкните, чтобы просмотреть изображение полного размера)
С помощью этих веб-элементов управления Label мы готовы изучить, как определить, когда произошло нарушение согласованности, в какой момент можно задать соответствующее Visible
свойство Labeltrue
, отображающее информационное сообщение.
Обработка нарушений конкуренции при обновлении
Сначала рассмотрим, как обрабатывать нарушения параллелизма при использовании шаблона пакетного обновления. Поскольку такие нарушения шаблона пакетного обновления вызывают DBConcurrencyException
исключение, нам необходимо добавить код на нашу страницу ASP.NET, чтобы выяснить, произошло ли DBConcurrencyException
исключение во время процесса обновления. Если это так, мы должны отобразить сообщение пользователю, объясняя, что их изменения не были сохранены, так как другой пользователь изменил те же данные между началом редактирования записи и нажатием кнопки "Обновить".
Как мы видели в учебнике по обработке BLL и DAL-Level исключений на странице ASP.NET, такие исключения могут быть обнаружены и подавлены в обработчиках событий уровня пост-обработки в управлении данными. Поэтому необходимо создать обработчик событий для события GridView RowUpdated
, который проверяет, было ли DBConcurrencyException
выброшено исключение. Этот обработчик событий передает ссылку на любое исключение, которое было создано во время обновления, как показано в коде обработчика событий ниже:
Protected Sub ProductsGrid_RowUpdated _
(ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
Handles ProductsGrid.RowUpdated
If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
' Display the warning message and note that the exception has
' been handled...
UpdateConflictMessage.Visible = True
e.ExceptionHandled = True
End If
End If
End Sub
Перед лицом DBConcurrencyException
исключения этот обработчик событий отображает UpdateConflictMessage
элемент управления Label и указывает, что исключение обработано. Если при обновлении записи возникает нарушение параллелизма, изменения пользователя теряются, так как они перезаписали изменения другого пользователя одновременно. В частности, GridView возвращается в состояние предварительного редактирования и привязан к текущим данным базы данных. Это приведет к обновлению строки GridView с изменениями другого пользователя, которые ранее не были видимы. Кроме того, UpdateConflictMessage
элемент управления Label объяснит пользователю, что только что произошло. Эта последовательность событий подробно описана на рис. 19.
Рис. 19. Обновления пользователя теряются в случае нарушений параллелизма (щелкните, чтобы увеличить)
Замечание
Кроме того, вместо возврата GridView в состояние предварительного редактирования можно оставить GridView в состоянии редактирования, установив KeepInEditMode
для свойства переданного GridViewUpdatedEventArgs
объекта значение true. Однако при таком подходе необходимо повторно привязать данные к GridView (вызывая его DataBind()
метод), чтобы значения другого пользователя загружались в интерфейс редактирования. Код, доступный для скачивания вместе с этим руководством, имеет две строки в обработчике событий RowUpdated
, которые закомментированы. Просто раскомментируйте эти строки, чтобы GridView оставался в режиме редактирования после нарушения параллелизма.
Реагирование на нарушения параллелизма при удалении
При использовании прямого шаблона базы данных исключение не возникает в случае нарушения параллелизма. Вместо этого инструкция базы данных просто не влияет на записи, так как предложение WHERE не совпадает с какой-либо записью. Все методы изменения данных, созданные в BLL, были разработаны таким образом, чтобы они возвращали логическое значение, указывающее, повлияли ли они именно на одну запись. Таким образом, чтобы определить, произошло ли нарушение параллелизма при удалении записи, мы можем проверить возвращаемое значение метода BLL DeleteProduct
.
Возвращаемое значение метода BLL можно проверить в обработчиках событий верхнего уровня ObjectDataSource через свойство ReturnValue
объекта ObjectDataSourceStatusEventArgs
, переданного в обработчик событий. Так как мы заинтересованы в определении возвращаемого значения из DeleteProduct
метода, необходимо создать обработчик событий для события ObjectDataSource Deleted
. Свойство ReturnValue
имеет тип object
и может быть null
, если возникло исключение, и метод был прерван, прежде чем он может вернуть значение. Поэтому сначала следует убедиться, что ReturnValue
свойство не null
является логическим значением. При условии прохождения этой проверки, мы показываем элемент управления DeleteConflictMessage
Label, если ReturnValue
является false
. Это можно сделать с помощью следующего кода:
Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
(ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
Handles ProductsOptimisticConcurrencyDataSource.Deleted
If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
If deleteReturnValue = False Then
' No row was deleted, display the warning message
DeleteConflictMessage.Visible = True
End If
End If
End Sub
В случае нарушения параллелизма запрос на удаление пользователя отменяется. GridView обновляется, отображая изменения, которые произошли для этой записи между временем загрузки страницы пользователем и нажатием кнопки "Удалить". При таком DeleteConflictMessage
нарушении отображается метка, объясняющая, что только что произошло (см. рис. 20).
Рис. 20. Удаление пользователя отменено в лице нарушения параллелизма (щелкните, чтобы просмотреть изображение полного размера)
Сводка
Возможности для нарушений параллелизма существуют в каждом приложении, позволяющем нескольким, параллельным пользователям обновлять или удалять данные. Если такие ситуации не учитываются, когда два пользователя одновременно обновляют одни и те же данные, то тот, кто внесёт последнее изменение, "побеждает", перезаписывая изменения другого пользователя. В качестве альтернативы разработчики могут реализовать оптимистичное или пессимистичное управление параллельностью. Элемент управления оптимистичным параллелизмом предполагает, что нарушения параллелизма нечасто и просто запрещают команду обновления или удаления, которая будет представлять собой нарушение параллелизма. Пессимистическое управление параллелизмом предполагает, что нарушения параллелизма случаются часто и просто отклонение команды обновления или удаления одного пользователя неприемлемо. При использовании пессимистичного контроля параллелизма обновление записи включает блокировку, что предотвращает изменение или удаление записи другими пользователями во время её блокировки.
Типизированный набор данных в .NET предоставляет функциональные возможности для поддержки управления оптимистическим параллелизмом. В частности, инструкции, выданные базе данных, UPDATE
и DELETE
, включают все столбцы таблицы, тем самым гарантируя, что обновление или удаление будет происходить только в том случае, если текущие данные записи совпадают с исходными данными, которые были у пользователя при выполнении обновления или удаления. После настройки DAL для поддержки оптимистического параллелизма необходимо обновить методы BLL. Кроме того, необходимо настроить страницу ASP.NET, которая обращается к BLL, так, чтобы ObjectDataSource извлекал исходные значения из веб-элемента управления данными и передает их BLL.
Как мы видели в этом руководстве, реализация управления оптимистическим параллелизмом в веб-приложении ASP.NET включает обновление DAL и BLL и добавление поддержки на странице ASP.NET. Независимо от того, является ли эта добавленная работа мудрым вложением времени и усилий, зависит от вашего приложения. Если у вас редко работают одновременно пользователи, обновляющие данные, или данные, которые они обновляют, различаются друг от друга, управление параллелизмом не является ключевой проблемой. Однако, если на вашем сайте несколько пользователей работают с одними и теми же данными, контроль параллельного выполнения может помочь предотвратить невольное перезаписывание обновлений или удалений одного пользователя другим.
Счастливое программирование!
Сведения о авторе
Скотт Митчелл, автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с технологиями Microsoft Web с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга — Sams Teach Yourself ASP.NET 2.0 за 24 часа. С ним можно связаться по адресу mitchell@4GuysFromRolla.com.