| Este artigo discute: | Este artigo usa as seguintes tecnologias: | ||||||
| Visual Basic .NET, XML e XSL, Embeding Download: | ||||||
Chapéu |
|
Criar controles personalizados é muito comum no dia a dia do desenvolvedor. O mais simples deles, o User Controls é facilmente implementado e muito popular, porém, em alguns cenários possui algumas desvantagens em relação aos Web Server Controls. O objetivo deste artigo é uma introdução a criação de Web Server Control demonstrando suas principais características.
A popularização dos blogs e uso cada vez maior do padrão Xml, vêm contribuindo para que cada vez mais sites disponibilizem informações no formato RSS. É comum hoje ler artigos, notícias e outros dados neste formato. Estas características nos levaram a escolher como base de exemplo a criação de um leitor de RSS para este artigo. O resultado final será um controle funcional, com feedback de layout em modo de design, podendo ser usado nos seus projetos sem escrever uma única linha de código.
O que são Web Server Controls.
Um dos principais critérios para qualificar um Web Server Control é a herança de System.Web.UI.Control ou System.Web.UI.Controls.WebControl de forma direta ou indireta (de outro Web Server Control). Outras características importantes são: capacidade de gerar uma saída quando ocorre uma solicitação de Http; capacidade de ser armazenado no GAC (Global Assemby Cache); compartilhamento em diversos projetos em um único assembly.
A Figura 1 apresenta a estrutura hierárquica da classe System.web.UI que deriva os web server controls.

Figura 1 - Organização hierárquica da classe System.Web.UI
As principais diferenças entre Web Sever controls e User Control:
| • | Os Web server controls são persistidos e compilados em uma DLL. Os User Controls são persistidos em arquivo texto (ascx); |
| • | Os Web Server Controls podem ser adicionados na Toolbox possuindo recursos completos para o ambiente de design. Os User Controls não podem ser adicionados na Toolbox e são instanciadas cópias a cada projeto; |
| • | Os Web Server Controls têm sua distribuição de forma genérica, independente do aplicativo. Os User Controls estão associados à aplicação e são distribuídos junto com as mesmas. |
Basicamente podemos citar três tipos de Web Server Controls:
Simples ou Básicos: são Web Server Controls que não possuem outros Web Server Controls em sua criação e sobreescreve o método Render, gerando o resultado em um tipo System.Web.UI.HtmlTextWriter. São os mais simples de serem criados, podendo ou não implementar a interação com o Postback. Os métodos e eventos são resolvidos em tempo de execução.
Compostos (Composite): são Web Server Controls que possuem outros Web Server Controls em sua criação, sobreescreve o método CreateChildControls instanciando uma coleção de controles: Controls collection. Não é necessário sobreescrever o método Render, pois os Childs Controls já provêem o Render Lógico. Embora sejam mais complexos, têm a vantagem de resolver os eventos e métodos em tempo de compilação.
Apresentação (Templates): são Web Server Controls com a capacidade de prover ferramentas para conter novos controles e separar a apresentação (layout) da lógica do controle.
Em .NET, devido a sua orientação a objetos, o ciclo de vida de uma página dispara vários eventos em uma determinada sequência. O desenvolvimento de Web Server Control exige do desenvolvedor um conhecimento mais detalhado destes eventos. Muitos problemas enfrentados durante o desenvolvimento de um Web Server Control estão relacionados com a inobservância das sequências de eventos de uma página.
Além destes eventos o ASP.NET é capaz de persistir os dados quando é feita a solicitação ao servidor para mesma página, o chamado PostBack. Desta forma temos duas sequências de eventos que são realizados dependendo da solicitação de request, conforme Figura 2.
Initialize (é o evento Init) - nesta fase são iniciadas as configurações necessárias para o ciclo de vida da página. Se existir um Web Server Control do tipo Composite ele será carregado para memória com seus valores default baseado no método CreateChildControl.
Load View State (se houve um postback) - carrega os dados persistidos no viewstate e busca pelos controles relacionados para carregar os valores
Process PostBack Data (se houve um postback) - executa os processos de entrada de dados somente para os controles que possuem a implementação com a interface IPostBackDataHandler. É neste evento que o desenvolvedor faz a interação dos dados do usuário pelo método LoadPostData.
Handle Post Events (se houve um postback) - sincroniza o evento do cliente no postback. Primeiro é feito uma busca pelo controle que disparou o postback. Depois é verificado se este controle implementa a interface IPostBackEventHandler. Estando implementada, é disparado o método RaisePostBackEvent e passado como parâmetro a uma string com o evento que foi disparado.
Sends post back notifications (se houve um postback) - dispara o evento de notificação de mudança de estado ("state changes") entre o postback atual e o anterior. O desenvolvedor controla este evento pela função RaisePostDataChangedEvent implementada pela interface RaisePostDataChangedEvent.
PreRender - é o último evento exposto ao desenvolvedor antes de ser executado o Render da página. Pode ser usando para fazer qualquer tipo de atualização antes da execução do Render.
Save View State - é neste evento que as propriedades dos controles são automaticamente persistidas caso a opção de EnabledViewState estiver em True.
Render - neste evento é gerada a saída que será apresentada ao cliente.
Dispose - executa a finalização dos controles, Conexões, arquivos e outros recursos utilizados.
Unload - executa a retirada de memória dos controles e objetos liberados pelo evento Dispose.

