Article Details                  
 
Construção de DataControlFields

Demonstra os passos necessários à construção de DataControlFields personalizados. O artigo apresenta três exemplos de classes que extendem a classe DataControlFields.

Construção de DataControlFields personalizados


Autor: Luís Abreu


Conteúdo: Demonstra a construção de novos
tipos de colunas BoundFields


Ferramentas: .NET 2.0


A nova versão da plataforma ASP.NET introduz inúmeras
novidades. Os novos controlos data bound são uma delas. Dois dos controlos
data bound mais usados (GridView e DetailsView) permitem-nos definir as colunas
(ou linhas) apresentadas ao utilizador final através de um conjunto de
classes derivadas de DataControlField. A figura seguinte apresenta os vários
tipos de colunas disponibilizadas pela plataforma.



Figura 1 - Diagrama de classes dos vários elementos do tipo DataControlField disponibilizados pela plataforma


A tabela seguinte apresenta um resumo das principais funcionalidades
disponibilizadas por cada um dos tipos de colunas existentes:











































Coluna Descrição
DataControlField Serve de base a todas as classes capazes de apresentarem
dados num controlo data bound.
BoundField Esta coluna apresenta os dados provenientes de campos devolvidos por um controlo data source. Esta classe expõe algumas
propriedades que permitem definir o tratamento aplicado antes dos dados
serem apresentados. Por exemplo, a propriedade HtmlEncode indica se o conteúdo
apresentado pelo campo deve ou não ser HTML-encoded.
AutoGeneratedField Os controlos data bound recorrem a este tipo de elementos quando a propriedade
AutoGenerateColumns (controlo GridView) ou AutoGenerateRows (controlo DetailsView)
possui o valor true. Apesar de podermos adicionar dinamicamente elementos
deste tipo (isto é, através de código), a verdade é
que não o devemos fazer já que este tipo de colunas foi construído
para uso interno dos controlos data bound.
ButtonFieldBase Esta classe abstracta serve de base às colunas capazes de apresentar
botões (ButtonField e CommandField). A classe limita-se a definir
um conjunto de métodos comuns que simplificam a construção
desse tipo de colunas.
ButtonField Esta classe permite-nos apresentar uma coluna com um botão por
cada registo apresentado pelo controlo data bound. Como seria de esperar,
podemos definir vários aspectos relacionados com o botão apresentado:
o tipo (ButtonType), o nome associado ao comando (CommandName), o texto
apresentado pelo botão (Text ou DataTextField - depende da proveniência
do texto), etc.
CommandField Ao contrário da coluna anterior, esta consegue apresentar vários
botões. A sua principal função é apresentar
os botões responsáveis pelo inicio das operações
de inserção, edição, eliminação
e selecção de linhas apresentadas pelos controlos data bound.
HyperLinkField Como podemos facilmente depreender a partir do seu nome, esta coluna apresenta
os seus dados sob a forma de âncora (hyperlink). A coluna pode apresentar
texto fixo (propriedade Text) ou proveniente de um campo de uma fonte de
dados associada ao controlo data bound que contém a coluna (propriedade
DataTextField). O destino associado a cada âncora pode ser definido
estaticamente (propriedade NavigateUrl) ou dinamicamente através
da propriedade DataNavigateUrlFields. Note-se ainda que podemos formatar
o texto e o url associados à âncora quando os dados provêm
de campos de um controlo data bound através das propriedades DataTextFormatString
e DataNavigateUrlFormatString.
ImageField Este tipo de coluna é adequando à apresentação de imagens.
Note-se que as colunas deste tipo apenas conseguem apresentar imagens se
o campo associado apresentar um url com o caminho até uma imagem
(por outras palavras, este tipo de coluna não consegue apresentar
imagens embebidas numa coluna de uma base de dados).
TemplateField Quando nenhuma das colunas anteriores serve, podemos construir o nosso
template que será usado para apresentar dados normalmente provenientes
de um campo de um controlo data bound.

Tabela 1 - Descrição dos principais tipos de colunas fornecidos pela plataforma


O código exemplifica a utilização tipica
de algumas das colunas anteriores:


<form id="form1" runat="server">
<asp:GridView runat="server" ID="grid" AutoGenerateColumns="false"
DataKeyNames="IdAluno" DataSourceID="source">
<Columns>
<asp:BoundField HeaderText="Nome Aluno" DataField="Nome" />
<asp:BoundField HeaderText="Morada Aluno" DataField="Morada" />
<asp:CommandField SelectText="Seleccionar"
ButtonType="Link" ShowSelectButton="true" />
</Columns>
</asp:GridView>
<asp:SqlDataSource runat="server" ID="source"
SelectCommand ="select IdAluno, Nome, Morada from alunos"
ConnectionString="<%$ connectionStrings:db %>" />
</form>

Portanto, a coluna adapta o HTML gerado ao estado
do controlo - por exemplo, as colunas BoundField apresentam caixas de texto
quando o controlo data bound está em modo de edição ou
em modo de inserção.


Limitações das colunas fornecidas pela plataforma


Apesar das colunas introduzidas pela plataforma serem adequadas
a muitas das situações do "mundo real", a verdade é que existem
algumas situações em que a resposta fornecida por este tipo de
elementos não é a melhor. Por exemplo, se precisarmos de configurar
uma célula para apresentar uma caixa de texto multilinha ou uma caixa
de combinação não podemos utilizar as colunas do tipo BoundField
(já que estas apenas apresentam caixas de texto simples). Na maior parte
das situações, a resposta passa pela utilização
de uma coluna do tipo TemplateField que nos obriga a definir a apresentação
da célula para os vários estados pelo qual o controlo passa.


O grande problema associado ao uso deste tipo de elementos reside no reaproveitamento:
a única opção que temos é copiar/colar o código
se necessitarmos de colunas semelhantes em controlos data bound diferentes (ou
até mesmo no próprio controlo). Para além disso, a perspectiva de termos
de definir a apresentação de uma coluna do tipo TemplateField
para os vários estados pelos quais um controlo data bound pode passar
não é muito atractiva. O principal objectivo deste artigo é
demonstrar quais os passos que devem ser dados para construirmos as nossas próprias
colunas. Ao longo deste documento, apresento três novos tipos de colunas
(LABoundDropDownListField, LATextBoxBoundField e LAThreeStateCheckBoxField)
que tentam colmatar algumas das lacunas das colunas introduzidas pela plataforma.


As novas colunas


As colunas LABoundDropDownListField, LATextBoxField e LAThreeStateCheckBoxField
usam a classe BoundField como base já que o seu principal objectivo é
apresentar/persistir dados provenientes de campos de controlos data source.
De todas, a classe LATextBoxField é a mais simples já que se limita
apenas a interceptar a caixa de texto criada de forma a adaptá-la ao
tipo desejado. As outras duas têm de realizar mais algum trabalho já
que são responsáveis por adaptarem-se ao estado actual do controlo
data bound. Vamos então começar pela mais simples...


LATextBoxField


O principal objectivo deste tipo de coluna é permitir
ao utilizador definir o tipo de caixa de texto que é apresentada quando
o controlo data bound está em modo de edição ou de inserção.
Na prática, esta classe limita-se a acrescentar três propriedades
à classe BoundField que é usada como base:























Propriedade Descrição
HideTextWhenInPasswordMode Esta propriedade boleana serve para indicar se a célula deve apresentar
caracteres especiais quando o controlo data bound está em modo
de leitura. Esta propriedade só é "respeitada" quando
a célula está configurada para usar o modo palavra-chave.
PasswordChar Esta propriedade permite definir o caracter usado quando a propriedade
anterior possui o valor true e a propriedade TextMode possui o valor Password. Por predefinição, é usado
o caracter '*'.
TextMode Serve para indicar qual o modo que deve ser usado pela TextBox apresentada
na célula quando esta está em modo de inserção
ou de edição. Os valores aplicáveis a esta propriedade
provêm da enumeração TexBoxMode
Rows Permite definir o número de linhas apresentadas pela caixa de texto quando a coluna está configurada em modo TextBoxMode.Multiline

Tabela 2 - Propriedades da classe LATextBoxField


