C# - Convertendo um DataReader para uma lista genérica e exibindo em um TreeView


O objetivo principal deste artigo é mostrar como podemos converter um DataReader para uma lista genérica de objetos.

Eu vou usar um exemplo onde irei acessar uma tabela Alunos de um banco de dados SQL Server chamado Escola.mdf e exibir os dados em um controle TreeView.

Vou aproveitar e mostrar como preencher o TreeView usando o modo tradicional com ADO .NET e o código embutido no formulário e o modo mais indicado onde irei criar uma classe que atuará como uma camada de acesso a dados e onde eu irei retornar uma coleção de objetos de forma que na camada de apresentação, o formulário Windows Forms, eu não tenha que referenciar nada relacionado a acesso aos dados como comandos SQL ou objetos ADO .NET para acesso a dados.

Para tornar o exemplo bem simples eu não vou criar a camada de negócios BLL e dessa forma algumas definições eu vou ter que fazer na camada de acesso a dados como a definição da string de consulta.

Para um solução mais robusta recomenda-se a complementação do exemplo criando pelo menos a camada de negócios.

Eu vou usar o Visual Studio 2012 Express for Desktop e a linguagem C# e criar uma solução (File->New Project) Windows Forms Application chamada PreenchendoTreeView.

No formulário form1.cs vamos os seguintes controles:

O leiaute do formulário form1.cs deverá ter a seguinte aparência:

Abaixo vemos a estutura da tabela Alunos do banco de dados Escola.mdf e alguns dados incluídos para o teste:

Acessando dados e preenchendo um TreeView - Modo Tradicional

Vamos começar usando o método tradicional, digo tradicional , porque o modo mais comumente usado onde todo o código é colocado no próprio formulário.

No formulário form1.cs vamos colocar todo o código necessário para acessar os dados e exibir as informações no TreeView.

Para isso teremos que declarar os seguintes namespaces no início do formulário form1.cs:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Data.SqlClient;
using System.Collections.Generic;

A seguir, logo após a declaração da classe form1 devemos definir as variáveis usadas para acessar os dados e a string de conexão

SqlConnection Conn = new SqlConnection("Data Source=.\\SQLExpress; Initial Catalog=Escola; Integrated Security=True");
SqlDataReader rdr;
SqlCommand cmd;

No evento Click do botão Carregar Dados vamos incluir o código que chama a rotina que acessa os dados e preenche o TreeView:

private void btnCarregar_Click(object sender, EventArgs e)
{
   treeView1.Nodes.Clear();
   Carregar();
}

A rotina Carregar() faz todo o trabalho : acessa o banco de dados executa a consulta obtendo um datareader e extrai os dados preenchendo o TreeView:

private void Carregar()
        {
            lblItem.Text = "";
            cmd = new SqlCommand("Select id,nome,email,curso From Alunos Order By id", Conn);

            Conn.Open();
            rdr = cmd.ExecuteReader();
            TreeNode parent = treeView1.Nodes.Add("Alunos");

            TreeNode child;
            parent.ForeColor = Color.Red;

            while (rdr.Read())
            {
                child = parent.Nodes.Add("Aluno ID: " + rdr.GetValue(0).ToString());
                child.ForeColor = Color.Blue;
                child.Nodes.Add("Nome: " + rdr.GetValue(1).ToString());
                child.Nodes.Add("Email: " + rdr.GetValue(2).ToString());
                child.Nodes.Add("Curso: " + rdr.GetValue(3).ToString());
            }
            parent.ExpandAll();

            rdr.Close();
            Conn.Close();
        }

A execução do código gera o seguinte resultado:

Apenas para conhecimento a seguir temos o código que obtém um item selecionado do TreeView e o exibe em um controle Label no formulário:

private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
   {
            int IDSelecionado = treeView1.SelectedNode.Index;
            String NomeSelecinado = treeView1.SelectedNode.Text;
            lblItem.Text = NomeSelecinado.ToString();
  }

Obs: Estamos obtendo o índice do item selecionado apenas para ilustrar como fazer isso.

O código para recolher os itens do TreeView que colocamos no evento Click do controle LinkLabel esta abaixo:

  private void lnkRecolherItens_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
   {
            lblItem.Text = "";
            treeView1.CollapseAll();
   }

Para limpar os itens do TreeView usamos: treeView1.Nodes.Clear();

Para expandir os itens do TreeView o código é: treeView1.ExpandAll();

Tudo muito simples, não é mesmo ?

Acessando dados e preenchendo um TreeView - Usando as boas práticas

Você não precisa ser um Phd em padrões de projetos para usar as boas práticas no desenvolvimento de software. Existem ações que podem ser tomadas desde o início do desenvolvimento até mesmo para aplicações muito simples que garantem o mínimo de robustez e tornam o código muito mais legível e fácil de manter.

