.NET

10 out, 2012

VB .NET – Atualizando tabelas relacionadas com LINQ – Parte 02

Publicidade

Na primeira parte deste artigo, mostrei como podemos atualizar dados de tabelas relacionadas usando ADO .NET. Usando o mesmo cenário vou mostrar, a título de comparação, como fazer a mesma tarefa usando LINQ to SQL.

Vamos ao nosso cenário… Vamos partir de um modelo de dados simples e que já esta pronto para uso, e por isso vamos usar o banco de dados Northwind.mdf.

Este banco de dados possui diversas tabelas relacionadas, mas nosso interesse está focado nas tabelas: Orders, Orders Details, Suppliers, Products, Customers e Employees.

Abaixo vemos o relacionamento existente entre essas tabelas:


Nosso objetivo será atualizar as tabelas Orders e Orders Details com informações obtidas de um formulário que simulará o envio de um pedido e seus detalhes. Embora as demais tabelas não sejam atualizadas, iremos precisar de suas informações.

Como temos o modelo de dados já pronto, eu vou iniciar mostrando como será o formulário de entrada de dados usado na aplicação. Os recursos necessários para acompanhar este artigo são:

  • Visual Basic 2010 Express Edition;
  • SQL Server 2008 Express;
  • Banco de dados Northwind.mdf.

Criando o mapeamento das tabelas usando LINQ to SQL

Abra o Visual Basic 2010 Express Edition e crie um novo projeto do tipo Windows Forms Application com o nome NW_Pedidos_LINQ. Altere o nome do formulário padrão para frmLINQ.vb e inclua os seguintes controles a partir da ToolBox no formulário:

  • 2 Combobox – cboClientes e cboFuncionarios;
  • 5 TextBox – txtID, txtProduto (ReadOnly=True), txtPreco (ReadOnly=True), txtQtde, txtSubtotal (ReadOnly=True);
  • 2 TextBox – txtItems e txtTotal;
  • 4 TextBox – txtNome, txtEndereco, txtCidade e txtRegiao;
  • 2 Buttons – btnSalvar e btnNovo.

Os dois controles Combobox deverão ser preenchidos quando o formulário for carregado, permitindo que o usuário selecione o cliente e o funcionário.

A seguir, o usuário deverá informar o código do produto e ao teclar ENTER será realizada uma busca e o nome do produto e seu preço unitário serão exibidos, bastando ao usuário informar a quantidade desejada para que o valor total seja calculado e o novo pedido exibido no controle ListView.

A seguir, o usuário deve informar os dados do destinatário: Nome, Endereço, Cidade e Região e clicar no botão Salvar Pedido para persistir os dados do novo pedido e seus detalhes.

Este cenário é muito frequente em aplicações comerciais. Vamos, então, mostrar como implementar o código das funcionalidades envolvidas nesta operação.

Definindo o mapeamento com LINQ to SQL

Diferentemente da versão ADO .NET (primeira parte do artigo), eu não vou criar as classes do nosso domínio via código, mas vou realizar o mapeamento objeto relacional usando o LINQ to SQL, que é uma implementação do LINQ para o SQL Server. Dessa forma, as classes serão geradas automaticamente a partir das tabelas do banco de dados.

  • O que é LINQ ?

LINQ – Language integrated Query – é um conjunto de recursos introduzidos no .NET Framework 3.5 que permitem a realização de consultas diretamente em base de dados , documentos XML , estrutura de dados, coleção de objetos, etc. usando uma sintaxe parecida com a linguagem SQL.

  • O que LINQ to SQL ?

LINQ to SQL é uma implementação específica to LINQ para o SQL Server que converte consultas escritas em C# ou Visual Basic em SQL dinâmico, provendo uma interface que permite mapear os objetos do banco de dados gerando as classes para realizar as operações usando a sintaxe LINQ; também permite realizar alterações nos objetos e atualizar o banco de dados.

Então, através do mapeamento com o LINQ to SQL, nossas classes serão geradas automaticamente e teremos acesso a elas através de um contexto gerado em nossa aplicação.

Vamos incluir em nosso projeto o recurso LINQ to SQL. Acione o menu Project e clique em Add New Item. Na janela Templates, selecione o item LINQ to SQL Classes alterando o seu nome para Northwind.dbml e clicando no botão Add:

Neste momento será exibida a janela do descritor Objeto Relacional. Expanda os objetos do banco de dados Northwind.mdf e selecione as tabelas Customers, Employees, Order Details, Orders, Prodcuts arrastando-as e soltando-as na janela Object Relational Designer:

As tabelas do banco de dados serão mapeadas como classes (campos como propriedades, procedures e funções como métodos)  e você terá no Descritor o conjunto de classes que representam o banco de dados;

O arquivo Northwind.dbml contém o arquivo XML com informações sobre o leiaute das tabelas que foram mapeadas e também o descritor contendo as classes geradas pelo mapeamento. Após encerrar o mapeamento, você já terá acesso aos recursos do LINQ To SQL com direito a intellisense completo das informações referente as tabelas mesmo sem conhecer nada sobre elas.

O acesso será feito através da criação de uma instância da classe DataContext identificada no nosso exemplo como NorthwindDataContext:


Nossa próxima tarefa será criar uma classe para tratar o contexto e criar os métodos para acessarem as informações das classes que foram mapeadas pelo LINQ to SQL.

Definindo o código do projeto

Vamos incluir uma classe em nosso projeto que irá instanciar o nosso Contexto e definir nesta classe alguns métodos para acessar as informações das classes mapeadas a partir das tabelas do Northwind.mdf.

No menu Project, clique em Add Class e, a seguir, selecione o template Class e informe o nome NWContexto.vb e clique no botão Add:

Vamos incluir uma referência ao namespace System.Transactions em nosso projeto. No menu Project clique em Add Reference e em seguida clique na guia .NET e selecione o componente System.Transactions e clique em OK:


O namespace System.Transactions contém classes que lhe permitem escrever a sua própria aplicação transacional e gerenciar os recursos. Você pode criar e participar em uma transação Local ou distribuída com um mais participantes.

Agora vamos definir os seguintes métodos na classe NWContexto:

  • GetProdutoPorID() – retorna um produto pelo seu código;
  • GetFuncionarios() – retorna uma lista de funcionários;
  • GetClientes() – retorna uma lista de clientes;
  • SalvarPedido() – salva o pedido no banco de dados.

O código destes métodos esta definido a seguir:

Public Class NWContexto

    Public Function GetProdutoPorID(ByVal productID As Integer) As Product
        Dim ctx As New NorthwindDataContext
        Dim prod = (From p In ctx.Products
                         Where p.ProductID = productID
                         Select p).FirstOrDefault
        Return prod
    End Function

    Public Function GetFuncionarios() As List(Of Employee)
        Dim ctx As New NorthwindDataContext
        Dim funcis = (From emp In ctx.Employees
                           Select emp).ToList
        Return funcis
    End Function

    Public Function GetClientes() As List(Of Customer)
        Dim ctx As New NorthwindDataContext
        Dim clientes = (From cust In ctx.Customers
                               Select cust).ToList
        Return clientes
    End Function

    Public Shared Function SalvarPedido(ByVal novoPedido As Order) As Integer
        Dim ctx As New NorthwindDataContext

        Using TR As New System.Transactions.TransactionScope
            ctx.Orders.InsertOnSubmit(novoPedido)
            Try
                ctx.SubmitChanges()
                TR.Complete()
            Catch ex As Exception
                Return (-1)
            End Try
        End Using
        Return novoPedido.OrderID
    End Function

End Class

Observe que em todos os métodos criamos uma instância da classe DataContext identificada no nosso exemplo por NorthwindDataContext;

Dim ctx As New NorthwindDataContext

Aqui o banco de dados é mapeado em um DataContext permitindo acesso a tabelas de forma transparente sem nos preocuparmos com conexão. O DataContext utiliza a interface IDbConnection do ADO.NET para acessar o armazenamento e pode ser inicializado tanto com um objeto de conexão ADO.NET estabelecido, ou com uma string de conexão que possa ser utilizada para criar a conexão.

O DataContext é o mecanismo usado para que seja feita a seleção no banco de dados, ficando responsável por traduzir as seleções e alterações executando-as no banco de dados e transformando o resultado em objetos.

A seguir, usamos consultas usando a sintaxe LINQ conforme abaixo:

Dim prod = (From p In ctx.Products
Where p.ProductID = productID
Select p).FirstOrDefault

A consulta LINQ To SQL inicia com a cláusula From e em seguida o operador de condição Where, depois ordenação com Order By e, no final, o operador de seleção Select.

A cláusula From é a mais importante do LINQ To SQL, pois é usada em todas as consultas. Uma consulta deve sempre começar com From (o Select pode estar implícito o From não). No exemplo, o método SingeOrDefault retorna um único e específico elemento da sequência
de valores ou um valor padrão – se o elemento não for encontrado.