A utilização de uma coluna deste tipo é
extremamente simples, como podemos verificar através do seguinte excerto:


 
<LA:LATextBoxBoundField HeaderText="Morada" DataField="Morada"
Textmode="Multiline" />


Neste caso, iríamos obter uma coluna que apresenta uma
TextBox multillinha em modo de inserção/edição.A
figura seguinte mostra o resultado final de uma GridView que utiliza uma coluna
deste tipo em modo de edição:



Figura 2 - Controlo GridView configurado com as novas colunas apresentadas neste artigo


Detalhes da implementação


A implementação da classe anterior foi extremamente simples,
já que a maior parte do trabalho é efectuada pela classe base
(BoundField). Assim, a classe limita-se a efectuar o override dos métodos
InitializeDataCell e OnDataBindField. O método InitializeDataCell é
um método protected responsável por inicializar a célula
que apresenta os dados. Neste caso, temos apenas de adicionar código
responsável por configurar a caixa de texto quando estamos em modo de
inserção/edição:



protected override void InitializeDataCell(DataControlFieldCell cell,
DataControlRowState rowState)
{
base.InitializeDataCell(cell, rowState);
if (cell.Controls.Count == 1 )
{
TextBox txt = cell.Controls[0] as TextBox;
if (txt != null)
{
txt.TextMode = this.TextMode;
}
}
}

Quando o controlo data bound está em modo de edição/inserção,
a célula actual contém apenas um controlo no seu interior: uma
TextBox que permite ao utilizador efectuar a modificação dos dados.
Em vez de recorrermos ao parâmetro rowState para verificarmos o estado
actual, limitamo-nos a validar o número de controlos mantidos no interior
da célula e a configurar a TextBox que aí se encontra de forma
a que esta use o modo adequado. Note-se que este código é apenas
usado quando o controlo data bound que contém a nossa coluna está
em modo de edição/inserção. Nos restantes casos,
a nossa classe recorre ao método OnDataBinding para efectuar a sua personalização:



protected override void OnDataBindField(object sender, EventArgs e)
{
if( this.TextMode == TextBoxMode.Password &&
this.HideTextWhenInPasswordMode )
{
Control ctl = sender as Control;
//so precisamos de configurar o campo durante apresentacao
//normal, ja q o proprio controlo TextBox encarrega-se de
//apresentar a informacao com "*"
if( ctl is TableCell )
{
( ( TableCell )ctl ).Text = this.GetPassText( );
return;
}
}
base.OnDataBindField(sender, e);
}

O método anterior é invocado a partir da classe base durante
a operação de data binding responsável por inicializar
os dados apresentados ao utilizador. A implementação deste método
na classe BoundField é responsável por efectuar várias
operações úteis, como por exemplo, a formatação
do texto apresentado. Infelizmente, a classe apresenta sempre o texto obtido
a partir do campo proveniente de um controlo do tipo data source, o que não
é adequado quando configuramos a coluna em modo Password. Assim, temos
de injectar código capaz de "esconder" o conteúdo apresentado
na célula quando estamos em modo de leitura. Convém ainda salientar
que o código anterior apenas influencia os dados apresentados em modo
de leitura.


LABoundDropDownListField


O principal objectivo desta coluna é permitir a utilização
de um controlo DropDownList em modo de inserção/edição
(nestes modos, o controlo deve ser populado automaticamente a partir de uma
fonte de dados). A classe LABoundDropDownListField apresenta as seguintes propriedades
(para além das herdadas da classe base BoundField):



























Propriedade Descrição
DataTextField Esta propriedade permite-nos definir o nome do campo que contém
o texto que deve ser apresentado pela célula quando o controlo data
bound está em modo de leitura. Quando não é definido
nenhum valor nesta propriedade, é usado o valor do campo DataField.
O campo indicado deve ser proveniente do controlo do tipo data source associado
ao controlo data bound que foi relacionado com o controlo data bound que
contém a coluna.
DefaultSelectedIndex Usado para definir qual o indice que deve ser seleccionado por predefinição
(usado quando o controlo data bound está em modo de inserção).
DropDownListDataSourceId Propriedade responsável por definir qual o controlo
data source que fornece os itens apresentados pela DropDownList quando a
célula está em modo de inserção/edição.
DropDownListDataTextField Propriedade que permite definir o nome do campo que contém o texto
apresentado pelo controlo DropDownList. O nome deste campo deve ser proveniente
dos dados devolvidos pelo controlo data source indicado através da
propriedade DropDownListDataSourceId.
DropDownListDataValueField Utilizado para definir o nome do campo que fornece o valor a um item apresentado
pelo controlo DropDownList.

