Clique aqui para ler este artigo em pdf imagem_pdf.jpg

msdn02_capa.jpg

Clique aqui para ler todos os artigos desta edição

 

Criando um DataGrid multitabela no ASP.NET

por Dino Esposito

 

Assim como outros controles de vinculação de dados (data-bound) Web, o DataGrid do ASP.NET pode ser vinculado a uma variedade de fontes de dados, inclusive aos objetos DataTable e DataView do ADO.NET. O Grid também pode ser vinculado a classes e arrays de coleções personalizadas, embora nesses casos você possa preferir usar modelos (templates) de colunas para obter melhor controle sobre a saída. Diferentemente do DataGrid do Windows® Forms, o controle DataGrid do ASP.NET não pode ser vinculado diretamente a um objeto DataSet. Você poderá vinculá-lo a um objeto DataSet através da propriedade DataSource do DataGrid, mas também precisará definir a propriedade DataMember para especificar a tabela a ser vinculada no DataSet. Com isso, o controle DataGrid do ASP.NET exibe apenas uma DataTable, que pode ser baseada em uma tabela ou view.

 

image001.gif

Figura 1 Vínculos de tabela

 

O controle Data Grid do Windows Forms não sofre da mesma limitação de projeto (design). Se você vincular um DataGrid do Windows Forms a um DataSet multitabela, o controle se adaptará automaticamente à interface de usuário para permitir a navegação entre as tabelas (observe a Figura 1). O controle exibe uma lista de vínculos, e cada um corresponde a uma determinada tabela no DataSet. Você clica na tabela e o grid é preenchido com o conteúdo da tabela clicada. Isso não acontece com os grids na Web. Este artigo discutirá uma forma de enriquecer a interface de usuário do controle DataGrid ASP.NET através de um menu de guias (tabbed menu), de modo que o desenvolvedor possa vincular um DataSet ao grid e o usuário possa selecionar uma determinada tabela dentro do DataSet a ser exibido.

 

Vinculando um DataSet multitabela

Se você vincular um DataSet multitabela a um DataGrid Web Forms, verá apenas a primeira tabela encontrada na coleção Tables; todas as outras serão ignoradas. A propriedade DataMember pode ser usada para selecionar uma determinada tabela no grid, mas você ficará limitado a ela. Não há nada que permita uma representação de dados multitabela na interface de programação padrão do DataGrid. O controle de guias é o único elemento de interface de usuário que pode ser usada como um seletor de tabela. O controle de guias também pode ser usado para selecionar páginas de dados provenientes da tabela exibida no momento. Se você adaptá-la para funcionar como um seletor de tabela, perderá a capacidade de realizar buscas no conteúdo da tabela ativa. Existe um modo de adaptar o controle de guias para que ele funcione como um menu, com todas as tabelas disponíveis. Você só precisa conectar o evento ItemCreated, modificar o texto padrão exibido para os links da página e escrever um manipulador (handler) próprio para o evento PageIndexChanged, de modo a controlar fisicamente o processo da seleção de tabela.

 

image002.gif

Figura 2 A idéia do controle com guias

 

Se você não quiser modificar o controle de guias — o que normalmente você não deve fazer — a única alternativa viável para a seleção de várias tabelas é utilizar um componente extra que liste as tabelas, criando um componente tabstrip associado ao grid. O layout gráfico desse novo controle seletor está representado na Figura 2. Neste artigo, criarei um controle seletor igual, vincularei ao DataGrid e, em seguida, criarei um novo User Control.

 

O novo componente TabStrip

A maneira mais fácil de criar um componente semelhante ao mostrado na Figura 2 é utilizar um controle Repeater para gerar um conjunto de botões de comando (command buttons), onde cada botão corresponde a uma determinada tabela no DataSet. Cada clique no botão será interceptado e o objeto DataTable correspondente será vinculado ao Grid. No entanto, vamos tratar de um problema de cada vez e criar um componente com a lista de botões.

O controle TabStrip oferece uma interface de usuário formada por uma seqüência de botões, e cada um deles corresponde a um elemento vinculado. A lista de botões é criada por meio do controle data-bound Repeater, e os cliques nos botões são manipulados através do manipulador de eventos ItemCommand do Repeater. O controle TabStrip também apresenta uma série de propriedades de cores que lhe permite definir a cor do fundo e do primeiro plano dos botões desmarcados e até mesmo dos botões que representam a tabela selecionada.  A propriedade CurrentTabIndex indica a posição do elemento (começando em zero) selecionado no momento. Veja o código-fonte completo do controle na Listagem 1.

 

