Article Details                  
 
Programação em Asp.Net - parte VII

A aventura continua. Este artigo apresenta extensões ao controlo GridView que mostram como podemos personalizar o controlo para permitir a instrodução de resgistos e a selecção de vários itens (numa mesma página).

Programação em Asp.Net - parte VII


Autor: Luís Abreu

Conteúdo: Introdução à programação em Asp.Net 2.0

Ferramentas: Visual Web Dev Express/Visual C# Express


Após acabar de escrever o último artigo, fiquei com a sensação que ainda tinha mais alguma coisa para escrever sobre o controlo GridView. Assim, decidi avançar um pouco mais na minha investigação e expandir o controlo de forma a que este premitisse efectuar a inserção de novos registos e permitir a selecção de vários elementos numa mesma página. Neste último caso, queria garantir que esta operação (selecção múltipla) estaria disponível em dois modos de funcionamento: postback e callback.


Bom, ninguém mais do que gostaria de estar aqui a afirmar que os meus objectivos anteriores foram atingidos. Infelizmente, tal não aconteceu uma vez que não consegui efectuar a selecção múltipla em callback (ou melhor, consegui efectuar a selecção múltipla, mas devo ter-me esquecido de algo pois o meu código de callback corrompe silenciosamente o ViewState - isto só se verifica ao efectuar um postback depois de realizar uma operação de callback). Esperemos que o controlo desperte o interesse de alguém com conhecimentos e tempo suficiente para resolver este problema :) (se assim for, não hesitem em adicionar um comentário ao artigo ou mesmo em contactar-me directamente).


Se chegaram até aqui, então penso que estão preparados para embarcar nesta viagem de forma a explorarmos melhor este controlo.


Introdução aos requisitos e agradecimentos


Antes de falar acerca do código necessário à construção deste controlo queria começar por apresentar (de uma forma razoavelmente detalhada) os principais objectivos deste controlo:



  • inserção de um novo registo na própria grid (algo que, como todos sabemos, não é possível por defeito);

  • possibilitar a selecção de vários registos na grid;

  • possibilitar a selecção de vários registos na grid sem refrescar completamente a página (ou seja, reutilizar o mecanismo de callback).


Como referi, infelizmente não consegui atingir todos os meus objectivos. Contudo, não posso deixar de referenciar a ajuda do João Paulo Carreiro que foi fundamental para permitir a utilização do mecanismo de callback na selecção múltipla. Se não fosse ele, ainda andava eu à procura do atributo certo para "pôr a coisa a funcionar". Obrigado João!


Começando pelo início: como permitir a adição de um novo registo


A primeira coisa a fazer é introduzir um mecanismo que permita ao utilizador indicar que deseja introduzir um novo registo. A forma mais fácil consiste em (surpresa!) adicionar um botão à grid. O segundo passo consiste em processar o clique de forma a que o utilizador consiga introduzir os dados. Esta operação terá de efectuar algumas "transformações na grid" por forma a que o utilizador passe a utilizar uma TextBox (ou outro controlo dependente do tipo de coluna) para introduzir os dados. Claro que neste caso teremos de possibilitar a realização (ou não) da operação (utilizando para tal um par de botões Ok/Cancel).


Agora que já temos os principios definidos, falta definirmos a forma como vamos implementar estas operações. Após alguma (pouca) reflexão, tornou-se óbvio que o local apropriado para implementar estas funcionalidades seria o Footer da grid. Para além disso, depreende-se do parágrafo anterior que a grid irá possuir um estado adicional: modo de inserção. Assim, quando a grid estiver em modo de inserção, as células contidas no Footer deverão permitir a introdução de novos registos; por outro lado, se a grid estiver num modo "normal" (ou seja, se a grid não estiver em modo de inserção ou edição - convém não esquecer este modo inerente ao controlo GridView), então deveremos mostrar o botão que permite ao utilizador inserir um novo registo.


Como seria de esperar, podemos configurar o texto associado ao botão. Para tal, a nova grid define a propriedade InsertText da seguinte forma:



public string InsertText
{
get
{
return _bt.Text;
}
set
{
_bt.Text = value;
}
}

Até aqui, nada de novo. O passo seguinte consiste em configurar a grid de forma a que esta apresente o botão ou as células do Footer em modo de inserção. Pareceu-me a mim que o método indicado para efectuar estas operações é o método OnRowCreated que é evocado aquando da criação de uma linha da grid. Uma vez que este método é protected e virtual, então podemos começar por escrever o seguinte código:



protected override void OnRowCreated( GridViewRowEventArgs args )
{
base.OnRowCreated( args );
if (args.Row.RowType == DataControlRowType.Footer)
{
if (this.IsInInsertMode)
{
TableCell cell = null;
for (int i = 0; i < args.Row.Cells.Count; i++)
{
cell = args.Row.Cells[i];
DataControlFieldCell a = cell as DataControlFieldCell;
DataControlField column = this.Columns[i];
if (column is CommandField)
((CommandField)column).ShowInsertButton = true;
column.InitializeCell(a, DataControlCellType.DataCell, DataControlRowState.Insert, -1);
}
}
else
{
int count = args.Row.Cells.Count;
for (int i = 0; i < count - 1; args.Row.Cells.RemoveAt(0), i++) ;
args.Row.Cells[0].ColumnSpan = count;
}
_bt.Text = InsertText;
_bt.Visible = !this.IsInInsertMode;
args.Row.Cells[0].Controls.Add(_bt);

}
else if ( args.Row.RowType == DataControlRowType.DataRow )
{
//hide new button which is shown when we're in insert mode
if ( IsInInsertMode )
{
for ( int i = 0; i < this.Columns.Count; i++ )
{
DataControlField column = this.Columns [i];
if ( column is CommandField )
( ( CommandField )column ).ShowInsertButton = false;
}
}
}
}

Vamos por partes e vamos tentar perceber o que se está a passar no código anterior. Vamos começar pelo Footer. O código tenta perceber se está em modo de inserção ou não através da propriedade IsInInsertMode. Se a propriedade contiver o valor false, então temos de adicionar o botão que permite a inserção de novos registos. Neste caso, para além de adicionar o botão, optei por remover todas as células por forma a que o Footer apenas contenha uma célula.


As operações associadas ao modo de inserção são um pouco mais complexas. Vamos lá com calma :) Felizmente para nós, os construtores das colunas associadas à GridView definiram um método designado de InitializeCell que permite initializar a célula. Esta inicialização é bastante "inteligente" pois permite configurar a célula em causa tendo em atenção o modo indicado. Apesar da GridView não suportar a operação de inserção, as colunas que podem ser utilizadas com este controlo suportam essa operação. Daí que a inicialização das células seja extremamente fácil (como documenta o código seguinte).


Convém ainda notar que o código tenta encontrar uma coluna do tipo CommandField. Se tal acontencer, então colocamos a propriedade ShowInsert a true. Esta operação é muito importante pois faz com que o controlo apresente automaticamente o par de botões de ok/cancelar na linha em questão (ou seja, no Footer). Já que estamos a falar de colunas do tipo CommandField, queria chamar a atenção para o processamento das linhas do tipo DataRow: se não estivermos em modo de edição, então temos de colocar a propriedade ShowInsertButton de uma eventual coluna do tipo CommandField a false (produzindo assimo efeito contrário, ou seja, escondendo o botão que permite inserir um registo das linhas da tabela).


Adicionando ou cancelando a adição de um novo registo


Supondo que um utilizador entra em modo de edição, então temos de adicionar código que nos permite processar cada um dos seguintes casos:



  • confirmação de um novo registo;

  • cancelamento da adição de um novo registo;

  • edição de outra linha ou selecção de outro elemento.


O local ideal para processarmos estes eventos será o método OnRowCommand que é evocado quando um dos botões contidos na grid despoleta um evento associado ao click. O código associado a este método foi construído com a ajuda daquela que eu considero a melhor ferramenta do univero .Net. Claro que só poderia estar a falar do .Net Reflector do Lutz Roeder. O excerto seguinte apresenta o código relativo a este método:



protected override void OnRowCommand( GridViewCommandEventArgs args )
{

if( _isInInsertMode )
{
if( args.CommandName == "Insert" )
{
LAGridViewInsertEventArgs insertArgs = new LAGridViewInsertEventArgs( );
for( int i = 0; i < this.Columns.Count; i++ )
{
DataControlFieldCell cell = this.FooterRow.Cells [i] as DataControlFieldCell;
DataControlField col = this.Columns [i];
col.ExtractValuesFromCell( insertArgs.Values, cell, DataControlRowState.Insert, false );
}
OnRowInserting( insertArgs );
if( insertArgs.Cancel )
{
_isInInsertMode = false;
return;
}
this._newValues = insertArgs.Values;
DataSourceView view = this.GetData( );
if( view == null )
{
throw new HttpException( "Unable to get datasourceview!" );
}
try
{
view.Insert( insertArgs.Values, new DataSourceViewOperationCallback( HandleInsertCallback ) );
}
catch( Exception ex )
{
LAGridViewInsertedEventArgs newArgs = new LAGridViewInsertedEventArgs( 0, ex );
newArgs.SetValues( insertArgs.Values );
OnRowInserted( newArgs );
if( !newArgs.ExceptionHandled )
throw;
if( !newArgs.KeepInInsertMode )
{
_isInInsertMode = false;
this.RequiresDataBinding = true;
}
return;
}
}

//same in all cases
IsInInsertMode = false;
//remove Insert Button
for( int i = 0; i < this.Columns.Count; i++ )
{
DataControlField column = this.Columns [i];
if( column is CommandField )
(( CommandField )column).ShowInsertButton = false;
}
}

base.OnRowCommand( args );
}

O código começa por verificar se estamos em modo de inserção. Se tal não acontecer, então evocamos apenas o método na classe base. Contudo, se estivermos em modo de inserção, começamos por validar de o comando foi enviado pelo botão que confirma a adição do novo registo. Uma vez que estamos a utilizar a coluna do tipo CommandField, então podemos ter a certeza que o nome associado ao botão de inserção terá o nome Insert.


Nesse caso, o código obtém os valores contidos nas células e armazena-os na propriedade Values da classe LAGridViewInsertEventArgs. Os mais curiosos poderão consultar o sample que acompanha este artigo de forma a apreenderem todos os pormenores associados a esta classe. Convinha ainda realçar um método extremamente importante: ExtractValuesFromCells. Este método permite-nos obter o valor contido no interior de uma célula independentemente do modo de edição.


O código restante limita-se a fornecer uma utilização semelhante à edição ao utilizador da grid (repare-se que podemos cancelar a edição - bastando para isso processar o evento RowInserting e modificando o valor da propriedade Cancel para false; Podemos também obter informação pós-inserção, desde que efectuemos o processamento do evento RowInserted). Interessante é também a forma como efectuamos o tratamento de uma eventual excepção (o comportamento é semelhante ao obtido aquando da edição): a excepção é adicionada a uma propriedade de um elemento do tipo LAGridViewInsertedEventArgs. Em seguida, é despoletado o evento RowInserted, sendo passada esta instância (que, como vimos contém no seu interior uma propriedade com a excepção gerada aquando da inserção). Se o utilizador quiser, pode tratar o evento durante o método associado ao evento RowInserted. Este tipo de acções deve ser sinalizado através da modificação da propriedade ExceptionHandled. Se o utilizador não tratar a excepção, esta será automaticamente propagada, como podemos verificar através do código apresentado.


Para finalizar esta análise, queria ainda chamar a atenção para o excerto final do código. Independentemente da acção realizada e que está a ser processada pelo método OnRowCommand (recorde-se que, para além de uma acção iniciada através do clique do botão associado à confirmação, podemos estar a processar o evento associado ao click efectuado sobre botão cancelar ou editar - já para não falar do botão que selecciona um elemento), no final temos de: 1) sair do modo de inserção e 2) remover eventuais botões de inserção contidos nas células das colunas do tipo CommandField.


Agora que o nosso controlo já suporta a inserção de elementos, não poderia deixar de suportar a selecção de vários elementos! Vamos em frente...


Selecção de várias linhas (por página)


A primeira decisão que tive de tomar prende-se com a forma como devemos permitir a selecção de várias colunas. Achei por bem (também) permitir a selecção através de uma checbox que deveria existir em todas as linhas da tabela. Infelizmente não existia nenhuma coluna capaz de satisfazer as minhas necessidades no que diz respeito a este tipo de funcionalidade. Claro que já existe uma coluna que automaticamente gera uma checkbox, mas essa coluna apenas pode ser utilizada através de binding. Por outras palavras, apenas podemos utilizar estas colunas quando ligadas a uma coluna proveniente do controlo que serve de fonte de dados. Ora bem, não era este o caso, pelo que fui forçado a criar a classe (mais uma surpresa :) ) LACheckBoxField. Não vou reproduzir aqui esta classe (até porque já falei acerca dela aqui). Aconselha-se a consulta do código source para obter uma ideia das funcionalidades desta classe.


Após criarmos uma nova classe, tínhamos de decidir sobre o melhor local para adicionar uma nova coluna. Resolvi adicionar a coluna durante o evento Init, efectuando assim o override do método OnInit. O código é simples:



protected override void OnInit( EventArgs args )
{
if ( this.AllowMultiSelectPerPage )
this.AddSelectColumn( );
base.OnInit( args );
}
private void AddSelectColumn()
{
this.Columns.Insert( 0, _selectColumn );
}


Para além da coluna, achei que deveria dar a hipótese do utilizador escolher todos os itens que constam da página. Daía até adicionar dois novos botões ao Footer (que apenas estão visíveis quando não estamos em modo de inserção e quando o utilizador configurou a grid para permitir a inserção de várias linhas) foi um instante (consultar código para ver alterações efectuadas em relação ao método OnRowCreated apresentado anteriormente).


Como temos botões, então temos de adicionar métodos de forma a processar os eventos. Esta foi uma área em que não conseguir ir mais longe (essencialmente por falta de tempo). Acho que para além de processar os eventos, deveria permitir ao utilizador da grid processar estes eventos. Para tal bastava seguir o mesmo raciocinio seguido durante o processamento do evento associado ao botão de confirmação de novo registo. Esta extensão é deixada como um exercício para o leitor.


A adição das colunas do tipo LACheckBoxField obriga (ainda) à adição de mais um excerto de código. Quando o utilizador clica sobre a checkbox, deverá ser produzido um postback. Essa funcionalidade encontra-se presente numa alteração associada ao método OnRowsCreated (consultar código que acompanha o artigo).


Então o que falta? Duas coisas: uma proprieadade que apresenta todos os elementos seleccionados e o código necessário à modificação do estado (leia-se estilo) associado às linhas seleccionadas. A implementação da propriedade é simples:



public GridViewRowCollection SelectedItems
{
get
{
if ( !AllowMultiSelectPerPage ) return null;
ArrayList lst = new ArrayList( );
foreach ( GridViewRow row in this.Rows )
{
LACheckBoxField fld = this.Columns [0] as LACheckBoxField;
if ( fld != null )
{
KeyedList items = new KeyedList( );
this.ExtractRowValues( items, row, false, false );
bool isChecked = Convert.ToBoolean( items [fld.Key] );
if ( isChecked )
lst.Add( row );
}
}
return new GridViewRowCollection( lst );
}
}

Como é possível verificar, a obtenção dos elementos seleccionados consiste em percorrer as linhas associadas e em verificar o estado da checkbox; se estiver seleccionada, então o elemento está seleccionado (claro que isto implica adicionar código por forma a processar o evento associado ao botão Select contido num CommandField - código esse que se encontra no interior do método OnSelectedIndexChanging).


Aha, finalmente pude gozar o meu primeiro momento de glória...e tudo correu sobre rodas durante os 30 segundos seguintes até que decidi acrescentar mais uma feature: permitir a selecção sem efectuar um postback.


Selecção múltipla sem postback: a feature incompleta


Nesta altura poderia seguir vários caminhos. Se a grid fosse utilizada somente por mim, então de certeza que iria permitir a selecção com base em classes css. Por outras palavras, iria construir código cliente de forma a que um click relativo à selecção (ou não) de uma linha consistisse na modificação da propriedade class associada ao elemento tr do lado cliente.


Como estava a tentar construir um controlo genérico, não pude seguir este caminho. Isto porque, como todos sabemos, a definição do estilo associado a um controlo pode ser efectuado de várias formas (das quais a atribuição de uma classe css é apenas uma). Assim, achei por bem processar o evento de callback disponível na nova versão da plataforma. Ora bem, foi aqui que começaram as dores de cabeça.


Como já tinha dito, a GridView já utiliza este mecanismo. Infelizmente para nós, os criados da grid não previram a necessidade efectuar a modificação deste tipo de comportamento. Por isso, implementaram o método do interface de forma explicita, ou seja, seguiram a sintaxe NomeInterface.NomeMétodo. Quando utilizamos esta estratégia, estamos a criar um método que ao mesmo tempo que é público, é também privado. É público porque podemos aceder a esse método de forma explicita através do interface. É privado porque não há nenhuma maneira convencional (e aqui o importante é a palavra convencional) de conseguirmos efectuar o override deste método numa classe derivada.


A solução para este problema consiste em implementar novamente o interface na nova classe. Após alguma investigação (já descrita neste post), acabei por ter de recorrer à reflection por forma a satisfazer as minhas necessidades. Uma vez que este método é um pouco extenso, vou redireccionar o leitor interessado para uma consulta do código que acompanha este artigo.


