Implementar a simultaneidade otimista (VB)

por Scott Mitchell

Baixar PDF

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

Introdução

Para aplicativos Web que permitem apenas que os usuários exibam dados ou para aqueles que incluem apenas um único usuário que possa modificar dados, não há ameaça de dois usuários simultâneos substituirem acidentalmente as alterações uns dos outros. Para aplicativos Web que permitem que vários usuários atualizem ou excluam dados, no entanto, há o potencial para que as modificações de um usuário entrem em conflito com os de outro usuário simultâneo. Sem nenhuma política de simultaneidade em vigor, quando dois usuários estiverem editando simultaneamente um único registro, o usuário que confirmar suas alterações por último substituirá as alterações feitas pelo primeiro.

Por exemplo, imagine que dois usuários, Jisun e Sam, estavam visitando uma página em nosso aplicativo que permitia aos visitantes atualizar e excluir os produtos por meio de um controle GridView. Ambos clicam no botão Editar no GridView ao mesmo tempo. O Jisun altera o nome do produto para "Chai Tea" e clica no botão Atualizar. O resultado líquido é uma instrução UPDATE enviada ao banco de dados, que define todos os campos atualizáveis do produto (embora Jisun tenha atualizado apenas um campo, ProductName). Neste momento, o banco de dados tem os valores "Chai Tea", a categoria Bebidas, o fornecedor Liquids Exóticos e assim por diante para este produto específico. No entanto, o GridView na tela do Sam ainda mostra o nome do produto na linha Editável GridView como "Chai". Alguns segundos após as alterações de Jisun terem sido confirmadas, Sam atualiza a categoria para Condimentos e clica em Atualizar. Isso resulta em uma instrução UPDATE enviada ao banco de dados que define o nome do produto como "Chai", a CategoryID para a ID de categoria de Bebidas correspondente e assim por diante. As alterações de Jisun no nome do produto foram substituídas. A Figura 1 ilustra graficamente esta série de eventos.

Quando dois usuários atualizam simultaneamente um registro, há potencial para alterações de um usuário para substituir os outros

Figura 1: quando dois usuários atualizam simultaneamente um registro, há potencial para alterações de um usuário para substituir os outros (clique para exibir a imagem em tamanho real)

Da mesma forma, quando dois usuários estão visitando uma página, um usuário pode estar no meio da atualização de um registro quando ele é excluído por outro usuário. Ou, entre quando um usuário carrega uma página e quando clica no botão Excluir, outro usuário pode ter modificado o conteúdo desse registro.

Há três estratégias de controle de simultaneidade disponíveis:

  • Não faça nada - se os usuários simultâneos estiverem modificando o mesmo registro, deixe o último commit ganhar (o comportamento padrão)
  • Simultaneidade otimista – suponha que, embora possa haver conflitos de simultaneidade de vez em quando, a grande maioria das vezes em que esses conflitos não surgirão; portanto, se um conflito surgir, basta informar ao usuário que suas alterações não podem ser salvas porque outro usuário modificou os mesmos dados
  • Simultaneidade pessimista – suponha que os conflitos de simultaneidade sejam comuns e que os usuários não tolerarão ser informados de que suas alterações não foram salvas devido à atividade simultânea de outro usuário; portanto, quando um usuário começar a atualizar um registro, bloqueie-o, impedindo assim que outros usuários editem ou excluam esse registro até que o usuário confirme suas modificações

Todos os nossos tutoriais até agora usaram a estratégia padrão de resolução de simultaneidade– ou seja, deixamos a última gravação ganhar. Neste tutorial, examinaremos como implementar o controle de simultaneidade otimista.

Observação

Não veremos exemplos pessimistas de simultaneidade nesta série de tutoriais. A simultaneidade pessimista raramente é usada porque esses bloqueios, se não forem renunciados corretamente, podem impedir que outros usuários atualizem dados. Por exemplo, se um usuário bloquear um registro para edição e sair um dia antes de desbloqueá-lo, nenhum outro usuário poderá atualizar esse registro até que o usuário original retorne e conclua sua atualização. Portanto, em situações em que a simultaneidade pessimista é usada, normalmente há um tempo limite que, se atingido, cancela o bloqueio. Sites de vendas de tíquetes, que bloqueiam um local específico de estações por um curto período enquanto o usuário conclui o processo de pedido, é um exemplo de controle pessimista de simultaneidade.

Etapa 1: Observando como a simultaneidade otimista é implementada

O controle de simultaneidade otimista funciona garantindo que o registro que está sendo atualizado ou excluído tenha os mesmos valores que quando o processo de atualização ou exclusão foi iniciado. Por exemplo, ao clicar no botão Editar em um GridView editável, os valores do registro são lidos do banco de dados e exibidos em TextBoxes e outros controles da Web. Esses valores originais são salvos pelo GridView. Posteriormente, depois que o usuário fizer suas alterações e clicar no botão Atualizar, os valores originais mais os novos valores serão enviados para a Camada de Lógica de Negócios e, em seguida, até a Camada de Acesso a Dados. A Camada de Acesso a Dados deve emitir uma instrução SQL que só atualizará o registro se os valores originais que o usuário começou a editar forem idênticos aos valores ainda no banco de dados. A Figura 2 ilustra essa sequência de eventos.

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 a exclusão sejam bem-sucedidas, os valores originais devem ser iguais aos valores atuais do banco de dados (clique para exibir a imagem em tamanho real)

Há várias abordagens para implementar a simultaneidade otimista (consulte a Lógica otimista de atualização de simultaneidade de Peter A. Bromberg para obter uma breve visão de várias opções). O ADO.NET Conjunto de Dados Tipado fornece uma implementação que pode ser configurada apenas com o tique de uma caixa de seleção. Habilitar a simultaneidade otimista para um TableAdapter no Typed DataSet aumenta as instruções e DELETE do UPDATE TableAdapter para incluir uma comparação de todos os valores originais na WHERE cláusula . A instrução a seguir UPDATE , por exemplo, atualiza o nome e o preço de um produto somente se os valores atuais do banco de dados forem iguais aos valores que foram originalmente recuperados ao atualizar o registro no GridView. Os @ProductName parâmetros e @UnitPrice contêm os novos valores inseridos pelo usuário, enquanto @original_ProductName e @original_UnitPrice contêm os valores que foram originalmente carregados no GridView quando o botão Editar foi clicado:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Observação

Esta UPDATE instrução foi simplificada para legibilidade. Na prática, o UnitPrice marcar na WHERE cláusula estaria mais envolvido, pois UnitPrice pode conter NULL s e verificar se NULL = NULL sempre retorna False (em vez disso, você deve usar IS NULL).

Além de usar uma instrução UPDATE subjacente diferente, configurar um TableAdapter para usar simultaneidade otimista também modifica a assinatura de seus métodos diretos de BD. Lembre-se do nosso primeiro tutorial, Criando uma camada de acesso a dados, de que os métodos diretos do BD eram aqueles que aceitam uma lista de valores escalares como parâmetros de entrada (em vez de uma instância de DataRow ou DataTable fortemente tipada). Ao usar simultaneidade otimista, os métodos diretos Update() e Delete() de banco de dados incluem parâmetros de entrada para os valores originais também. Além disso, o código na BLL para usar o padrão de atualização em lote (as Update() sobrecargas de método que aceitam DataRows e DataTables em vez de valores escalares) também devem ser alteradas.

Em vez de estender os TableAdapters da DAL existentes para usar a simultaneidade otimista (o que exigiria a alteração da BLL para acomodar), vamos criar um novo Conjunto de Dados Digitado chamado NorthwindOptimisticConcurrency, ao qual adicionaremos um Products TableAdapter que usa simultaneidade otimista. Depois disso, criaremos uma ProductsOptimisticConcurrencyBLL classe camada de lógica empresarial que tem as modificações apropriadas para dar suporte ao DAL de simultaneidade otimista. Depois que essa base for colocada, estaremos prontos para criar a página ASP.NET.

Etapa 2: Criando uma camada de acesso a dados que dá suporte à simultaneidade otimista

Para criar um novo Conjunto de Dados Digitado, clique com o botão direito do DAL mouse na pasta dentro da App_Code pasta e adicione um novo Conjunto de Dados chamado NorthwindOptimisticConcurrency. Como vimos no primeiro tutorial, isso adicionará um novo TableAdapter ao Typed DataSet, iniciando automaticamente o Assistente de Configuração do TableAdapter. Na primeira tela, é solicitado que especifique o banco de dados ao qual se conectar – conecte-se ao mesmo banco de dados Northwind usando a NORTHWNDConnectionString configuração de Web.config.

Conectar-se ao mesmo banco de dados Northwind

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.

Especificar os dados a serem recuperados usando uma instrução SQL Ad-Hoc

Figura 4: especifique os dados a serem recuperados usando uma instrução SQL Ad Hoc (clique para exibir a imagem em tamanho real)

Na tela a seguir, insira a consulta SQL a ser usada para recuperar as informações do produto. Vamos usar exatamente a mesma consulta SQL usada para o Products TableAdapter de nosso DAL original, que retorna todas as colunas junto com os Product nomes de fornecedor e categoria do produto:

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

Usar a mesma consulta SQL do Products TableAdapter no DAL Original

Figura 5: usar a mesma consulta SQL do Products TableAdapter no DAL Original (Clique para exibir a imagem em tamanho real)

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

Habilitar o controle de simultaneidade otimista verificando a caixa de seleção

Figura 6: Habilitar o controle de simultaneidade otimista verificando a caixa de seleção "Usar simultaneidade otimista" (clique para exibir a imagem em tamanho real)

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

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

Depois de concluir o assistente, o dataset Designer incluirá um DataTable e TableAdapter fortemente tipadoProducts. Reserve um momento para renomear o DataTable de Products para ProductsOptimisticConcurrency, o que você pode fazer clicando com o botão direito do mouse na barra de título do DataTable e escolhendo Renomear no menu de contexto.

Um DataTable e TableAdapter foram adicionados ao Conjunto de Dados Tipado

Figura 8: Um DataTable e TableAdapter foram adicionados ao Conjunto de Dados Digitado (Clique para exibir a imagem em tamanho real)

Para ver as diferenças entre as UPDATE consultas e DELETE entre o ProductsOptimisticConcurrency TableAdapter (que usa simultaneidade otimista) e o Products TableAdapter (o que não faz), clique no TableAdapter e vá para o janela Propriedades. DeleteCommand Nas subpropriedades das CommandText propriedades eUpdateCommand, você pode ver a sintaxe SQL real que é enviada ao banco de dados quando os métodos relacionados à atualização ou exclusão do DAL são invocados. Para o ProductsOptimisticConcurrency TableAdapter, a DELETE instrução usada é:

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

Enquanto a instrução DELETE para o Product TableAdapter em nosso DAL original é muito mais simples:

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

Como você pode ver, a WHERE cláusula na DELETE instrução tableAdapter que usa simultaneidade otimista inclui uma comparação entre cada um dos valores de Product coluna existentes da tabela e os valores originais no momento em que GridView (ou DetailsView ou FormView) foi preenchido pela última vez. Como todos os campos que não ProductIDsejam , ProductNamee Discontinued podem ter NULL valores, parâmetros adicionais e verificações são incluídos para comparar NULL corretamente os valores na WHERE cláusula .

Não adicionaremos mais DataTables ao DataSet habilitado para simultaneidade otimista para este tutorial, pois nossa página ASP.NET fornecerá apenas informações de atualização e exclusão do produto. No entanto, ainda precisamos adicionar o GetProductByProductID(productID) método ao ProductsOptimisticConcurrency TableAdapter.

Para fazer isso, clique com o botão direito do mouse na barra de título do TableAdapter (a área logo acima dos nomes do Fill método e GetProducts ) e escolha Adicionar Consulta no menu de contexto. Isso iniciará o Assistente de Configuração de Consulta TableAdapter. Assim como acontece com a configuração inicial do TableAdapter, opte por criar o GetProductByProductID(productID) método usando uma instrução SQL ad hoc (consulte a Figura 4). Como o GetProductByProductID(productID) método retorna informações sobre um produto específico, indique que essa consulta é um SELECT tipo de consulta que retorna linhas.

Marcar o Tipo de Consulta como um

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.

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

Por fim, altere os nomes de método gerados para FillByProductID e GetProductByProductID.

Renomear os métodos para FillByProductID e GetProductByProductID

Figura 11: Renomear os métodos para FillByProductID e GetProductByProductID (clique para exibir a imagem em tamanho real)

Com esse assistente concluído, o TableAdapter agora contém dois métodos para recuperar dados: GetProducts(), que retorna todos os produtos; e GetProductByProductID(productID), que retorna o produto especificado.

Etapa 3: Criando uma camada de lógica de negócios para o DAL de Concurrency-Enabled otimista

Nossa classe existente ProductsBLL tem exemplos de como usar a atualização em lote e os padrões diretos do BD. O AddProduct método e UpdateProduct as sobrecargas usam o padrão de atualização em lote, passando uma ProductRow instância para o método Update do TableAdapter. O DeleteProduct método, por outro lado, usa o padrão direto do BD, chamando o método tableAdapter Delete(productID) .

Com o novo ProductsOptimisticConcurrency TableAdapter, os métodos diretos do BD agora exigem que os valores originais também sejam passados. Por exemplo, o Delete método agora espera dez parâmetros de entrada: o original ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevele Discontinued. Ele usa os valores desses parâmetros de entrada adicionais na WHERE cláusula da DELETE instrução enviada ao banco de dados, excluindo apenas o registro especificado se os valores atuais do banco de dados forem mapeados para os originais.

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

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

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

A ProductsOptimisticConcurrencyBLLpropriedade do Adapter fornece acesso rápido a uma instância da ProductsOptimisticConcurrencyTableAdapter classe e segue o padrão usado em nossas classes BLL originais (ProductsBLL, CategoriesBLLe assim por diante). Por fim, o GetProducts() método simplesmente chama para baixo no método da GetProducts() DAL e retorna um ProductsOptimisticConcurrencyDataTable objeto preenchido com uma ProductsOptimisticConcurrencyRow instância para cada registro de produto no banco de dados.

Excluindo um produto usando o padrão direto do BD com simultaneidade otimista

Ao usar o padrão direto do BD em relação a um DAL que usa simultaneidade otimista, os métodos devem ser passados os valores novos e originais. Para excluir, não há novos valores, portanto, somente os valores originais precisam ser passados. Em nossa BLL, então, devemos aceitar todos os parâmetros originais como parâmetros de entrada. Vamos fazer com que o DeleteProduct método na ProductsOptimisticConcurrencyBLL classe use o método direto do BD. Isso significa que esse método precisa usar todos os dez campos de dados do produto como parâmetros de entrada e passá-los para o DAL, conforme mostrado no código a seguir:

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = Adapter.Delete(
                                    original_productID, _
                                    original_productName, _
                                    original_supplierID, _
                                    original_categoryID, _
                                    original_quantityPerUnit, _
                                    original_unitPrice, _
                                    original_unitsInStock, _
                                    original_unitsOnOrder, _
                                    original_reorderLevel, _
                                    original_discontinued)
    ' Return true if precisely one row was deleted, otherwise false
    Return rowsAffected = 1
