Programação em Asp.Net - parte VI
Autor: Luís Abreu
Conteúdo: Introdução à programação em Asp.Net 2.0
Ferramentas: Visual Web Dev Express
Agora que já dominamos os novos controlos de acesso a dados, está na hora de iniciarmos a fase relativa à apresentação dos dados. Para além dos novos controlos de acesso a dados, a nova versão da framework apresenta novos controlos para apresentação dos dados ao utilizador. Ao longo deste artigo vamos utilizar a nova grid do Asp.net 2.0.
Introdução ao controlo GridView
Aposto que se afirmar que a GridView tem um modelo semelhante ao utilizado na DataGrid, não vou estar a dar uma grande novidade. Assim sendo, um utilizador regular da versão 1.X não sente grandes dificuldades para utilizar este novo controlo. Para além de ter uma utilização familiar, este controlo introduz novas funcionalidades (que passo a apresentar):
- suporte ao novo modelo de acesso a dados;
- em determinadas situações efectua a ordenação, paginação, edição e eliminação dos dados;
- suporte do chamado page rendering (a renderização da página pode variar de acordo com o tipo de browser/dispositivo);
- suporte de chaves primárias complexas (constituídas por mais do que uma coluna);
Ao contrário do que acontecia com a DataGrid, este novo controlo apresenta uma colecção de linhas através da propriedade Rows (no controlo DataGrid, esta informação era obtida através da propriedade Items). Já agora, convém também chamar a atenção para o facto de cada linha ser representada por um elemento do tipo GridViewRow. Interessante é também o facto deste controlo possuir a noção de estado para cada linha apresentada (o estado é um valor obtido a partir da enumeração DataControlRowState).
Uma das novidades muito interessantes do novo controlo reside na utilização de um novo elemento que é utilizado nos casos em que não existem linhas para ser mostradas ao utilizador. Por outras palavras, podemos definir a informação mostrada ao utilizador quando a fonte de dados associada ao controlo não retorna nenhum registo. Esta informação pode ser definida através de duas opções: ou utilizamos a propriedade EmptyDataText ou, alternativamente, definimos o conteúdo desta informação através do elemento EmptyDataTemplate. Como seria de esperar, podemos definir o estilo associado a esta linha através do elemento EmptyDataRowStyle.
Já que estamos a falar de novidades, então também podemos referir que neste controlo as colunas são definidas através de uma das seguintes opções:
- geração automática das colunas: neste caso, a propriedade AutoGenerateColumns possui o valor true. Cada coluna é gerada com base num elemento do tipo AutoGeneratedField. Este tipo de coluna mostra a informação através de uma CheckBox inactiva, caso o valor proveniente da fonte de dados seja do tipo boolean. Nos restantes casos, a informação é apresentada através de texto simples. como seria de esperar, se estivermos em modo de edição, então as CheckBoxes poderão ser clicadas e os campos de texto serão representados através TextBoxes;
- definição especifica das colunas mostradas no controlo. Neste caso, podemos definir o tipo de coluna de forma explicita. Existem várias opções viáveis: BoundField, ButtonField, CheckBoxField, HyperLinkField, ImageField, TemplateField e CommandField. Iremos abordar estas colunas de forma detalha na secção seguinte.
Tal como acontecia com o controlo DataGrid, podemos definir os estilos associados às linhas através de propriedades ou de elementos colocados no interior da grid (esta última opção é a minha favorita).
Também é possível efectuar operações de eliminação e modificação de dados apresentados pela grid (nada de novo aqui; este tipo de operações também era possível com a DataGrid - se bem que com a nova versão, temos menos código para escrever). Bom, vamos lá começar com um exemplo simples. Vamos recorrer à base de dados Northwind para obter os dados utilizados ao longo dos nossos testes.
Neste primeiro exemplo vamos apenas obter uma lista de Categorias existentes na base de dados. Uma vez que já sabemos utilizar os controlos de acesso a dados (ver os artigos anteriores da série), então podemos simplesmente escrever o seguinte código:
<asp:GridView ID="grid" Runat="server" AutoGenerateColumns="true" DataSourceID="SqlDataSource1">
</asp:GridView>
<asp:SqlDataSource ID="SqlDataSource1" Runat="server"
SelectCommand="SELECT [CategoryID], [CategoryName], [Description] FROM [Categories]"
ConnectionString="<%$ ConnectionStrings:cnnString %>">
</asp:SqlDataSource>
Mais simples do que isto é dificil! Uma vez que colocámos o valor AutoGenerateColumns a true, então nem temos de definir as colunas que devem ser mostradas (a grid é suficientemente esperta para obter todas as colunas a partir do SqlDataSource). Apesar desta ser a forma mais rápida de definir uma grid, não irá ser utilizada muitas vezes. Para começar, nem sempre é necessário mostrarmos o campo chave da tabela ao utilizador. Muitas vezes necessitamos apenas do campo chave para conseguirmos identificar unicamente o registo relativo a uma linha da grid. O código seguinte mostra as alterações efectuadas de forma a apenas mostrarmos os campos CategoryName e Description:
<asp:GridView Runat="Server" ID="grid"
AutoGenerateColumns="false" CssClass="MainGrid" DataSourceID="source" DataKeyNames="CategoryId">
<Columns>
<asp:BoundField DataField="CategoryName" HeaderText="Category Name"
NullDisplayText="---" ShowHeader="true">
<HeaderStyle Width="30%" />
</asp:BoundField>
<asp:BoundField DataField="Description" HeaderText="CategoryDescription"
NullDisplayText="--------" ShowHeader="true">
<HeaderStyle Width="70%" />
</asp:BoundField>
</Columns>
<HeaderStyle CssClass="Header" />
<RowStyle CssClass="Item" />
</asp:GridView>
Bem, a primeira alteração reside na definição da propriedade AutoGenerateColumns. Como desta vez colocámos esta propriedade a false, então temos de definir explicitamente as colunas que irão ser mostradas ao utilizador. Para tal, definimos as colunas através de colunas do tipo BoundField no interior do elemento Columns. Para além disso, utilizamos também uma propriedade muito importante: estou a referir-me à propriedade DataKeyNames. Esta propriedade é utilizada para indicarmos o nome (ou nomes) das colunas que definem o campo chave relativo aos elementos apresentados na grid. Deixem-me repetir novamente isto: nome ou nomes das colunas que definem o campo chave. Sim, ao contrário do que acontece actualmente com o controlo DataGrid, o novo controlo GridView permite a utilização de campos chave complexos. Se estivermos a utilizar uma chave complexa, então devemos indicar os nomes dos campos separados por vírgulas.
Para demonstrarmos esta situação, vamos manter o código e adicionar uma coluna que nos permita seleccionar os elementos na grid. O excerto seguinte mostra as alterações efecutadas:
<asp:GridView Runat="Server" ID="grid" AutoGenerateColumns="false" CssClass="MainGrid"
DataSourceID="source" DataKeyNames="CategoryId, CategoryName" OnSelectedIndexChanged="ProcessSelect">
<Columns>
<asp:BoundField DataField="CategoryName" HeaderText="Category Name"
NullDisplayText="---" ShowHeader="true">
<HeaderStyle Width="30%" />
</asp:BoundField>
<asp:BoundField DataField="Description" HeaderText="CategoryDescription"
NullDisplayText="--------" ShowHeader="true">
<HeaderStyle Width="65%" />
</asp:BoundField>
<asp:ButtonField Text="Select" CommandName="Select"
ShowHeader="false" />
</Columns>
<HeaderStyle CssClass="Header" />
<RowStyle CssClass="Item" />
<SelectedRowStyle CssClass="Selected" />
</asp:GridView>
Apesar de a tabela apenas possuir um chave primária simples, resolvi simular uma chave primária complexa consituída à custa da combinação das colunas CategoryId e CategoryName. Para além disso, resolvi também introduzir um novo tipo de coluna (ButtonField) que permite ao utilizador seleccionar uma linha da grid. Convém referir que uma chave primária passa a ser encapsulada por uma instância da classe DataKey. O código seguinte mostra como podemos obter a informação proveniente deste elemento:
private void ProcessSelect( object sender, EventArgs args )
{
DataKey key = grid.SelectedDataKey;
string id = key.GetValueByName( "CategoryId" ).ToString();
string name = key.GetValueByName( "CategoryName" ).ToString();
info.Text = "Category Id: " + id + @"<br>Category Name: " + name;
}
Bom, mas está na altura de avançarmos e falarmos acerca de outras operações. Está na altura de efectuarmos a paginação/ordenação dos registos que estão contidos na nossa grid.
Paginação e ordenação da grid
A paginação do controlo GridView é controlada através do atributo AllowPaging. Quando este atributo contém o valor true, a grid é responsável por adicionar os novos controlos que permitem navegar entre as várias páginas que serã necessárias para mostrar os dados. Quando permitimos a paginação, podemos controlar o número de itens por página através da propriedade PageSize. Se for necessário, podemos personalizar a apresentação do pager através do elemento PagerSettings e das respectivas propriedades .
Por outro lado, também podemos controlar a ordenação dos dados contidos na grid através da propriedade AllowSorting. Neste caso, activamos as colunas que querem participar na operação de ordenação definindo a propriedade SortExpression. O melhor de tudo isto é que, ao contrário do que acontece actualmente com a versão 1.x, não temos que escrever mais nenhuma linha de código para que a grid efectue a ordenação.
Já que falamos de paginação e ordenação, não podia terminar sem falar acerca da utilização do novo mecanismo de callbacks. Se quisermos, podemos utilizar este mecanismo para que a grid efectue estas operações sem efectuar uma operação de postback. Para tal, temos apenas de colocar a propriedade EnableSortingAndPagingCallbacks a true.
Infelizmente a grid ainda não é perfeita. Por isso, apesar de efectuar as operações de sorting não identifica nem a coluna nem o sentido que foi utilizada para efectuar a ordenação. Felizmente, conseguimos resolver esse problema através do processamento do evento RowCreated. O código seguinte apresenta o código que poderá ser utilizado para resolver esta questão (encontra-se na página Page4.aspx e foi inicialmente apresentado pelo Dino Esposito; para além da personalização do header, o excerto demonstra como podemos personalizar o código associado ao pager da grid):
void ProcessRowCreated( object sender, GridViewRowEventArgs args )
{
if ( args.Row.RowType == DataControlRowType.Pager )
{
TableCell cell = args.Row.Cells [0];
cell.CssClass = grid.PagerStyle.CssClass;
//estrutura da pager esta encapsulada numa class interna chamada PagerTable
//que nao e publica
//como esse elemento herda de uma tabela, entao podemos utilizar a classe Table
//para percorrer os elementos
//NOTA: na versao 1.X a disposicao dos elementos e diferente
Table pager = cell.Controls [0] as Table;
if ( pager != null )
{
ArrayList lst = new ArrayList();
foreach ( TableCell c in pager.Controls [0].Controls )
{
Control aux = c.Controls [0];
if ( aux is LinkButton )
{
LinkButton bt = aux as LinkButton;
bt.Text = "[" + bt.Text + "]";
lst.Add( bt );
}
else
{
Label lbl = aux as Label;
lbl.Text = "Página " + lbl.Text;
lst.Add( lbl );
}
}
//remover a tabela tradicional e adicionar um span que contem os controlos
cell.Controls.Remove( pager );
foreach (Control ctl in lst)
{
cell.Controls.Add( ctl );
}
}
}
else if (args.Row.RowType == DataControlRowType.Header)
{
if (!grid.AllowSorting) return;
Label aux = new Label( );
aux.EnableTheming = false;
aux.Font.Name = "Webdings";
aux.Font.Size = FontUnit.XSmall;
aux.Text = grid.SortDirection == SortDirection.Ascending ? "5" : "6";
DataControlFieldHeaderCell headerCell = args.Row.Controls[0] as DataControlFieldHeaderCell;
for (int i = 0; i < grid.Columns.Count; i++)
{
if (grid.SortExpression == grid.Columns[i].SortExpression && grid.SortExpression != "" )
{
args.Row.Cells[i].Controls.Add( aux );
}
}
}
}
O sample que acompanha este artigo contém uma página (page4.aspx) com o código completo. Vamos avançar e abordar a edição de informação contida numa linha.
Edição/Eliminação de dados contidos numa GridView
As operações de edição e eliminação são controladas através de um botão que despoleta um comando com o nome Edit. Existem (pelo menos) duas hipóteses para configurarmos a grid de forma a suportar esta operação. A primeira consiste em atribuir o valor true à propriedade AutoGenerateEditButton. Neste caso a grid irá gerar automaticamente uma coluna do tipo CommandField que irá conter o botão de edição. O problema desta aproximação resulta no facto da coluna que contém o botão estar sempre colocada à esquerda. A segunda opção a que podemos recorrer consiste em colocarmos a propriedade AutoGenerateEditButton a false e adicionarmos um novo tipo de coluna à colecção de colunas delimitadas pelo elemento Columns. Neste caso, podemos utilizar um coluna do tipo ButtonField com o valor Edit associado à propriedade CommandName.
O problema desta aproximação reside no facto de ser necessário modificar os botões contidos na coluna aquando da edição da linha de forma a que quando a grid entrar em modo de edição contenha os botões de confirmar e cancelar. Apesar da utilização desta coluna ser um bom exercício, irá ser (provavelmente) melhor utilizarmos uma coluna do tipo CommandField. Este tipo de coluna permite-nos definir quais os botões activos (através das propriedades ShowXXXButton).
Como seria de esperar, a grid fornece um par de eventos que permite interagir com este a operação. Assim, durante o evento RowUpdating podemos influenciar os valores que irão ser utilizados no comando de update. Por outro lado, após a operação ser efectuada podemos interagir com o evento RowUpdated de forma a obter o número de registos afectados ou uma eventual excepção que tenha sido gerada aquando da execução do comando. Já agora, temos também o evento RowEditing, que é despoletado quando a grid entra em modo de edição.
Tudo isto é muito engraçado mas, como é de esperar, só funciona se o nosso controlo de fonte de dados tiver definido uma operação de actualização. Muito interessante é o facto de, ao contrário do que pensava inicialmente, não ser necessário definir os parâmetros utilizados para a inserção. Pois é... a grid é suficientemente esperta para descobrir os parâmetros e automaticamente criá-los desde que tenhamos o cuidado de dar o mesmo nome das colunas aos parâmetros (ex.: se possuirmos uma coluna teste, então devemos definir o parâmetro com o nome @teste). Repare-se que não é necessário mostrar eventuais colunas relativas a campos chave desde que a propriedade DataKeyName seja definida (estes aspectos são demonstrados na página Page5.aspx).
Nota: a partir da versão beta 1, é necessário definir a propriedade OldValuesParameterFormatString="{0}"; caso contrário, a instrução de actualização não irá ser executada pois irá ser gerado um erro que informa o utilizador que falta uma parâmetro. Este tipo de comportamento tem lógica uma vez que ao efectuar a actualização a grid constrói duas listas de parâmetros: uma com o valor antigo e outra com o novo valor.
Eliminação de registos
As colunas do tipo CommandField também permitem definir e associar a cada linha um botão que irá ser responsável pela eliminação. As observações efectuadas anteriormente são válidas para este. Por outras palavras, temos de definir no controlo DataSource um comando de eliminação (instrução SQL DELETE).
Normalmente a operação de eliminação é uma operação melindrosa. Devido a isso é costume perguntar-se ao utilizador se deseja realmente eliminar o registo. Infelizmente a GridView não suporta esta funcionalidade. Felizmente para nós, as alterações necessárias a esta operação são simples, conforme demonstra o seguinte excerto de código (oriundo da página Page5.aspx ):
void OnRowCreated( object sender, GridViewRowEventArgs args )
{
if ( args.Row.RowType == DataControlRowType.DataRow )
{
TableCell cell = null;
for ( int i = 0; i < args.Row.Cells.Count; i++ )
{
cell = args.Row.Cells[i];
CommandField column = grid.Columns [i] as CommandField;
if ( column != null )
{
foreach ( Control ctl in cell.Controls )
{
if ( ctl is LinkButton && ( ( LinkButton )ctl ).CommandName == "Delete" )
{
( ( LinkButton )ctl ).Attributes ["onclick"] = "if( !confirm('Do you really want to delete the item?' ) ) {event.cancelBubble = true; return false;} ";
}
}
}
}
}
}
Inserção de novos registos
Infelizmente a GridView não permite a inserção de novos registos pois a Microsoft apostou nos controlos DetailsView e FormView. Contudo, na minha opinião, existem várias alturas em que a introdução de dados através de uma grid é útil (aliás, na versão actual da framework, já adaptei a DataGrid a esse tipo de operações). Assim, resolvi pesquisa na Internet e ver se alguém já tinha construído uma grid que permitisse a inserção. É verdade que não procurei muito, mas a única (e primeira) referência que encontrei foi no blog do Fredrik Normén.
Apesar de interessante, esta técnica não satisfazia todas as minhas necessidades. Bem, a verdade é que estava desejoso de encontrar um motivo que me permitisse arregaçar as mangas e extender este controlo. Foi assim que nasceu a LAGridView. Antes de avançar, queria apenas advertir para o facto de (por falta de tempo) não ter testado esta nova grid exaustivamente pelo que poderão encontrar enventuais bugs.
Arquitectura da LAGridView
O principio de construção da grid é simples: expandir as funcionalidades da grid existente. Assim, não será surpresa se eu afirmar que a LAGridView herda da classe GridView. O principio de funcionamento é simples: a grid utiliza o footer como forma de disponibilizar um botão que permite a inserção de um novo registo. Este botão poderá estar activo apenas na última página da grid (caso a grid esteja em modo de paginação) .
A nova grid efectua o override do método OnRowCreated de forma a puder gerar o botão que permite a inserção ou, caso o utilizador tenha clicado nesse botão, uma nova linha de acordo com as colunas definidas ao nível da grid. Convém salientar que o controlo utiliza os métodos auxiliares (InitializeCell) para inicializar os controlos com os dados necessários. Outro aspecto interessante reside na forma como o controlo determina se o postback foi motivado pelo click sobre o botão de adição.
Actualmente a grid apresenta um problema relativo à utilização de TemplateFields. O problema reside na utilização da nova sintaxe de Binding (mais precisamente, do novo método Bind) em conjunto com uma DropDownList. Ora bem, uma vez que queria adicionar a este sample uma página que contivesse uma DropDownList em modo de inserção, não tive outro remédio senão desenvolver um novo tipo de coluna que, em modo de edição, rende uma DropDownList. Esta classe designa-se de DropDownBoundListField e encontra-se definida no ficheiro LAGridView.cs.
Hoje não me resta mais tempo para descrever este controlo (e a sua construção de forma aprofundada). Contudo, num futuro próximo, irei tentar melhorar esta grid (com suporte cliente para as novas operações, melhor integração com o designer, etc.) e irei (quase de certeza) escrever acerca dessa experiência.
Nota Final
Antes de terminar, uma nota relativa aos testes efectuados. A sample que acompanha este artigo foi testada em duas máquinas em que foram instaladas as versões beta 1 e CTP de Outubro do Visual Web Developer (que faz parte das express tools). Na versão CTP, a coluna ImageField não funciona correctamente devido à remoção do handler que efectuava o processamento dinâmico das imagens.
Conclusões finais
Ao longo deste artigo falámos sobre algumas das funcionalidades da nova GridView. Como foi possível verificar, a nova grid apresenta funcionalidades deveras interessantes que vêm simplificar o trabalho do programador. Para além do básico, o sample que acompanha este artigo apresenta também uma solução para permitir a inserção de novos registos através da GridView. No próximo artigo vamos falar um pouco mais sobre este controlo (LAGridView) e sobre algumas das novas funcionalidades disponiveis para a construção de controlos em Asp.Net. Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.
Fiquem bem e boa programação! Até à próxima. O código que acompanha este artigo está disponível na secção dos downloads do site.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu