Partilhar via


Implementando Concorrência Otimista (VB)

por Scott Mitchell

Descarregar PDF

Para um aplicativo Web que permite que vários usuários editem dados, há o risco de que dois usuários possam estar editando os mesmos dados ao mesmo tempo. Neste tutorial, implementaremos um controle de simultaneidade otimista para lidar com esse risco.

Introdução

Para aplicativos Web que permitem que os usuários visualizem dados apenas ou para aqueles que incluem apenas um único usuário que pode modificar dados, não há ameaça de dois usuários simultâneos substituirem acidentalmente as alterações um do outro. Para aplicativos da Web que permitem que vários usuários atualizem ou excluam dados, no entanto, há a possibilidade de as modificações de um usuário entrarem em conflito com as de outro usuário simultâneo. Sem qualquer política de simultaneidade em vigor, quando dois usuários estão 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. Jisun muda o nome do produto para "Chai Tea" e clica no botão Atualizar. O resultado líquido é uma UPDATE instrução que é enviada para o banco de dados, que define todos os campos atualizáveis do produto (mesmo que 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 em particular. No entanto, o GridView na tela de Sam ainda mostra o nome do produto na linha editável do GridView como "Chai". Alguns segundos após as mudanças de Jisun terem sido confirmadas, Sam atualiza a categoria para Condimentos e clica em Atualizar. Isso resulta em uma UPDATE instrução enviada para o banco de dados que define o nome do produto como "Chai", o 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 representa graficamente esta série de eventos.

Quando dois usuários atualizam simultaneamente um registro, há potencial para que as alterações de um usuário substituam as do outro

Figura 1: Quando dois usuários atualizam simultaneamente um registro, há potencial para que as alterações de um usuário substituam as do outro (Clique para visualizar 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 controlo de concorrência disponíveis:

  • Não fazer nada Se -if usuários simultâneos estiverem a modificar o mesmo registo, permite que a última confirmação prevaleça (o comportamento padrão).
  • Simultaneidade Otimista - suponha que, embora possa haver conflitos de simultaneidade de vez em quando, na grande maioria das vezes tais conflitos não surgirão; Portanto, se surgir um conflito, basta informar ao usuário que suas alterações não podem ser salvas porque outro usuário modificou os mesmos dados
  • Simultaneidade Pessimista - assuma que os conflitos de simultaneidade são 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 de resolução de simultaneidade padrão - ou seja, deixamos a última escrita vencer. Neste tutorial, examinaremos como implementar um controle de simultaneidade otimista.

Observação

Não veremos exemplos de simultaneidade pessimista nesta série de tutoriais. A simultaneidade pessimista raramente é usada porque esses bloqueios, se não forem devidamente abandonados, podem impedir que outros usuários atualizem os dados. Por exemplo, se um usuário bloquear um registro para edição e, em seguida, sair para o 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 ingressos, que bloqueiam um determinado local de assentos por um curto período enquanto o usuário conclui o processo de pedido, são um exemplo de controle pessimista de simultaneidade.

Etapa 1: Examinar como a concorrência 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. Mais tarde, depois que o usuário fizer as 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, para 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 mostra esta sequência de eventos.

Para que a atualização ou exclusão seja bem-sucedida, os valores originais devem ser iguais aos valores atuais do banco de dados

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 visualizar a imagem em tamanho real)

Existem várias abordagens para implementar simultaneidade otimista (veja a lógica de atualização da simultaneidade otimista de Peter A. Bromberg para uma breve análise de um número de opções). O ADO.NET Typed DataSet fornece uma implementação que pode ser configurada com apenas o tick de uma caixa de seleção. Habilitar a simultaneidade otimista para um TableAdapter no DataSet tipado UPDATE aumenta as instruções e DELETE do 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 declaração foi simplificada para facilitar a leitura. Na prática, o UnitPrice check-in da WHERE cláusula seria mais envolvido, uma vez que 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 subjacente UPDATE diferente, configurar um TableAdapter para usar simultaneidade otimista também modifica a assinatura de seus métodos diretos de banco de dados. Lembre-se de nosso primeiro tutorial, Criando uma camada de acesso a dados, que os métodos diretos de banco de dados eram aqueles que aceitam uma lista de valores escalares como parâmetros de entrada (em vez de uma instância DataRow ou DataTable fortemente tipada). Ao utilizar concorrência otimista, os métodos diretos do banco de dados 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 nossos TableAdapters de DAL existentes para usar simultaneidade otimista (o que exigiria alterar a BLL para acomodar), vamos criar um novo DataSet Tipado chamado NorthwindOptimisticConcurrency, ao qual adicionaremos um TableAdapter que usa simultaneidade Products otimista. Depois disso, criaremos uma ProductsOptimisticConcurrencyBLL classe Business Logic Layer que tem as modificações apropriadas para dar suporte à simultaneidade otimista DAL. Uma vez que este trabalho de base tenha sido estabelecido, estaremos prontos para criar a página ASP.NET.

Etapa 2: Criando uma camada de acesso a dados que ofereça suporte à simultaneidade otimista

Para criar um novo DataSet Tipado, clique com o botão direito do rato 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 Typed DataSet, iniciando automaticamente o Assistente de Configuração do TableAdapter. Na primeira tela, somos solicitados a especificar o banco de dados ao qual nos conectarmos - conectar-se ao mesmo banco de dados Northwind usando a NORTHWNDConnectionString configuração de Web.config.

Conectar-se ao mesmo banco de dados Northwind

Figura 3: Conectar-se ao mesmo banco de dados Northwind (Clique para visualizar a imagem em tamanho real)

Em seguida, somos solicitados sobre como 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.

Especificar os dados a serem recuperados usando uma instrução SQL ad-hoc

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 seguinte, 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 do 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

Utilizar a mesma consulta SQL do TableAdapter de Produtos na DAL original

Figura 5: Usar a mesma consulta SQL do Products TableAdapter na DAL original (Clique para visualizar a imagem em tamanho normal)

Antes de passar para a próxima tela, clique no botão Opções avançadas. Para que este TableAdapter empregue controle de simultaneidade otimista, basta marcar a caixa de seleção "Usar simultaneidade otimista".

Habilite o controle de simultaneidade otimista marcando a caixa de seleção

Figura 6: Habilite o controle de simultaneidade otimista marcando a caixa de seleção "Usar simultaneidade otimista" (Clique para visualizar a imagem em tamanho real)

Por fim, indique que o TableAdapter deve usar os padrões de acesso a dados que preenchem uma DataTable e retornam uma DataTable; indique também que os métodos diretos do banco de dados devem ser criados. Altere o nome do método para o padrão Return a DataTable de GetData para GetProducts, de modo a espelhar as convenções de nomenclatura que usamos em nosso DAL original.

Faça com que o TableAdapter utilize todos os padrões de acesso a dados

Figura 7: Fazer com que o TableAdapter utilize todos os padrões de acesso a dados (Clique para visualizar a imagem em tamanho real)

Depois de concluir o assistente, o DataSet Designer incluirá um DataTable e um TableAdapter fortemente tipados Products . Dedique um momento para renomear a DataTable de Products para ProductsOptimisticConcurrency, o que pode fazer clicando com o botão direito do rato na barra de título da DataTable e escolhendo Renomear no menu de contexto.

Um DataTable e um TableAdapter foram adicionados ao DataSet tipado

Figura 8: Um DataTable e um TableAdapter foram adicionados ao DataSet Tipado (Clique para ver a imagem em tamanho completo)

Para ver as diferenças entre as UPDATE consultas e DELETE entre o ProductsOptimisticConcurrency TableAdapter (que usa simultaneidade otimista) e o Products TableAdapter (que não usa), clique no TableAdapter e vá para a janela Propriedades. Nas subpropriedades das propriedades DeleteCommand e UpdateCommand, pode-se ver a sintaxe SQL real que é enviada ao banco de dados quando os métodos de 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))