Tabela 3 - Propriedades da classe LABoundDropDownListField


A utilização deste tipo de coluna é extremamente
simples, como podemos verificar através do excerto seguinte:



<LA:LABoundDropDownList HeaderText="CodigoPostal"
DataField="idcodigopostal" DataTextField="codigo"
DropDownListDataValueField="IdCodigoPostal" DropDownListDataTextField="CodigoTotal"
DropDownListDataSourceId="codigopostalsource" />

Detalhes de implementação


Tal como acontecia com a classe LATextBoxBoundField, a classe LADropDownListBoundField
usa a classe BoundField como base. Contudo, neste caso temos de escrever mais
algum código já que temos de gerar uma DropDownList para apresentar
os dados em modo de edição. A implementação do método
InitializeDataCell é ligeiramente diferente da implementação
apresentada na secção anterior. O método começa
por verificar o estado actual através do parâmetro rowState: quando
estamos em modo de edição ou inserção, o método
limita-se a construir uma DropDownList que é inicializada e adicionada
à célula passada como primeiro parâmetro do método
(note-se ainda que, em modo de inserção, a propriedade DefaultSelectedIndex
é responsável por definir qual o item seleccionado):



protected override void InitializeDataCell(DataControlFieldCell cell,
DataControlRowState rowState)
{
Control ctl1 = null;
Control ctl2 = null;
_currentState = rowState;
//check if in edition or insertion
if (((rowState & DataControlRowState.Edit) != DataControlRowState.Normal && !this.ReadOnly) ||
(rowState & DataControlRowState.Insert) != DataControlRowState.Normal)
{
DropDownList lst = new DropDownList();
lst.DataSourceID = this.DropDownListDataSourceId;
lst.DataValueField = this.DropDownListDataValueField;
lst.DataTextField = this.DropDownListDataTextField;
lst.ToolTip = this.HeaderText;
ctl1 = lst;
if (this.DataField.Length != 0)
{
ctl2 = lst;
}
if ( ( rowState & DataControlRowState.Insert ) == DataControlRowState.Insert )
{
lst.SelectedIndex = this.DefaultSelectedIndex;
}
}
else if ( this.DataTextField.Length != 0 || this.DataField.Length != 0)
{
ctl2 = cell;
}
if (ctl1 != null)
{
cell.Controls.Add(ctl1);
}
if (ctl2 != null && ((rowState & DataControlRowState.Insert) != DataControlRowState.Insert))
{
ctl2.DataBinding += new EventHandler(OnDataBinding);
}
}

Quando estamos em modo de edição, o método
OnDataBinding é encarregue de inicializar correctamente os dados apresentados
ao utilizador. Como é possível verificar através do excerto
seguinte, o método começa por obter o valor a apresentar (através
da invocação do método GetValue). Em seguida, limita-se
a apresentar esse valor como texto de uma célula (modo leitura) ou como
valor seleccionado na DropDownList:



protected virtual void OnDataBinding(object sender, EventArgs args)
{
Control namingContainer = ((Control)sender).NamingContainer;
object obj1 = this.GetValue(namingContainer);
string txt = obj1.ToString();
if (sender is TableCell)
{
if (txt.Length == 0)
txt = "&nbsp;";
((TableCell)sender).Text = obj1.ToString();
}
else
{
if (!(sender is DropDownList))
throw new HttpException("This control must be a DropDownList");
else if (obj1 != null)
((DropDownList)sender).SelectedValue = obj1.ToString();
}
}



