domingo, 3 de junho de 2007

Criando componentes para paginas ASP.Net

Hoje vou falar sobre a criação de componentes ASP.Net, a minha proposta e criar um componente para o cadastro de datas, que tenham 3 DropDowns um para o dia outro pra a data e outro para o ano, que uma coisa relativamente comum que se acha por ai, já que muitos não gostam do componente de calendário do ASP.Net devido ao espaço que ele ocupa, e também por achar que este componente que iremos criar e bastante didático para aprender este tipo implementação, então vamos ao trabalho.

Primeiramente abre o Visual Studio e crie um novo projeto escolha a linguagem que você mais gosta, no exemplo vou usar o VB.Net, escolha windows e selecione Web Control Library. Eu chamei o componente de DropDownDate.

Neste momento você deve ter um arquivo com um código de exemplo de controles parecido com este.


Imports System

Imports System.Collections.Generic

Imports System.ComponentModel

Imports System.Text

Imports System.Web

Imports System.Web.UI

Imports System.Web.UI.WebControls

<defaultproperty("Text"), ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")> _

Public Class DropDownDate

Inherits WebControl

<bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Text() As String

Get

Dim s As String = CStr(ViewState("Text"))

If s Is Nothing Then

Return String.Empty

Else

Return s

End If

End Get

Set(ByVal Value As String)

ViewState("Text") = Value

End Set

End Property

Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)

output.Write(Text)

End Sub

End Class

Vamos alterar alguns destes parâmetros para adaptá-lo ao nosso projeto, primeira coisa que vemos após os imports e uma decoração da classe que indica qual e a propriedade default do componente, que será a primeira a abrir no intelisense, a decoração também define o nome da tag html do nosso componente. O componente que estamos propondo ira retornar uma data e não um texto e o nome também não estar bom então vamos corrigir isto. Eu vou chamar a propriedade default de value já que a idéia e que seja retornado o valor corrente selecionado no componente e a tag vai ser DropDownDate, o código deve ficar assim.

<defaultproperty("Value"), ToolboxData("<{0}:DropDownDate runat=server> </{0}:DropDownDate>")>_
Public
Class DropDownDate

Após a herança estar à declaração de uma propriedade Text como exemplo, já que e a propriedade mais popular entres os componentes mas no nosso caso ela não nos importa, então vamos altera-la para termos nossa propriedade value, o código deve ficar como mostrado a baixo,observe os comentários.

<bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Value() As Date

Get

' Aqui estaremos recuperando o valor que foi gravada na viewstate

' o ctype e por que a viewstate só armazena texto

Dim s As Date = CType(ViewState("Value"), Date)

Return s

End Get

Set(ByVal Value As Date)

'Aqui estamos gravando o valor de value na viewstate,

'a nome da variavel na viewstate pode ser qualquer texto,

'mas e bom que tenha o mesmo nome da propriedade que ela mapeia

ViewState("Value") = Value.ToString

End Set

End Property

Como você pode ver o valor das propriedades são quardadas na viewstate que é um campo hide que o ASP.Net gera na pagina, para guardar o estado dos objetos, não podemos usar variaveis privadas como e o mais comum, já que a instancia do objeto e refeita a cada rederização de pagina o que ocorre a cada postback, se usarmos variáveis privadas perderemos os valores.

Continuando o método RenderContents, e onde aconte a rederização do conteudo, é onde determinamos o que nos queremos que tenha no nosso componente.A baixo segue o codigo com os comentarios, em seguida algumas explicações.

Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)

Dim Options As String ="<</option>" 'Um template dos options só para não ficarmos escrevendo toda hora.

Dim Selecionado As String = String.Empty

output.RenderBeginTag(HtmlTextWriterTag.Div) 'Definemos o div como a tag de rederização do componente.

'Escrevendo o DropDown dos Dias

output.Write("¨<select name=""" & MyClass.ClientID & "$dia"">")

Dim i As Short
For i = 1 To 31
If i = MyClass.Value.Day Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, i.ToString))
Next
output.Write("</select>")


'Escrevendo o DropDown do Mes