Como referi, esta funcionalidade introduz um bug que (apesar de todos os meus esforços) não consegui resolver. Se alguém tiver paciência, tempo e inspiração para resolver este bug, não se esqueça de partilhar a solução para toda comunidade beneficiar (parece-me a mim que a minha nova coluna introduz um problema a nível de corrupção do viewstate).


De referir ainda que a selecção no lado cliente obrigou a nova modificação do método OnRowCreated. Neste caso, temos de verificar se o utilizador deseja utilizar o mecanismo de callback. Nesse caso, torna-se necessário associar o evento click do lado cliente a um método javascript que irá produzir o callback. A forma mais fácil que eu encontrei consiste em reutilizar o código gerado automaticamente pela GridView (por isso é que existe aquela chamada ao método callback associado ao elemento __gv+this.ClientID).


Convém ainda notar que durante o callback tenho o cuidado de reproduzir a informação enviada pela grid do lado servidor para o cliente de forma a não gerar nenhum erro aquando do processamento do método associado ao callback no lado cliente).


Notas finais


O leitor curioso (que já consultou o código) deverá estar-se a interrogar acerca da utilização da propriedade InternalSelCallback. Esta propriedade só é utilizada devido a um bug (?) existente na versão actual. Quando temos a paginação activa e passamos para a segunda página (ou para qualquer outra diferente da primeira) a colecção de items associado à grid não é refrescada. Devido a este problema, tive de trabalhar directamente sobre a colecção de rows associada à tabela contida no interior da grid (para aqueles que ainda não repararam, a grid não é mais do que controlo composto em que o elemento de topo é um div e o controlo filho é uma tabela).


Queria tabém chamar a atenção para dois pormenores que podem ter escapado ao leitor destraido: utilização do chamado Control State e utilização de um ficheiro de script embebido na própria dll que contém o controlo. Estas são duas das (boas!) novidades associadas ao desenvolvimento de controlos. Na versão 1.x da framework o programador de controlos apenas podia guardar informação associada ao controlo no ViewState. Para além deste tipo de operações poder prejudicar a performance (não esquecer que por defeito a informação associada ao view state dos controlos é serializada e enviada para o cliente), trazia alguns problemas graves. Por exemplo, se o utilizador desactivasse o ViewState ao nível da página o controlo deixava de funcionar correctamente.


Com a nova versão da framework, o programador de controlos já não está limitado ao ViewState. Para além dessa forma de armazenar dados associados ao controlo, pode também recorrer ao chamado Control State, que é independente do view state. Para utilizar este mecanismo, temos de serializar a informação que queremos e voltar a obtê-la aquando de um postback/callback.


Outra das novidades interessantes da nova versão reside na possibilidade de embeber recursos no interior dos nossos assemblies. Na versão 1.x tínhamos de colocar eventuais ficheiros script necessários ao controlo numa pasta associada à aplicação. Este tipo de estratégia tinha vários inconvenientes. Agora já podemos embeber recursos (incluíndo ficheiros de script) no interior dos nossos assemblies e obtê-los facilmente na nossa página. Para tal contribui o novo Handler que processa os pedidos associados ao ficheiro WebResource.axd. Já falei acerca deste tipo de funcionalidades aqui.


Para além das funcionalidades apresentadas, queria ainda referir que o controlo apresenta também uma nova coluna (DropDownBoundListField) que permite apresentar uma combo box no interior de uma coluna (este controlo já foi apresentado no aritgo anterior da série).


Conclusões finais


Ao longo deste artigo apresentei algumas extensões ao novo controlo GridView que irá acompanhar a nova versão do Asp.net. Como referi anteriormente, este novo controlo (LAGridVIew) não é perfeito pois contém um bug que inviabiliza a sua utilização em modo callback. De qualquer forma, queria terminar dizendo que o sample que acompanha este artigo (e que poderá ser encontrado na secção de downloads do site - logo que seja aprovado;) ) apresenta algumas funcionalidades interessantes associadas à construção de controlos.


O código que acompanha este artigo contém dois projectos: um projecto com o site web e outro C# que contém uma livraria com o controlo. De referir que poderá ser necessário efectuar algumas alterações a nível dos nomes das pastas contidas no site para que este funcione (ex.: na minha versão, a pasta que contém as referências tem o nome Application_Assemblies; contudo, segundo ouvi dizer nos newsgroups da Microsoft, parece que afinal o nome relativo às pastas onde ficam armazenados os assemblies irá continuar com o nome bin). Ah, e já sabem: se desobrirem o que é que está a corromper o ViewState avisem! 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


Written By: labreu
Date Posted: 4/14/2006
Number of Views: 672

Return