O modo tradicional usado é muito simples e funciona, mas ele nunca deve ser usado em aplicações de produção pois ele oculta em sua simplicidade grandes problemas que irão aparecer quando você precisar dar manutenção ou estender a aplicação com novas funcionalidades.

A primeira coisa que chama a atenção é que a camada de apresentação tem que conhecer como acessar os dados, e isso não é a sua função. A camada de apresentação deve apenas saber apresentar os dados ao usuário deixando a responsabilidade de saber como acessar e obter os dados para outra camada.

Chamamos isso de separação das responsabilidades em uma aplicação e aplicamos essa separação trabalhando em camadas. Isso não é um capricho ou uma moda, existe uma razão muito forte para se recomendar essa metodologia de trabalho pois ela facilita a manutenção e permite a reutilização de código.

Vamos então criar uma camada de acesso a dados que no nosso exemplo será representada por uma classe chamada ConexaoDB. O mais correto seria criar um novo projeto do tipo Class Library e neste projeto definir as classes mas devido a simplicidade do nosso projeto irei criar apenas a classe que atuará como a camada de acesso a dados.

No menu PROJECT clique em Add Class e informe o nome ConexaoDB.cs e a seguir defina o código abaixo nesta classe:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections.Generic;

namespace PreenchendoTreeView
{
    public class ConexaoDB 
    {
        private SqlConnection _connection;

        public ConexaoDB()
        {
            try
            {
                string strConnectionString = ConfigurationManager.ConnectionStrings["ConexaoSQL"].ToString();
                _connection = new SqlConnection(strConnectionString);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public List<Aluno> GetAlunosOB()
        {
           string consulta = "Select * from Alunos";
           List<Aluno> _alunos = new List<Aluno>();
           try
            {
               _connection.Open();
               SqlCommand cmd = new SqlCommand(consulta, _connection);
               cmd.CommandType = CommandType.Text;
               SqlDataReader dr = cmd.ExecuteReader();
               while (dr.Read())
               {
                  _alunos.Add(new Aluno()
                  {
                      Id = Convert.ToInt32(dr["Id"]),
                      Nome = dr["Nome"].ToString(),
                      Email = dr["Email"].ToString(),
                      Curso = dr["Curso"].ToString()
                  });
               }
               dr.Close();             
               return _alunos;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                _connection.Close();
            }
        }
    }
}

Esta classe será a responsável pelo acesso ao banco de dados e pela extração das informações da tabela. Todo o conhecimento de como acessar dados é com ela mesmo e ela deve ser uma especialista nisto e deve saber somente isso.

Um dos princípios básicos para desenho de software com o objetivo evitar as más práticas é o principio da responsabilidade única ou SRP - Single Responsability Principle.

Este princípio foi introduzido por Tom DeMarco em 1979 no seu livro Structured Analysis and Systems Specification, Yourdon Press Computing Series.

O princípio da responsabilidade única é um princípio fundamental no projeto de software que reza o seguinte :

"Deve existir um e somente UM MOTIVO para que uma classe mude"

Portanto uma classe deve ser implementada tendo apenas um único objetivo.

Quando uma classe possui mais que um motivo para ser alterada é por que provavelmente ela esta fazendo mais coisas do que devia, ou seja, ela esta tendo mais de um objetivo.

Podemos então inferir as seguintes premissas a partir da definição da responsabilidade única:

– Baseado no princípio da coesão funcional, uma classe deve ter uma única responsabilidade;
– Se uma classe possuir mais de uma responsabilidade, deve-se considerar sua
decomposição em duas ou mais classes;
– Cada responsabilidade é um “eixo de mudança” e as fontes de mudança devem ser isoladas;

Este conceito é fácil de entender mas difícil de ser posto em prática.

A nossa classe ConexaoDB deve portanto ter apenas um objetivo: acessar dados. Vamos então cria a classe removendo o código responsável pelo acesso aos dados do formulário.

Nesta nossa classe temos o construtor iniciando a string de conexão que esta sendo obtida do arquivo de configuração, App.Config , usando a classe ConfigurationManager:

public ConexaoDB()
{
  try
  {

    string strConnectionString = ConfigurationManager.ConnectionStrings["
ConexaoSQL"].ToString();
    _connection = new SqlConnection(strConnectionString);

  }
  catch (Exception ex)
  {
    throw ex;
  }
}

Assim temos que declarar no arquivo App.Config a string de conexão da seguinte forma:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <connectionStrings>
    <add name="ConexaoSQL" connectionString="Data Source=.\SQLExpress; Initial Catalog=Escola; Integrated Security=True;" />
  </connectionStrings>
</configuration>

O método GetAlunos() da classe ConexaoDB esta definindo uma string de consulta usando o comando SQL - Select * from Alunos -, e, na verdade isso não deveria estar definido nesse método mas deveria estar definido na camada de negócios. Como eu não criei esta camada, para ficar mais simples, eu embuti a definição da consulta do método, mas NUNCA faça isso em uma aplicação de produção, primeiro por que nunca devemos usar Select * visto que isso retorna TODOS os registros da tabela. Imagine se a tabela possuir 1.000.000.00 de registros. Por isso sempre restringimos a quantidade de dados retornados de uma tabela usando a classe Where com um critério de filtro: Ex: Select id, nome, email from Alunos Where id < 10.

      public List<Aluno> GetAlunosOB()
        {
           string consulta = "Select * from Alunos";
           List<Aluno> _alunos = new List<Aluno>();
           try
            {
               _connection.Open();
               SqlCommand cmd = new SqlCommand(consulta, _connection);
               cmd.CommandType = CommandType.Text;
               SqlDataReader dr = cmd.ExecuteReader();
               while (dr.Read())
               {
                  _alunos.Add(new Aluno()
                  {
                      Id = Convert.ToInt32(dr["Id"]),
                      Nome = dr["Nome"].ToString(),
                      Email = dr["Email"].ToString(),
                      Curso = dr["Curso"].ToString()
                  });
               }
               dr.Close();             
               return _alunos;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                _connection.Close();
            }
        }

Outro detalhe importante neste método é que estamos retornando uma coleção de objetos Aluno e não um datareader. Fiz isso nesta classe por que não criei a bendita camada de negócios que seria a responsável por este tratamento. Dessa forma a nossa camada de apresentação vai receber um coleção de objetos e não um datareader o que não seria muito indicado pois ela teria que saber como tratar um datareader e isso SÓ camada de acesso a dados deve saber.

Precisamos então definir uma classe que irá representar o nosso modelo de dados e faremos isso criando a classe Aluno (PROJECT->Add Class) e definindo o código desta classe conforme abaixo:

namespace PreenchendoTreeView
{
    public class Aluno
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public string Email { get; set; }
        public string Curso { get; set; }
    }
}

Esta é uma classe POCO - Plain Old CLR Object - e ela mapeia os campos definidos na tabela Alunos, onde para cada campo definimos uma propriedade com o mesmo nome.

Essa classe vai agir como um DTO - Data Transfer Object - servindo apenas para que possamos passar os dados entre as camadas. No nosso exemplo entre a classe ConexaoBD e a camada de apresentação.

Finalmente na camada de apresentação no evento Click do botão Carregar Dados temos o código que vai receber os objetos e usando um laço foreach vai percorrer e exibir as informações no TreeView:

 private void btnCarga_Click(object sender, EventArgs e)
        {
            lblItem.Text = "";
            treeView1.Nodes.Clear();
            try
            {
                ConexaoDB cdb = new ConexaoDB();

                TreeNode parent = treeView1.Nodes.Add("Alunos");

                TreeNode child;
                parent.ForeColor = Color.Red;

                List<Aluno> _alunos = new List<Aluno>();
                _alunos = cdb.GetAlunosOB();

                foreach (Aluno _aluno in _alunos)
                {
                    child = parent.Nodes.Add("Aluno ID: " + _aluno.Id.ToString());
                    child.ForeColor = Color.Blue;
                    child.Nodes.Add("Nome: " + _aluno.Nome.ToString());
                    child.Nodes.Add("Email: " + _aluno.Email.ToString());
                    child.Nodes.Add("Curso: " + _aluno.Curso.ToString());
                }

                treeView1.ExpandAll();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Erro : " + ex.Message);
            }
        }

Note que tivemos que criar uma instância da classe ConexaoDB para poder usar o método GetAlunosOB() pois o método é um método de instância e não é um método estático. Poderíamos ter criado um método estático na classe ConexaoDB de forma a não precisar criar uma instância da classe para acessá-lo.

Ao criar uma instância da classe no formulário acabamos criando uma dependência , ou seja um acoplamento entre ela e o formulário, e isso não é bom mas pode se evitado usando a injeção de dependência. Assunto que eu não vou tratar neste artigo mas vou citar nas referências para você saber como se faz.

A execução vai produzir o mesmo resultado :

Vimos neste artigos coisas importantes, partindo de uma abordagem tradicional e avançando na direção das boas práticas apenas criando uma classe e separando o código da camada de apresentação; já é um grande avanço mas o caminho é longo e para percorrê-lo precisamos de mais artigos.

Pegue o projeto completo aqui: PreenchendoTreeView.zip (sem o banco de dados)

João 12:26 Se alguém me quiser servir, siga-me; e onde eu estiver, ali estará também o meu servo; se alguém me servir, o Pai o honrará.

Referências:


José Carlos Macoratti