Implementar a simultaneidade otimista (VB)
por Scott Mitchell
Para um aplicativo Web que permite que vários usuários editem dados, há o risco de dois usuários estarem editando os mesmos dados ao mesmo tempo. Neste tutorial, implementaremos o controle de simultaneidade otimista para lidar com esse risco.
Introdução
Para aplicativos Web que permitem apenas que os usuários exibam dados ou para aqueles que incluem apenas um único usuário que possa modificar dados, não há ameaça de dois usuários simultâneos substituirem acidentalmente as alterações uns dos outros. Para aplicativos Web que permitem que vários usuários atualizem ou excluam dados, no entanto, há o potencial para que as modificações de um usuário entrem em conflito com os de outro usuário simultâneo. Sem nenhuma política de simultaneidade em vigor, quando dois usuários estiverem editando simultaneamente um único registro, o usuário que confirmar suas alterações por último substituirá as alterações feitas pelo primeiro.
Por exemplo, imagine que dois usuários, Jisun e Sam, estavam visitando uma página em nosso aplicativo que permitia aos visitantes atualizar e excluir os produtos por meio de um controle GridView. Ambos clicam no botão Editar no GridView ao mesmo tempo. O Jisun altera o nome do produto para "Chai Tea" e clica no botão Atualizar. O resultado líquido é uma instrução UPDATE
enviada ao banco de dados, que define todos os campos atualizáveis do produto (embora Jisun tenha atualizado apenas um campo, ProductName
). Neste momento, o banco de dados tem os valores "Chai Tea", a categoria Bebidas, o fornecedor Liquids Exóticos e assim por diante para este produto específico. No entanto, o GridView na tela do Sam ainda mostra o nome do produto na linha Editável GridView como "Chai". Alguns segundos após as alterações de Jisun terem sido confirmadas, Sam atualiza a categoria para Condimentos e clica em Atualizar. Isso resulta em uma instrução UPDATE
enviada ao banco de dados que define o nome do produto como "Chai", a CategoryID
para a ID de categoria de Bebidas correspondente e assim por diante. As alterações de Jisun no nome do produto foram substituídas. A Figura 1 ilustra graficamente esta série de eventos.
Figura 1: quando dois usuários atualizam simultaneamente um registro, há potencial para alterações de um usuário para substituir os outros (clique para exibir a imagem em tamanho real)
Da mesma forma, quando dois usuários estão visitando uma página, um usuário pode estar no meio da atualização de um registro quando ele é excluído por outro usuário. Ou, entre quando um usuário carrega uma página e quando clica no botão Excluir, outro usuário pode ter modificado o conteúdo desse registro.
Há três estratégias de controle de simultaneidade disponíveis:
- Não faça nada - se os usuários simultâneos estiverem modificando o mesmo registro, deixe o último commit ganhar (o comportamento padrão)
- Simultaneidade otimista – suponha que, embora possa haver conflitos de simultaneidade de vez em quando, a grande maioria das vezes em que esses conflitos não surgirão; portanto, se um conflito surgir, basta informar ao usuário que suas alterações não podem ser salvas porque outro usuário modificou os mesmos dados
- Simultaneidade pessimista – suponha que os conflitos de simultaneidade sejam comuns e que os usuários não tolerarão ser informados de que suas alterações não foram salvas devido à atividade simultânea de outro usuário; portanto, quando um usuário começar a atualizar um registro, bloqueie-o, impedindo assim que outros usuários editem ou excluam esse registro até que o usuário confirme suas modificações
Todos os nossos tutoriais até agora usaram a estratégia padrão de resolução de simultaneidade– ou seja, deixamos a última gravação ganhar. Neste tutorial, examinaremos como implementar o controle de simultaneidade otimista.
Observação
Não veremos exemplos pessimistas de simultaneidade nesta série de tutoriais. A simultaneidade pessimista raramente é usada porque esses bloqueios, se não forem renunciados corretamente, podem impedir que outros usuários atualizem dados. Por exemplo, se um usuário bloquear um registro para edição e sair um dia antes de desbloqueá-lo, nenhum outro usuário poderá atualizar esse registro até que o usuário original retorne e conclua sua atualização. Portanto, em situações em que a simultaneidade pessimista é usada, normalmente há um tempo limite que, se atingido, cancela o bloqueio. Sites de vendas de tíquetes, que bloqueiam um local específico de estações por um curto período enquanto o usuário conclui o processo de pedido, é um exemplo de controle pessimista de simultaneidade.
Etapa 1: Observando como a simultaneidade otimista é implementada
O controle de simultaneidade otimista funciona garantindo que o registro que está sendo atualizado ou excluído tenha os mesmos valores que quando o processo de atualização ou exclusão foi iniciado. Por exemplo, ao clicar no botão Editar em um GridView editável, os valores do registro são lidos do banco de dados e exibidos em TextBoxes e outros controles da Web. Esses valores originais são salvos pelo GridView. Posteriormente, depois que o usuário fizer suas alterações e clicar no botão Atualizar, os valores originais mais os novos valores serão enviados para a Camada de Lógica de Negócios e, em seguida, até a Camada de Acesso a Dados. A Camada de Acesso a Dados deve emitir uma instrução SQL que só atualizará o registro se os valores originais que o usuário começou a editar forem idênticos aos valores ainda no banco de dados. A Figura 2 ilustra essa sequência de eventos.
Figura 2: para que a atualização ou a exclusão sejam bem-sucedidas, os valores originais devem ser iguais aos valores atuais do banco de dados (clique para exibir a imagem em tamanho real)
Há várias abordagens para implementar a simultaneidade otimista (consulte a Lógica otimista de atualização de simultaneidade de Peter A. Bromberg para obter uma breve visão de várias opções). O ADO.NET Conjunto de Dados Tipado fornece uma implementação que pode ser configurada apenas com o tique de uma caixa de seleção. Habilitar a simultaneidade otimista para um TableAdapter no Typed DataSet aumenta as instruções e DELETE
do UPDATE
TableAdapter para incluir uma comparação de todos os valores originais na WHERE
cláusula . A instrução a seguir UPDATE
, por exemplo, atualiza o nome e o preço de um produto somente se os valores atuais do banco de dados forem iguais aos valores que foram originalmente recuperados ao atualizar o registro no GridView. Os @ProductName
parâmetros e @UnitPrice
contêm os novos valores inseridos pelo usuário, enquanto @original_ProductName
e @original_UnitPrice
contêm os valores que foram originalmente carregados no GridView quando o botão Editar foi clicado:
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
Observação
Esta UPDATE
instrução foi simplificada para legibilidade. Na prática, o UnitPrice
marcar na WHERE
cláusula estaria mais envolvido, pois UnitPrice
pode conter NULL
s e verificar se NULL = NULL
sempre retorna False (em vez disso, você deve usar IS NULL
).
Além de usar uma instrução UPDATE
subjacente diferente, configurar um TableAdapter para usar simultaneidade otimista também modifica a assinatura de seus métodos diretos de BD. Lembre-se do nosso primeiro tutorial, Criando uma camada de acesso a dados, de que os métodos diretos do BD eram aqueles que aceitam uma lista de valores escalares como parâmetros de entrada (em vez de uma instância de DataRow ou DataTable fortemente tipada). Ao usar simultaneidade otimista, os métodos diretos Update()
e Delete()
de banco de dados incluem parâmetros de entrada para os valores originais também. Além disso, o código na BLL para usar o padrão de atualização em lote (as Update()
sobrecargas de método que aceitam DataRows e DataTables em vez de valores escalares) também devem ser alteradas.
Em vez de estender os TableAdapters da DAL existentes para usar a simultaneidade otimista (o que exigiria a alteração da BLL para acomodar), vamos criar um novo Conjunto de Dados Digitado chamado NorthwindOptimisticConcurrency
, ao qual adicionaremos um Products
TableAdapter que usa simultaneidade otimista. Depois disso, criaremos uma ProductsOptimisticConcurrencyBLL
classe camada de lógica empresarial que tem as modificações apropriadas para dar suporte ao DAL de simultaneidade otimista. Depois que essa base for colocada, estaremos prontos para criar a página ASP.NET.
Etapa 2: Criando uma camada de acesso a dados que dá suporte à simultaneidade otimista
Para criar um novo Conjunto de Dados Digitado, clique com o botão direito do DAL
mouse na pasta dentro da App_Code
pasta e adicione um novo Conjunto de Dados chamado NorthwindOptimisticConcurrency
. Como vimos no primeiro tutorial, isso adicionará um novo TableAdapter ao Typed DataSet, iniciando automaticamente o Assistente de Configuração do TableAdapter. Na primeira tela, é solicitado que especifique o banco de dados ao qual se conectar – conecte-se ao mesmo banco de dados Northwind usando a NORTHWNDConnectionString
configuração de Web.config
.
Figura 3: Conectar-se ao mesmo banco de dados Northwind (clique para exibir a imagem em tamanho real)
Em seguida, somos solicitados a consultar os dados: por meio de uma instrução SQL ad hoc, um novo procedimento armazenado ou um procedimento armazenado existente. Como usamos consultas SQL ad hoc em nosso DAL original, use essa opção aqui também.
Figura 4: especifique os dados a serem recuperados usando uma instrução SQL Ad Hoc (clique para exibir a imagem em tamanho real)
Na tela a seguir, insira a consulta SQL a ser usada para recuperar as informações do produto. Vamos usar exatamente a mesma consulta SQL usada para o Products
TableAdapter de nosso DAL original, que retorna todas as colunas junto com os Product
nomes de fornecedor e categoria do produto:
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
Figura 5: usar a mesma consulta SQL do Products
TableAdapter no DAL Original (Clique para exibir a imagem em tamanho real)
Antes de passar para a próxima tela, clique no botão Opções Avançadas. Para que esse TableAdapter empregue um controle de simultaneidade otimista, basta marcar caixa de seleção "Usar simultaneidade otimista".
Figura 6: Habilitar o controle de simultaneidade otimista verificando a caixa de seleção "Usar simultaneidade otimista" (clique para exibir a imagem em tamanho real)
Por fim, indique que o TableAdapter deve usar os padrões de acesso a dados que preenchem um DataTable e retornam uma DataTable; também indicam que os métodos diretos do BD devem ser criados. Altere o nome do método para o padrão Return a DataTable de GetData para GetProducts, de modo a espelho as convenções de nomenclatura que usamos em nosso DAL original.
Figura 7: Fazer com que o TableAdapter utilize todos os padrões de acesso a dados (clique para exibir a imagem em tamanho real)
Depois de concluir o assistente, o dataset Designer incluirá um DataTable e TableAdapter fortemente tipadoProducts
. Reserve um momento para renomear o DataTable de Products
para ProductsOptimisticConcurrency
, o que você pode fazer clicando com o botão direito do mouse na barra de título do DataTable e escolhendo Renomear no menu de contexto.
Figura 8: Um DataTable e TableAdapter foram adicionados ao Conjunto de Dados Digitado (Clique para exibir a imagem em tamanho real)
Para ver as diferenças entre as UPDATE
consultas e DELETE
entre o ProductsOptimisticConcurrency
TableAdapter (que usa simultaneidade otimista) e o Products TableAdapter (o que não faz), clique no TableAdapter e vá para o janela Propriedades. DeleteCommand
Nas subpropriedades das CommandText
propriedades eUpdateCommand
, você pode ver a sintaxe SQL real que é enviada ao banco de dados quando os métodos relacionados à atualização ou exclusão do DAL são invocados. Para o ProductsOptimisticConcurrency
TableAdapter, a DELETE
instrução usada é:
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))
Enquanto a instrução DELETE
para o Product TableAdapter em nosso DAL original é muito mais simples:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
Como você pode ver, a WHERE
cláusula na DELETE
instrução tableAdapter que usa simultaneidade otimista inclui uma comparação entre cada um dos valores de Product
coluna existentes da tabela e os valores originais no momento em que GridView (ou DetailsView ou FormView) foi preenchido pela última vez. Como todos os campos que não ProductID
sejam , ProductName
e Discontinued
podem ter NULL
valores, parâmetros adicionais e verificações são incluídos para comparar NULL
corretamente os valores na WHERE
cláusula .
Não adicionaremos mais DataTables ao DataSet habilitado para simultaneidade otimista para este tutorial, pois nossa página ASP.NET fornecerá apenas informações de atualização e exclusão do produto. No entanto, ainda precisamos adicionar o GetProductByProductID(productID)
método ao ProductsOptimisticConcurrency
TableAdapter.
Para fazer isso, clique com o botão direito do mouse na barra de título do TableAdapter (a área logo acima dos nomes do Fill
método e GetProducts
) e escolha Adicionar Consulta no menu de contexto. Isso iniciará o Assistente de Configuração de Consulta TableAdapter. Assim como acontece com a configuração inicial do TableAdapter, opte por criar o GetProductByProductID(productID)
método usando uma instrução SQL ad hoc (consulte a Figura 4). Como o GetProductByProductID(productID)
método retorna informações sobre um produto específico, indique que essa consulta é um SELECT
tipo de consulta que retorna linhas.
Figura 9: Marcar o Tipo de Consulta como um "SELECT
que retorna linhas" (Clique para exibir a imagem em tamanho real)
Na próxima tela, é solicitado que a consulta SQL seja usada, com a consulta padrão do TableAdapter pré-carregada. Aumente a consulta existente para incluir a cláusula WHERE ProductID = @ProductID
, conforme mostrado na Figura 10.
Figura 10: Adicionar uma WHERE
cláusula à consulta pré-carregada para retornar um registro de produto específico (clique para exibir a imagem em tamanho real)
Por fim, altere os nomes de método gerados para FillByProductID
e GetProductByProductID
.
Figura 11: Renomear os métodos para FillByProductID
e GetProductByProductID
(clique para exibir a imagem em tamanho real)
Com esse assistente concluído, o TableAdapter agora contém dois métodos para recuperar dados: GetProducts()
, que retorna todos os produtos; e GetProductByProductID(productID)
, que retorna o produto especificado.
Etapa 3: Criando uma camada de lógica de negócios para o DAL de Concurrency-Enabled otimista
Nossa classe existente ProductsBLL
tem exemplos de como usar a atualização em lote e os padrões diretos do BD. O AddProduct
método e UpdateProduct
as sobrecargas usam o padrão de atualização em lote, passando uma ProductRow
instância para o método Update do TableAdapter. O DeleteProduct
método, por outro lado, usa o padrão direto do BD, chamando o método tableAdapter Delete(productID)
.
Com o novo ProductsOptimisticConcurrency
TableAdapter, os métodos diretos do BD agora exigem que os valores originais também sejam passados. Por exemplo, o Delete
método agora espera dez parâmetros de entrada: o original ProductID
, ProductName
, SupplierID
, CategoryID
, QuantityPerUnit
, UnitPrice
, UnitsInStock
, UnitsOnOrder
, ReorderLevel
e Discontinued
. Ele usa os valores desses parâmetros de entrada adicionais na WHERE
cláusula da DELETE
instrução enviada ao banco de dados, excluindo apenas o registro especificado se os valores atuais do banco de dados forem mapeados para os originais.
Embora a assinatura do método do Update
método TableAdapter usado no padrão de atualização em lote não tenha sido alterada, o código necessário para registrar os valores originais e novos foi alterado. Portanto, em vez de tentar usar o DAL habilitado para simultaneidade otimista com nossa classe existente ProductsBLL
, vamos criar uma nova classe camada de lógica de negócios para trabalhar com nosso novo DAL.
Adicione uma classe chamada ProductsOptimisticConcurrencyBLL
à BLL
pasta dentro da App_Code
pasta .
Figura 12: Adicionar a ProductsOptimisticConcurrencyBLL
classe à pasta BLL
Em seguida, adicione o seguinte código à ProductsOptimisticConcurrencyBLL
classe :
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
Observe a instrução using NorthwindOptimisticConcurrencyTableAdapters
acima do início da declaração de classe. O NorthwindOptimisticConcurrencyTableAdapters
namespace contém a ProductsOptimisticConcurrencyTableAdapter
classe , que fornece os métodos do DAL. Além disso, antes da declaração de classe, você encontrará o atributo , que instrui o System.ComponentModel.DataObject
Visual Studio a incluir essa classe na lista suspensa do assistente ObjectDataSource.
A ProductsOptimisticConcurrencyBLL
propriedade do Adapter
fornece acesso rápido a uma instância da ProductsOptimisticConcurrencyTableAdapter
classe e segue o padrão usado em nossas classes BLL originais (ProductsBLL
, CategoriesBLL
e assim por diante). Por fim, o GetProducts()
método simplesmente chama para baixo no método da GetProducts()
DAL e retorna um ProductsOptimisticConcurrencyDataTable
objeto preenchido com uma ProductsOptimisticConcurrencyRow
instância para cada registro de produto no banco de dados.
Excluindo um produto usando o padrão direto do BD com simultaneidade otimista
Ao usar o padrão direto do BD em relação a um DAL que usa simultaneidade otimista, os métodos devem ser passados os valores novos e originais. Para excluir, não há novos valores, portanto, somente os valores originais precisam ser passados. Em nossa BLL, então, devemos aceitar todos os parâmetros originais como parâmetros de entrada. Vamos fazer com que o DeleteProduct
método na ProductsOptimisticConcurrencyBLL
classe use o método direto do BD. Isso significa que esse método precisa usar todos os dez campos de dados do produto como parâmetros de entrada e passá-los para o DAL, conforme mostrado no código a seguir:
<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
Se os valores originais - os valores que foram carregados pela última vez no GridView (ou DetailsView ou FormView) - forem diferentes dos valores no banco de dados quando o usuário clicar no botão Excluir, a WHERE
cláusula não corresponderá a nenhum registro de banco de dados e nenhum registro será afetado. Portanto, o método tableAdapter Delete
retornará 0
e o método da DeleteProduct
BLL retornará false
.
Atualizando um produto usando o padrão de atualização em lote com simultaneidade otimista
Conforme observado anteriormente, o método do Update
TableAdapter para o padrão de atualização em lote tem a mesma assinatura de método, independentemente de a simultaneidade otimista ser empregada ou não. Ou seja, o Update
método espera um DataRow, uma matriz de DataRows, uma DataTable ou um DataSet Tipado. Não há parâmetros de entrada adicionais para especificar os valores originais. Isso é possível porque o DataTable controla os valores originais e modificados para seus DataRow(s). Quando o DAL emite sua UPDATE
instrução, os @original_ColumnName
parâmetros são preenchidos com os valores originais do DataRow, enquanto os @ColumnName
parâmetros são preenchidos com os valores modificados do DataRow.
ProductsBLL
Na classe (que usa nosso DAL de simultaneidade original e não otimista), ao usar o padrão de atualização em lote para atualizar as informações do produto, nosso código executa a seguinte sequência de eventos:
- Ler as informações atuais do produto de banco de dados em uma
ProductRow
instância usando o método tableAdapterGetProductByProductID(productID)
- Atribuir os novos valores à instância da
ProductRow
Etapa 1 - Chamar o método tableAdapter
Update
, passando aProductRow
instância
Essa sequência de etapas, no entanto, não oferecerá suporte correto à simultaneidade otimista porque o ProductRow
preenchido na Etapa 1 é preenchido diretamente do banco de dados, o que significa que os valores originais usados pelo DataRow são aqueles que existem atualmente no banco de dados e não aqueles que estavam associados ao GridView no início do processo de edição. Em vez disso, ao usar um DAL habilitado para simultaneidade otimista, precisamos alterar as sobrecargas de UpdateProduct
método para usar as seguintes etapas:
- Ler as informações atuais do produto de banco de dados em uma
ProductsOptimisticConcurrencyRow
instância usando o método tableAdapterGetProductByProductID(productID)
- Atribuir os valores originais à
ProductsOptimisticConcurrencyRow
instância da Etapa 1 - Chame o
ProductsOptimisticConcurrencyRow
método daAcceptChanges()
instância , que instrui o DataRow de que seus valores atuais são os "originais" - Atribuir os novos valores à
ProductsOptimisticConcurrencyRow
instância - Chamar o método tableAdapter
Update
, passando aProductsOptimisticConcurrencyRow
instância
A etapa 1 lê todos os valores de banco de dados atuais para o registro de produto especificado. Essa etapa é supérflua na UpdateProduct
sobrecarga que atualiza todas as colunas do produto (pois esses valores são substituídos na Etapa 2), mas é essencial para essas sobrecargas em que apenas um subconjunto dos valores de coluna é passado como parâmetros de entrada. Depois que os valores originais tiverem sido atribuídos à ProductsOptimisticConcurrencyRow
instância, o AcceptChanges()
método será chamado, o que marca os valores atuais de DataRow como os valores originais a serem usados nos @original_ColumnName
parâmetros na UPDATE
instrução . Em seguida, os novos valores de parâmetro são atribuídos ao ProductsOptimisticConcurrencyRow
e, por fim, o Update
método é invocado, passando o DataRow.
O código a seguir mostra a UpdateProduct
sobrecarga que aceita todos os campos de dados do produto como parâmetros de entrada. Embora não seja mostrada aqui, a ProductsOptimisticConcurrencyBLL
classe incluída no download deste tutorial também contém uma UpdateProduct
sobrecarga que aceita apenas o nome e o preço do produto como parâmetros de entrada.
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
Etapa 4: passando os valores original e novo da página ASP.NET para os métodos BLL
Com o DAL e a BLL concluídos, tudo o que resta é criar uma página de ASP.NET que possa utilizar a lógica de simultaneidade otimista incorporada ao sistema. Especificamente, o controle da Web de dados (GridView, DetailsView ou FormView) deve se lembrar de seus valores originais e o ObjectDataSource deve passar os dois conjuntos de valores para a Camada de Lógica de Negócios. Além disso, a página ASP.NET deve ser configurada para lidar normalmente com violações de simultaneidade.
Comece abrindo a OptimisticConcurrency.aspx
página na EditInsertDelete
pasta e adicionando um GridView ao Designer, definindo sua ID
propriedade ProductsGrid
como . Na marca inteligente do GridView, opte por criar um novo ObjectDataSource chamado ProductsOptimisticConcurrencyDataSource
. Como queremos que este ObjectDataSource use o DAL que dá suporte à simultaneidade otimista, configure-o para usar o ProductsOptimisticConcurrencyBLL
objeto .
Figura 13: Fazer com que o ObjectDataSource use o ProductsOptimisticConcurrencyBLL
objeto (clique para exibir a imagem em tamanho real)
Escolha os GetProducts
métodos , UpdateProduct
e DeleteProduct
das listas suspensas no assistente. Para o método UpdateProduct, use a sobrecarga que aceita todos os campos de dados do produto.
Configurando as propriedades do controle ObjectDataSource
Depois de concluir o assistente, a marcação declarativa do ObjectDataSource deve ser semelhante à seguinte:
<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>
Como você pode ver, a DeleteParameters
coleção contém uma instância para cada um Parameter
dos dez parâmetros de entrada no ProductsOptimisticConcurrencyBLL
método da DeleteProduct
classe. Da mesma forma, a UpdateParameters
coleção contém uma Parameter
instância para cada um dos parâmetros de entrada em UpdateProduct
.
Para os tutoriais anteriores que envolveram a modificação de dados, removeríamos a propriedade objectDataSource OldValuesParameterFormatString
neste ponto, pois essa propriedade indica que o método BLL espera que os valores antigos (ou originais) sejam passados, bem como os novos valores. Além disso, esse valor de propriedade indica os nomes de parâmetro de entrada para os valores originais. Como estamos passando os valores originais para a BLL, não remova essa propriedade.
Observação
O valor da OldValuesParameterFormatString
propriedade deve ser mapeado para os nomes de parâmetro de entrada na BLL que esperam os valores originais. Como nomeamos esses parâmetros original_productName
, original_supplierID
e assim por diante, você pode deixar o valor da OldValuesParameterFormatString
propriedade como original_{0}
. Se, no entanto, os parâmetros de entrada dos métodos BLL tivessem nomes como old_productName
, old_supplierID
e assim por diante, você precisaria atualizar a OldValuesParameterFormatString
propriedade para old_{0}
.
Há uma configuração de propriedade final que precisa ser feita para que o ObjectDataSource passe corretamente os valores originais para os métodos BLL. O ObjectDataSource tem uma propriedade ConflictDetection que pode ser atribuída a um dos dois valores:
OverwriteChanges
– o valor padrão; não envia os valores originais para os parâmetros de entrada originais dos métodos BLLCompareAllValues
– envia os valores originais para os métodos BLL; escolha essa opção ao usar a simultaneidade otimista
Reserve um momento para definir a ConflictDetection
propriedade como CompareAllValues
.
Configurando as propriedades e os campos do GridView
Com as propriedades do ObjectDataSource configuradas corretamente, vamos voltar nossa atenção para a configuração do GridView. Primeiro, como queremos que o GridView dê suporte à edição e exclusão, clique nas caixas de seleção Habilitar Edição e Habilitar Exclusão da marca inteligente gridView. Isso adicionará um CommandField cujos ShowEditButton
e ShowDeleteButton
estão definidos como true
.
Quando associado ao ProductsOptimisticConcurrencyDataSource
ObjectDataSource, o GridView contém um campo para cada um dos campos de dados do produto. Embora esse GridView possa ser editado, a experiência do usuário é tudo menos aceitável. O CategoryID
e SupplierID
BoundFields serão renderizados como TextBoxes, exigindo que o usuário insira a categoria e o fornecedor apropriados como números de ID. Não haverá formatação para os campos numéricos e nenhum controle de validação para garantir que o nome do produto tenha sido fornecido e que o preço unitário, as unidades em estoque, as unidades na ordem e os valores de nível de reordenação sejam valores numéricos adequados e sejam maiores ou iguais a zero.
Como discutimos nos tutoriais Adicionando controles de validação às interfaces de edição e inserção e personalizando a interface de modificação de dados , a interface do usuário pode ser personalizada substituindo BoundFields por TemplateFields. Modifiquei este GridView e sua interface de edição das seguintes maneiras:
- Removidos boundFields
ProductID
,SupplierName
eCategoryName
- Converteu BoundField
ProductName
em um TemplateField e adicionou um controle RequiredFieldValidation. - Converteu e
CategoryID
SupplierID
BoundFields em TemplateFields e ajustou a interface de edição para usar DropDownLists em vez de TextBoxes. Nesses TemplateFieldsItemTemplates
, osCategoryName
campos de dados eSupplierName
são exibidos. - Converteu ,
UnitPrice
UnitsInStock
,UnitsOnOrder
eReorderLevel
BoundFields em TemplateFields e adicionou controles CompareValidator.
Como já examinamos como realizar essas tarefas em tutoriais anteriores, vou apenas listar a sintaxe declarativa final aqui e deixar a implementação como prática.
<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>
Estamos muito perto de ter um exemplo de trabalho completo. No entanto, há algumas sutilezas que vão subir e nos causar problemas. Além disso, ainda precisamos de alguma interface que alerte o usuário quando ocorreu uma violação de simultaneidade.
Observação
Para que um controle da Web de dados passe corretamente os valores originais para ObjectDataSource (que são passados para a BLL), é vital que a propriedade gridView EnableViewState
seja definida true
como (o padrão). Se você desabilitar o estado de exibição, os valores originais serão perdidos no postback.
Passando os valores originais corretos para o ObjectDataSource
Há alguns problemas com a forma como o GridView foi configurado. Se a propriedade ObjectDataSource ConflictDetection
estiver definida CompareAllValues
como (como é nossa), quando os métodos ou Delete()
objectDataSource Update()
forem invocados pelo GridView (ou DetailsView ou FormView), o ObjectDataSource tentará copiar os valores originais do GridView em suas instâncias apropriadasParameter
. Consulte a Figura 2 para obter uma representação gráfica desse processo.
Especificamente, os valores originais do GridView recebem os valores nas instruções bidirecionais de vinculação de dados sempre que os dados são associados ao GridView. Portanto, é essencial que todos os valores originais necessários sejam capturados por meio da vinculação de dados bidirecional e que sejam fornecidos em um formato conversível.
Para ver por que isso é importante, reserve um momento para visitar nossa página em um navegador. Conforme esperado, o GridView lista cada produto com um botão Editar e Excluir na coluna mais à esquerda.
Figura 14: Os produtos são listados em um GridView (clique para exibir imagem em tamanho real)
Se você clicar no botão Excluir para qualquer produto, um FormatException
será gerado.
Figura 15: Tentando excluir todos os resultados do produto em um FormatException
(clique para exibir a imagem em tamanho real)
O FormatException
é gerado quando ObjectDataSource tenta ler no valor original UnitPrice
. Como o ItemTemplate
tem formatado UnitPrice
como uma moeda (<%# Bind("UnitPrice", "{0:C}") %>
), ele inclui um símbolo de moeda, como US$ 19,95. O FormatException
ocorre quando o ObjectDataSource tenta converter essa cadeia de caracteres em um decimal
. Para contornar esse problema, temos várias opções:
- Remova a formatação de moeda do
ItemTemplate
. Ou seja, em vez de usar<%# Bind("UnitPrice", "{0:C}") %>
, basta usar<%# Bind("UnitPrice") %>
. A desvantagem disso é que o preço não está mais formatado. - Exiba o
UnitPrice
formatado como uma moeda noItemTemplate
, mas use oEval
palavra-chave para fazer isso. Lembre-se de queEval
executa a vinculação de dados unidirecional. Ainda precisamos fornecer oUnitPrice
valor para os valores originais, portanto, ainda precisaremos de uma instrução de vinculação de dados bidirecional noItemTemplate
, mas isso pode ser colocado em um controle Web Label cujaVisible
propriedade está definida comofalse
. Poderíamos usar a seguinte marcação no 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>
- Remova a formatação de moeda do
ItemTemplate
, usando<%# Bind("UnitPrice") %>
. No manipulador de eventos doRowDataBound
GridView, acesse programaticamente o controle Web Label no qual oUnitPrice
valor é exibido e defina suaText
propriedade como a versão formatada. - Deixe o
UnitPrice
formatado como uma moeda. No manipulador de eventos doRowDeleting
GridView, substitua o valor originalUnitPrice
existente (US$ 19,95) por um valor decimal real usandoDecimal.Parse
. Vimos como realizar algo semelhante noRowUpdating
manipulador de eventos no tutorial Manipulando exceções de BLL e DAL-Level em um tutorial de página ASP.NET .
Para meu exemplo, optei por usar a segunda abordagem, adicionando um controle Web de rótulo oculto cuja Text
propriedade é dados bidirecionais associados ao valor não formatado UnitPrice
.
Depois de resolver esse problema, tente clicar no botão Excluir para qualquer produto novamente. Desta vez, você obterá um InvalidOperationException
quando ObjectDataSource tentar invocar o método da UpdateProduct
BLL.
Figura 16: O ObjectDataSource não pode encontrar um método com os parâmetros de entrada que deseja enviar (clique para exibir a imagem em tamanho real)
Examinando a mensagem da exceção, está claro que ObjectDataSource deseja invocar um método BLL DeleteProduct
que inclua original_CategoryName
parâmetros de entrada e original_SupplierName
. Isso ocorre porque os ItemTemplate
s para o CategoryID
e SupplierID
TemplateFields atualmente contêm instruções Bind bidirecionais com os CategoryName
campos de dados e SupplierName
. Em vez disso, precisamos incluir Bind
instruções com os CategoryID
campos de dados e SupplierID
. Para fazer isso, substitua as instruções Bind existentes por Eval
instruções e adicione controles label ocultos cujas Text
propriedades estão associadas aos CategoryID
campos de dados e SupplierID
usando a vinculação de dados bidirecional, conforme mostrado abaixo:
<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>
Com essas alterações, agora podemos excluir e editar informações do produto com êxito! Na Etapa 5, examinaremos como verificar se violações de simultaneidade estão sendo detectadas. Mas, por enquanto, leve alguns minutos para tentar atualizar e excluir alguns registros para garantir que a atualização e a exclusão de um único usuário funcionem conforme o esperado.
Etapa 5: Testar o suporte de simultaneidade otimista
Para verificar se violações de simultaneidade estão sendo detectadas (em vez de resultar em dados sendo substituídos cegamente), precisamos abrir duas janelas do navegador para esta página. Em ambas as instâncias do navegador, clique no botão Editar para Chai. Em seguida, em apenas um dos navegadores, altere o nome para "Chai Tea" e clique em Atualizar. A atualização deve ter êxito e retornar o GridView para seu estado de pré-edição, com "Chai Tea" como o novo nome do produto.
Na outra instância da janela do navegador, no entanto, o nome do produto TextBox ainda mostra "Chai". Nesta segunda janela do navegador, atualize o UnitPrice
para 25.00
. Sem suporte de simultaneidade otimista, clicar em atualizar na segunda instância do navegador alteraria o nome do produto de volta para "Chai", substituindo assim as alterações feitas pela primeira instância do navegador. Com a simultaneidade otimista empregada, no entanto, clicar no botão Atualizar na segunda instância do navegador resulta em uma DBConcurrencyException.
Figura 17: quando uma violação de simultaneidade é detectada, um DBConcurrencyException
é gerado (clique para exibir a imagem em tamanho real)
O DBConcurrencyException
só é gerado quando o padrão de atualização em lote do DAL é utilizado. O padrão direto do BD não gera uma exceção, apenas indica que nenhuma linha foi afetada. Para ilustrar isso, retorne GridView de ambas as instâncias do navegador ao estado de pré-edição. Em seguida, na primeira instância do navegador, clique no botão Editar e altere o nome do produto de "Chai Tea" de volta para "Chai" e clique em Atualizar. Na segunda janela do navegador, clique no botão Excluir para Chai.
Ao clicar em Excluir, a página retornará, o GridView invocará o método objectDataSource Delete()
e ObjectDataSource chamará para baixo no ProductsOptimisticConcurrencyBLL
método da DeleteProduct
classe, passando os valores originais. O valor original ProductName
da segunda instância do navegador é "Chai Tea", que não corresponde ao valor atual ProductName
no banco de dados. Portanto, a DELETE
instrução emitida para o banco de dados afeta zero linhas, pois não há nenhum registro no banco de dados que a WHERE
cláusula satisfaça. O DeleteProduct
método retorna false
e os dados do ObjectDataSource são recuperados para o GridView.
Da perspectiva do usuário final, clicar no botão Excluir do Chai Tea na segunda janela do navegador fez com que a tela piscasse e, ao voltar, o produto ainda estivesse lá, embora agora esteja listado como "Chai" (a alteração do nome do produto feita pela primeira instância do navegador). Se o usuário clicar no botão Excluir novamente, a exclusão terá êxito, pois o valor original ProductName
do GridView ("Chai") agora corresponde ao valor no banco de dados.
Em ambos os casos, a experiência do usuário está longe de ser ideal. Claramente, não queremos mostrar ao usuário os detalhes da DBConcurrencyException
exceção ao usar o padrão de atualização em lote. E o comportamento ao usar o padrão direto do BD é um pouco confuso, pois o comando de usuários falhou, mas não havia uma indicação precisa do motivo.
Para corrigir esses dois problemas, podemos criar controles Da Web de rótulo na página que fornecem uma explicação sobre por que uma atualização ou exclusão falhou. Para o padrão de atualização em lote, podemos determinar se ocorreu ou não uma DBConcurrencyException
exceção no manipulador de eventos pós-nível do GridView, exibindo o rótulo de aviso conforme necessário. Para o método direto do BD, podemos examinar o valor retornado do método BLL (que é true
se uma linha foi afetada, false
caso contrário) e exibir uma mensagem informativa conforme necessário.
Etapa 6: Adicionar mensagens informativas e exibi-las diante de uma violação de simultaneidade
Quando ocorre uma violação de simultaneidade, o comportamento exibido depende se a atualização em lote do DAL ou o padrão direto do BD foi usado. Nosso tutorial usa ambos os padrões, com o padrão de atualização em lote sendo usado para atualização e o padrão direto do BD usado para exclusão. Para começar, vamos adicionar dois controles Da Web de rótulo à nossa página que explicam que ocorreu uma violação de simultaneidade ao tentar excluir ou atualizar dados. Defina as propriedades e do Visible
controle Rótulo como false
; isso fará com que eles fiquem ocultos em cada visita de página, exceto para as visitas de página específicas em que sua Visible
propriedade é definida programaticamente como true
.EnableViewState
<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." />
Além de definir suas Visible
propriedades , EnabledViewState
e Text
, também defina a CssClass
propriedade como Warning
, o que faz com que o Rótulo seja exibido em uma fonte grande, vermelha, itálica e em negrito. Essa classe CSS Warning
foi definida e adicionada a Styles.css novamente no tutorial Examinando os eventos associados à inserção, atualização e exclusão .
Depois de adicionar esses Rótulos, o Designer no Visual Studio deve ser semelhante à Figura 18.
Figura 18: Dois controles de rótulo foram adicionados à página (clique para exibir a imagem em tamanho real)
Com esses controles Da Web de Rótulo em vigor, estamos prontos para examinar como determinar quando ocorreu uma violação de simultaneidade, momento em que a propriedade apropriada do Visible
Rótulo pode ser definida como true
, exibindo a mensagem informativa.
Tratamento de violações de simultaneidade ao atualizar
Primeiro, vamos examinar como lidar com violações de simultaneidade ao usar o padrão de atualização em lote. Como essas violações com o padrão de atualização em lote fazem com que uma DBConcurrencyException
exceção seja gerada, precisamos adicionar código à nossa página ASP.NET para determinar se ocorreu uma DBConcurrencyException
exceção durante o processo de atualização. Nesse caso, devemos exibir uma mensagem para o usuário explicando que suas alterações não foram salvas porque outro usuário modificou os mesmos dados entre quando começou a editar o registro e quando clicou no botão Atualizar.
Como vimos no tutorial Manipulando exceções de BLL e DAL-Level em um tutorial de página ASP.NET , essas exceções podem ser detectadas e suprimidas nos manipuladores de eventos pós-nível do controle Web de dados. Portanto, precisamos criar um manipulador de eventos para o evento gridview RowUpdated
que verifica se uma DBConcurrencyException
exceção foi gerada. Esse manipulador de eventos é passado uma referência a qualquer exceção gerada durante o processo de atualização, conforme mostrado no código do manipulador de eventos abaixo:
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
Diante de uma DBConcurrencyException
exceção, esse manipulador de eventos exibe o UpdateConflictMessage
controle Rótulo e indica que a exceção foi tratada. Com esse código em vigor, quando ocorre uma violação de simultaneidade ao atualizar um registro, as alterações do usuário são perdidas, pois teriam substituído as modificações de outro usuário ao mesmo tempo. Em particular, o GridView é retornado ao seu estado de pré-edição e associado aos dados atuais do banco de dados. Isso atualizará a linha GridView com as alterações do outro usuário, que anteriormente não estavam visíveis. Além disso, o UpdateConflictMessage
controle Rótulo explicará ao usuário o que acabou de acontecer. Essa sequência de eventos é detalhada na Figura 19.
Figura 19: Os Atualizações de um usuário são perdidos em face de uma violação de simultaneidade (clique para exibir a imagem em tamanho real)
Observação
Como alternativa, em vez de retornar o GridView para o estado de pré-edição, poderíamos deixar o GridView em seu estado de edição definindo a KeepInEditMode
propriedade do objeto passado como GridViewUpdatedEventArgs
true. No entanto, se você adotar essa abordagem, certifique-se de reassociar os dados ao GridView (invocando seu DataBind()
método) para que os valores do outro usuário sejam carregados na interface de edição. O código disponível para download com este tutorial tem essas duas linhas de código no RowUpdated
manipulador de eventos comentadas; basta remover a marca de comentário dessas linhas de código para que o GridView permaneça no modo de edição após uma violação de simultaneidade.
Respondendo a violações de simultaneidade ao excluir
Com o padrão direto do BD, não há exceção gerada diante de uma violação de simultaneidade. Em vez disso, a instrução de banco de dados simplesmente não afeta nenhum registro, pois a cláusula WHERE não corresponde a nenhum registro. Todos os métodos de modificação de dados criados na BLL foram projetados de modo que eles retornem um valor booliano indicando se eles afetaram precisamente um registro. Portanto, para determinar se ocorreu uma violação de simultaneidade ao excluir um registro, podemos examinar o valor retornado do método da DeleteProduct
BLL.
O valor retornado de um método BLL pode ser examinado nos manipuladores de eventos pós-nível do ObjectDataSource por meio da ReturnValue
propriedade do ObjectDataSourceStatusEventArgs
objeto passado para o manipulador de eventos. Como estamos interessados em determinar o valor retornado do DeleteProduct
método , precisamos criar um manipulador de eventos para o evento objectDataSource Deleted
. A ReturnValue
propriedade é do tipo object
e pode ser null
se uma exceção foi gerada e o método foi interrompido antes que pudesse retornar um valor. Portanto, devemos primeiro garantir que a ReturnValue
propriedade não null
seja e seja um valor booliano. Supondo que esse marcar passe, mostraremos o DeleteConflictMessage
controle Rótulo se o ReturnValue
for false
. Isso pode ser feito usando o seguinte código:
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
Diante de uma violação de simultaneidade, a solicitação de exclusão do usuário é cancelada. O GridView é atualizado, mostrando as alterações que ocorreram para esse registro entre a hora em que o usuário carregou a página e quando clicou no botão Excluir. Quando essa violação ocorre, o DeleteConflictMessage
Rótulo é mostrado, explicando o que acabou de acontecer (consulte a Figura 20).
Figura 20: Uma exclusão de usuário é cancelada em face de uma violação de simultaneidade (clique para exibir a imagem em tamanho real)
Resumo
Existem oportunidades de violações de simultaneidade em todos os aplicativos que permitem que vários usuários simultâneos atualizem ou excluam dados. Se essas violações não forem contabilizados, quando dois usuários atualizarem simultaneamente os mesmos dados que receberem na última gravação "vence", a substituição das alterações do outro usuário será alterada. Como alternativa, os desenvolvedores podem implementar o controle de simultaneidade otimista ou pessimista. O controle de simultaneidade otimista pressupõe que as violações de simultaneidade são pouco frequentes e simplesmente não permitem um comando de atualização ou exclusão que constituiria uma violação de simultaneidade. O controle de simultaneidade pessimista pressupõe que violações de simultaneidade são frequentes e simplesmente rejeitar o comando de atualização ou exclusão de um usuário não é aceitável. Com o controle de simultaneidade pessimista, atualizar um registro envolve bloqueá-lo, impedindo assim que outros usuários modifiquem ou excluam o registro enquanto ele estiver bloqueado.
O Conjunto de Dados Tipado no .NET fornece funcionalidade para dar suporte ao controle de simultaneidade otimista. Em particular, as UPDATE
instruções e DELETE
emitidas para o banco de dados incluem todas as colunas da tabela, garantindo assim que a atualização ou exclusão só ocorrerá se os dados atuais do registro corresponderem aos dados originais que o usuário tinha ao executar sua atualização ou exclusão. Depois que o DAL tiver sido configurado para dar suporte à simultaneidade otimista, os métodos BLL precisarão ser atualizados. Além disso, a página ASP.NET que chama a BLL deve ser configurada de modo que o ObjectDataSource recupere os valores originais de seu controle da Web de dados e os passe para a BLL.
Como vimos neste tutorial, implementar o controle de simultaneidade otimista em um aplicativo Web ASP.NET envolve atualizar o DAL e a BLL e adicionar suporte na página ASP.NET. Se esse trabalho adicionado é ou não um investimento sábio do seu tempo e esforço depende do seu aplicativo. Se você raramente tiver usuários simultâneos atualizando dados ou se os dados que eles estão atualizando forem diferentes uns dos outros, o controle de simultaneidade não será um problema fundamental. Se, no entanto, você tiver rotineiramente vários usuários em seu site trabalhando com os mesmos dados, o controle de simultaneidade poderá ajudar a impedir que as atualizações ou exclusões de um usuário substituam involuntariamente as de outro.
Programação feliz!
Sobre o autor
Scott Mitchell, autor de sete livros do ASP/ASP.NET e fundador da 4GuysFromRolla.com, trabalha com tecnologias da Microsoft Web desde 1998. Scott trabalha como consultor independente, treinador e escritor. Seu último livro é Sams Teach Yourself ASP.NET 2.0 em 24 Horas. Ele pode ser contatado em mitchell@4GuysFromRolla.com. ou através de seu blog, que pode ser encontrado em http://ScottOnWriting.NET.