Listagem 1 O novo controle

<%@ Control Language="C#" ClassName="TabStrip" %>

<%@ Import Namespace="System.Drawing" %>

 

<script runat="server">

 

// Tabs a serem mostrados

public ArrayList Tabs = new ArrayList();

 

// Tab corrente

public int CurrentTabIndex

{

    get {return (int) ViewState["CurrentTabIndex"];}

}

 

// Cor de fundo

public Color BackColor

{

    get {return (Color) ViewState["BackColor"];}

    set {ViewState["BackColor"] = value;}

}

 

// background color selecionado

public Color SelectedBackColor

{

    get {return (Color) ViewState["SelectedBackColor"];}

    set {ViewState["SelectedBackColor"] = value;}

}

 

// Cor do primeiro plano

public Color ForeColor

{

    get {return (Color) ViewState["ForeColor"];}

    set {ViewState["ForeColor"] = value;}

}

 

// Cor do primeiro plano selecionado

public Color SelectedForeColor

{

    get {return (Color) ViewState["SelectedForeColor"];}

    set {ViewState["SelectedForeColor"] = value;}

}

 

// Selecionar método

public void Select(int index) {

    // Garante um valor válido para o índice

 

    if (index <0 || index >=Tabs.Count)

        index = 0;

 

    // Atualiza o index atual. Deve escrever para o view state

    // porque a propriedade CurrentTabIndex é read-only

 

    ViewState["CurrentTabIndex"] = index;

 

    // Dá um refresh na UI

    BindData();

 

    // Chama o evento

    SelectionChangedEventArgs ev = new SelectionChangedEventArgs();

    ev.Position = CurrentTabIndex;

    OnSelectionChanged(ev);

}

 

// Class personalizada do evento

public class SelectionChangedEventArgs : EventArgs

{

    public int Position;

}

 

public delegate void SelectionChangedEventHandler(object sender,

    SelectionChangedEventArgs e);

public event SelectionChangedEventHandler SelectionChanged;

 

// Função auxiliar que chama o evento executando código definido pelo usuário

void OnSelectionChanged(SelectionChangedEventArgs e)

{

    // SelectionChanged é a propriedade do evento

    if (SelectionChanged != null)

        SelectionChanged(this, e);

}

 

 

//////////////////////////////////////////////////////////////////////

 

private void Page_Init(object sender, EventArgs e)

{

    if (ViewState["SelectedBackColor"] == null)

        SelectedBackColor = Color.White;

    if (ViewState["SelectedForeColor"] == null)

        SelectedForeColor = Color.Blue;

    if (ViewState["BackColor"] == null)

        BackColor = Color.Gray;

    if (ViewState["ForeColor"] == null)

        ForeColor = Color.White;

    if (ViewState["CurrentTabIndex"] == null)

        ViewState["CurrentTabIndex"] = 0;

}

 

private void Page_Load(object sender, EventArgs e)

{

    if (!IsPostBack)

    {

        BindData();

    }

}

 

private void BindData()

{

    __theTabStrip.DataSource = Tabs;

    __theTabStrip.DataBind();

}

 

private Color SetBackColor(object elem)

{

    RepeaterItem item = (RepeaterItem) elem;

    if (item.ItemIndex == CurrentTabIndex)

        return SelectedBackColor;

    return BackColor;

}

 

private Color SetForeColor(object elem)

{

    RepeaterItem item = (RepeaterItem) elem;

    if (item.ItemIndex == CurrentTabIndex)

        return SelectedForeColor;

    return ForeColor;

}

 

private Color SetBorderColor(object elem)

{

    RepeaterItem item = (RepeaterItem) elem;

    if (item.ItemIndex == CurrentTabIndex)

        return SelectedBackColor;

    return Color.Black;

}

 

private void ItemCommand(object sender, RepeaterCommandEventArgs e)

{

    Select(e.Item.ItemIndex);

}

</script>

 