Figura 2 - Seqüência de eventos em um Web Server Control
Após esta introdução de conceitos, já podemos especificar as características do controle RSS que será usado como exemplo. O padrão RSS (Really Simples Syndication) originalmente foi criado para disponibilizar e trocar notícias na web no formato XML. Sua aceitação se deu com a popularização dos blogs que adotaram o RSS como meio de gerar informações para os clientes que acessavam os seus conteúdos.
Uma descrição completa de toda a estrutura de um arquivo RSS foge ao escopo deste artigo. Ao final, o leitor poderá encontrar referência a dois artigos que tratam em detalhes este padrão. Costumamos dizer que o melhor amigo do XML é o XSL ou XSLT (XML documentSLanguage for Transforming). O arquivo XSL possibilita a transformação dos dados contidos no XML, em uma forma mais rica em estilo. Outra vantagem do XSL é sua capacidade de receber parâmetros. Em nosso controle faremos uso de ambos os recursos, recuperando as informações do grupo , transformando em uma Table HTML e controlando a quantidade de dados a serem apresentados com o uso de parâmetros.
Outro recurso interessante usado neste controle é a implementação desta transformação feita toda em memória usando a técnica de Embeding, que anexa junto com a DLL o arquivo XSL necessário. O tipo de controle será simples ou básico, uma vez que não necessitamos de outros Web Server Controls para executar a apresentação dos dados. O processo de criação se torna simplificado e centralizado no evento Render.
Concluindo a característica do controle teremos como grande atrativo a possibilidade de ver em tempo de design o que exatamente vai ter de resultado, não precisando escrever nenhuma linha de código para que o controle funcione em seu aplicativo. Todas as funcionalidades estarão disponíveis na grid de propriedades em design e utilizadas durante o processo de Render do controle.
Finalmente chegamos a parte que o desenvolvedor mais gosta: código, código e código. Para criar o controle, abra o Visual Studio e crie um um novo projeto doTipo Web Control Library conforme a Figura 3:

Figura 3 - Seleção do tipo de Projeto
Uma vez criado o projeto podemos partir para codificação dos eventos e métodos do controle. Para não tornar extenso e cansativo iremos apresentar apenas os principais trechos do código comentando cada um deles, no entanto, o código completo encontra-se para download.
Definindo a classe principal do Controle
Este controle herda a classe System.Web.UI.WebControls. Antes da definição da classe, colocamos dois atributos que informa qual será a tag usada no HTML a ser gerado baseado na informação do assembly, conforme a Listagem 1:
Listagem 1 - Definição da classe
Imports System.ComponentModel
Imports System.Web.UI
Imports System.Xml.XPath
Imports System.Xml.Xsl
Imports System.Xml
<Assembly: TagPrefix("NextStep","NextStep")>
<ToolboxData("<{0}:RssControl runat=server></{0}:RssControl>")> _
Public Class RSSControl
Inherits WebControl
...
...
Protected Overrides Sub Render(ByVal output As System.Web.UI.HtmlTextWriter)
End Sub
End Class
Criando as Propriedades
As propriedades do Web Server control são iguais a uma classe tradicional, porém foram acrescentados alguns atributos as propriedades para que a apresentação na grid e seu comportamento possam representar os dados mais complexos de uma forma amigável. Na Listagem 2 tem alguns trechos das definições das proriedades do controle:
Listagem 2 - Propriedades dos controles
Private _RSSDataSource As String = ""
<Bindable(False), Category("RSS"), DefaultValue(""), _
Editor(GetType(System.Web.UI.Design.UrlEditor), GetType(System.Drawing.Design.UITypeEditor)), _
Description("The RSS File")> _
Public Property RSSDataSource() As String
Get
Return _RSSDataSource
End Get
Set(ByVal Value As String)
_RSSDataSource = Value
End Set
End Property
...
...
Private _AlignCopyRight As HorizontalAlign = HorizontalAlign.Left
<Bindable(False), Category("RSS Appearance"), DefaultValue(GetType(HorizontalAlign), "Left"), _
Description("Align CopyRight of RSS")> _
Property AlignCopyRight() As System.Web.UI.WebControls.HorizontalAlign
Get
Return _AlignCopyRight
End Get
Set(ByVal Value As System.Web.UI.WebControls.HorizontalAlign)
_AlignCopyRight = Value
End Set
End Property
...
...
Private _ImageHeight As System.Web.UI.WebControls.Unit
<Bindable(False), Category("RSS Appearance"),
DefaultValue(GetType(System.Web.UI.WebControls.Unit), ""),Description("Height Image")> _
Public Property ImageHeight() As System.Web.UI.WebControls.Unit
Get
Return _ImageHeight
End Get
Set(ByVal Value As System.Web.UI.WebControls.Unit)
_ImageHeight = Value
End Set
End Property
Os atributos modificam e/ou definem as características de uma propriedade ou classe. A classe System.ComponentModel expõe ao desenvolvedor uma série de atributos que ajudam a organizar e apresentar os dados de uma forma mais intuitiva, contando com editores de conteúdo para os principais tipos de dados e ainda com um arsenal de métodos para se criar seu próprio editor de tipos. Veja alguns dos atributos mais utilizados:
Bindable - define se a propriedade poderá participar do evento Databind();
Category - organiza as propriedades em grupos dentro da grid de propriedades, facilitando o desenvolvedor a encontrar as propriedades de um controle que tem a mesma afinidade;
DefaultValue - define o valor default para uma determinada propriedade. Deve possuir o mesmo tipo de dado aceitando conversão de tipos, por exemplo DefaultValue(GetType(<tipo>), "<valor>"). Toda vez que ocorre uma alteração no valor é comparado o novo valor com o DefaultValue. Caso seja diferente o valor fica em negrito;
Description - usado como um help da propriedade, normalmente contendo uma breve descrição de sua funcionalidade, aparecendo na parte de baixo da grid de propriedades;
Editor - define qual o editor de tipo que será usado para apresentar os dados na grid de propriedades.
Criando o arquivo XSL de transformação.
O arquivo XSL usa a sintaxe Xpath para localizar e interagir com os nós (nodes) do arquivo XML. Uma descrição completa da sintaxe está no arquivo de download. O desenvolvedor deve estar atento também para alguns caracteres especiais cuja representação conflita com a sintaxe da estrutura do XSL, sendo necessário neste caso, representá-lo em outro formato como é o exemplo do caractere "<" que aparece na forma de "<" conforme a Listagem 3 que mostra na integra o conteúdo do arquivo XSL que será usado no projeto do controle:
Listagem 3 - Estrutura do XSL
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:param name="maxqtd"/>
<xsl:template match="rss">
<xsl:for-each select="channel/item [position() <= $maxqtd]">
<a href="{link}" target="_blank">
<xsl:value-of select="title" disable-output-escaping="yes" />
</a>
<xsl:if test="pubDate != '' ">
(<xsl:value-of select="pubDate" />)
</xsl:if>
<xsl:if test="description != '' ">
<br></br>
<xsl:value-of select="description" disable-output-escaping="yes"/>
<br></br>
<br></br>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
O arquivo XSL usando no projeto será anexado junto ao assembly do controle. Para que isso seja possível devemos alterar a propriedade Build Action para Embeded Resource, conforme a Figura 4.

Figura 4 - Alterando o Build Action
Executando a transformação em memória
A classe XML (System.Xml) fornece aos desenvolvedores diversos recursos para manipulação de dados neste formato. Utilizando-se outras classes (System.IO , Assembly e System.Text.Encoding) é possível extrair o arquivo XSL deste controle, executar a leitura do arquivo XML remoto, converter os dados em um array e por fim, executar a transformação em memória. O código responsável por esta transformação pode ser visto na Listagem 4:
Listagem 4 - código da transformação do XML
'*****************************************************
'* load xsl from resource
'*****************************************************
Dim xslt As XslTransform = New XslTransform
Dim xslArg As XsltArgumentList = New XsltArgumentList
Dim sr As System.IO.StreamReader = New System.IO.StreamReader(Me.GetType().Assembly.GetManifestResourceStream("NextStep.XslControl.xsl"))
Dim reader As XmlTextReader = New XmlTextReader(sr)
'skip 2 node of xsl
reader.Read()
reader.Read()
xslt.Load(reader, Nothing, Nothing)
reader.Close()
sr.Close()
'*****************************************************
'* setup param
'*****************************************************
Dim MaxQtd As Integer = _RSSMaxItem
If MaxQtd = 0 Then
MaxQtd = 999999
End If
xslArg.AddParam("maxqtd", "", MaxQtd.ToString)
'*******************************************************
'* Read URL with RSS and save in viewstate
'*******************************************************
Dim InfoXML As XmlDocument
If IsEnvironmentDesign() Then
If XMLData Is Nothing And _RSSDataSource <> "" Then
Try
InfoXML = New XmlDocument
InfoXML.Load(_RSSDataSource)
XMLData = InfoXML
Catch ex As Exception
_ItemTransform = ex.Message
End Try
Else
InfoXML = XMLData
End If
Else
InfoXML = XMLData
End If
'*****************************************************
'* get propertys of RSS and transform Data
'*****************************************************
Dim _Title As String = ""
Dim _TitleUrl As String = ""
...
...
If Not InfoXML Is Nothing Then
_ItemTransform = ""
Dim _ItemNode As XmlNode
For Each _ItemNode In InfoXML.GetElementsByTagName("channel").Item(0).ChildNodes
If _ItemNode.Name.ToLower = "title" Then
_Title = _ItemNode.InnerText
ElseIf _ItemNode.Name.ToLower = "link" Then
_TitleUrl = _ItemNode.InnerText
ElseIf _ItemNode.Name.ToLower = "description" Then
...
...
End If
Next
'*****************************************************
'* Exec memory Transforming
'*****************************************************
Dim Ms As New System.IO.MemoryStream
xslt.Transform(InfoXML, xslArg, Ms, Nothing)
Dim encoding As System.Text.Encoding = System.Text.Encoding.UTF8
_ItemTransform = encoding.GetString(Ms.ToArray())
Ms.Close()
End If
Executando a Render do Controle
O controle RSS por ser um Web Server Control do tipo Simples ou Básico necessita que seja codificado o método Render para representar os dados em tempo de design e/ou de execução. O processo de Render é bastante simples, consistindo na criação de uma Tabela HTML, preenchendo suas células com os dados do RSS e utilizando os valores informados nas propriedades para alterar alguns atributos da Tabela. Este processo pode ser visto no trecho do código apresentando na Listagem 5.
Listagem 5 - Renderização dos controles
'Start render
Dim RssCtr As New HtmlControls.HtmlGenericControl("Span")
RssCtr.Style.Add("border-width", Me.BorderWidth.Value)
RssCtr.Style.Add("border-style", Me.BorderStyle.ToString)
...
...
'Tag Table
Dim Tb As HtmlControls.HtmlTable
Dim TbCell As HtmlControls.HtmlTableCell
...
...
Tb.Style.Add("Color", System.Drawing.ColorTranslator.ToHtml(Me.ForeColor))
If Me.Font.Size.ToString <> "" Then
Tb.Style.Add("Font-Size", Me.Font.ToString)
End If
...
...
'End render
RssCtr.Controls.Add(Tb)
RssCtr.RenderControl(output)
Persintindo os dados entre os PostBack
No ambiente web, o quesito performance tem um peso fundamental e o desenvolvedor deve estar constantemente atento a criar as condições necessárias para atender a este quesito. Se analisarmos a operacionalidade do controle, o maior custo será encontrado na leitura do arquivo remoto contendo o XML no formato RSS. Como o Render é executado a cada requisição de postback, seria ótimo persistimos os dados, poupando o custo da leitura remota a cada postback. Neste ponto deve-se ficar atento na seqüência de eventos que a página realiza. Se observar o código na Listagem 6 notará que a persistência foi codificada no método OnPreRender (salvando o resultado em uma string em viewstate). Não seria possível fazer esta persistência no Load nem no Render do controle por um motivo simples: ambos os métodos ocorrem depois do evento Save View State.
Listagem 6 - Persistência no código
Private Function IsEnvironmentDesign() As Boolean
Return (Context Is Nothing)
End Function
Private Property XMLData() As XmlDocument
Get
If Not viewstate(Me.UniqueID & "XmlDocument") Is Nothing Then
Dim Infoxml As New XmlDocument
Infoxml.InnerXml = viewstate(Me.UniqueID & "XmlDocument")
Return Infoxml
Else
Return Nothing
End If
End Get
Set(ByVal Value As XmlDocument)
If Not Value Is Nothing Then
viewstate(Me.UniqueID & "XmlDocument") = Value.InnerXml
Else
viewstate(Me.UniqueID & "XmlDocument") = Value
End If
End Set
End Property
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
If XMLData Is Nothing And _RSSDataSource <> "" Then
Try
Dim InfoXML As New XmlDocument
InfoXML.Load(_RSSDataSource)
XMLData = InfoXML
Catch ex As Exception
_ItemTransform = ex.Message
End Try
End If
End Sub
Uma pergunta freqüente feita pelo desenvolvedor é em que ambiente se encontra o código que esta sendo executado, design ou runtime?. Na documentação não irá encontrar nenhuma propriedade ou método voltado diretamente para esta pergunta. Felizmente, os Web Server Control herdam uma propriedade chamada Context, que retorna o HttpContext associado ao objeto. Como em ambiente de design não temos HttpContext, uma simples comparação com Nothing é suficiente para determinarmos o ambiente que está sendo executado (ver a função IsEnvironmentDesign que aparece na Listagem 6).
Incluímos este tópico para mostrar algumas dicas interessantes na criação de Web Server Controls. Depois de adicionado o controle na ToolBox, aparece um ícone default associado ao controle. É mais que natural querermos que os controles que criamos tenham um ícone personalizado associado. Para que isto seja possível algumas regras devem ser seguidas:
| • | Criar um bitmap (Bitmap file) com as dimensões de 16 X 16 usando apenas 16 cores; |
| • | O nome do arquivo bitmap deve ser exatamente igual a classe que define o controle; |
| • | O arquivo criado deve fazer parte do resource (propriedade Build Action igual a Embeded Resource); |
| • | A transparência de fundo é definida pelo pixel (16x1), ou seja, o primeiro pixel da última linha; |
A seguir, um exemplo da edição do Ícone com suas propriedades, destacando o pixel que define a transparência.