No método SalvarPedido, estamos incluindo um pedido na tabela Orders usando o método InsertOnSubmit(). No método SalvarPedido, após definirmos valores para um novo pedido, estamos usando dois métodos LINQ:

  • InsertOnSubmit() – Este método adiciona uma entidade , no caso a entidade novoPedido que é do tipo Orders, em um estado pendente de inclusão a tabela Orders;
  • SubmitChanges() – Este método efetiva a inclusão atual feita pelo InsertOnSubmit() na tabela do banco de dados. Deve ser chamado após o método InsertOnSubmit().

Quando SubmitChanges() é invocado, o LINQ To SQL automaticamente gera e executa comandos SQL a fim de transmitir as alterações de volta ao banco de dados. Você pode, no entanto, sobrescrever este comportamento chamando uma stored procedure.

Vejamos agora o tratamento dos eventos do controles do formulário e as rotinas auxiliares.

No evento Load do formulário:

Private Sub FrmLINQ_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim NWDados As New NWContexto
        cboFuncionarios.DisplayMember = "FirstName"
        cboFuncionarios.ValueMember = "EmployeeID"
        For Each func In NWDados.GetFuncionarios
            cboFuncionarios.Items.Add(func)
        Next
        cboClientes.ValueMember = "CustomerID"
        cboClientes.DisplayMember = "CompanyName"
        For Each cli In NWDados.GetClientes
            cboClientes.Items.Add(cli)
        Next
    End Sub

No código criamos uma instância da classe NWContexto e usamos os métodos GetFuncionarios e GetClientes. As rotinas AdicionarProduto(), AtualizaTotal() e LimpaTextBox() não sofreram alteração:

Private Sub AdicionarProduto()
        Dim LI As New ListViewItem
        LI.Text = txtID.Text
        LI.SubItems.Add(txtProduto.Text)
        LI.SubItems.Add(txtPreco.Text)
        LI.SubItems.Add(txtQtde.Text)
        LI.SubItems.Add(txtSubtotal.Text)
        ListView1.Items.Add(LI)
        AtualizaTotal()
    End Sub

    Private Sub AtualizaTotal()
        Dim items As Integer
        Dim total As Decimal
        For Each LI As ListViewItem In ListView1.Items
            items += Integer.Parse(LI.SubItems(3).Text)
            total += Decimal.Parse(LI.SubItems(4).Text)
        Next
        txtItems.Text = items.ToString
        txtTotal.Text = total.ToString("#,###.00")
    End Sub

    Private Sub LimpaTextBox()
        txtID.Text = ""
        txtProduto.Text = ""
        txtPreco.Text = ""
        txtQtde.Text = ""
        txtSubtotal.Text = ""
    End Sub

Os eventos ColumnWidthChanged, KeyUp e SelectedIndexChanged do controle ListView também não sofreram alteração alguma:

Private Sub ListView1_ColumnWidthChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.ColumnWidthChangedEventArgs) Handles ListView1.ColumnWidthChanged
        txtItems.Left = ListView1.Left + ListView1.Columns(0).Width + ListView1.Columns(1).Width + ListView1.Columns(2).Width
        txtItems.Width = ListView1.Columns(3).Width
        txtTotal.Left = txtItems.Left + txtItems.Width
        txtTotal.Width = ListView1.Columns(4).Width
    End Sub

    Private Sub ListView1_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles ListView1.KeyUp
        If e.KeyCode = Keys.Delete And ListView1.SelectedItems.Count > 0 Then
            ListView1.SelectedItems(0).Remove()
            AtualizaTotal()
        End If
        If e.KeyCode = Keys.Escape Then
            LimpaTextBox()
            txtID.Focus()
        End If
    End Sub

    Private Sub ListView1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ListView1.SelectedIndexChanged
        ExibeDetalhe()
    End Sub

A rotina ExibeDetalhe() também não sofreu mudança:

 Private Sub ExibeDetalhe()
        If ListView1.SelectedItems.Count > 0 Then
            txtID.Text = ListView1.SelectedItems(0).Text
            txtProduto.Text = ListView1.SelectedItems(0).SubItems(1).Text
            txtPreco.Text = ListView1.SelectedItems(0).SubItems(2).Text
            txtQtde.Text = ListView1.SelectedItems(0).SubItems(3).Text
            txtSubtotal.Text = ListView1.SelectedItems(0).SubItems(4).Text
        End If
    End Sub

No evento KeyUp do controle TxtID onde logo após o usuário digitar um código de produto ao pressionar a tecla ENTER é feita uma busca na tabela Products e são obtidos os dados do produto para exibição no formulário.