output.Write("<select name=""" & MyClass.ClientID & "$mes"">")
'Estaciamos um CultureInfo, baseado na cultura corrente da aplicação
'isto posibilitara que os meses sejam preenchidos no edioma para a qual a pagina esta preparada
Dim Cultura As New CultureInfo(CultureInfo.CurrentCulture.ToString)
For i = 1 To 12
If i = MyClass.Value.Month Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, Cultura.DateTimeFormat.GetAbbreviatedMonthName(i)))
Next
output.Write("
</select>")

'Escrevendo a construção do DrowpDown do Ano

output.Write("<select name=""" & MyClass.ClientID & "$ano"">")
For i = Date.Now.Year - 18 To Date.Now.Year + 100
If i = MyClass.Value.Year Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, i.ToString))
Next
output.Write("</select>")

output.RenderEndTag()

End Sub

Alguns pontos são importante de serem distacados, um deles e que neste metodo você pode escrever qualquer tipo de html, javascript , css etc.. em fim qualquer coisa que você escreveria normalmente em uma pagina, embora exista algumas regras para os conteudos dinamicos que não vem ao caso agora por não serem do escorpo deste artigo.No exemlo estamos escrevendo as tags "<select> para a contrução de nossos dropdowns, outra coisa fundamental são os nomes dados aos dropdowns, eles serão filhos de nossos componente, O ASP.Net no momento que ele gera o HTML da pagina ele garante que cada componente terá um nome único na arvore DOM da pagina.isto e obtido pelo ClientID do componente que e a sua identificação junto ao cliente, que não obrigatoriamente terá o mesmo nome do servidor, e no caso de componentes filhos por padrão o ClientID e composto com o nome do componente pai mas o nome do filho separados por $, isto garante que não haverá conflitos com os nomes, caso você tenho por exemplo dois componentes iguais na pagina, portanto respeitar esta regra e muito importante para não termos problemas na recuperação dos valores. Por ultimo gostaria de destacar esta linha output.RenderBeginTag(HtmlTextWriterTag.Div), ela endica que o conteudo ficara entre uma taque

, eu acho interesante por uma questão de organização do codigo html resultante, facilitara uma eventual programação cliente que venha a ser usada em nosso componente.

Com isto o componente já esta praticamente pronto, na verdade se você o compilar agora e o por em um projeto já tera o visual dele em sua pagina, mas se vc tiver a curiosidade de fazer isto vera que nada acontece se você fizer um post.Isto por que falta o mas importente.

Temos que implementar em nosso componente a interface IPostBackDataHandler, esta interface implementa os dois metodos nescesarios para que seja dado um endereço ao nosso postback pelo .Net FrameWork.você deve ir logo a baixo da estrução Inherits para implementar a inteface escreva como no codigo demostrado e aperte o enter.

Public Class DropDownDate
Inherits WebControl
Implements IpostBackDataHandler

Presionando o enter os metodos LoadPostData e RaisePostDataChangedEvent devem ser inseridos a pagina por conta da interface.Agora deixa eu explicar o que eles fazem, o metodo LoadPostData e uma função que permite que você recupere os valores que foram enviados pela pagina que foi submetida ao servidor. Ela tem um retorno booleano que funciona da seguinte forma,se o falor for verdadeiro o metodo RaisePostDataChangedEvent sera disparado, se for falso ele não será chamado.Dito isto iremos explicar agora o que faz RaisePostDataChangedEvent para que você possa escolher se o retorno sera verdadeiro ou falso.O RaisePostDataChangedEvent e o metodo onde se espera que se concentre as chamadas dos eventos de seu componente, normalmente você vera uma sere de ifs chamando os eventos na ordem que o criador do componente definil como resposta dos eventos.Iremos implementar um evento para responder a mudança da data como exemplo até o final do artigo. Por hora temos que fazer o registro de nosso componente no postback da pagina onde ele sera usado. Se não ele vai continuar sem fazer nada.isto e simples todo webcontrol tem um metodo page que uma ligação com a pagina proprietaria do objeto. Então e só chamar o metodo de registro desta pagina, e informa o nosso componente, faremos isto ao inicializar o componente no metodo init, neste metodo inicializamos tudo que for nescesario para o funcionamento do componente então também iremos inicializar o valor de value, veja o codigo.

Private Sub DropDownDate_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init

Page.RegisterRequiresPostBack(Me)

ViewState("Value") = Date.Now.Date.ToString

End Sub

Vamos agora implementar o LoadPostData,que e feito como mostrado a seguir.