<asp:Repeater runat="server" id="__theTabStrip"

     OnItemCommand="ItemCommand">

    <headertemplate>

        <table cellpadding="0" cellspacing="0" border="0" ><tr>

    </headertemplate>

 

    <itemtemplate>

        <td>

            <asp:button runat="server" id="__theTab"

                BorderWidth="1px"

                Border

                BorderColor='<%# SetBorderColor(Container) %>'

                text='<%# Container.DataItem %>'

                font-bold='<%# (Container.ItemIndex == CurrentTabIndex) %>'

                backcolor='<%# SetBackColor(Container) %>'

                forecolor='<%# SetForeColor(Container) %>' />

        </td>

    </itemtemplate>   

   

    <footertemplate>

        </tr></table>

    </footertemplate>

</asp:Repeater>

 

Toda vez que você clicar em um botão da lista, o controle disparará um evento personalizado para a página no servidor. Este evento, SelectionChanged, necessita do seguinte delegate:

 

public delegate

void SelectionChangedEventHandler(object sender,

    SelectionChangedEventArgs e);

 

Como o evento precisa transportar dados personalizados, é necessário um delegate personalizado, pois os eventos são empacotados na classe definida pelo usuário SelectionChangedEventArgs. A classe herda da classe de evento base, EventArgs, e a estende adicionando uma propriedade de inteiros chamada Position, cujo índice inicia em zero (zero-based) do botão clicado:

 

 

public class SelectionChangedEventArgs : EventArgs 

   {

       public int Position;

   }

 

Os valores do texto de legenda (header text) dos diversos botões são armazenados em um membro público do ArrayList chamado Tabs. O trecho de código a seguir mostra como preencher a propriedade Tabs com as tabelas incluídas em um objeto DataSet:

 

foreach(DataTable dt in dataSet.Tables)

    tableSelector.Tabs.Add(dt.TableName);

 

A Figura 3 mostra uma lista típica de botões criada com o user control TabStrip. Como os elementos integrantes dos controles são botões, os cliques são disparados através do evento ItemCommand que atualiza a propriedade CurrentTabIndex, renova as cores das guias (tab colors) e dispara o evento SelectionChanged para o processamento da página no servidor.

image003.gif

Figura 3 Controle TabStrip

 

O controle TabStrip é importante ao se configurar o DataGrid multitabela, pois ele propicia um mecanismo de seleção para as diversas tabelas incluídas no DataSet. Cada botão representa uma tabela, e o clique em um botão faz com que a tabela correspondente seja selecionada dentro do grid. Vejamos uma página de exemplo que contém um componente TabStrip e um DataGrid vinculado a um DataSet multitabela com o seguinte código:

 

<table><tr><td>

<msdn:tabstrip runat="server" id="__tableSelector"

    SelectedBackColor="cyan"

    OnSelectionChanged="SelectionChanged" />

<asp:datagrid runat="server" id="__theGrid"

    AllowPaging="true"

    OnPageIndexChanged="PageIndexChanged">

    <pagerstyle position="top" />

    <alternatingitemstyle backcolor="ivory" />

    <itemstyle backcolor="#eeeeee" />

</asp:datagrid>

</td></tr></table>

 

O DataGrid está localizado bem abaixo do TabStrip e, se você manipular devidamente as cores, poderá criar a ilusão de que eles formam um único controle. Na Listagem 2, você poderá ver o código-fonte necessário para manipular o evento SelectionChanged e vincular a tabela correspondente do DataSet ao Grid. Uma vez iniciada a vinculação, o código também ajusta as cores no controle de guias e do cabeçalho, de modo que haja impressão de continuidade entre o botão selecionado e a grade abaixo dele. Observe na Figura 4 a imagem de tela do aplicativo de exemplo. Pelo fato de ser independente do user control TabStrip, o controle DataGrid pode ser navegado de forma autônoma por meio dos links no controle de guias.

 

Listagem 2 Gerencie SelectionChanged e vincule-o à grade

<script runat="server">

 

void SelectionChanged(object sender,

    TabStrip.SelectionChangedEventArgs e)

{

    __theGrid.CurrentPageIndex = 0;

    BindData();

}

 

void BindData()

{

    __theGrid.DataSource = data.Tables[__tableSelector.CurrentTabIndex];

    __theGrid.DataBind();

    SetupGrid();

}

 

void PageIndexChanged(object sender, DataGridPageChangedEventArgs e)

