Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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 só permitem que os usuários exibam dados ou para aqueles que incluem apenas um único usuário que possa modificar dados, não há nenhuma 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 qualquer 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 pela primeira.
Por exemplo, imagine que dois usuários, Jisun e Sam, estavam visitando uma página em nosso aplicativo que permitia que os visitantes atualizassem e excluíssem 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 o Jisun tenha atualizado apenas um campo). ProductName
Neste momento, o banco de dados tem os valores "Chai Tea", a categoria Bebidas, o fornecedor Exotic Liquids 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 GridView editável como "Chai". Alguns segundos após as alterações do 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
ID da 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 concorrência disponíveis.
- Do Nothing -if usuários simultâneos estão modificando o mesmo registro, permitir que a última confirmação ganhe (o comportamento padrão)
- Simultaneidade otimista – suponha que, embora possa haver conflitos de simultaneidade de vez em quando, a grande maioria das vezes tais 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 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ça a atualizar um registro, bloqueie-o, impedindo 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 examinaremos exemplos pessimistas de simultaneidade nesta série de tutoriais. A simultaneidade pessimista raramente é usada porque esses bloqueios, se não forem entregues 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. Os sites de venda de tíquetes, que bloqueiam um local específico de assentos por um curto período enquanto o usuário conclui o processo de pedido, são um exemplo de controle de simultaneidade pessimista.
Etapa 1: Examinando 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 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 exclusão seja bem-sucedida, 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 concorrência otimista (consulte a Lógica de Atualização de Concorrência Otimista de Peter A. Bromberg para uma breve análise de várias opções). O ADO.NET Typed DataSet 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 do TableAdapter UPDATE
e DELETE
para incluir uma comparação de todos os valores originais na cláusula WHERE
.
UPDATE
A instrução a seguir, 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 parâmetros @ProductName
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, a checagem na cláusula UnitPrice
seria mais complexa, pois WHERE
pode conter UnitPrice
s, e a verificação se NULL
sempre retorna False (em vez disso, você deve usar NULL = NULL
).
Além de usar uma instrução UPDATE
subjacente diferente, configurar um TableAdapter para usar a concorrência otimista também modifica a assinatura de seus métodos diretos de acesso ao banco de dados. Lembre-se do nosso primeiro tutorial, Criando uma Camada de Acesso a Dados, de que os métodos diretos do BD eram aqueles que aceitavam uma lista de valores escalares como parâmetros de entrada (em vez de uma instância de DataRow ou DataTable fortemente tipada). Ao usar a simultaneidade otimista, os métodos DB direto Update()
e Delete()
também incluem parâmetros de entrada para os valores originais. 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 deve ser alterado.
Em vez de estender os TableAdapters do DAL existentes para usar a simultaneidade otimista (que exigiria alterar a 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 de Camada lógica de negócios que tem as modificações apropriadas para dar suporte ao DAL de simultaneidade otimista. Depois que essa base for estabelecida, 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 Typed DataSet, clique com o botão direito na pasta DAL
dentro da pasta App_Code
e adicione um novo DataSet chamado NorthwindOptimisticConcurrency
. Como vimos no primeiro tutorial, isso adicionará um novo TableAdapter ao Conjunto de Dados Tipado, iniciando automaticamente o Assistente de Configuração do TableAdapter. Na primeira tela, solicita-se que você especifique o banco de dados ao qual se conectar – conecte-se ao mesmo banco de dados Northwind usando a configuração NORTHWNDConnectionString
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: Especificar 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 Product
colunas junto com os 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 verificar a 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 padrões de acesso a dados que tanto preenchem quanto retornam um DataTable; além de indicar que os métodos diretos do BD devem ser criados. Altere o nome do método para retornar um padrão DataTable de GetData para GetProducts, de modo a espelhar 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 Designer de Conjunto de Dados incluirá um DataTable Products
e um TableAdapter tipado estritamente. 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 da DataTable e escolhendo Renomear no menu de contexto.
Figura 8: Um DataTable e TableAdapter foram adicionados ao Conjunto de Dados Tipado (clique para exibir a imagem em tamanho real)
Para ver as diferenças entre as consultas do UPDATE
e DELETE
entre o TableAdapter ProductsOptimisticConcurrency
(que usa simultaneidade otimista) e o TableAdapter de Products (que não usa), clique no TableAdapter e vá para a janela de Propriedades. Nas subpropriedades DeleteCommand
das propriedades UpdateCommand
e CommandText
, é possível ver a sintaxe SQL real que é enviada para o banco de dados quando os métodos de atualização ou exclusão do DAL são invocados. Para o ProductsOptimisticConcurrency
TableAdapter, a instrução DELETE
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 DELETE
instrução para o Product TableAdapter em nosso DAL original é muito mais simples:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
Como você pode ver, a cláusula WHERE
na declaração DELETE
do TableAdapter que usa simultaneidade otimista inclui uma comparação entre cada um dos valores existentes das colunas da tabela Product
e os valores originais no momento em que o GridView (ou DetailsView ou FormView) foi preenchido pela última vez. Como todos os campos, exceto ProductID
, ProductName
e Discontinued
, podem ter valores NULL
, parâmetros e verificações adicionais são incluídos para comparar corretamente os valores na cláusula NULL
.
Não adicionaremos quaisquer DataTables adicionais ao DataSet habilitado para simultaneidade otimista para este tutorial, pois nossa página ASP.NET fornecerá apenas informações de produtos para atualização e exclusão. 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 dos métodos Fill
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
.GetProductByProductID
Figura 11: Renomear os métodos para FillByProductID
e GetProductByProductID
(Clique para exibir a imagem em tamanho real)
Com este 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 otimista Concurrency-Enabled
Nossa classe existente ProductsBLL
tem exemplos de como usar tanto os padrões de atualização em lote quanto os diretos de banco de dados. O método AddProduct
e as sobrecargas UpdateProduct
ambos utilizam o padrão de atualização em lote, passando uma instância ProductRow
para o método Update do TableAdapter. O DeleteProduct
método, por outro lado, usa o padrão direto da base de dados, chamando o método TableAdapter Delete(productID)
.
Com o novo ProductsOptimisticConcurrency
TableAdapter, os métodos diretos de acesso ao banco de dados agora exigem que os valores originais também sejam passados. Por exemplo, o Delete
método agora espera dez parâmetros de entrada: original ProductID
, , ProductName
, SupplierID
, CategoryID
, QuantityPerUnit
, , UnitPrice
, UnitsInStock
, UnitsOnOrder
, , ReorderLevel
e Discontinued
. Ele usa os valores desses parâmetros de entrada adicionais na cláusula WHERE
da instrução DELETE
enviada ao banco de dados, excluindo apenas o registro especificado se os valores atuais do banco de dados corresponderem aos valores originais.
Embora a assinatura do método Update
do 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. Portanto, em vez de tentar usar o DAL habilitado para simultaneidade otimista com nossa classe existente ProductsBLL
, vamos criar uma nova classe de Camada de Lógica de Negócios para trabalhar com nosso novo DAL.
Adicione uma classe nomeada 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 System.ComponentModel.DataObject
, que instrui o Visual Studio a incluir esta classe na lista suspensa do assistente ObjectDataSource.
A propriedade ProductsOptimisticConcurrencyBLL
de Adapter
fornece acesso rápido a uma instância da classe ProductsOptimisticConcurrencyTableAdapter
e segue o padrão usado em nossas classes BLL originais (ProductsBLL
, CategoriesBLL
e assim por diante). Por fim, o método GetProducts()
simplesmente chama o método GetProducts()
da DAL e retorna um objeto ProductsOptimisticConcurrencyDataTable
preenchido com uma instância ProductsOptimisticConcurrencyRow
para cada registro de produto no banco de dados.
Excluindo um produto usando o padrão DB Direct com concorrência otimista
Ao usar o padrão de acesso direto ao banco de dados em relação a um DAL que utiliza simultaneidade otimista, os métodos devem receber 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 tableAdapter Update
para o padrão de atualização em lote tem a mesma assinatura de método, independentemente de a simultaneidade otimista ser ou não empregada. Ou seja, o Update
método espera um DataRow, uma matriz de DataRows, uma DataTable ou um Conjunto de Dados Digitado. 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:
- Leia as informações atuais do produto do banco de dados em uma instância
ProductRow
usando o método do TableAdapterGetProductByProductID(productID)
. - Atribua os novos valores à instância de
ProductRow
da Etapa 1 - Chame o método TableAdapter
Update
, passando a instânciaProductRow
Essa sequência de etapas, no entanto, não oferecerá suporte corretamente à 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 foram 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 do UpdateProduct
método de forma a usar as seguintes etapas:
- Leia as informações atuais do produto do banco de dados em uma instância
ProductsOptimisticConcurrencyRow
usando o método do TableAdapterGetProductByProductID(productID)
. - Atribuir valores originais à instância
ProductsOptimisticConcurrencyRow
da Etapa 1 - Chame o método
ProductsOptimisticConcurrencyRow
da instânciaAcceptChanges()
, que instrui o DataRow sobre seus valores atuais como sendo os "originais". - Atribua os novos valores à
ProductsOptimisticConcurrencyRow
instância - Chame o método TableAdapter
Update
, passando a instânciaProductsOptimisticConcurrencyRow
A etapa 1 lê todos os valores de banco de dados atuais para o registro do produto especificado. Esta etapa é supérflua na UpdateProduct
sobrecarga que atualiza todas as colunas do produto (pois esses valores são sobrescritos na Etapa 2), mas é essencial para aquelas sobrecargas em que apenas um subconjunto dos valores de coluna é fornecido como parâmetros de entrada. Assim que os valores originais são atribuídos à ProductsOptimisticConcurrencyRow
instância, o AcceptChanges()
método é chamado, marcando os valores atuais do DataRow como os valores originais que serão usados como parâmetros no comando @original_ColumnName
. Em seguida, os novos valores de parâmetro são atribuídos ao ProductsOptimisticConcurrencyRow
e, por fim, o método Update
é invocado, passando o DataRow como argumento.
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 mostrado 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 originais e novos 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 ASP.NET que possa utilizar a lógica de simultaneidade otimista interna no sistema. Especificamente, o controle da Web de dados (GridView, DetailsView ou FormView) deve lembrar seus valores originais e o ObjectDataSource deve passar os dois conjuntos de valores para a Camada 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 como ProductsGrid
. Na tag 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 métodos GetProducts
, UpdateProduct
e DeleteProduct
das listas suspensas no assistente. Para o método UpdateProduct, utilize uma sobrecarga que aceite 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 coleção DeleteParameters
contém uma instância Parameter
para cada um dos dez parâmetros de entrada no método ProductsOptimisticConcurrencyBLL
da classe DeleteProduct
. Da mesma forma, a UpdateParameters
coleção contém uma instância para cada um Parameter
dos parâmetros de entrada em UpdateProduct
.
Para os tutoriais anteriores que envolviam modificação de dados, removeríamos a propriedade objectDataSource OldValuesParameterFormatString
neste momento, 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 BLL -
CompareAllValues
- envia os valores originais para os métodos BLL; escolha essa opção ao usar simultaneidade otimista
Reserve um momento para definir a ConflictDetection
propriedade como CompareAllValues
.
Configurando as propriedades e campos do GridView
Com as propriedades do ObjectDataSource configuradas corretamente, vamos voltar nossa atenção para configurar o 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 do GridView. Isso adicionará um CommandField cujo ShowEditButton
e ShowDeleteButton
ambos 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, unidades em estoque, unidades em ordem e valores de nível de reordenação sejam valores numéricos adequados e sejam maiores ou iguais a zero.
Como discutimos na adição de controles de validação às interfaces de edição e inserção e personalização dos tutoriais da 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:
- Removeu os
ProductID
,SupplierName
eCategoryName
BoundFields - Converteu o
ProductName
BoundField em um TemplateField e adicionou um controle RequiredFieldValidation. - Converteu o
CategoryID
eSupplierID
BoundFields em TemplateFields e ajustou a interface de edição para usar DropDownLists em vez de TextBoxes. Nestes TemplateFieldsItemTemplates
, os campos de dadosCategoryName
eSupplierName
são exibidos. - Converteu os
UnitPrice
,UnitsInStock
,UnitsOnOrder
eReorderLevel
BoundFields em TemplateFields e adicionou controles CompareValidator.
Como já examinamos como realizar essas tarefas em tutoriais anteriores, vou 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 próximos de ter um exemplo totalmente funcional. No entanto, há algumas sutilezas que vão surgir 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 web de dados passe corretamente os valores originais para o ObjectDataSource (que são então repassados para a BLL), é vital que a propriedade EnableViewState
do GridView seja definida como true
(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 ConflictDetection
do ObjectDataSource estiver definida como CompareAllValues
(como é nossa), quando os métodos Update()
ou Delete()
do ObjectDataSource são invocados pelo GridView (ou DetailsView ou FormView), o ObjectDataSource tentará copiar os valores originais do GridView em suas instâncias apropriadas Parameter
. Consulte a Figura 2 para obter uma representação gráfica desse processo.
Especificamente, os valores originais do GridView são atualizados com os valores nas instruções de vinculação de dados bidirecionais 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. Como 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 a 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 imagem em tamanho real)
FormatException
é acionado quando o ObjectDataSource tenta ler o valor original UnitPrice
. Como o ItemTemplate
tem formatado UnitPrice
como moeda (<%# Bind("UnitPrice", "{0:C}") %>
), ele inclui um símbolo de moeda, como US$ 19,95. Ocorre FormatException
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 aEval
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
do GridView, acesse programaticamente o controle Web de Label no qual o valorUnitPrice
é exibido e defina sua propriedadeText
como a versão formatada do valor. - 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 tratamento de exceções de BLL e DAL-Level em um tutorial de página ASP.NET .
Para o meu exemplo, optei por usar a segunda abordagem, adicionando um controle Web de rótulo oculto cuja Text
propriedade está associada por dados de forma bidirecional 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 o ObjectDataSource tentar invocar o método UpdateProduct
da 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, é claro que o ObjectDataSource deseja invocar um método BLL DeleteProduct
que inclui original_CategoryName
e original_SupplierName
parâmetros de entrada. Isso ocorre porque os ItemTemplate
s para os CategoryID
e SupplierID
TemplateFields atualmente contêm declarações Bind bidirecionais com os campos de dados CategoryName
e SupplierName
. Em vez disso, precisamos incluir Bind
instruções com os campos de dados CategoryID
e SupplierID
. Para fazer isso, substitua as instruções Bind existentes por Eval
instruções e adicione controles de rótulo 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 com êxito as informações do produto! Na Etapa 5, examinaremos como verificar se as 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: Testando o suporte à concorrência 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 ao 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 na caixa de texto 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 concorrência é detectada, uma DBConcurrencyException
é gerada (clique para exibir a imagem em tamanho real)
O DBConcurrencyException
é lançado apenas quando o padrão de atualização em lote do DAL é utilizado. O padrão direto do banco de dados não gera uma exceção; ele apenas indica que nenhuma linha foi afetada. Para ilustrar isso, retorne o GridView de ambas as instâncias do navegador para seu 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 faz uma atualização, o GridView invoca o método Delete()
do ObjectDataSource e o ObjectDataSource chama o método ProductsOptimisticConcurrencyBLL
da classe DeleteProduct
, 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 existe 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.
Do ponto de vista 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 ele 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 nenhuma indicação precisa do motivo.
Para corrigir esses dois problemas, podemos criar controles de rótulo na Web na página que fornecem uma explicação para o motivo de uma atualização ou exclusão ter falhado. 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 concorrência
Quando ocorre uma violação de simultaneidade, o comportamento exibido depende de ter sido utilizada a atualização em lote do DAL ou o padrão direto do banco de dados. Nosso tutorial usa ambos os padrões, com o padrão de atualização em lote sendo utilizado para atualização e o padrão direto do banco de dados usado para exclusão de dados. Para começar, vamos adicionar dois controles de rótulo Web à nossa página para explicar que ocorreu uma violação de simultaneidade ao tentar excluir ou atualizar dados. Defina as propriedades Visible
e EnableViewState
do controle de 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 propriedade Visible
é programaticamente definida como 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." />
Além de definir as suas propriedades Visible
, EnabledViewState
e Text
, eu também defini a propriedade CssClass
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 Examinar 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 Web de etiqueta implementados, estamos prontos para examinar como determinar quando ocorre uma violação de simultaneidade, momento em que a propriedade correta do Visible
pode ser configurada para true
, exibindo a mensagem informativa.
Tratamento de violações de concorrência durante a atualização
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 BLL e DAL-Level em uma Página ASP.NET, essas exceções podem ser detectadas e suprimidas nos manipuladores de eventos do controle Web de dados. Portanto, precisamos criar um manipulador de eventos para o evento do RowUpdated
GridView que verifica se uma DBConcurrencyException
exceção foi gerada. Esse manipulador de eventos recebe uma referência a qualquer exceção que foi gerada durante o processo de atualização, como 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 Label 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 controle de Rótulo UpdateConflictMessage
explicará para o usuário o que acabou de acontecer. Esta sequência de eventos é detalhada na Figura 19.
Figura 19: As atualizações de um usuário são perdidas diante de uma violação de concorrência (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 propriedade KeepInEditMode
do objeto passado GridViewUpdatedEventArgs
como verdadeiro. No entanto, se você adotar essa abordagem, certifique-se de vincular novamente 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 descomentar essas linhas de código para que o GridView permaneça no modo de edição após uma violação de concorrência.
Respondendo a violações de simultaneidade ao excluir
Com o padrão direto do banco de dados, não há exceção gerada diante de uma violação de concorrência. 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 para 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 do Deleted
ObjectDataSource. 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 propriedade ReturnValue
não seja null
e seja um valor booleano. Supondo que essa verificação passe, mostraremos o controle de rótulo DeleteConflictMessage
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 ele 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 feita por um usuário é cancelada devido a uma violação de simultaneidade (clique para exibir a imagem em tamanho real)
Resumo
Há 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 contabilizadas, quando dois usuários atualizarem simultaneamente os mesmos dados, aquele que fizer a última gravação "ganha," sobrescrevendo as mudanças do outro usuário. 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 instruções UPDATE
emitidas para o banco de dados DELETE
incluem todas as colunas da tabela, garantindo que a atualização ou exclusão ocorrerá somente 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 faz chamadas para a BLL deve ser configurada de forma que o ObjectDataSource recupere os valores originais de seu controle de dados da Web e os envie 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 os de outro.
Divirta-se programando!
Sobre o autor
Scott Mitchell, autor de sete livros 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 alcançado em mitchell@4GuysFromRolla.com.