Considerando que a DELETE declaração para o Product TableAdapter no nosso DAL original é bem mais simples.

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

Como pode ver, na instrução de TableAdapter, a cláusula WHEREDELETE que usa simultaneidade otimista inclui uma comparação entre cada valor das colunas existentes na tabela Product e os valores originais aquando de o GridView (ou DetailsView ou FormView) foi por último preenchido. Como todos os campos diferentes de ProductID, ProductName e Discontinued podem ter NULL valores, parâmetros e verificações adicionais são incluídos para comparar os valores NULL corretamente na cláusula WHERE.

Não adicionaremos DataTables adicionais ao DataSet habilitado para simultaneidade otimista para este tutorial, pois a nossa página ASP.NET fornecerá apenas a atualização e exclusão de informações 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 rato 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 do TableAdapter. Assim como na configuração inicial do nosso 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 determinado produto, indique que essa consulta é um tipo de SELECT consulta que retorna linhas.

Marque o tipo de consulta como

Figura 9: Marque o tipo de consulta como um "SELECT que retorna linhas" (Clique para visualizar a imagem em tamanho real)

Na próxima tela, somos solicitados a usar a consulta SQL, com a consulta padrão do TableAdapter pré-carregada. Aumente a consulta existente para incluir a cláusula WHERE ProductID = @ProductID, como mostra a Figura 10.