O método GetValue é responsável por obter o valor adequado
ao estado actual da coluna. Este valor pode ser proveniente do campo identificado
pela propriedade DataTextField (quando estamos em modo leitura e este campo
possui um valor válido) ou da propriedade DataField (quando as condições
anteriores não se verificam):


 
protected override object GetValue(Control controlContainer)
{
if (ShouldGetFromDataTextField())
{
//neste caso, vamos comecar por obter o item q contem os dados associados a linha actual
object item = DataBinder.GetDataItem(controlContainer);
//se n estivermos em modo de design e item for null, entao estamos perante um caso excepcional
if (item == null && !this.DesignMode)
{
throw new HttpException("data item not found!");
}
//vamos recorrer a um property descriptor para obter o valor do campo definido no datatextfield
if( this._fieldDesc == null)
{
this._fieldDesc = TypeDescriptor.GetProperties(item).Find(this.DataTextField, true);
if (this._fieldDesc == null && !this.DesignMode)
{
throw new HttpException("property descriptor not found!");
}
}
if (this._fieldDesc != null)
{
return _fieldDesc.GetValue( item );
}
//verificar se estamos em modo de design...
if (this.DesignMode)
{
return this.GetDesignTimeValue();
}
}
return base.GetValue(controlContainer);
}

Note-se como a utilização de um PropertyDescriptor
permite-nos obter alguma abstracção na recuperação
do valor da propriedade desejada! Importa ainda referir que o método
ShouldGetFromDataTextField é usado para decidir se devemos obter o valor
do campo indicado através da propriedade DataTextField ou se devemos
recorrer ao campo DataField.


Para além da obtenção de dados provenientes
do controlo data source associado, temos ainda de adicionar código capaz
de efectuar a operação inversa. A classe LADropDownListBoundField
efectua este tipo de operações através do override do método
ExtractValuesFromCell:



public override void ExtractValuesFromCell(IOrderedDictionary dictionary,
DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly)
{
Control cellCtl = null;
object inner = null;
if (((rowState & DataControlRowState.Insert) != DataControlRowState.Normal) &&
!this.InsertVisible)
return;
if (cell.Controls.Count > 0)
{
cellCtl = cell.Controls[0];
DropDownList lst = cellCtl as DropDownList;
if (lst != null)
inner = lst.SelectedValue;
}
else if (includeReadOnly)
{
string txt = cell.Text;
if (txt == "&nbsp;")
inner = "";
else if (this.SupportsHtmlEncode && this.HtmlEncode)
inner = HttpUtility.HtmlDecode(txt);
else
inner = txt;
}
//check values
if (inner == null)
return;
if (inner is string && ((string)inner) == this.NullDisplayText &&
this.NullDisplayText.Length > 0)
inner = null;
if (dictionary.Contains(this.DataField))
dictionary[this.DataField] = inner;
else
dictionary.Add(this.DataField, inner);
}

Algumas observações em relação ao
método :



  • Quando possuimos controlos no interior da célula,
    então devemos recuperar o valor seleccionado da DropDownList; se tal
    não acontecer, então apenas obtemos o valor existente na célula
    da tabela (propriedade Text da classe TableCell) se o parâmetro includeReadOnly
    possuir o valor true;

  • A adição do valor ao dicionário
    é obrigatória e é feita sempre em função
    do nome do campo definido na propriedade DataField. Note-se que temos sempre
    de verificar se já existe um item com essa chave no dicionário
    (se tal não acontecer, somos responsáveis por adicionar um novo
    item com essa chave).


E com estes passos acabámos de construir uma coluna capaz
de apresentar uma DropDownList em modo de inserção/edição
que pode ser facilmente reutilizada ao longo de vários projectos. O código
seguinte ilustra uma página que recorre a uma grid para apresentar dados
ao utilizador final e que ilustra o uso dos dois tipos de colunas apresentados
até aqui:



<asp:GridView runat="server" ID="grid"
AutoGenerateColumns="false" DataKeyNames="IdAluno" DataSourceID="source">
<Columns>
<asp:BoundField HeaderText="Nome Aluno" DataField="Nome" />
<LA:LATextBoxBoundField HeaderText="Morada" DataField="Morada"
Textmode="Password" />
<LA:LABoundDropDownList HeaderText="CodigoPostal" DataField="idcodigopostal"
DataTextField="codigo"
DropDownListDataValueField="IdCodigoPostal" DropDownListDataTextField="CodigoTotal"
DropDownListDataSourceId="codigopostalsource" />
<LA:LAThreeStateCheckBoxField HeaderText="Sexo" DataField="sexo"
ItemStyle-HorizontalAlign="Center" />
<asp:CommandField EditText="Editar..." CancelText="Cancelar"
UpdateText="Actualizar"
ButtonType="Link" ShowEditButton="true" />
</Columns>
</asp:GridView>
<asp:SqlDataSource runat="server" ID="codigopostalsource"
SelectCommand ="select IdCodigoPostal, codigo + ' ' + localidade as codigototal from codigopostal"
ConnectionString="<%$ connectionStrings:db %>" />
<asp:SqlDataSource runat="server" ID="source"
SelectCommand ="select IdAluno, Nome, Morada, Sexo, cp.codigo + ' ' + cp.localidade
as codigo, a.idcodigopostal as idcodigopostal from alunos a inner join codigopostal
cp on a.idcodigopostal=cp.idcodigopostal"
UpdateCommand="update alunos set Nome=@nome, morada=@morada, idcodigopostal=@idcodigopostal,
sexo=@sexo where idaluno=@idaluno"
ConnectionString="<%$ connectionStrings:db %>" />


Note-se que as linhas que contêm as instruções
de SQL foram modeladas de forma a poderem aparecer correctamente no layout.
O segundo controlo SqlDataSource é responsável por fornecer os
dados apresentados na caixa de combinação. Repare-se ainda como
a instrução de SELECT do primeiro controlo SqlDataSource efectua
um INNER JOIN de forma a poder apresentar um texto quando a célula está
em modo de leitura em vez do código correspondente a esse código
postal.


LAThreeStateCheckBoxField


A última das classes apresentada neste artigo
permite a construção de uma coluna que permita apresentar
uma three state checkbox. Infelizmente, não existe nenhum controlo HTML
capaz de suportar este tipo de operações (a checkbox suporta apenas
dois estados: seleccionado e não seleccionado). Os principios usados
na construção de uma coluna deste tipo são precisamente
os mesmos que os apresentados anteriormente: vamos herdar da classe BoundField
e personalizar alguns dos seus métodos (nomeadamente, os métodos
ExtractValuesFromCells e InitializeDataCell). O "grande" problema
que temos pela frente passa pela construção de um controlo capaz
de simular uma three state checkbox!


Controlo LAThreeStateCheckBox

A construção de um controlo deste tipo obriga-nos
a criar uma nova classe capaz de participar no processo de obtenção
de dados em postback. Logo, a nossa classe deverá reaproveitar o máximo
de características da classe WebControl e implementar o interface IPostbackDataHandler.
A tabela seguinte começa por apresentar as propriedades da classe:























Propriedade Descrição
CheckState Ao invés de possuir uma propriedade designada de Checked, a classe
disponibiliza esta propriedade capaz de indicar o estado actual do controlo.
A enumeração LACheckState define os três estados possiveis.
LabelAttributes À semelhança do que acontece com o controlo WebControl,
a nossa classe permite a definição dos atributos
da label apresentada.
Text Usado para definir o texto apresentado pelo controlo.
TextAlign Permite definir a posição do texto em relação
à checkbox apresentada. A enumeração TextAlign fornece
os valores aplicáveis a esta propriedade.

Tabela 4 - Propriedades da classe LAThreeStateCheckBox


A utilização desta API força-nos desde
logo a algumas decisões. Para começar, vamos ter de recorrer a
um StateBag personalizado para manter as definições de estilo
associadas à propriedade LabelAttributes. Para além disso, e ainda
como consequência da utilização desta propriedade, vamos
ter de personalizar a persistência e recuperação dos dados
mantidos em view state (override dos métodos LoadViewState e SaveViewState).
Vamos então começar por apresentar o código relativo a
estas propriedades:



public LACheckState CheckState
{
get
{
return _checkState;
}
set
{
_checkState = value;
}
}
public string Text
{
get
{
return base.ViewState["Text"] == null ?
"" : (string)(base.ViewState["Text"]);
}
set
{
if (!object.Equals(value, base.ViewState["Text"]))
{
base.ViewState["Text"] = value;
}
}
}
public TextAlign TextAlign
{
get
{
return ViewState["TextAlign"] == null ?
TextAlign.Right : (TextAlign)( ViewState["TextAlign"] );
}
set
{
ViewState["TextAlign"] = value;
}
}
public AttributeCollection LabelAttributes

{
get
{
if( this._labelAttributes == null )
{
if( this._labelAttributesState == null )
{
this._labelAttributesState = new StateBag(true);
if (this.IsTrackingViewState)
{
((IStateManager)this._labelAttributesState).TrackViewState();
}
}
this._labelAttributes = new AttributeCollection(this._labelAttributesState);
}
return this._labelAttributes;

}
}


A propriedade LabelAttributes merece alguns comentários
extra. Esta propriedade recorre a um campo designado de _labelAttributes responsável
por manter os atributos que devem ser aplicados à LABEL que será
gerada pelo controlo para apresentar o texto desejado. Note-se como a inicialização
do campo obriga-nos a usar um elemento do tipo StateBag que servirá de
repositório aos atributos definidos. A utilização deste
elemento força-nos a modificar a forma como é feita a persistência
do viewstate do controlo: para além dos dados mantidos no StateBag associado
ao controlo, temos ainda de guardar os dados mantidos no StateBag associado
aos atributos aplicados à LABEL.



protected override object SaveViewState( )
{
object baseState = base.SaveViewState( );
object labelState = _labelAttributesState == null ?
null : ( ( IStateManager )_labelAttributesState ).SaveViewState( );
return new Pair( baseState, labelState );
}
protected override void LoadViewState( object savedState )

{
if( savedState != null )
{
Pair state = ( Pair )savedState;
if( state.First != null )
{
base.LoadViewState( state.First );
}
if( state.Second != null )
{
( ( IStateManager )_labelAttributesState ).LoadViewState( state.Second );
}
}
}

Apesar de já termos apresentado algum código associado ao controlo,
ainda não falámos acerca da forma como representaremos a checkbox
no lado cliente. A forma mais simples de o fazermos passa pela utilização
de imagens que representam os vários estados do controlo. Para além
disso, temos ainda de configurar o controlo de forma a que um clique resulte
na apresentação de uma imagem diferente. Falta apenas decidir
como vamos fazer para conseguir transmitir o estado entre cliente e servidor...uma
das hipóteses (provavelmente a mais viável) consiste na utilização
de um campo escondido que é responsável por manter o estado actual
da checkbox no lado cliente e que é automaticamente enviado para o lado
servidor durante o postback.


Finalmente, temos ainda de decidir sobre a melhor forma de gerarmos
o código script que será usado no cliente para efectuar a transição
entre os vários estados possíveis da checkbox. Durante a versão
1.X, a resposta a esta questão residia na utilização de
métodos disponibilizados pela classe Page que permitem gerar script cliente.
Felizmente para nós, a nova versão possibilita-nos embeber recursos
na dll que contém o código dos controlos servidor já que
a nova handler webresource.axd é capaz de recuperar esses elementos em runtime.
Logo, neste caso vamos limitar-nos a embeber as imagens na dll e a escrever
praticamente todo o código cliente num ficheiro de script que também
será embebido na dll final. Note-se que após adicionar os recursos
ao projecto temos de modificar as suas propriedades de forma a que estes sejam
embebidos no projecto e temos ainda de torná-los públicos através
do atributo WebResourceAttribute.


Falta apenas referir a estratégia usada para gerar o
HTML do controlo. Ao invés de usarmos um controlo composto, optámos
por gerar todo o código durante o método Render. Assim, todos
os elementos gerados pelo controlo são apenas criados durante este método.
O método contém algum código, pelo que redireccionamos
o leitor para a consulta dos ficheiros que acompanham este projecto. Antes de
terminarmos esta apresentação rápida do controlo LAThreeStateCheckBox,
importa referir que no lado cliente o controlo recorre a um objecto javascript
para representar cada instância deste elemento presente na página.