Figura 5 - Características do ícone do controle.
Para consumir um Web Server Control, basta que seja feita a compilação do projeto e incluído na solução um novo projeto do tipo ASP.NET Web Application. No projeto da aplicação web deve-se inserir dentro da ToolBox a referência à DLL gerada pelo controle, clicando com o botão direito sobre a Toolbox e escolha a opção Add/Remove Itens conforme visto na Figura 6.

Figura 6 - Menu popup sobre a toolbox para adicionar um novo web server control.
Nosso controle RSS para mostrar o preview precisa que seja informado na propriedade RSSDataSource uma URL válida que retorne um arquivo RSS. Feito isso, teremos o Preview em tempo de design do controle visto na Figura 7.

Figura 7 - O RSS Control em modo de design
Neste artigo foram apresentados alguns conceitos e técnicas que envolvem a criação de Web Server Control. Existem muito mais detalhes e características que não foram citadas que fazem os Web Server Control ganharem funcionalidades poderosas. Nossa intenção ao escrever o artigo foi oferecer o fundamento inicial para a construção de um Web server Control, deixando ao leitor a criatividade de implementar melhorias no controle, como a inclusão de paginação, melhor controle da apresentação, inclusão de cachê, controle de timeout, etc.
Fernando Cerqueira (fernandocerqueira@msn.com) é MVP. Ministra treinamentos e palestras sobre .NET. Fernando é autor de diversos artigos no MSDN Brasil e faz parte da coluna "Ask de Expert" do site Linha de Código. Líder do Grupo GURJ (www.gurj.net), membro do Comitê SB da INETA e um dos responsáveis pela versão Brasileira do site INETA (www.inetabr.org).
Especificação XSL e XML
http://www.w3.org/1999/XSL/Transform
Estrutura de um Arquivo RSS
http://www.microsoft.com/brasil/msdn/Tecnologias/aspnet/RSS/Default.mspx
http://www.microsoft.com/brasil/msdn/Tecnologias/aspnet/RSS2/Default.mspx
OLHOS:
Os Web Server controls são persistidos e compilados em uma DLL. Os User Controls são persistidos em arquivo texto (ascx)
Muitos problemas enfrentados durante o desenvolvimento de um Web Server Control estão relacionados com a inobservância das sequências de eventos de uma página.
teremos como grande atrativo a possibilidade de ver em tempo de design o que exatamente vai ter de resultado, não precisando escrever nenhuma linha de código
Costumamos dizer que o melhor amigo do XML é o XSL