Public Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean Implements System.Web.UI.IPostBackDataHandler.LoadPostData

Dim stdia As String = postCollection(MyClass.ClientID & "$dia")

Dim stmes As String = postCollection(MyClass.ClientID & "$mes")

Dim stano As String = postCollection(MyClass.ClientID & "$ano")

MyClass.Value = CType(stdia & "/" & stmes & "/" & stano, Date)

Return false 'Isto sera mudado quando criarmos o evento

End Function

O que fazemos aqui e muito simples, o parametro postCollection é uma coleção do que foi postado, ele tem todos os objetos de formulario que fazem parte do DOM da pagina HTML resultante.Note que usamos a mesma formula para nomear as nossas dropdowns quando a criamos para recuperar os seus valores.E o atribuimos a nossa propriedade value, no ciclo de vida do componente o proximo passo sera executar o RaisePostDataChangedEvent se o retorno de LoadPostData for verdadeiro o que não vai acontecer já que o definimos como falso temporariamente, e depois executar o RenderContents, assim o componente sera rederizado com os valores atualizados.

Para fins ditaticos iremos criar um evento como exemplo,primeiro temos que criar uma forma de controlar se aquele evento aconteceu ou não, no nosso controle nem seria nescesario já que so temos um evento, mas se tivesemos varios, nem todos seriam disparados, somente os que tivessem sofrido a ação a eles associada.O que queremos e saber se a data mudou ou não para dispararmos no nosso evento. Vamos criar então uma propriedade booleana chamada DataDiferente que por padrão sera false, o atributo interno dela sera _DataDiferente.

Private _DataDiferente As Boolean = False

Private Property DataDiferente() As Boolean
Get
Return _DataDiferente
End Get
Set(ByVal value As Boolean)
_DataDiferente = value
End Set

End Property

Na minha opinião o melhor lugar para controlarmos se a propriedade value mudou ou não e justamente no momento em que ela esta sendo alterada então vamos fazer uma pequena mudança na propriedade value para fazermos este controle.

<Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Value() As Date

Get

' Aqui estaremos recuperando o valor que foi gravada na viewstate

' o ctype e por que a viewstate só armazena texto

Dim s As Date = CType(ViewState("Value"), Date)

Return s

End Get

Set(ByVal Value As Date)

'Aqui estamos gravando o valor de value na viewstate,

'a nome da variavel na viewstate pode ser qualquer texto,

'mas e bom que tenha o mesmo nome da propriedade que ela mapeia

MyClass.DataDiferente = Not MyClass.Value = Value

ViewState("Value") = Value.ToString

End Set

End Property

Assim, nos já temos o nosso controle se a tada foi alterada ou não,vamos agora mudar o LoadPostData.

Public Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean Implements System.Web.UI.IPostBackDataHandler.LoadPostData

Dim stdia As String = postCollection(MyClass.ClientID & "$dia")

Dim stmes As String = postCollection(MyClass.ClientID & "$mes")

Dim stano As String = postCollection(MyClass.ClientID & "$ano")

MyClass.Value = CType(stdia & "/" & stmes & "/" & stano, Date)


'Agora estamos chamando a propriedade e
'verificando se ouve mudaça na data

Return MyClass.DataDiferente

End Function

Agora vou enfeitar um pouco o que não seria nescesario, mas para ficarmos mas proximos da arquitetura da linguagem vamos criar uma classe de argumento de eventos especifica de nosso componente.Não e nada complicado, e que normalmente os eventos retornam no sender o objeto que disparou o evento, é no e argumentos do evento informações deste objeto, iremos criar um que tenha a data antiga e a nova data. Logo antes do fim da classe escreva o seguinte codigo a classe de argumento de eventos do nosso componente deverar ficar dentro da classe do componente.

Class DropDownDateEventArgs

Inherits EventArgs


'Estes atributo são friend para serem vistos
'pelo componente mas não fora dele.

Friend _DataNova As Date

Friend _DataAntiga As Date

Public ReadOnly Property DataAntiga() As Date

Get

Return _DataAntiga

End Get

End Property

Public ReadOnly Property DataNova() As Date

Get

Return _DataNova

End Get

End Property

End Class

Devemos declarar agora o evento, normalmente fazemos isto no inicio do programa proximo ao nome da classe