{

    __theGrid.CurrentPageIndex = e.NewPageIndex;

    BindData();

}

 

void SetupGrid()

{

    __theGrid.HeaderStyle.BackColor = __tableSelector.BackColor;

    __theGrid.HeaderStyle.ForeColor = __tableSelector.ForeColor;

    __theGrid.PagerStyle.BackColor = __tableSelector.SelectedBackColor;

}

•••

</script>

 

image004.gif

Figura 4 Duas grades agindo como uma

 

Embora funcione, este exemplo ainda é uma combinação de controles no servidor e códigos misturados em uma página Web. Com essa implementação, fica difícil reutilizar o código. Para tornar a solução reutilizável, você precisa combinar em um único controle o TabStrip, o DataGrid e o código que os une.

 

O Componente DataSetGrid

Até agora, você tem uma página ASP.NET em funcionamento, a qual deseja transformar em um controle reutilizável para que novas páginas possam ser incluídas. Os Web Users Controls do ASP.NET oferecem o que você está procurando. Com apenas alguns passos, é praticamente possível converter qualquer página Web em um User Control, salvá-la como um recurso .ascx e torná-la um componente reutilizável.

Os Users Controls e as páginas ASP.NET possuem tanta coisa em comum que fica fácil convertê-los entre si. Para começar, salve o arquivo original .aspx como um arquivo .ascx e abra-o em seu editor de códigos favorito. Em seguida, remova as tags HTML: <html>, <body> e <form>. Esse procedimento evita conflitos e erros de análise (parsing errors) na hora da renderização, quando o layout do controle é mesclado ao layout da página onde ele ficará, com suas próprias tags <html> e <body>.

Nem todas as tags <form> precisam ser removidas. No ASP.NET, você pode ter quantos elementos de formulário forem necessários, mas apenas um poderá ser marcado com o atributo runat e definido como visible. Em face disso, remova todos os elementos existentes <form runat="server"> da página original. Se você possuir formulários HTML (tags <form> sem o atributo runat), não precisará mexer neles.

Você notará que é necessário gravar o arquivo com uma extensão .ascx para que o runtime HTTP do ASP.NET seja executado corretamente. Por fim, se a página que você estiver convertendo contiver uma diretiva @Page, certifique-se de mudá-la para uma diretiva @Control. As diretivas @Control e @Page compartilham diversos atributos em comum.

 

Observe que, dentro de um User Control, você não consegue definir nenhuma propriedade que afete o comportamento geral da página. Por exemplo, você não consegue ativar ou desativar o rastreamento nem o gerenciamento do estado de sessão. Você poderá criar o User Control utilizando um código inline ou especificando o modo codebehind. Se você preferir inline, poderá colocar todos os códigos específicos ao controle dentro da tag <script> do lado servidor; no caso do codebehind, escreva o código para o arquivo de classe C# ou Visual Basic® .NET apontado pelo atributo "Src" da diretiva @Control. Observe, no entanto, que o atributo Src atualmente não é reconhecido nem suportado por ferramentas RAD como o Visual Studio® .NET. No entanto, ele é totalmente suportado no Web Matrix (http://www.asp.net/webmatrix).

Se você desenvolver o User Control com o Visual Studio .NET, a classe codebehind será vinculada ao arquivo de origem de uma maneira diferente. O código de classe será compilado no assembly do projeto e disponibilizado ao runtime ASP.NET através do atributo Inherits. Apenas para fins de edição, o Visual Studio .NET rastreia o nome do arquivo codebehind usando o atributo personalizado "CodeBehind". Se você usar o Web Matrix, o atributo Src poderá ser empregado para fazer com que o runtime ASP.NET saiba de onde o código do componente deve ser lido e compilado dinamicamente, o que também pode ser feito com páginas comuns.

O código-fonte do novo User Control DataSetGrid não é muito diferente do código da página exibida na Figura 4. (A parte principal do código-fonte do controle aparece na Listagem 2; o código-fonte completo está disponivel para download.) No entanto, a página que utiliza o controle é radicalmente diferente, como você pode observar na Listagem 3.

 

Listagem 3 Usando o novo controle

<%@ Page Language="C#" %>

<%@ Register TagPrefix="msdn" TagName="DataSetGrid"

    Src="DataSetGrid.ascx" %>

<%@ Import Namespace="System.Data" %>

<%@ Import Namespace="System.Data.SqlClient" %>

 

 

<script runat="server">

void Page_Load(object sender, EventArgs e)

{

    SqlDataAdapter adapter = new SqlDataAdapter(

        "SELECT lastname, firstname FROM employees;SELECT customerid,

        companyname FROM customers;",

        "SERVER=localhost;DATABASE=northwind;UID=sa;");

    DataSet data = new DataSet();

    adapter.Fill(data);

    data.Tables[0].TableName = "Employees";

    data.Tables[1].TableName = "Customers";

   

    dataGrid.DataSource = data;

    dataGrid.DataBind();   

}

 

</script>

 

<html>

<title>Multi-table DataGrid</title>

<body >

 

<form runat="server">

    <msdn:datasetgrid runat="server" id="dataGrid" />

</form>

 

</body>

</html>

 

Você registra o novo controle através da diretiva @Register:

 

<%@ Register TagPrefix="msdn" TagName="DataSetGrid"

 Src="DataSetGrid.ascx" %>

 

Em seguida, use o controle dentro da página, empregando o prefixo do namespace e o nome da tag definidos na diretiva @Register:

 

<form runat="server">

    <msdn:datasetgrid runat="server" id="dataGrid" />

</form>

 

O DataSetGrid fornece um par de membros públicos — a propriedade DataSource e o método DataBind. Certamente esses nomes podem lembrá-lo dos membros de classes e built-int controls do Microsoft® .NET Framework. Eu escolhi usá-los apenas para fins de consistência, mas suas implementações no arquivo de recurso .ascx são totalmente locais:

 

public DataSet DataSource;

public void DataBind()

{

    foreach(DataTable dt in DataSource.Tables)

        __tableSelector.Tabs.Add(dt.TableName);   

   

    BindData();

}

 

O método interno BindData é responsável por vincular a tabela selecionada ao controle DataGrid incorporado.

O controle DataGrid mantém algumas configurações padrões. Em particular, ele precisa que a sua propriedade AutoGenerateColumns seja definida como true. Como o componente do Grid não é exposto publicamente, não existe nenhuma maneira de o autor da página indicar quais colunas devem ser vinculadas nem como isso deve ser feito. Isso também ocorre com os cabeçalhos de coluna, que usam como padrão o nome da coluna retornada pelo resultset. Uma forma simples de personalizar o texto no cabeçalho das colunas da grade é dar um nome alternativo (alias) às colunas de dados na consulta SQL, conforme mostrado a seguir:

 

SELECT lastname AS 'Family Name'

FROM employees

 

Os nomes das tabelas no DataSet vinculado devem ser personalizados. O mecanismo de mapeamento de tabelas do ADO.NET atribui nomes padrão aos vários resultsets gerados por uma consulta. Esses nomes seguem uma convenção padrão de nomenclatura, como Table, Table1, Table2. Embora você possa personalizar o prefixo (mudá-lo de Table para Product, por exemplo), os resultsets adicionais são nomeados com um índice progressivo. Você tem duas opções para mudar os nomes das tabelas no DataSet. A mais simples é renomear as tabelas antes que tenha início o processo de vinculação de dados (data binding):

 

DataSet data = new DataSet();

adapter.Fill(data);

data.Tables[0].TableName = "Employees";

data.Tables[1].TableName = "Customers";

   

dataGrid.DataSource = data;

dataGrid.DataBind();   

 

Nesse caso, a renomeação das tabelas ocorrerá depois que os dados tiverem sido recuperados. A segunda opção envolve a configuração do mecanismo de mapeamento de tabelas do ADO.NET, de modo que as tabelas do DataSet sejam automaticamente geradas com o nome desejado. Como você verá em breve, personalizar o mecanismo de mapeamento de tabelas também resulta em leve melhoria no desempenho.

O mapeamento de tabelas é o processo que controla a maneira como os data adapters do ADO.NET criam tabelas e colunas na memória a partir de uma fonte de dados física. O objeto DataAdapter utiliza o método Fill para preencher um objeto DataSet ou DataTable com dados recuperados, através de um comando SELECT. Internamente, o método Fill utiliza um DataReader para chegar aos dados e metadados que descrevem a estrutura e o conteúdo das tabelas de origem. Em seguida, o DataReader é copiado para um container de memória (o DataSet). O mecanismo de mapeamento de tabelas consiste no conjunto de regras e parâmetros configuráveis que lhe permite controlar a maneira como um resultset é mapeado para os objetos na memória.