LAThreeStateCheckBoxField - parte II


Agora que já possuímos um controlo funcional capaz de emular
o comportamento de uma three state checkbox, podemos voltar a concentrar-nos
na contrução da classe LAThreeStateCheckBoxField. Como dissémos,
a classe segue os mesmos principios apresentados para as colunas anteriores.
As diferenças residem no tipo de controlo usado e na forma como lidamos
com valores nulos (NULL). Vamos começar por analisar o método
ExtractValuesFromCell:



public override void ExtractValuesFromCell(System.Collections.Specialized.IOrderedDictionary dictionary,
DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly)
{
object result = null;
if (cell.Controls.Count > 0)
{
LAThreeStateCheckBox ctl = cell.Controls[0] as LAThreeStateCheckBox;
if (ctl != null && (includeReadOnly || ctl.Enabled))
{
switch (ctl.CheckState)
{
case LACheckState.Checked:
result = true;break;
case LACheckState.Unchecked:
result = false; break;
default:
result = DBNull.Value;break;
}
}
}
if (dictionary.Contains(this.DataField))
{
dictionary[this.DataField] = result;
}
else
{
dictionary.Add(this.DataField, result);
}
}

O principal aspecto a reter reside na utilização
da classe DBNull que nos permite enviar um valor correspondente ao estado indeterminado
para o controlo data source (por outras palavras, esta propriedade permite-nos
enviar o valor NULL para um campo).


A inicialização de uma célula é
feita através do método InitializeDataCell. O método limita-se
a verificar o modo actual e a configurar correctamente o controlo LAThreeStateCheckBox
usado:



protected override void InitializeDataCell(DataControlFieldCell cell,
DataControlRowState rowState)
{
LAThreeStateCheckBox checkBox = null;
if (((rowState & DataControlRowState.Edit) == DataControlRowState.Edit && !this.ReadOnly) ||
((rowState & DataControlRowState.Insert) == DataControlRowState.Insert))
{
LAThreeStateCheckBox check = new LAThreeStateCheckBox();
check.ToolTip = this.HeaderText;
if( this.DataField.Length != 0 &&
( ( rowState & DataControlRowState.Edit ) == DataControlRowState.Edit ) )
{
checkBox = check;
}
else
{
//usado em modo de insercao...
cell.Controls.Add( check );
}
}
else if( this.DataField.Length != 0 )
{
LAThreeStateCheckBox check = new LAThreeStateCheckBox();
check.Text = this.Text;
check.Enabled = false;
checkBox = check;
}
if (checkBox != null)
{
cell.Controls.Add(checkBox);
if (this.Visible)
{
checkBox.DataBinding += new EventHandler(this.OnDataBindField);
}
}
}

O método OnDataBindField é apenas responsável
por obter o valor proveniente do controlo data source e actualizar o estado
do controlo LAThreeStateCheckBox apresentado pela célula. Como podemos
verificar, o código limita-se a obter o valor a partir do naming container
associado e a definir o estado da checkbox:



private new void OnDataBindField(object sender, EventArgs args)
{
Control ctl = sender as Control;
Control container = ctl.NamingContainer;
object val = this.GetValue(container);
if ( !( ctl is LAThreeStateCheckBox ) )
{
throw new HttpException("oopss....should be a LAThreeStateCheckBox");
}
if (val == DBNull.Value)
{
((LAThreeStateCheckBox)ctl).CheckState = LACheckState.Indeterminate;
}
else if (val is bool)
{
bool ischecked = Convert.ToBoolean(val);
((LAThreeStateCheckBox)ctl).CheckState = ischecked ?
LACheckState.Checked : LACheckState.Unchecked;
}
((LAThreeStateCheckBox)ctl).Text = this.Text;
}


Conclusões finais


Ao longo deste artigo apresentámos três exemplos
de extensões de novos tipos de data fields que podem ser usados com os
novos controlos data bound. Como foi possível observar, a construção
de novos elementos deste tipo é simples e resume-se à implementação
de dois ou três métodos chave.


O código que acompanha este artigo pode ser obtido a partir daqui.
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


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

Return