Public Event MudouData(ByVal Sender As Object, ByVal e As DropDownDateEventArgs)

Vamos querer enviar no evento a data antiga então teremos que quardala,vamos criar uma propriedade para isto.a chamaremos de DataAntiga, neste caso eu vou criala privada e não pretendo mantela, então não a quardei na viewstate, caso ache interesante não tem nada que o impessa de fazer isto.

Private _DataAntiga As Date

Private Property DataAntiga() As Date

Get

Return _DataAntiga

End Get

Set(ByVal value As Date)

_DataAntiga = value

End Set

End Property

E onde nos vamos fazer a atribuição da data?, assim como para saber se a data mudou o melhor momento e quando a nova data e atribuida, tambem cosidero que o melhor momento para quardar a antiga seja este também, então faremos uma ultima alteração na propriedade value.

<Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Value() As Date

Get

' Aqui estaremos recuperando o valor que foi gravado na viewstate

' o ctype e por que a viewstate só armazena texto

Dim s As Date = CType(ViewState("Value"), Date)

Return s

End Get

Set(ByVal Value As Date)

'Aqui estamos gravando o valor de value na viewstate,

'a nome da variavel na viewstate pode ser qualquer texto,

'mas e bom que tenha o mesmo nome da propriedade que ela mapeia

MyClass.DataDiferente = Not MyClass.Value = Value

'A data antiga e guardada quando mudada.

If MyClass.DataDiferente Then

MyClass.DataAntiga = MyClass.Value

End If

ViewState("Value") = Value.ToString

End Set

End Property

Respeitando a arquitetura .Net, todo evendo e chamado por um metodo que tem o mesmo nome do evento com o prefixo On portando vamos criar o metodo OnMudouData que ira chamar o nosso evento, e retorna o valor de DataDiferente para falso novamente sinalizando que nosso resposta a mudança de data já foi disparada.

Public Sub OnMudouData(ByVal Sender As Object, ByVal e As DropDownDateEventArgs)

MyClass.DataDiferente = False

RaiseEvent MudouData(Me, e)

End Sub

Estamos chegando ao fim do nosso componente, agora so falta implementarmos o RaisePostDataChangedEvent, o codigo dele também e muito simples como pode ser visto.

Public Sub RaisePostDataChangedEvent() Implements System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent

If MyClass.DataDiferente Then

Dim Arg As New DropDownDateEventArgs

Arg._DataAntiga = MyClass.DataAntiga

Arg._DataNova = MyClass.Value

OnMudouData(Me, Arg)

End If

End Sub

Neste momento o componente já esta 100% funcional, mas ainda tem um detalhe só para dar um acabamento ao componente,va até a decoração da classe e acresente o atributo DefaultEvent, e configure o nosso recem criado evento,desta forma quando for dado um duplo click no componente sera criado o codigo para o uso do evento de nosso componente. O codigo deve ficar assim

<"Value"), DefaultEvent("MudouData"), ToolboxData("<{0}:DropDownDate runat=server></{0}:DropDownDate>")> _Public Class DropDownDate

Pronto, o nosso componente esta acabado, agora e só criar um novo projeto web site para testalo, faça a referencia a dll do componente que ele ira aparecer na tool bar como qualquer componente ASP.Net

Chegamos ao final deste artigo espero que ele tenha sido util, este componente pode ser melhorado e muito mas isto eu vou deixar para você acredito que pelo menos os pontos principais foram todos cobertos, o codigo completo do componente vai ficar em outro post

Um abraço e até

Codigo do Componente DropDownDate

Imports System

Imports System.Collections.Generic

Imports System.ComponentModelImports System.Text

Imports System.Web

Imports System.Web.UI

Imports System.Web.UI.WebControls

Imports System.Globalization

<defaultproperty(), DefaultEvent("MudouData"), ToolboxData("<{0}:DropDownDate runat=server></{0}:DropDownDate>")> _

Public Class DropDownDate

Inherits WebControl

Implements IPostBackDataHandler

Private _DataDiferente As Boolean= False
Private _DataAntiga As Date

Public Event MudouData(ByVal Sender As Object, ByVal e As DropDownDateEventArgs)
<Bindable(True), Category("Appearance"),DefaultValue(""), Localizable(True)>Property Value() As Date
Get
' Aqui estaremos recuperando o valor que foi gravada na viewstate
' o ctype e por que a viewstate só armazena texto
Dim s As Date = CType(ViewState("Value"), Date)
Return s
End
Get

Set(ByVal Value As Date)
'Aqui estamos gravando o valor de value na viewstate, 'a nome da variavel na viewstate pode ser qualquer texto, 'mas e bom que tenha o mesmo nome da propriedade que ela mapeia
MyClass
.DataDiferente = Not MyClass.Value =Value
If MyClass.DataDiferente Then
MyClass.DataAntiga = MyClass.Value
End If
ViewState("Value") = Value.ToString
End Set
End Property

Private
Property DataAntiga() As Date
Get
Return _DataAntiga
End Get
Set(ByVal value As Date)
_DataAntiga =value
End Set
End Property

Private
Property DataDiferente() As Boolean
Get
Return _DataDiferente
End Get
Set(ByVal value As Boolean)
_DataDiferente =value
End Set
End Property

Protected
Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
'Um template dos options só para não ficarmos escrevendo toda hora.
Dim Options As String = "<option {0}>{1}</option>"
Dim Selecionado As String= String.Empty

'Definemos o div como a tag de rederização do componente.
output.RenderBeginTag(HtmlTextWriterTag.Div)

'Escrevendo o DropDown dos Dias
'Nome do DropDown do Dia no Cliente

output.Write("<select name=""" & MyClass.ClientID & "$dia"">") 'Nome do DropDown do Dia no Cliente
Dim i As Short
For i = 1 To 31
If i = MyClass.Value.Day Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, i.ToString))
Next
output.Write("</select>")

'Escrevendo o DropDown do Mes
'Nome do DropDown do Mes no Cliente

output.Write("<select name=""" & MyClass.ClientID & "$mes"">")
'Estaciamos um CultureInfo, baseado na cultura corrente da aplicação
'isto posibilitara que os meses sejam preenchidos no ediama para a qual a pagina esta preparada
Dim Cultura As New CultureInfo(CultureInfo.CurrentCulture.ToString)
For i = 1 To 12
If i = MyClass.Value.Month Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, Cultura.DateTimeFormat.GetAbbreviatedMonthName(i)))
Next
output.Write("</select>")

'Escrevendo a construção do DrowpDown do Ano
'Nome do DropDown do Ano no Cliente

output.Write("<select name=""" & MyClass.ClientID & "$ano"">")
For i = Date.Now.Year - 18 To Date.Now.Year + 100
If i = MyClass.Value.Year Then
Selecionado = "selected"
Else
Selecionado = String.Empty
End If
output.Write(String.Format(Options, Selecionado, i.ToString))
Next
output.Write("</select>")
output.RenderEndTag()
End Sub

Private Sub DropDownDate_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
Page.RegisterRequiresPostBack(Me)
ViewState("Value") = Date.Now.Date.ToString
End Sub

Public Function LoadPostData(ByVal postDataKey As String,ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean Implements System.Web.UI.IPostBackDataHandler.LoadPostData

Dim
stdia As String = postCollection(MyClass.ClientID & "$dia")
Dim stmes As String = postCollection(MyClass.ClientID & "$mes")
Dim stano As String = postCollection(MyClass.ClientID & "$ano")
MyClass.Value = CType(stdia & "/" & stmes & "/" & stano, Date)
Return MyClass.DataDiferente
End Function

Public Sub RaisePostDataChangedEvent() Implements System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent
If MyClass.DataDiferente Then
Dim Arg As New DropDownDateEventArgs
Arg._DataAntiga = MyClass.DataAntiga
Arg._DataNova = MyClass.Value
OnMudouData(Me, Arg)
End If
End
Sub

Public Sub OnMudouData(ByVal Sender As Object, ByVal e As DropDownDateEventArgs)

MyClass
.DataDiferente= False
RaiseEvent MudouData(Me, e)
End Sub

Class
DropDownDateEventArgs
Inherits EventArgs
Friend _DataNova As Date
Friend _DataAntiga As Date
Public ReadOnly Property DataAntiga() As Date
Get
Return _DataAntiga
End Get
End Property
Public ReadOnly Property DataNova() As Date
Get
Return _DataNova
End
Get

End Property
End Class
End Class