O processo de mapeamento do resultset até o DataSet compreende duas fases: mapeamento da tabela e mapeamento da coluna. Durante a etapa de mapeamento da tabela, o data adapter precisa encontrar um nome para o DataTable que conterá as linhas no resultset que está sendo processado. Cada resultset recebe um nome padrão (que o programador poderá modificar à vontade); esse nome padrão depende da assinatura do método Fill utilizado para a chamada. Por exemplo, vamos considerar estas sobrecargas (overloads):

 

adapter.Fill(ds);

adapter.Fill(ds, "MyTable");

 

  Neste caso, a primeira sobrecarga (overload) nomeia o primeiro resultset como Table, enquanto a segunda o nomeia como MyTable. O objeto DataAdapter procura em sua coleção TableMappings por uma entrada que seja compatível com o nome padrão do resultset. O ideal é que a propriedade TableMappings da classe DataAdapter tivesse o nome e o valor do resultset, sendo que o nome representasse a Table (Table, Table1, MyTable2 e equivalentes) e o valor representasse o nome que o desenvolvedor renomeou:

.

 

 

adapter.TableMappings.Add("Table", "Employees");

adapter.TableMappings.Add("Table1", "Products");

adapter.TableMappings.Add("Table2", "Orders");

 

// O Segundo argumento não especificado é o nome default TableX

adapter.Fill(ds);

 

Se a coleção TableMapping tiver uma correspondência, o data adapter usará o nome especificado para o DataTable correspondente no DataSet de destino (denominado ds no trecho de código). Se nenhum mapeamento for encontrado, o comportamento do data adapter dependerá do valor da propriedade MissingMappingAction. O data adapter pode ignorar o resultset não-mapeado, gerar uma Exception ou apenas se ater ao nome padrão.

Uma vez determinado o nome, o data adapter tentará localizar um objeto DataTable correspondente no DataSet de destino. Se tal tabela já existir no DataSet, seus conteúdos atuais serão mesclados ao novo resultset. Caso contrário será necessário o MissingSchemaAction. A propriedade MissingSchemaAction determina a próxima ação. O data adapter pode apenas ignorar a tabela (que não será carregada no DataSet), gerar uma Exception ou criar uma tabela vazia para preenchê-la com o resultset (a ação padrão). Existe um mecanismo de mapeamento semelhante para as colunas da tabela. A propriedade de mapeamento de colunas é chamada ColumnMappings e é definida na classe TableMappings. O fragmento de código a seguir mostra como usá-la:

 

   DataTableMapping dtm1;

   dtm1 = da.TableMappings.Add("Table",

          "Employees");

   dtm1.ColumnMappings.Add("employeeid", "ID");

   dtm1.ColumnMappings.Add("firstname", "Name");

   dtm1.ColumnMappings.Add("lastname", "Surname");

   da.Fill(ds);

 

O mapeamento de tabelas e colunas ocorre de forma transparente sempre que você chama o método Fill em um objeto DataAdapter. O mapeamento é parte importante do estágio de preenchimento do DataSet realizado pelos objetos data adapters no ADO.NET. Se configurar a infra-estrutura de mapeamentos de tabelas e colunas de modo que as tabelas no DataSet tenham os nomes previstos, você conseguirá um desempenho melhor para o data adapter. Quando o data adapter encontrar uma coleção Mapping não-vazia, ele trabalhará com mais rapidez porque não precisa analisá-la. Como o DataSet já contém tabelas devidamente nomeadas, você poupa as instruções adicionais que, de outra forma, seriam necessárias para renomear as tabelas.

 

Um exemplo real

Reconhecidamente, o controle DataSetGrid não é exatamente o tipo de controle que você usaria em todos os aplicativos. Ele termina sendo mais útil àqueles aplicativos rápidos que os desenvolvedores freqüentemente precisam criar na outra ponta de um Web Site para facilitar a personalização e a manutenção. Nesse tipo de cenário, a escalabilidade não impede o armazenamento em cache de grandes blocos de dados no objeto Session. Você pode empacotar tudo em um objeto DataSet e vinculá-lo ao controle DataSetGrid. Rápido e eficaz.