End Function

Se os valores originais - os valores que foram carregados pela última vez no GridView (ou DetailsView ou FormView) - forem diferentes dos valores no banco de dados quando o usuário clicar no botão Excluir, a WHERE cláusula não corresponderá a nenhum registro de banco de dados e nenhum registro será afetado. Portanto, o método tableAdapter Delete retornará 0 e o método da DeleteProduct BLL retornará false.

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

Conforme observado anteriormente, o método do Update TableAdapter para o padrão de atualização em lote tem a mesma assinatura de método, independentemente de a simultaneidade otimista ser empregada ou não. Ou seja, o Update método espera um DataRow, uma matriz de DataRows, uma DataTable ou um DataSet Tipado. Não há parâmetros de entrada adicionais para especificar os valores originais. Isso é possível porque o DataTable controla os valores originais e modificados para seus DataRow(s). Quando o DAL emite sua UPDATE instrução, os @original_ColumnName parâmetros são preenchidos com os valores originais do DataRow, enquanto os @ColumnName parâmetros são preenchidos com os valores modificados do DataRow.

ProductsBLL Na classe (que usa nosso DAL de simultaneidade original e não otimista), ao usar o padrão de atualização em lote para atualizar as informações do produto, nosso código executa a seguinte sequência de eventos:

  1. Ler as informações atuais do produto de banco de dados em uma ProductRow instância usando o método tableAdapter GetProductByProductID(productID)
  2. Atribuir os novos valores à instância da ProductRow Etapa 1
  3. Chamar o método tableAdapter Update , passando a ProductRow instância

Essa sequência de etapas, no entanto, não oferecerá suporte correto à simultaneidade otimista porque o ProductRow preenchido na Etapa 1 é preenchido diretamente do banco de dados, o que significa que os valores originais usados pelo DataRow são aqueles que existem atualmente no banco de dados e não aqueles que estavam associados ao GridView no início do processo de edição. Em vez disso, ao usar um DAL habilitado para simultaneidade otimista, precisamos alterar as sobrecargas de UpdateProduct método para usar as seguintes etapas:

  1. Ler as informações atuais do produto de banco de dados em uma ProductsOptimisticConcurrencyRow instância usando o método tableAdapter GetProductByProductID(productID)
  2. Atribuir os valores originais à ProductsOptimisticConcurrencyRow instância da Etapa 1
  3. Chame o ProductsOptimisticConcurrencyRow método da AcceptChanges() instância , que instrui o DataRow de que seus valores atuais são os "originais"
  4. Atribuir os novos valores à ProductsOptimisticConcurrencyRow instância
  5. Chamar o método tableAdapter Update , passando a ProductsOptimisticConcurrencyRow instância

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

O código a seguir mostra a UpdateProduct sobrecarga que aceita todos os campos de dados do produto como parâmetros de entrada. Embora não seja mostrada aqui, a ProductsOptimisticConcurrencyBLL classe incluída no download deste tutorial também contém uma UpdateProduct sobrecarga que aceita apenas o nome e o preço do produto como parâmetros de entrada.

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0)
    'STEP 2: Assign the original values to the product instance
    AssignAllProductValues( _
        product, original_productName, original_supplierID, _
        original_categoryID, original_quantityPerUnit, original_unitPrice, _
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _
        original_discontinued)
    'STEP 3: Accept the changes
    product.AcceptChanges()
    'STEP 4: Assign the new values to the product instance
    AssignAllProductValues( _
        product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _
        unitsInStock, unitsOnOrder, reorderLevel, discontinued)
    'STEP 5: Update the product record
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

Etapa 4: passando os valores original e novo da página ASP.NET para os métodos BLL

Com o DAL e a BLL concluídos, tudo o que resta é criar uma página de ASP.NET que possa utilizar a lógica de simultaneidade otimista incorporada ao sistema. Especificamente, o controle da Web de dados (GridView, DetailsView ou FormView) deve se lembrar de seus valores originais e o ObjectDataSource deve passar os dois conjuntos de valores para a Camada de Lógica de Negócios. Além disso, a página ASP.NET deve ser configurada para lidar normalmente com violações de simultaneidade.

Comece abrindo a OptimisticConcurrency.aspx página na EditInsertDelete pasta e adicionando um GridView ao Designer, definindo sua ID propriedade ProductsGridcomo . Na marca inteligente do GridView, opte por criar um novo ObjectDataSource chamado ProductsOptimisticConcurrencyDataSource. Como queremos que este ObjectDataSource use o DAL que dá suporte à simultaneidade otimista, configure-o para usar o ProductsOptimisticConcurrencyBLL objeto .