O código deste evento foi alterado conforme abaixo:

Private Sub txtID_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles txtID.KeyUp
        If e.KeyCode = Keys.Enter Then
            If txtID.Text.Length > 0 Then
                Dim P As Product = (New NWContexto).GetProdutoPorID(txtID.Text.Trim)
                If P IsNot Nothing Then
                    txtProduto.Text = P.ProductName
                    txtPreco.Text = P.UnitPrice.ToString
                    txtQtde.Focus()
                Else
                    txtID.Clear()
                End If
            End If
        End If
        If e.KeyData = Keys.Down Then
            If ListView1.Items.Count > 0 Then
                LimpaTextBox()
                ListView1.Items(0).Selected = True
                ListView1.Focus()
            End If
        End If
    End Sub

Neste código estamos usando o método GetProdutoPorID() para obter o produto pelo código informado usando uma instância da classe DataContext.

Agora vamos tratar o evento KeyUp do TextBox txtQtde de forma que após informar a quantidade e pressionada a tecla ENTER, seja calculado o total e as informações sejam exibidas no controle ListView configurando assim um novo pedido:

Private Sub txtQtde_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles txtQtde.KeyUp
        If e.KeyData = Keys.Enter Then
            Dim qty As Integer
            Integer.TryParse(txtQtde.Text, qty)
            If qty > 0 Then
                txtSubtotal.Text = (Decimal.Parse(txtPreco.Text) * qty).ToString("#,###.00")
                AdicionarProduto()
                LimpaTextBox()
                txtID.Focus()
            Else
                txtQtde.Text = ""
            End If
        End If
        If e.KeyData = Keys.Escape Then
            LimpaTextBox()
            txtID.Focus()
        End If
    End Sub

No evento Click do botão Salvar Pedido temos o código que salvar o pedido e seus detalhes :

 Private Sub btnSalvar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSalvar.Click
        If ListView1.Items.Count = 0 Then
            MsgBox("Inclua um ou mais itens para o pedido.")
            Exit Sub
        End If

        If cboClientes.SelectedIndex = -1 Then
            MsgBox("Selecione um cliente")
            Exit Sub
        End If

        If cboFuncionarios.SelectedIndex = -1 Then
            MsgBox("Selecione um funcionário")
            Exit Sub
        End If

        Dim novoPedido As New Order
        novoPedido.CustomerID = CType(cboClientes.SelectedItem, Customer).CustomerID
        novoPedido.EmployeeID = CType(cboFuncionarios.SelectedItem, Employee).EmployeeID
        novoPedido.OrderDate = Today
        For Each LI As ListViewItem In ListView1.Items
            Dim novoDetalhe As New Order_Detail
            novoDetalhe.ProductID = LI.Text
            novoDetalhe.UnitPrice = Decimal.Parse(LI.SubItems(2).Text)
            novoDetalhe.Quantity = Integer.Parse(LI.SubItems(3).Text)
            novoDetalhe.Discount = 0D
            novoPedido.Order_Details.Add(novoDetalhe)
        Next

        Dim ID As Integer
        ID = NWContexto.SalvarPedido(novoPedido)
        If ID > 0 Then
            MsgBox("Order " & novoPedido.OrderID.ToString & " submetido com sucesso...")
        Else
            MsgBox("Falha ao inserir um novo pedido no banco de dados")
        End If
    End Sub

No evento Click do botão Novo Pedido apenas limpamos as caixas de texto e colocamos o foco no controle txtID:

  Private Sub btnNovo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNovo.Click
        LimpaTextBox()
        txtID.Focus()
    End Sub

Agora é só alegria! Mas antes de prosseguir, vamos comparar o que mudou com a utilização do LINQ to SQL em nosso projeto. Notou que não tivemos que utilizar instruções SQL para acessar e persistir informações no banco de dados? otou também que não tivemos que nos preocupar com a definição de objetos connection, command, adapter, etc? A utilização do LINQ to SQL abstraiu toda essa parte, permitindo um ganho de produtividade, pois o projeto ficou mais enxuto.

Vamos executar o projeto e incluir um novo pedido. Após informar os dados e clicar no botão – Salvar Pedido – teremos o seguinte resultado:

Dessa forma, neste projeto tivemos que definir as nossas classes de negócio para poder implementar a funcionalidade desejada e ao invés de fazer isso via código usamos o LINQ to SQL para gerar as classes via mapeamento OR/M.

Pegue o projeto completo aqui: NW_Pedidos_LINQ.zip