O controle DataSetGrid também pode ser utilizado para oferecer uma representação de dados mais atraente, que normalmente caberia em uma única tabela. A idéia é subdividir o conteúdo da consulta original em subtabelas devidamente nomeadas — por exemplo, pelas iniciais de uma pessoa ou pelo mês. Vamos supor que você necessite recuperar a lista de todos os clientes. Em vez de exibir os clientes em uma única lista que possa rodar em diversas páginas, você poderia particionar os registros em várias subtabelas no mesmo DataSet. Exibido através do controle DataSetGrid, o resultset em guias fornece aos usuários uma interface de busca mais lógica. Se o usuário precisar acessar um cliente que começa pela letra M, não há necessidade de forçá-lo a rolar páginas e páginas até encontrar o cliente. Com apenas um clique, você pode exibir uma subtabela que agrupe todos os clientes cujos nomes comecem por M. O código a seguir executa uma consulta que retorna quatro tabelas, e cada uma contém um subconjunto de clientes:

 

SqlDataAdapter adapter = new SqlDataAdapter(

   "SELECT * FROM customers WHERE companyname >= 'A'

   AND companyname < 'F';" + "SELECT * FROM customers

   WHERE companyname >= 'F' AND companyname < 'M';" +

   "SELECT * FROM customers WHERE companyname >=

   'M' AND companyname

   < 'S';" + "SELECT * FROM customers WHERE companyname >= 'S'",

   "SERVER=localhost;DATABASE=northwind;UID=sa;");

DataSet data = new DataSet();

adapter.Fill(data);

 

Se nomear as diversas tabelas de acordo com as iniciais dos clientes armazenados, você criará um efeito muito interessante (observe a Figura 5):

 

data.Tables[0].TableName = "A-E";

data.Tables[1].TableName = "F-L";

data.Tables[2].TableName = "M-R";

data.Tables[3].TableName = "S-Z";

dataGrid.DataSource = data;

dataGrid.DataBind();

 

image005.gif

Figura 5 Guias por ordem alfabética

 

Até aqui, tudo bem. No entanto, você pode realmente afirmar que esse simples recurso torna o controle DataGrid Web Forms similar à versão Windows Forms? Sem dúvida, não. Um dos aspectos do controle DataGrid do Windows Forms de que mais gosto consiste na sua capacidade de representar qualquer relação pai/filho (parent/child) que tenha sido impostas nas tabelas. Se duas tabelas no DataSet vinculado estão ligadas por uma relação (através de um objeto DataRelation ADO.NET), o grid do Windows Forms adiciona um botão extra a todas as linhas da tabela-pai. Quando você clica nesse botão, a linha do DataGrid se expande, revelando todos os registros-filho daquela linha. Por exemplo, suponhamos que você possua as tabelas de Clientes e Pedidos e uma relação pai/filho estabelecida entre as duas. No grid do Windows Forms, clicar no botão adicional na linha de um cliente exibiria todos os pedidos emitidos por ele. Vejamos se é possível implementar o mesmo recurso no DataGrid do Web Forms.

 

Um Controle DataGrid Master/Detail

Para oferecer suporte a relações de dados no DataSet, o controle DataSetGrid precisa ser capaz de detectar quando uma tabela, que está prestes a ser exibida, possui filhos. Se ele puder fazer isso, você precisa adicionar um botão extra para ativar a seleção. O layout do user control DataSetGrid também precisa ser estendido; no mínimo, ele precisará incluir um segundo DataGrid para exibir as linhas de detalhes. O código a seguir determina se há necessidade de um botão de seleção de coluna:

 

DataTable __currentTable = (DataTable)__theGrid.DataSource;

if (__currentTable.ChildRelations.Count <=0)

    return;

       

// Enable selection on the grid

AddSelectColumn();

 

A coluna de botões é uma instância da classe ButtonColumn. É importante definir a propriedade CommandName da coluna como SELECT. Quando você clica no botão, toda a linha da grade é repintada com os estilos definidos na tag <selecteditemstyle>, o qual definirá uma nova cor de fundo: 

 

 

<selecteditemstyle backcolor="yellow" />

 

Clicar em um botão marcado com o comando SELECT também dispara o evento SelectedIndexChanged. O DataGrid embutido no controle DataSetGrid precisa fornecer um manipulador de eventos adequado, conforme demonstrado a seguir:

 