Fazer com que o ObjectDataSource use o objeto ProductsOptimisticConcurrencyBLL

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

Escolha os GetProductsmétodos , UpdateProducte DeleteProduct das listas suspensas no assistente. Para o método UpdateProduct, use a sobrecarga que aceita todos os campos de dados do produto.

Configurando as propriedades do controle ObjectDataSource

Depois de concluir o assistente, a marcação declarativa do ObjectDataSource deve ser semelhante à seguinte:

<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

Como você pode ver, a DeleteParameters coleção contém uma instância para cada um Parameter dos dez parâmetros de entrada no ProductsOptimisticConcurrencyBLL método da DeleteProduct classe. Da mesma forma, a UpdateParameters coleção contém uma Parameter instância para cada um dos parâmetros de entrada em UpdateProduct.

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

Observação

O valor da OldValuesParameterFormatString propriedade deve ser mapeado para os nomes de parâmetro de entrada na BLL que esperam os valores originais. Como nomeamos esses parâmetros original_productName, original_supplierIDe 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_supplierIDe 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 a simultaneidade otimista

Reserve um momento para definir a ConflictDetection propriedade como CompareAllValues.

Configurando as propriedades e os campos do GridView

Com as propriedades do ObjectDataSource configuradas corretamente, vamos voltar nossa atenção para a configuração do GridView. Primeiro, como queremos que o GridView dê suporte à edição e exclusão, clique nas caixas de seleção Habilitar Edição e Habilitar Exclusão da marca inteligente gridView. Isso adicionará um CommandField cujos ShowEditButton e ShowDeleteButton estão definidos como true.

Quando associado ao ProductsOptimisticConcurrencyDataSource ObjectDataSource, o GridView contém um campo para cada um dos campos de dados do produto. Embora esse GridView possa ser editado, a experiência do usuário é tudo menos aceitável. O CategoryID e SupplierID BoundFields serão renderizados como TextBoxes, exigindo que o usuário insira a categoria e o fornecedor apropriados como números de ID. Não haverá formatação para os campos numéricos e nenhum controle de validação para garantir que o nome do produto tenha sido fornecido e que o preço unitário, as unidades em estoque, as unidades na ordem e os valores de nível de reordenação sejam valores numéricos adequados e sejam maiores ou iguais a zero.

Como discutimos nos tutoriais Adicionando controles de validação às interfaces de edição e inserção e personalizando a interface de modificação de dados , a interface do usuário pode ser personalizada substituindo BoundFields por TemplateFields. Modifiquei este GridView e sua interface de edição das seguintes maneiras:

  • Removidos boundFields ProductID, SupplierNamee CategoryName
  • Converteu BoundField ProductName em um TemplateField e adicionou um controle RequiredFieldValidation.
  • Converteu e CategoryIDSupplierID BoundFields em TemplateFields e ajustou a interface de edição para usar DropDownLists em vez de TextBoxes. Nesses TemplateFields ItemTemplates, os CategoryName campos de dados e SupplierName são exibidos.
  • Converteu , UnitPriceUnitsInStock, 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 de trabalho completo. No entanto, há algumas sutilezas que vão subir e nos causar problemas. Além disso, ainda precisamos de alguma interface que alerte o usuário quando ocorreu uma violação de simultaneidade.

Observação

Para que um controle da Web de dados passe corretamente os valores originais para ObjectDataSource (que são passados para a BLL), é vital que a propriedade gridView EnableViewState seja definida true como (o padrão). Se você desabilitar o estado de exibição, os valores originais serão perdidos no postback.

Passando os valores originais corretos para o ObjectDataSource

Há alguns problemas com a forma como o GridView foi configurado. Se a propriedade ObjectDataSource ConflictDetection estiver definida CompareAllValues como (como é nossa), quando os métodos ou Delete() objectDataSource Update() forem invocados pelo GridView (ou DetailsView ou FormView), o ObjectDataSource tentará copiar os valores originais do GridView em suas instâncias apropriadasParameter. Consulte a Figura 2 para obter uma representação gráfica desse processo.

Especificamente, os valores originais do GridView recebem os valores nas instruções bidirecionais de vinculação de dados sempre que os dados são associados ao GridView. Portanto, é essencial que todos os valores originais necessários sejam capturados por meio da vinculação de dados bidirecional e que sejam fornecidos em um formato conversível.

Para ver por que isso é importante, reserve um momento para visitar nossa página em um navegador. Conforme esperado, o GridView lista cada produto com um botão Editar e Excluir na coluna mais à esquerda.

Os produtos são listados em um GridView

Figura 14: Os produtos são listados em um GridView (clique para exibir imagem em tamanho real)

Se você clicar no botão Excluir para qualquer produto, um FormatException será gerado.

Tentando excluir todos os resultados do produto em um FormatException

Figura 15: Tentando excluir todos os resultados do produto em um FormatException (clique para exibir a imagem em tamanho real)

