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


Реализация оптимистичной конкуренции (VB)

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

Скачать в формате PDF

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

Введение

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

Например, представьте, что два пользователя 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.

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

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

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

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

Рис. 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

Используйте тот же SQL-запрос из TableAdapter продуктов в оригинальном DAL

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

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

Активируйте оптимистическое управление конкуренцией, установив галочку

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

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

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

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

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

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

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

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

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 (область справа над FillGetProducts именами методов) и выберите "Добавить запрос" в контекстном меню. Откроется мастер настройки запросов TableAdapter. Как и в случае с начальной конфигурацией TableAdapter, выберите создать метод GetProductByProductID(productID) с помощью временного SQL-запроса (см. рис. 4). GetProductByProductID(productID) Так как метод возвращает сведения о конкретном продукте, укажите, что этот запрос является типом SELECT запроса, возвращающим строки.

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

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

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

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

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

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

Переименование методов в FillByProductID и GetProductByProductID

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

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

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

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

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

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

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

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

Рис. 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), при использовании шаблона пакетного обновления для обновления сведений о продукте код выполняет следующую последовательность событий:

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

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

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

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

Заставьте ObjectDataSource использовать объект 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 может быть изменено, пользовательский опыт оставляет желать лучшего. Поля CategoryIDSupplierID и BoundFields будут отображаться как текстовые поля, требуя от пользователя ввести соответствующую категорию и поставщика в качестве идентификаторов. Для числовых полей не будет ни форматирования, ни элементов управления проверкой для обеспечения наличия имени продукта, а также для проверки, что цена за единицу, количество единиц на складе, заказанные единицы и значения уровня переупорядочения являются корректными числовыми значениями и больше или равны нулю.

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

  • Удалены ProductID, SupplierName, и CategoryName BoundFields
  • Преобразовал BoundField в ProductName TemplateField и добавил элемент управления RequiredFieldValidation.
  • Преобразовал CategoryID и SupplierID BoundFields в TemplateFields и отрегулировал интерфейс редактирования, чтобы использовать DropDownLists, а не TextBoxes. В этих TemplateFields ItemTemplates отображаются поля данных 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 выводит список каждого продукта с кнопкой "Изменить и удалить" в самом левом столбце.

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

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

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

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

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

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

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

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

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

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

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

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

<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.

При обнаружении нарушения параллелизма создается исключение 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.