Adicionar uma cláusula WHERE à consulta pré-carregada para retornar um registro de produto específico

Figura 10: Adicionar uma WHERE cláusula à consulta pré-carregada para retornar um registro de produto específico (Clique para visualizar a imagem em tamanho real)

Finalmente, altere os nomes dos métodos gerados para FillByProductID e GetProductByProductID.

Renomeie os métodos para FillByProductID e GetProductByProductID

Figura 11: Renomeie os métodos para FillByProductID e GetProductByProductID (Clique para visualizar 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 Concurrency-Enabled DAL otimista

Nossa classe existente ProductsBLL tem exemplos de uso da atualização em lote e dos padrões diretos de banco de dados. O método AddProduct e as suas sobrecargas UpdateProduct usam o padrão de atualização em lote, fornecendo uma instância de ProductRow ao método Update do TableAdapter. O DeleteProduct método, por outro lado, usa o padrão direto do banco de dados, chamando o método Delete(productID) do TableAdapter.

Com o novo ProductsOptimisticConcurrency TableAdapter, os métodos diretos do 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: o original ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStockUnitsOnOrder, , e ReorderLevelDiscontinued. Utiliza os valores desses parâmetros de entrada adicionais na cláusula WHERE da instrução enviada ao banco de dados, excluindo apenas o registro especificado se os valores atuais do banco de dados corresponderem DELETE aos originais.

Embora a assinatura do método para o método do Update TableAdapter usado no padrão de atualização em lote não tenha sido alterada, o código necessário para registrar os valores original e novo foi alterado. Portanto, em vez de tentar usar a DAL habilitada para simultaneidade otimista com nossa classe existente ProductsBLL , vamos criar uma nova classe Business Logic Layer para trabalhar com nossa nova DAL.

Adicione uma classe nomeada ProductsOptimisticConcurrencyBLL à BLL pasta dentro da App_Code pasta.

Adicione a classe ProductsOptimisticConcurrencyBLL à pasta BLL

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 System.ComponentModel.DataObject atributo, que instrui o Visual Studio a incluir essa classe na lista suspensa do assistente ObjectDataSource.

A propriedade ProductsOptimisticConcurrencyBLL de Adapter fornece acesso rápido a uma instância da classe ProductsOptimisticConcurrencyTableAdapter, que segue o padrão utilizado nas nossas classes BLL originais (ProductsBLL, CategoriesBLL, e assim por diante). Finalmente, o método GetProducts() simplesmente chama o método GetProducts() do DAL e retorna um objeto ProductsOptimisticConcurrencyDataTable preenchido com uma instância ProductsOptimisticConcurrencyRow para cada registro de produto no banco de dados.

Eliminando um produto usando o padrão BD Direct com concorrência otimista

Ao utilizar o padrão DB direto em relação a uma DAL que usa simultaneidade otimista, deve-se passar aos métodos os valores novos e originais. Para excluir, não há novos valores, portanto, apenas 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 método DeleteProduct na classe ProductsOptimisticConcurrencyBLL use o método direto de DB. Isso significa que esse método precisa incluir 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 - aqueles valores que foram carregados pela última vez no GridView (ou DetailsView ou FormView) - diferirem 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. Assim, o método do Delete TableAdapter retornará 0 e o método da BLL DeleteProduct retornará false.

Atualizando um produto usando o padrão de atualização em lote com simultaneidade otimista

Como 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, um DataTable ou um Typed DataSet. 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 a DAL emite sua UPDATE instrução, os parâmetros são preenchidos @original_ColumnName 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:

  1. Usando o método do ProductRow TableAdapter, leia as informações atuais do produto de banco de dados numa instância GetProductByProductID(productID).
  2. Atribua os novos valores à instância a ProductRow partir da Etapa 1
  3. Chame o método do TableAdapter Update, passando a instância ProductRow

Essa sequência de etapas, no entanto, não suportará corretamente a 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 vinculados ao GridView no início do processo de edição. Em vez disso, ao usar uma DAL ativada para simultaneidade otimista, precisamos alterar as sobrecargas do método UpdateProduct para usar as seguintes etapas.

  1. Usando o método do ProductsOptimisticConcurrencyRow TableAdapter, leia as informações atuais do produto de banco de dados numa instância GetProductByProductID(productID).
  2. Atribua os valores originais à instância a ProductsOptimisticConcurrencyRow partir da Etapa 1
  3. Chame o ProductsOptimisticConcurrencyRow método da AcceptChanges() instância, que instrui o DataRow que seus valores atuais são os "originais"
  4. Atribua os novos valores à ProductsOptimisticConcurrencyRow instância
  5. Chame o método do TableAdapter Update, passando a instância ProductsOptimisticConcurrencyRow

A etapa 1 lê todos os valores atuais do banco de dados para o registro de produto especificado. Esta etapa é supérflua UpdateProduct na sobrecarga que atualiza todas as colunas do produto (pois esses valores são substituídos na Etapa 2), mas é essencial para aquelas sobrecargas em que apenas um subconjunto dos valores da coluna é passado como parâmetros de entrada. Depois de atribuir os valores originais à instância ProductsOptimisticConcurrencyRow, chama-se o método AcceptChanges(), que define os valores atuais de DataRow como os valores originais a serem usados nos parâmetros @original_ColumnName na instrução UPDATE. Em seguida, os novos valores de parâmetro são atribuídos ao ProductsOptimisticConcurrencyRow e, finalmente, o Update método é invocado, passando no DataRow.

O código a seguir mostra a sobrecarga do UpdateProduct 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 para este 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 o BLL completos, 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 lembrar seus valores originais e o ObjectDataSource deve passar ambos os 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 graciosamente com violações de simultaneidade.

Comece por abrir a página OptimisticConcurrency.aspx na pasta EditInsertDelete e adicionar um GridView ao Designer, definindo a propriedade ID como ProductsGrid. Na smart tag do GridView, opte por criar um novo ObjectDataSource chamado ProductsOptimisticConcurrencyDataSource. Como queremos que este ObjectDataSource utilize a DAL que oferece suporte à simultaneidade otimista, configure-a para usar o objeto ProductsOptimisticConcurrencyBLL.

Fazer com que o ObjectDataSource use o objeto ProductsOptimisticConcurrencyBLL

Figura 13: Fazer com que o ObjectDataSource use o objeto (ProductsOptimisticConcurrencyBLL imagem em tamanho real)

Escolha os métodos GetProducts, UpdateProduct e DeleteProduct nas listas suspensas do assistente. Para o método UpdateProduct, use uma sobrecarga que aceite todos os campos de dados relativos ao produto.

Configurando as propriedades do controle ObjectDataSource

Depois de concluir o assistente, a marcação declarativa do ObjectDataSource deve ter a seguinte aparência:

<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 Parameter instância para cada um dos parâmetros de entrada no UpdateProduct.

Para os tutoriais anteriores que envolveram a modificação de dados, removemos a propriedade ObjectDataSource neste ponto, pois essa propriedade indica que o método BLL espera que os valores antigos (ou originais) sejam passados OldValuesParameterFormatString , bem como os novos valores. Além disso, esse valor de propriedade indica os nomes dos parâmetros de entrada para os valores originais. Como estamos a passar os valores originais para a BLL, não deve remover essa propriedade.

Observação

O valor da propriedade OldValuesParameterFormatString deve ser mapeado para os nomes dos parâmetros de entrada na BLL que esperam pelos 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, , e assim por diante, você precisaria atualizar a old_supplierID propriedade para OldValuesParameterFormatStringold_{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 de dois valores:

  • OverwriteChanges - o valor padrão; não envia os valores iniciais para os parâmetros originais de entrada dos métodos BLL
  • CompareAllValues - envia os valores originais para os métodos da BLL; Escolha esta opção ao usar a 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 a configuração do GridView. Primeiro, como queremos que o GridView ofereça 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 cujos ShowEditButton e ShowDeleteButton estão ambos definidos como true.

Quando vinculado ao ProductsOptimisticConcurrencyDataSource ObjectDataSource, o GridView contém um campo para cada um dos campos de dados do produto. Embora tal 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 nem controles 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 sob pedido e os valores de nível de reordem 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. Eu modifiquei este GridView e sua interface de edição das seguintes maneiras:

  • Foram removidos os BoundFields ProductID, SupplierName e CategoryName
  • Converteu o ProductName BoundField em um TemplateField e adicionou um controle RequiredFieldValidation.
  • Converteu o CategoryID e SupplierID BoundFields em TemplateFields e ajustou a interface de edição para usar DropDownLists em vez de TextBoxes. Nestes TemplateFields' ItemTemplates, os campos de dados CategoryName e SupplierName são exibidos.
  • Converteu o UnitPrice, UnitsInStock, UnitsOnOrdere ReorderLevel 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 que funcione plenamente. No entanto, existem algumas subtilezas que vão surgir e nos causar problemas. Além disso, ainda precisamos de alguma interface que alerte o usuário quando uma violação de simultaneidade ocorreu.

Observação

Para que um controle Web de dados passe corretamente os valores originais para o ObjectDataSource (que são passados EnableViewState para a BLL), é vital que a propriedade 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 do ObjectDataSource ConflictDetection estiver definida como CompareAllValues (tal como a nossa), quando os métodos Update() ou Delete() do ObjectDataSource forem 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 atribuídos aos valores nas instruções de vinculação de dados bidirecionais cada vez que os dados são vinculados ao GridView. Portanto, é essencial que os valores originais necessários sejam capturados por meio de 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.

Os produtos estão listados em um GridView

Figura 14: Os produtos estão listados em um GridView (Clique para visualizar a imagem em tamanho real)

Se utilizar o botão Eliminar para qualquer produto, um FormatException será acionado.

A tentativa de excluir qualquer produto resulta em um FormatException

Figura 15: Tentando excluir quaisquer resultados de produto em um FormatException (Clique para visualizar a imagem em tamanho real)

O FormatException é gerado quando o ObjectDataSource tenta ler o valor original UnitPrice . Uma vez que o ItemTemplate tem o formatado UnitPrice como uma moeda (<%# Bind("UnitPrice", "{0:C}") %>), inclui um símbolo de moeda, como $19.95. O FormatException ocorre quando o ObjectDataSource tenta converter esta cadeia de caracteres em um decimal. Para contornar este problema, temos uma série de opções:

  • Remova a formatação de moeda do ItemTemplatearquivo . 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 moeda no ItemTemplate, mas use a palavra-chave Eval para fazer isso. Lembre-se de que Eval executa a vinculação de dados unidirecional. Ainda precisamos fornecer o UnitPrice valor para os valores originais, portanto, ainda precisaremos de uma instrução de vinculação de dados bidirecional no ItemTemplate, mas isso pode ser colocado em um controle Label Web cuja Visible propriedade está definida como false. 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 do RowDataBound GridView, acesse programaticamente o controle Label Web no qual o UnitPrice valor é exibido e defina sua Text propriedade para a versão formatada.
  • Deixe o UnitPrice formatado como moeda. No manipulador de RowDeleting eventos do GridView, substitua o valor original UnitPrice existente ($19,95) por um valor decimal real usando Decimal.Parse. Vimos como realizar algo semelhante no RowUpdating manipulador de eventos no tutorial Manipulando exceções de BLL e DAL-Level em uma página ASP.NET .

Para o meu exemplo, optei por seguir a segunda abordagem, adicionando um controle Label Web oculto cuja Text propriedade são dados bidirecionais vinculados ao valor não formatado UnitPrice .

Depois de resolver esse problema, tente clicar no botão Excluir de qualquer produto novamente. Desta vez, irás obter um InvalidOperationException quando o ObjectDataSource tentar invocar o método da UpdateProduct BLL.

O ObjectDataSource não é possível encontrar um método com os parâmetros de entrada que deseja enviar

Figura 16: O ObjectDataSource não pode encontrar um método com os parâmetros de entrada que deseja enviar (Clique para visualizar a imagem em tamanho real)

pt-PT: Observando a mensagem da exceção, fica claro que o ObjectDataSource deseja invocar um método BLL DeleteProduct que inclui parâmetros de entrada original_CategoryName e original_SupplierName. Isso ocorre porque as ItemTemplates para os campos de dados CategoryID e SupplierID TemplateFields atualmente contêm declarações de Bind bidirecional com os campos de dados CategoryName e SupplierName. Em vez disso, precisamos incluir Bind instruções com os CategoryID campos e SupplierID dados. Para fazer isso, substitua as instruções Bind existentes por Eval instruções e, em seguida, adicione controles Label ocultos cujas Text propriedades são vinculadas aos CategoryID campos e SupplierID dados usando 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 sucesso as informações do produto! Na Etapa 5, veremos como verificar se as violações de simultaneidade estão sendo detetadas. Mas, por enquanto, reserve alguns minutos para tentar atualizar e excluir alguns registros para garantir que a atualização e a exclusão para um único usuário funcionem conforme o esperado.

Etapa 5: Testar o suporte de concorrência otimista

Para verificar se violações de simultaneidade estão sendo detetadas (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 ser bem-sucedida 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 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. Quando se emprega simultaneidade otimista, clicar no botão Atualizar na segunda instância do navegador, no entanto, resulta em uma DBConcurrencyException.

Quando uma violação de concorrência é detetada, uma exceção DBConcurrency é gerada

Figura 17: Quando uma violação de simultaneidade é detetada, uma DBConcurrencyException é lançada (Clique para visualizar a imagem em tamanho real)

O DBConcurrencyException só é lançado quando o padrão de atualização em lote do DAL é utilizado. O padrão direto do DB não gera uma exceção, apenas indica que nenhuma linha foi afetada. Para ilustrar isso, retorne o GridView de ambas as instâncias do navegador ao 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 Delete, a página é recarregada, o GridView invoca o método Delete() do ObjectDataSource, e o ObjectDataSource aciona o método ProductsOptimisticConcurrencyBLL da classe DeleteProduct, passando os valores originais. O valor original ProductName para a segunda instância do navegador é "Chai Tea", que não corresponde ao valor atual ProductName no banco de dados. Portanto, a instrução DELETE emitida para o banco de dados não afeta nenhuma linha, uma vez que não há nenhum registro no banco de dados que satisfaça a cláusula WHERE. O DeleteProduct método retorna false e os dados do ObjectDataSource são redirecionados para o GridView.

Do ponto de vista do usuário final, clicar no botão Excluir para Chai Tea na segunda janela do navegador fez com que a tela piscasse e, ao voltar, o produto ainda está 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, o Excluir 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 a ideal. Claramente, não queremos mostrar ao utilizador os detalhes minuciosos da exceção ao usar o padrão de atualização em lote DBConcurrencyException. E o comportamento ao usar o padrão direto de banco de dados é um pouco confuso, pois o comando users falhou, mas não houve uma indicação precisa do motivo.

Para resolver esses dois problemas, podemos criar controles Label Web na página que fornecem uma explicação para o motivo pelo qual 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 DB direct, podemos examinar o valor de retorno 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: Adicionando mensagens informativas e exibindo-as em face 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 de banco de dados 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 de banco de dados usado para exclusão. Para começar, vamos adicionar dois controles Label Web à nossa página que explicam que ocorreu uma violação de simultaneidade ao tentar excluir ou atualizar dados. Defina as propriedades Visible e EnableViewState do controlo Label para false; isto fará com que fiquem ocultos em cada visita à página, exceto nas visitas específicas em que a sua propriedade Visible é definida programaticamente 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, também defini a propriedade CssClass como Warning, o que faz com que os rótulos sejam exibidos em uma fonte grande, vermelha, itálica e em negrito. Essa classe CSS Warning foi definida e adicionada ao Styles.css 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.

Dois controles de rótulo foram adicionados à página

Figura 18: Dois controles de rótulo foram adicionados à página (Clique para visualizar a imagem em tamanho real)

Com esses controlos Web de etiquetas implementados, estamos prontos para examinar como determinar quando ocorre uma violação de concorrência, permitindo definir a propriedade apropriada da etiqueta Visible para true, exibindo a mensagem informativa.

Tratamento das violações de concorrência ao atualizar

Vamos primeiro ver 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 lançada, precisamos adicionar código à nossa página de ASP.NET para determinar se uma DBConcurrencyException exceção ocorreu durante o processo de atualização. Em caso afirmativo, 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 uma página ASP.NET , essas exceções podem ser detetadas e suprimidas nos manipuladores de eventos de pós-nível do controle da Web de dados. Portanto, precisamos criar um manipulador de eventos para o evento do RowUpdated GridView que verifica se uma DBConcurrencyException exceção foi lançada. Este manipulador de eventos recebe uma referência a qualquer exceção que foi 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

Em face 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 elas 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 vinculado 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 Label explicará ao usuário o que acabou de acontecer. Esta sequência de eventos é detalhada na Figura 19.

As atualizações de um utilizador são perdidas devido a uma violação de simultaneidade

Figura 19: As atualizações de um usuário são perdidas em face de uma violação de simultaneidade (Clique para visualizar a imagem em tamanho real)

Observação

Como alternativa, em vez de retornar o GridView ao estado de pré-edição, poderíamos deixar o GridView em seu estado de edição definindo a KeepInEditMode propriedade do objeto passado GridViewUpdatedEventArgs como true. Se você adotar essa abordagem, no entanto, certifique-se de revincular 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 simultaneidade.

Respondendo a violações de concorrência ao eliminar

Com o padrão direto do banco de dados, não é levantada nenhuma exceção quando ocorre uma violação de simultaneidade. Em vez disso, a instrução database 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 forma que retornem um valor booleano indicando se afetaram ou não precisamente um registro. Portanto, para determinar se ocorreu uma violação de simultaneidade ao excluir um registro, podemos examinar o valor de retorno do DeleteProduct método da BLL.

O valor de retorno de um método BLL pode ser examinado nos manipuladores de eventos de pós-nível do ObjectDataSource através da propriedade ReturnValue do objeto ObjectDataSourceStatusEventArgs passado para o manipulador de eventos. Como estamos interessados em determinar o valor de retorno do método DeleteProduct, precisamos criar um manipulador de eventos Deleted do 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 ele pudesse retornar um valor. Portanto, devemos primeiro garantir que a ReturnValue propriedade não null é e é um valor booleano. Supondo que essa verificação seja aprovada, mostraremos o DeleteConflictMessage controle Label 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

Em face 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 nesse registro entre o momento em que o usuário carregou a página e quando clicou no botão Excluir. Quando tal violação ocorre, o DeleteConflictMessage rótulo é mostrado, explicando o que acabou de acontecer (ver Figura 20).

A exclusão de um usuário é cancelada em face de uma violação de simultaneidade

Figura 20: A exclusão de um usuário é cancelada em face de uma violação de simultaneidade (Clique para visualizar a imagem em tamanho real)

Resumo

Existem oportunidades para violações de simultaneidade em todos os aplicativos que permitem que vários usuários simultâneos atualizem ou excluam dados. Se tais violações não forem contabilizadas, quando dois usuários atualizam simultaneamente os mesmos dados quem recebe na última gravação "ganha", substituindo as alterações do outro usuário muda. Como alternativa, os desenvolvedores podem implementar um 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 desautoriza um comando de atualização ou exclusão que constituiria uma violação de simultaneidade. O controle pessimista de simultaneidade pressupõe que as 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 um 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 DataSet Tipado no .NET fornece funcionalidade para oferecer suporte ao controle de simultaneidade otimista. Em particular, as instruções UPDATE e DELETE emitidas para o banco de dados incluem todas as colunas da tabela, garantindo assim que a atualização ou eliminação só ocorrerá se os dados atuais do registo coincidirem com os dados originais que o utilizador tinha ao efetuar a sua atualização ou eliminação. Uma vez que a DAL tenha sido configurada para suportar simultaneidade otimista, os métodos BLL precisam ser atualizados. Além disso, a página de ASP.NET que chama a BLL deve ser configurada de modo que o ObjectDataSource recupere os valores originais do seu controlo de dados Web e os transfira para a BLL.

Como vimos neste tutorial, a implementação de um controle de simultaneidade otimista em um aplicativo Web ASP.NET envolve a atualização do DAL e da BLL e a adição de suporte na página ASP.NET. Se este trabalho adicional é ou não um investimento sábio do seu tempo e esforço depende da sua aplicação. Se você raramente tem usuários simultâneos atualizando dados, ou os dados que eles estão atualizando são diferentes uns dos outros, então o controle de simultaneidade não é 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 pode ajudar a impedir que as atualizações ou exclusões de um usuário substituam involuntariamente as de outro.

Feliz Programação!

Sobre o Autor

Scott Mitchell, autor de sete livros sobre ASP/ASP.NET e fundador da 4GuysFromRolla.com, trabalha com tecnologias Web da Microsoft desde 1998. Scott trabalha como consultor, formador e escritor independente. Seu último livro é Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Ele pode ser contatado em mitchell@4GuysFromRolla.com.