void SelectedIndexChanged(object sender, EventArgs e)

{

   SelectedItemIndex = __theGrid.SelectedItem.DataSetIndex;

   BindDetails();

}

 

Usar o método GetChildRows do objeto DataRow não é a maneira mais eficiente de recuperar as linhas-filho de uma linha pai dentro de um DataGrid. Embora essa ainda seja a única maneira predefinida de chegar às linhas-filho correspondentes, o uso desse método no contexto de um DataGrid não é suficiente. O método, na verdade, retorna um array de objetos DataRow — uma estrutura de dados que não pode ser diretamente vinculada a um Grid. Uma abordagem muito melhor seria criar uma view filho de relações baseadas no registro selecionado. Para que isto aconteça, você precisa do objeto DataRowView que corresponde à linha selecionada. Um objeto DataRowView só pode ser obtido pela posição de um objeto DataView. Depois disso, você precisa criar primeiro um objeto DataView baseado na tabela pai e, em seguida, rastrear o índice absoluto da linha clicada dentro da tabela. Felizmente, essa informação é bastante fácil de se obter graças à propriedade DataSetIndex da classe DataGridItem. A interface de programação do DataGrid oferece a propriedade SelectedItem para acessar o DataGridItem que representa a linha clicada. O trecho de código a seguir resume as mudanças necessárias no código-fonte:

 

void BindDetails()

{

   DataTable table;

   table = DataSource.Tables[__tableSelector.CurrentTabIndex];

   DataView view = new DataView(table);

      DataRowView drv = view[SelectedItemIndex];

      DataView detailsView =

          drv.CreateChildView("Customer2Orders");

 

      theDetailsGrid.DataSource = detailsView;

      theDetailsGrid.DataBind();

   }

 

 Primeiramente, o código recupera a tabela atual e cria uma instância de DataView. Em seguida, é recuperado o objeto DataRowView correspondente. O método CreateChildView recebe o nome da relação como um argumento e retorna uma view filha. Por fim, a view filha é vinculada ao DataGrid filho. A Figura 6 mostra o resultado.

 

image006.gif

Figura 6 Produto final

 

Vale a pena observar que uma grande parte da saída exibida na figura é gerado automaticamente pelo user control DataSetGrid. A página de chamada é responsável apenas pela preparação de entrada do DataSet com suas tabelas e relações. Se o DataSet vinculado não possuir relações, o controle funcionará conforme discutido na primeira parte deste artigo, agindo meramente como um seletor e visualizador de tabelas.

Você poderá fechar a view filho simplesmente clicando mais uma vez no botão Select. Quando o botão SELECT de uma grade for clicado, dois eventos serão disparados em seqüência — ItemCommand e SelectedIndexChanged. O primeiro evento surge enquanto a propriedade SelectedIndex do DataGrid ainda não está atualizada. Desta vez, armazene em cache o valor de SelectedIndex em uma variável global e a compare com o valor de propriedade quando o evento SelectedIndexChanged for disparado. Se os dois valores forem compatíveis, então o usuário clicou duas vezes na mesma linha; o primeiro clique abre a view filha, enquanto o segundo a fecha.

 

Conclusão

O controle DataSetGrid que apresentei aqui está longe da perfeição. Existem algumas limitações. Primeiro, o controle possui vários recursos de interface de usuário bloqueados. Tanto o Grid mestre quanto o detalhado estão embutidos e não são configuráveis em nível de página. Isto significa que você não pode controlar quais colunas são exibidas e como elas são formatadas. Posteriormente, você não poderá personalizar a aparência dos grids usando tooltips, templates, e summary rows.

Além disso, o controle é arquitetado e implementado como um Web Forms User Control, significando que dois arquivos .ascx devem ser distribuídos — tabstrip.ascx e datasetgrid.ascx. Estes controles de usuário não podem ser herdados e oferecem um suporte pobre para a configuração em design-time a ambientes RAD como o Visual Studio .NET. Estas desvantagens desaparecem se você recriar o controle como um controle de servidor personalizado (custom server control). Nesta discussão, utilizei User Controls porque os achei ideais para os protótipos de soluções rápidas. Envie-me sua opinião sobre este assunto. Eu poderei utilizá-la em artigos futuros para reconstruir o DataSetGrid como um controle personalizado ASP.NET.