O FormatException é gerado quando ObjectDataSource tenta ler no valor original UnitPrice . Como o ItemTemplate tem formatado UnitPrice como uma moeda (<%# Bind("UnitPrice", "{0:C}") %>), ele inclui um símbolo de moeda, como US$ 19,95. O FormatException ocorre quando o ObjectDataSource tenta converter essa cadeia de caracteres em um decimal. Para contornar esse problema, temos várias opções:

  • Remova a formatação de moeda do ItemTemplate. Ou seja, em vez de usar <%# Bind("UnitPrice", "{0:C}") %>, basta usar <%# Bind("UnitPrice") %>. A desvantagem disso é que o preço não está mais formatado.
  • Exiba o UnitPrice formatado como uma moeda no ItemTemplate, mas use o Eval palavra-chave 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 Web Label 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 Web Label no qual o UnitPrice valor é exibido e defina sua Text propriedade como a versão formatada.
  • Deixe o UnitPrice formatado como uma moeda. No manipulador de eventos do RowDeleting GridView, substitua o valor original UnitPrice existente (US$ 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 um tutorial de página ASP.NET .

Para meu exemplo, optei por usar a segunda abordagem, adicionando um controle Web de rótulo oculto cuja Text propriedade é dados bidirecionais associados ao valor não formatado UnitPrice .

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

O ObjectDataSource não pode 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 exibir a imagem em tamanho real)

Examinando a mensagem da exceção, está claro que ObjectDataSource deseja invocar um método BLL DeleteProduct que inclua original_CategoryName parâmetros de entrada e original_SupplierName . Isso ocorre porque os ItemTemplate s para o CategoryID e SupplierID TemplateFields atualmente contêm instruções Bind bidirecionais com os CategoryName campos de dados e SupplierName . Em vez disso, precisamos incluir Bind instruções com os CategoryID campos de dados e SupplierID . Para fazer isso, substitua as instruções Bind existentes por Eval instruções e adicione controles label ocultos cujas Text propriedades estão associadas aos CategoryID campos de dados e SupplierID usando a vinculação de dados bidirecional, conforme mostrado abaixo:

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

Com essas alterações, agora podemos excluir e editar informações do produto com êxito! Na Etapa 5, examinaremos como verificar se violações de simultaneidade estão sendo detectadas. Mas, por enquanto, leve alguns minutos para tentar atualizar e excluir alguns registros para garantir que a atualização e a exclusão de um único usuário funcionem conforme o esperado.

Etapa 5: Testar o suporte de simultaneidade otimista

Para verificar se violações de simultaneidade estão sendo detectadas (em vez de resultar em dados sendo substituídos cegamente), precisamos abrir duas janelas do navegador para esta página. Em ambas as instâncias do navegador, clique no botão Editar para Chai. Em seguida, em apenas um dos navegadores, altere o nome para "Chai Tea" e clique em Atualizar. A atualização deve ter êxito e retornar o GridView para seu estado de pré-edição, com "Chai Tea" como o novo nome do produto.

Na outra instância da janela do navegador, no entanto, o nome do produto TextBox ainda mostra "Chai". Nesta segunda janela do navegador, atualize o UnitPrice para 25.00. Sem suporte de simultaneidade otimista, clicar em atualizar na segunda instância do navegador alteraria o nome do produto de volta para "Chai", substituindo assim as alterações feitas pela primeira instância do navegador. Com a simultaneidade otimista empregada, no entanto, clicar no botão Atualizar na segunda instância do navegador resulta em uma DBConcurrencyException.

Quando uma violação de simultaneidade é detectada, um DBConcurrencyException é gerado

Figura 17: quando uma violação de simultaneidade é detectada, um DBConcurrencyException é gerado (clique para exibir a imagem em tamanho real)

O DBConcurrencyException só é gerado quando o padrão de atualização em lote do DAL é utilizado. O padrão direto do BD não gera uma exceção, apenas indica que nenhuma linha foi afetada. Para ilustrar isso, retorne GridView de ambas as instâncias do navegador ao estado de pré-edição. Em seguida, na primeira instância do navegador, clique no botão Editar e altere o nome do produto de "Chai Tea" de volta para "Chai" e clique em Atualizar. Na segunda janela do navegador, clique no botão Excluir para Chai.

Ao clicar em Excluir, a página retornará, o GridView invocará o método objectDataSource Delete() e ObjectDataSource chamará para baixo no ProductsOptimisticConcurrencyBLL método da DeleteProduct classe, passando os valores originais. O valor original ProductName da segunda instância do navegador é "Chai Tea", que não corresponde ao valor atual ProductName no banco de dados. Portanto, a DELETE instrução emitida para o banco de dados afeta zero linhas, pois não há nenhum registro no banco de dados que a WHERE cláusula satisfaça. O DeleteProduct método retorna false e os dados do ObjectDataSource são recuperados para o GridView.

Da perspectiva do usuário final, clicar no botão Excluir do Chai Tea na segunda janela do navegador fez com que a tela piscasse e, ao voltar, o produto ainda estivesse lá, embora agora esteja listado como "Chai" (a alteração do nome do produto feita pela primeira instância do navegador). Se o usuário clicar no botão Excluir novamente, a exclusão terá êxito, pois o valor original ProductName do GridView ("Chai") agora corresponde ao valor no banco de dados.

Em ambos os casos, a experiência do usuário está longe de ser ideal. Claramente, não queremos mostrar ao usuário os detalhes da DBConcurrencyException exceção ao usar o padrão de atualização em lote. E o comportamento ao usar o padrão direto do BD é um pouco confuso, pois o comando de usuários falhou, mas não havia uma indicação precisa do motivo.

Para corrigir esses dois problemas, podemos criar controles Da Web de rótulo na página que fornecem uma explicação sobre por que uma atualização ou exclusão falhou. Para o padrão de atualização em lote, podemos determinar se ocorreu ou não uma DBConcurrencyException exceção no manipulador de eventos pós-nível do GridView, exibindo o rótulo de aviso conforme necessário. Para o método direto do BD, podemos examinar o valor retornado do método BLL (que é true se uma linha foi afetada, false caso contrário) e exibir uma mensagem informativa conforme necessário.

Etapa 6: Adicionar mensagens informativas e exibi-las diante de uma violação de simultaneidade

Quando ocorre uma violação de simultaneidade, o comportamento exibido depende se a atualização em lote do DAL ou o padrão direto do BD foi usado. Nosso tutorial usa ambos os padrões, com o padrão de atualização em lote sendo usado para atualização e o padrão direto do BD usado para exclusão. Para começar, vamos adicionar dois controles Da Web de rótulo à nossa página que explicam que ocorreu uma violação de simultaneidade ao tentar excluir ou atualizar dados. Defina as propriedades e do Visible controle Rótulo como false; isso fará com que eles fiquem ocultos em cada visita de página, exceto para as visitas de página específicas em que sua Visible propriedade é definida programaticamente como true.EnableViewState

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

Além de definir suas Visiblepropriedades , EnabledViewStatee Text , também defina a CssClass propriedade como Warning, o que faz com que o Rótulo seja exibido em uma fonte grande, vermelha, itálica e em negrito. Essa classe CSS Warning foi definida e adicionada a Styles.css novamente no tutorial Examinando os eventos associados à inserção, atualização e exclusão .

Depois de adicionar esses Rótulos, o Designer no Visual Studio deve ser semelhante à Figura 18.

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

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

Com esses controles Da Web de Rótulo em vigor, estamos prontos para examinar como determinar quando ocorreu uma violação de simultaneidade, momento em que a propriedade apropriada do Visible Rótulo pode ser definida como true, exibindo a mensagem informativa.

Tratamento de violações de simultaneidade ao atualizar

Primeiro, vamos examinar como lidar com violações de simultaneidade ao usar o padrão de atualização em lote. Como essas violações com o padrão de atualização em lote fazem com que uma DBConcurrencyException exceção seja gerada, precisamos adicionar código à nossa página ASP.NET para determinar se ocorreu uma DBConcurrencyException exceção durante o processo de atualização. Nesse caso, devemos exibir uma mensagem para o usuário explicando que suas alterações não foram salvas porque outro usuário modificou os mesmos dados entre quando começou a editar o registro e quando clicou no botão Atualizar.

Como vimos no tutorial Manipulando exceções de BLL e DAL-Level em um tutorial de página ASP.NET , essas exceções podem ser detectadas e suprimidas nos manipuladores de eventos pós-nível do controle Web de dados. Portanto, precisamos criar um manipulador de eventos para o evento gridview RowUpdated que verifica se uma DBConcurrencyException exceção foi gerada. Esse manipulador de eventos é passado uma referência a qualquer exceção gerada durante o processo de atualização, conforme mostrado no código do manipulador de eventos abaixo:

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

Diante de uma DBConcurrencyException exceção, esse manipulador de eventos exibe o UpdateConflictMessage controle Rótulo e indica que a exceção foi tratada. Com esse código em vigor, quando ocorre uma violação de simultaneidade ao atualizar um registro, as alterações do usuário são perdidas, pois teriam substituído as modificações de outro usuário ao mesmo tempo. Em particular, o GridView é retornado ao seu estado de pré-edição e associado aos dados atuais do banco de dados. Isso atualizará a linha GridView com as alterações do outro usuário, que anteriormente não estavam visíveis. Além disso, o UpdateConflictMessage controle Rótulo explicará ao usuário o que acabou de acontecer. Essa sequência de eventos é detalhada na Figura 19.

Os Atualizações de um usuário são perdidos em face de uma violação de simultaneidade

Figura 19: Os Atualizações de um usuário são perdidos em face de uma violação de simultaneidade (clique para exibir a imagem em tamanho real)

Observação

Como alternativa, em vez de retornar o GridView para o estado de pré-edição, poderíamos deixar o GridView em seu estado de edição definindo a KeepInEditMode propriedade do objeto passado como GridViewUpdatedEventArgs true. No entanto, se você adotar essa abordagem, certifique-se de reassociar os dados ao GridView (invocando seu DataBind() método) para que os valores do outro usuário sejam carregados na interface de edição. O código disponível para download com este tutorial tem essas duas linhas de código no RowUpdated manipulador de eventos comentadas; basta remover a marca de comentário dessas linhas de código para que o GridView permaneça no modo de edição após uma violação de simultaneidade.

Respondendo a violações de simultaneidade ao excluir

Com o padrão direto do BD, não há exceção gerada diante de uma violação de simultaneidade. Em vez disso, a instrução de banco de dados simplesmente não afeta nenhum registro, pois a cláusula WHERE não corresponde a nenhum registro. Todos os métodos de modificação de dados criados na BLL foram projetados de modo que eles retornem um valor booliano indicando se eles afetaram precisamente um registro. Portanto, para determinar se ocorreu uma violação de simultaneidade ao excluir um registro, podemos examinar o valor retornado do método da DeleteProduct BLL.

O valor retornado de um método BLL pode ser examinado nos manipuladores de eventos pós-nível do ObjectDataSource por meio da ReturnValue propriedade do ObjectDataSourceStatusEventArgs objeto passado para o manipulador de eventos. Como estamos interessados em determinar o valor retornado do DeleteProduct método , precisamos criar um manipulador de eventos para o evento objectDataSource Deleted . A ReturnValue propriedade é do tipo object e pode ser null se uma exceção foi gerada e o método foi interrompido antes que pudesse retornar um valor. Portanto, devemos primeiro garantir que a ReturnValue propriedade não null seja e seja um valor booliano. Supondo que esse marcar passe, mostraremos o DeleteConflictMessage controle Rótulo se o ReturnValue for false. Isso pode ser feito usando o seguinte código:

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

Diante de uma violação de simultaneidade, a solicitação de exclusão do usuário é cancelada. O GridView é atualizado, mostrando as alterações que ocorreram para esse registro entre a hora em que o usuário carregou a página e quando clicou no botão Excluir. Quando essa violação ocorre, o DeleteConflictMessage Rótulo é mostrado, explicando o que acabou de acontecer (consulte a Figura 20).

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

Figura 20: Uma exclusão de usuário é cancelada em face de uma violação de simultaneidade (clique para exibir a imagem em tamanho real)

Resumo

Existem oportunidades de violações de simultaneidade em todos os aplicativos que permitem que vários usuários simultâneos atualizem ou excluam dados. Se essas violações não forem contabilizados, quando dois usuários atualizarem simultaneamente os mesmos dados que receberem na última gravação "vence", a substituição das alterações do outro usuário será alterada. Como alternativa, os desenvolvedores podem implementar o controle de simultaneidade otimista ou pessimista. O controle de simultaneidade otimista pressupõe que as violações de simultaneidade são pouco frequentes e simplesmente não permitem um comando de atualização ou exclusão que constituiria uma violação de simultaneidade. O controle de simultaneidade pessimista pressupõe que violações de simultaneidade são frequentes e simplesmente rejeitar o comando de atualização ou exclusão de um usuário não é aceitável. Com o controle de simultaneidade pessimista, atualizar um registro envolve bloqueá-lo, impedindo assim que outros usuários modifiquem ou excluam o registro enquanto ele estiver bloqueado.

O Conjunto de Dados Tipado no .NET fornece funcionalidade para dar suporte ao controle de simultaneidade otimista. Em particular, as UPDATE instruções e DELETE emitidas para o banco de dados incluem todas as colunas da tabela, garantindo assim que a atualização ou exclusão só ocorrerá se os dados atuais do registro corresponderem aos dados originais que o usuário tinha ao executar sua atualização ou exclusão. Depois que o DAL tiver sido configurado para dar suporte à simultaneidade otimista, os métodos BLL precisarão ser atualizados. Além disso, a página ASP.NET que chama a BLL deve ser configurada de modo que o ObjectDataSource recupere os valores originais de seu controle da Web de dados e os passe para a BLL.

Como vimos neste tutorial, implementar o controle de simultaneidade otimista em um aplicativo Web ASP.NET envolve atualizar o DAL e a BLL e adicionar suporte na página ASP.NET. Se esse trabalho adicionado é ou não um investimento sábio do seu tempo e esforço depende do seu aplicativo. Se você raramente tiver usuários simultâneos atualizando dados ou se os dados que eles estão atualizando forem diferentes uns dos outros, o controle de simultaneidade não será um problema fundamental. Se, no entanto, você tiver rotineiramente vários usuários em seu site trabalhando com os mesmos dados, o controle de simultaneidade poderá ajudar a impedir que as atualizações ou exclusões de um usuário substituam involuntariamente as de outro.

Programação feliz!

Sobre o autor

Scott Mitchell, autor de sete livros do ASP/ASP.NET e fundador da 4GuysFromRolla.com, trabalha com tecnologias da Microsoft Web desde 1998. Scott trabalha como consultor independente, treinador e escritor. Seu último livro é Sams Teach Yourself ASP.NET 2.0 em 24 Horas. Ele pode ser contatado em mitchell@4GuysFromRolla.com. ou através de seu blog, que pode ser encontrado em http://ScottOnWriting.NET.