O regresso da grid - parte II
Autor: Luís Abreu
Conteúdo: Introdução à programação em Asp.Net 2.0
Ferramentas: Visual Studio beta 2
Não, esta não é mais uma saga tipo Star Wars!
Aqueles que costumam ler regularmente os artigos do site devem estar recordados da publicação de um artigo sobre ASP.Net onde tentei modificar o controlo GridView de forma a permitir a execução de operações de inserção e de selecção múltipla. Na altura deparei-me com imensos problemas relacionados com 1) implementação da grid (da altura) e 2) conjugação da selecção múltipla com a paginação e ordenação em modo de callback.
Ora bem, alguns meses depois resolvi tentar novamente e o resultado foi o controlo LACheckBoxGrid. Este controlo permite a selecção múltipla através do recurso a uma coluna gerida internamente pelo controlo que permite efectuar a selecção de uma linha na grid. Actualmente, o controlo permite efectuar a selecção através de postback ou de callback. Existem ainda algumas áreas que requerem algum trabalho: inexistência de evento associado à selecção de uma linha, melhor integração com o designer e um refactoring interno de forma a dividir melhor as responsabilidades dos vários elementos usados.
Coluna LACheckBoxField
Tal como acontecia com a primeira grid, foi necessário efectuar a construção de uma coluna personalizada capaz de permitir a selecção de vários elementos. Neste caso, optei por construir uma nova coluna que, por defeito, apresenta uma CheckBox para todos os tipos de elementos. As checkboxes colocadas no header e no footer da grid permitem a fácil selecção de todos os elementos apresentados pelo controlo. Para permitir este tipo de funcionamento, a classe define um evento (SpecialCheckBoxClick) que é gerado em resposta a um click sobre o controlo. O delegate que serve de base ao evento tem a seguinte assinatura:
public delegate void LACheckBoxFieldCheckedEventHandler( object sender, LACheckBoxFieldCheckedEventArgs args );
A classe LACheckBoxFieldCheckedEventArgs permite-nos apenas descobrir o estado do elemento que despoletou o evento (e, que como vimos, é uma checkbox situada no header ou footer da grid):
public sealed class LACheckBoxFieldCheckedEventArgs : EventArgs
{
private bool _checkState;
public LACheckBoxFieldCheckedEventArgs( bool checkState )
{
_checkState = checkState;
}
public bool IsCheckBoxChecked
{
get
{
return _checkState;
}
}
}
A construção de um novo tipo de coluna implica a utilização da classe DataControlField como classe base. Esta classe define alguns métodos abstractos que têm de ser implementados por todos os tipos de coluna. Os principais métodos expostos pela classe são:
- CreateField: este método deve ser usado para efectuar a criação de uma nova coluna (este método deve ser especializado por cada classe de forma a conseguir efectuar a criação de uma nova instância dessa coluna);
- ExtractValuesFromCell: este método é invocado para extrair o valor actual de uma célula (neste caso, vamos usar um boolean para indicar o tipo de estado da checkbox);
- InitializeCell: o método é executado durante a inicialização de uma célula associada a este tipo de coluna;
A classe LACheckBoxField recorre ao seguinte código para implementar estes métodos:
public sealed class LACheckBoxField : DataControlField
{
public string Key
{
get
{
return ViewState ["Key"] == null ? "CheckKey" : ViewState ["Key"].ToString( );
}
set
{
ViewState ["Key"] = value;
}
}
public bool ShowClassicHeaderAndFooter
{
get
{
return ViewState ["ShowClassicHeaderFooter"] == null ? false : ( bool )( ViewState ["ShowClassicHeaderFooter"] );
}
set
{
ViewState["ShowClassicHeaderFooter"] = value;
}
}
protected override DataControlField CreateField( )
{
return new LACheckBoxField( );
}
public override void ExtractValuesFromCell( IOrderedDictionary dictionary, DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly )
{
System.Web.UI.Control ctl = null;
object ret = null;
if ( cell.Controls.Count > 0 )
{
ctl = cell.Controls [0];
CheckBox chk = ctl as CheckBox;
if ( chk != null && ( includeReadOnly || chk.Enabled ) )
ret = chk.Checked;
}
if ( ret == null )
return;
if ( dictionary.Contains( this.Key ) )
dictionary [this.Key] = ret;
else
dictionary.Add( this.Key, ret );
}
public override void InitializeCell( DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex )
{
if( ( cellType == DataControlCellType.Footer || cellType == DataControlCellType.Header ) && ShowClassicHeaderAndFooter )
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
}
//create checkbox
CheckBox chk = new CheckBox( );
chk.AutoPostBack = true;
if ( cellType == DataControlCellType.Footer || cellType == DataControlCellType.Header )
{
chk.CheckedChanged += new EventHandler(CheckboxClicked);
}
cell.Controls.Add( chk );
}
public override void ValidateSupportsCallback()
{
}
void CheckboxClicked( object sender, EventArgs args )
{
OnSpecialCheckboxClicked( new LACheckBoxFieldCheckedEventArgs( ((CheckBox)sender).Checked ) );
}
public event LACheckBoxFieldCheckedEventHandler SpecialCheckboxClicked;
void OnSpecialCheckboxClicked( LACheckBoxFieldCheckedEventArgs args )
{
if ( SpecialCheckboxClicked != null )
{
SpecialCheckboxClicked(this, args);
}
}
}
Durante o método InitializeCell, a class procede à construção de uma CheckBox que representa o conteúdo do elemento. Note-se como o header e o footer necessitam de atenção especial! O evento de click sobre as CheckBoxes colocadas nesses elementos resulta na geração do evento SpecialCheckBoxClicked. Como veremos, este evento é importante e é responsável por permitir a selecção de todos os elementos por parte da grid.
LACheckBoxGrid
A grid LACheckBoxGrid apresenta sempre uma coluna do tipo LACheckBoxField como primeira coluna. A classe cria um elemento deste tipo durante o construtor e adiciona-o à sua colecção de colunas. A configuração da grid para permitir a selecção múltipla em postback é simples, uma vez que cada click sobre a checkbox resulta num postback para o servidor. Nesta altura, a única coisa que temos de fazer é actualizar o estilo das linhas de forma a reflectirem a selecção. O local ideal para efectuarmos este tipo de operações é durante o método OnPreRender (que, como o próprio nome indica, é executado imediatamente antes da "renderização" do conteúdo do controlo. O método UpdateStyle é responsável por efectuar essa actualização:
void UpdateStyle( )
{
foreach ( GridViewRow row in this.Rows )
{
row.ControlStyle.Reset();
if ( IsChecked(row) )
{
row.ControlStyle.CopyFrom(this.SelectedRowStyle);
}
else if ( row.RowIndex % 2 == 0 )
{
row.ControlStyle.CopyFrom(this.RowStyle);
}
else
{
row.ControlStyle.CopyFrom(this.AlternatingRowStyle);
}
}
}
Como é possível observar, o método limita-se a percorrer todas as linhas e a aplicar o estilo correcto. O método auxiliar IsChecked consulta a checkbox mantida na primeira célula de forma a obter informação sobre o estado da linha. Para além do estado, também é necessário obter informação sobre os elementos seleccionados. O controlo fornece duas propriedades que permitem obter esse tipo de informação:SelectedRows e SelectedValues. A primeira, retorna uma colecção de rows seleccionada; por sua vez, a segunda retorna uma array de strings com os valores seleccionados. Para completar o estudo do mecanismo de postback, falta apenas referir o tratamento do evento SpecialCheckBoxClicked: durante este método, o controlo limita-se a modificar o estado de todas as linhas de forma a reflectir o estado da checkbox situada no header ou footer responsável pela acção. O método UpdateAllCheckBoxes é o responsável pela modificação do estado das linhas:
void UpdateAllCheckBoxes( bool state )
{
foreach ( GridViewRow row in this.Rows )
{
UpdateCheckBoxState(row, state);
}
//update header
if ( this.ShowHeader )
{
UpdateCheckBoxState(this.HeaderRow, state);
}
if ( this.ShowFooter )
{
UpdateCheckBoxState(this.FooterRow, state);
}
}
E com isto, já temos uma grid capaz de possibilitar a selecção múltipla através de postbacks. Contudo, numa altura em que toda a gente fala de AJAX (e não se referem ao clube de futebol!), não podemos ficar por aqui e temos de adaptar a nossa grid de forma possibilitar a selecção múltipla sem efectuar um refresh. O grande problema da selecção múltipla sem refresh reside no facto de termos de aplicar correctamente os estilos definidos na grid. Se conseguirmos limitar a aplicação de estilos através de classes CSS, então não haveria qualquer problema em relação a utilização de código cliente para modificar o estilo (bastava apenas modificar o estilo aplicado à linha). Infelizmente, o estilo de cada linha pode ser definido através de classes CSS ou, alternativamente, através dos vários atributos de estilo expostos pela classe ControlStyle (tipo das propriedades AlternatingRowStyle, RowStyle, etc).
Portanto, a solução para a correcta aplicação dos estilos passa pela utilização do novo mecanismo de callbacks. Aliás, refira-se que a grid já recorre a este tipo de operações para permitir a selecção e paginação de forma automática. Na altura em que escrevi o primeiro artigo sobre a grid, a implementação do interface obrigava-nos a usar reflection para efectuar este tipo de operações uma vez que o único método do interface ICallbackEventHandler era implementado de forma privada. na beta 2, tal já não acontece e o método é implementado como um método virtual protegido que pode ser modificado pela classe derivada (em contrapartida, a paginação e ordenação em callback deixou de funcionar devido a um erro interno que será corrigido na próxima versão!).
Utilização de callbacks
Bom, agora que sabemos que temos de usar o mecanismo de callback, temos de identificar as alterações necessárias ao controlo. Para além da introdução de uma nova propriedade (que permite configurar a grid para usar este mecanismo), temos ainda de pensar na melhor estratégia para iniciar a operação de callback. Antes de falarmos nas solução, convém apresentarmos a estratégia usada pela grid para efectuar este tipo de operações (como iremos ver, a nossa estratégia baseia-se na reutilização do máximo de funcionalidades fornecidas pela grid).
No lado cliente, cada grid é representada por um objecto javascript (designado de GridView) que contém as propriedades necessárias ao correcto funcionamento do controlo em modo de callback. Assim, este objecto permite-nos saber, por exemplo, a página actual, expressões de ordenação, sentido de ordenação e ainda possui uma referência para objecto cujo interior contém o conteúdo da grid. A configuração da grid numa operação de callback segue os seguintes passos:
- durante o evento PreRender, verifica se deve injectar o código cliente responsável por iniciar o postback. A injecção de código introduz algumas instruções de script na página que instanciam um objecto de javascript devidamente inicializado que é usado durante o inicio e conclusao de uma operacao deste tipo;
- registo de um ficheiro de script (embebido na dll) que contém o código usado pela grid no lado cliente
- registo de um campo escondido usado para manter os dados necessários ao correcto funcionamento da grid.
É importante salientar que durante um postback o controlo recorre ao campo escondido (quando este contém um valor válido) de forma a inicializar correctamente o seu estado interno. Portanto, se quisermos usar um callback, vamos ter de recorrer a este tipo de estratégia. Antes de avançarmos, uma nota: a estratégia apresentada neste artigo baseia-se no acesso a alguns campos internos da grid que podem ser modificados no futuro. Contudo, esta pareceu-me ser a única estratégia viável de forma a conseguir reaproveitar o código fornecido pela grid. Após esta análise inicial, está na hora de falarmos acerca dos detalhes de implementação.
Neste caso, optámos por, durante o evento PreRender, efectuar a actualização do mecanismo usado quando ocorre um click sobre uma checkbox. Como vimos, por defeito é iniciado um postback; contudo, quando usamos callbacks, temos de modificar este comportamento de forma a que um click não resulte num callback. A parte "dura" do trabalho fica a cargo do método UpdateStatus:
void UpdateStatus( GridViewRow row, bool useCallback )
{
CheckBox chk = row.Controls [0].Controls [0] as CheckBox;
chk.AutoPostBack = !useCallback;
if ( useCallback )
{
string clientID = "__gv" + this.ClientID;
string script = this.Page.ClientScript.GetCallbackEventReference(
this,
string.Format("BeginCallback( {0}, {1}, 'LA__{2}', {3}, {4}, {5} )",
clientID, row.RowIndex, this.ClientID, this.Rows.Count, this.ShowHeader ? "true" : "false",
this.ShowFooter ? "true" : "false"),
"MyCallback",
clientID);
chk.Attributes ["onclick"] = script + "; return false";
}
}
Para evitar o postback, recorremos adicionamos a instrução "return false;" de forma a cancelar o click que normalmente inicia o pedido de postback. A string clientID não foi escolhida ao acaso! Foi escolhida com base no ID atribuido internamente pela grid quando usa o mecanismo de callback para paginação e ordenação (como referimos, a grid gera script que introduz automaticamente um objecto javascript com aquele ID que contém dados importantes para processarmos correctamente o callback). O nosso método javascript que inicia o callback recebe vários valores como parâmetros: ID do objecto javascript, número de linhas, posição actual da linha, ID do controlo no lado cliente e indicações sobre a utilização de header e de footer.
O método cliente BeginCallback encontra-se definido num ficheiro de script embebido na dll que contém o controlo e utiliza o seguinte código:
function BeginCallback( grid, actualRowIndex, hiddenField, rowCount, showingHeader, showingFooter )
{
//obter referencia para a checkbox responsavel pela seleccao
var input = event.srcElement;
var content = document.getElementById( hiddenField); //campo escondido usado para persistir os dados
var arr = new Array(); //array usado para manter a lista de itens seleccionados
//se clicarmos no header ou footer, entao estamos perante
//duas situacoes: ou seleccionamos todos os elementos ou removemos a seleccao previa
if( actualRowIndex == "-1" )
{
//aqui estamos perante a seleccao do header ou footer
//portanto, vamos perccorrer a linha e seleccionar todos os elementos
//e vamos adiciona-los ao array usado para manter a lsita de items seleccionados
if( input.checked )
{
var tbody = input.parentElement.parentElement.parentElement;
for( var u = showingHeader ? 1 : 0; u < rowCount + 1; u++ )
{
tbody.childNodes[u].childNodes[0].childNodes[0].checked = true;
arr.push( u - 1 ); //adicionar a posicao -1
}
}
else
{
//neste caso, acabamos de remover todas as seleccoes efectuadas previamente
arr.splice( 0, arr.length );
}
}
else
{
//nesta situacao, clicamos numa das linhas
//devemos percorrer todas as linhas de forma a verificar o estado de cada uma das checkboxes
var tbody = input.parentElement.parentElement.parentElement;
for( var u = showingHeader ? 1 : 0; u < rowCount + 1; u++ )
{
if( tbody.childNodes[u].childNodes[0].childNodes[0].checked )
{
arr.push( u - 1 ); //adicionar a posicao -1
}
}
}
content.value = arr.join( ";" );
//vamos serializar os dados necessarios ao correcto funcionamento da grid em callback
//neste caso, necessitamos de : inidice, expressao sort, direccao e das posicoes das linhas seleccionadas
var str = "->" + grid.pageIndex +
"|" + grid.sortExpression + "|" + grid.sortDirection +
"|" + arr.join( ";" );
return str;
}
Na prática, o método limita-se a percorrer todas as linhas do controlo de forma a obter informação sobre quais as que estão seleccionadas. O controlo recorre a um campo escondido para manter a lista de linhas seleccionadas. O único aspecto importante reside no facto das checkboxes situadas no header e footer devolverem o número -1 como posição (portanto, nestes casos, temos de ou seleccionar todos os elementos ou remover essa selecção). Note-se como o método efectua a concatenação dos valores necessários á reconstituição do controlo no servidor (convém salientar que o parâmetro grid contém uma referência para o objecto javascript que representa a grid no lado cliente).
No lado servidor, temos de efectuar o override do método RaiseCallbackEvent. Neste caso, limitamo-nos a verificar se a operação de callback foi originada devido à selecção de uma linha ou devido à paginação/ordenação dos dados (neste caso, temos de invocar o método da classe base). O código do método é apresentado na listagem seguinte:
protected override string RaiseCallbackEvent( string eventArgument )
{
if ( eventArgument.IndexOf("->") != -1 )
{
string [] elems = eventArgument.Substring(eventArgument.IndexOf("->") + 2).Split('|');
this.PageIndex = Convert.ToInt32(elems [0]); //pagina actual;
string sortExpression = this.Page.CreateStateFormatter().Deserialize(elems [1]).ToString();
this.SetSortExpression(sortExpression);
this.SetSortDirection(elems [2]);
StringWriter writer1 = new StringWriter(CultureInfo.InvariantCulture);
HtmlTextWriter writer2 = new HtmlTextWriter(writer1);
this.DataBind();
this.PrepareControlHierarchy();
CheckFields(elems [3].Split(';'));
if ( this.SelectedValues.Length == this.Rows.Count )
{
this.UpdateCheckBoxState(this.HeaderRow, true);
this.UpdateCheckBoxState(this.FooterRow, true);
}
else if ( this.SelectedValues.Length == 0 )
{
this.UpdateCheckBoxState(this.HeaderRow, false);
this.UpdateCheckBoxState(this.FooterRow, false);
}
UpdateStyle();
UpdateCheckChangeStatus();
this.RenderContents(writer2);
writer2.Flush();
writer2.Close();
return writer1.ToString();
}
else
{
return base.RaiseCallbackEvent(eventArgument.Substring(eventArgument.IndexOf("->") + 2));
}
}
Se tivermos de processar o evento de callback, temos de actualizar a página e a expressão de ordenação (e respectivo sentido) através de reflection (é essa a função dos métodos SetSortExpression e SetSortDirection). O método CheckFields limita-se a percorrer o array com as posições seleccionadas de forma a actualizar o estado das checkboxes dessas linhas. O método termina com a geração explicita do HTML usado para representar o controlo no lado cliente. Após a conclusão deste método, o método javascript MyCallback é usado para actualizar a página.
function MyCallback( info, context )
{
context.panelElement.innerHTML = info;
}
O primeiro parâmetro (info) contém o HTML devolvido pelo método servidor apresentado no excerto anterior. O segundo parâmetro (context) é uma referência para o objecto javascript que representa a grid no lado cliente (a configuração do segundo parâmetro é feita durante o método GetCallbackEventReference). A única ponta solta prende-se com a actualização do estado do controlo durante um postback (esta operação é critica se pensarmos que podemos efectuar vários callbacks e, em seguida, um postback devido a um click sobre um botão). O override do método OnPreLoad permite-nos injectar o código necessário:
protected override void OnPagePreLoad( object sender, EventArgs e )
{
base.OnPagePreLoad(sender, e);
if ( this.Page != null &&
this.Page.IsPostBack &&
!this.Page.IsCallback && this.EnableSelectingCallbacks )
{
this.UnselectAll();
string str = this.Page.Request.Params ["LA__" + this.ClientID];
if ( !string.IsNullOrEmpty(str) )
{
CheckFields(str.Split(';'));
}
}
}
O método limita-se a obter as linhas seleccionadas mantidas no campo escondido de forma a actualizar as linhas seleccionadas. A consulta do restante código contém todos os restantes pormenores associados ao controlo (existem ainda alguns detalhes relacionados com a correcta actualização das checkboxes apresentadas no header e no footer que não foram apresentados). Antes de terminarmos, convém referir que a selecção múltipla em callback apenas funciona se a grid tiver sido configurada para usar a paginação e ordenação em callback.
Conclusões finais
Ao longo do artigo apresentei os principais passos relacionados com a construção de uma grid que permite efectuar a selecção múltipla de linhas em postback ou, alternativamente, em callback. Convém referir que o controlo funciona mas ainda não está pronto para ser distribuido comercialmente :) Infelizmente, a integração com o designer introduz um bug não resolvido (apresenta duas colunas com checkboxes em vez de uma!). Para além disso, o controlo não produz nenhum evento associado à selecção da linha (este problema é de fácil resolução!). A decisão de publicar o controlo prende-se com o facto de ou publicar hoje ou então mantê-lo na "gaveta" nos próximos dois meses! (presumo que os interessados conseguem efectuar as alterações necessárias e, quem sabe, publicá-las de forma a que toda a comunidade beneficie)
O código que acompanha este artigo irá ser disponibilizado na secção de downloads do site (está a aguardar autorização). 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.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu