Revista MSDN Magazine Edição 04 - Crie seu Weblog com ASP.NET, JavaScript e OleDB (Parte II)

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (1)  (0)

Artigo Originalmente Publicado na MSDN Magazine Edição 04

msdn04_capa.JPG

Clique aqui para ler todos os artigos desta edição

 

Crie seu Weblog com ASP.NET, JavaScript e OleDB (Parte II)

por Marco Bellinaso

Esta é a segunda parte do artigo sobre Weblog, onde você verá como selecionar o blog em um período de datas, trabalhar com janelas de calendário pop-up, inserir comentários e administrar o blog.

 

Selecionando um Intervalo e Carregando o Blog

Na primeira parte foi abordada a definição de conteúdo de página no arquivo ASPX, mas não foi analisado nenhum código que realmente carregue o conteúdo do blog. Para permitir que o usuário selecionasse um intervalo de um único dia, de uma semana ou de todo o mês, foi utilizado o controle Calendar com a propriedade SelectionMode definida como DayWeekMonth. É boa idéia fornecer caixas de texto para as datas de início e fim desejadas pelo usuário, caso ele queira selecionar as últimas duas semanas ou os últimos 45 dias, por exemplo. A Figura 1 mostra os novos controles adicionados à página com a semana toda selecionada no Calendar.

 

image001.gif

Figura 1 Intervalos

 

Se, devido a um postback, a página não for carregada, você deve selecionar um intervalo padrão, por exemplo, a última semana. No entanto, no caso de blogs atualizados com muita freqüência, poderá ser mais apropriado carregar os dados para menos dias e, no caso de blogs raramente atualizados, para todo o mês. Assim como no recurso de notificação de comentários, a melhor opção é deixar a escolha para o administrador do blog, usando uma chave personalizada no arquivo web.config que permita especificar o número de dias para o intervalo padrão. O código a seguir mostra como a chave personalizada é lida a partir do arquivo, feito o Parse para Integer e usada para calcular um intervalo para os últimos n dias, bem como a maneira como o intervalo é destacado no calendário:

 

Private Sub Page_Load(...) Handles MyBase.Load

    If Not IsPostBack Then

        Dim defPeriod As Integer = Integer.Parse( _

            ConfigurationSettings.AppSettings("Blog_DefaultPeriod"))

        Dim fromDate = Date.Today.Subtract(New TimeSpan(defPeriod -_

            1,0,0,0))

        BlogCalendar.SelectedDates.SelectRange(fromDate, Date.Today)

        BindData()

    End If

End Sub

 

A data de início é calculada subtraindo-se n-1 dias da data de hoje. A chamada para BindData carrega os dados para o intervalo selecionado e os vincula ao Repeater e a seus controles internos. Esse método chama o método GetData da classe de negócio do Blog desenvolvido anteriormente (parte I) e passa as datas de início e fim da data selecionada no calendário, que são lidas a partir da coleção SelectedDates:

 

Private Sub BindData()

    Dim ds As DataSet = m_BlogManager.GetData( _

        BlogCalendar.SelectedDates(0), _

        BlogCalendar.SelectedDates(_

        BlogCalendar.SelectedDates.Count - 1))

    Blog.DataSource = ds.Tables("Messages").DefaultView

    Blog.DataBind()

End Sub

 

Caso tenha sido selecionado um dia, a coleção SelectedDates terá apenas um item e as datas de início e fim serão iguais. Quando o usuário clica no calendário, a página é enviada novamente (é feito o postback), o novo intervalo é automaticamente selecionado e você manipula o evento SelectionChanged do calendário para que ele chame outra vez o BindData para o novo intervalo. Por fim, você deve manipular o evento Click do botão Load para selecionar o intervalo personalizado especificado no calendário e carregar os dados do blog. No procedimento deste evento, é feito o parse no conteúdo dos dois controles de entrada e são obtidas duas datas. Embora seja possível adicionar validadores no final do procedimento para assegurar que o formato de data esteja correto antes de enviar o formulário, a análise poderia gerar uma exceção se o formato de data não fosse válido. Nesse caso, foi utilizada a data atual (veja a Listagem 1).

 

Listagem 1 Carregando as datas

Private Sub LoadBlog_Click(...) Handles LoadBlog.Click

    Dim fromDate, toDate As Date

    Try

        fromDate = Date.Parse(IntervalFrom.Text)

    Catch

        fromDate = Date.Today

    End Try

    ' Faça o mesmo para o toDate...

    ' Se toDate for anterior a fromDate, defina toDate = fromDate

    If toDate < fromDate Then toDate = fromDate

    BlogCalendar.SelectedDates.SelectRange(fromDate, toDate)

    BlogCalendar.VisibleDate = toDate

    BindData()

End Sub

 

Observe o uso do VisibleDate do calendário (para assegurar que a data final esteja visível no calendário). Isso é necessário porque, se o usuário selecionasse um período de dois meses anteriores, a seleção não estaria visível no calendário. Em vez disso, o calendário mostraria o mês atual, o que não seria muito claro.

 

Calendário Pop-up

Na configuração atual, se o usuário quiser selecionar um intervalo diferente de um único dia, semana ou mês, ele precisará digitar manualmente a data de início e de fim nas duas caixas de texto e fazer referência a um calendário externo. Além disso, ele poderá digitar a data em um formato inválido. Por esses motivos, é boa prática oferecer um calendário pop-up. Quando o usuário clicar em uma data, o calendário será fechado e a data aparecerá no controle textbox da janela principal. Esse recurso pode ser facilmente reproduzido em ASP.NET por meio do controle Calendar e de um pouco de JavaScript no lado cliente.

Primeiro, vamos cuidar do código ASPX da janela-pai. Adicione um link em uma imagem que abre a janela pop-up por meio de uma chamada à seguinte função JavaScript:

 

   

 

O procedimento JavaScript espera receber o nome do controle textbox que será preenchido com a data selecionada, juntamente com a largura e a altura da janela do calendário pop-up que será aberta. Veja aqui o código JavaScript que deverá entrar na seção já definida no canto superior da página:

 

function PopupPicker(ctl,w,h)

{

    var PopupWindow=null;

    settings='width='+ w + ',height='+ h;

    PopupWindow=window.open('DatePicker.aspx?Ctl=' +

        ctl,'DatePicker',settings);

    PopupWindow.focus();

}

 

Esse código usa window.open para abrir a janela pop-up com uma dimensão especificada, não-ajustável, sem nenhuma barra de rolagem, menu, barra de ferramentas ou barra de status. O primeiro parâmetro é a URL da página a ser carregada na nova janela; no código mostrado, ele carrega uma página DatePicker.aspx com um parâmetro ctl, cujo valor é passado como uma entrada para o código PopupPicker.

Agora que terminamos a página principal, é hora de escrever a página DatePicker.aspx que processa o calendário. Essa página possui um controle Calendar cujas propriedades Width e Height estão definidas como 100%, a fim de que cubra a página inteira. Outro item importante a ser observado no arquivo ASPX é o código JavaScript no lado cliente, que pega uma seqüência de caracteres como entrada e a utiliza como valor para o controle de entrada do formulário-pai e cujo nome é passado na seqüência de caracteres de consulta (query string). Por fim, ele fecha o formulário pop-up propriamente dito. O código JavaScript pode ser visto na Listagem 2.

 

Listagem 2 Definindo a data

function SetDate(dateValue)

{

    // pega na querystring o valor do parâmetro ctl,

    // ou seja, o nome do controle de entrada do formulário-pai

    // que o usuário deseja definir com a data selecionada

    ctl = window.location.search.substr(5);

    // define o valor do controle com a data fornecida

    thisForm =

      window.opener.document.forms[0].elements[ctl].value = dateValue;

    // fecha esta pop-up

    self.close();

}

 

Quando o usuário clicar em um link no controle Calendar, em vez do processamento normal que submete o formulário e seleciona o dia clicado, faremos com que seja invocado o procedimento JavaScript personalizado. Por padrão, todos os links processados pelo controle Calendar geram um postback para o servidor. Em vez disso, vamos fazer com que eles apontem para o código SetDate personalizado. Graças ao evento DayRender do Calendar (acionado a cada vez que um dia é processado e o qual fornece uma referência para a célula da tabela que está sendo criada), alterar a saída padrão das células de tabela que contêm os links para o dia é muito fácil. O fragmento de código a seguir substitui o conteúdo da célula padrão por meu próprio controle de hyperlink, que possui o mesmo texto mas aponta para o código JavaScript:

 

Private Sub DatePicker_DayRender(...) Handles DatePicker.DayRender

    Dim hl As New HyperLink()

    hl.Text = CType(e.Cell.Controls(0), LiteralControl).Text

    hl.NavigateUrl = "javascript:SetDate('" & _

        e.Day.Date.ToShortDateString() & "');"

    e.Cell.Controls.Clear()

    e.Cell.Controls.Add(hl)

End Sub

 

O valor passado para o código JavaScript é a data do dia selecionado, em formato abreviado (geralmente mm/dd/aa). Esse valor será usado para o controle de entrada no formulário-pai. A Figura 2 mostra a janela pop-up resultante.

 

image002.gif

Figura 2 Calendário Pop-up

 

Como você pode ver, os controles do servidor (Server Controls) ASP.NET podem ser altamente personalizados. Outra situação em que você poderá usar o evento DayRender dessa maneira consiste no redirecionamento para outra página, com a data incluída na seqüência de caracteres de consulta (query string) - em vez de redirecionar para a segunda página no servidor após um postback com a data. Para fazer isso, simplesmente substitua a linha na qual você define a propriedade NavigateUrl do hyperlink para algo semelhante ao seguinte:

 

hl.NavigateUrl = "SecondPage.aspx?Date=" & e.Day.Date.ToShortDateString()

 

 

Postando Comentários

Na Figura 2 da parte I do artigo (edição número 3 - janeiro/2004), você viu que, abaixo de cada mensagem, existe um link "Post your own comment" (Publique seu próprio comentário). Quando o usuário clica nesse link, é exibida uma caixa com controles de entrada que permite a inclusão de comentários. Você pode adivinhar como ela funciona porque usamos a mesma técnica para criar a lista de comentários. A caixa Comments com seus controles de entrada é declarada dentro de uma DIV cujo estilo de exibição é inicialmente definido como "none", para que ela não esteja visível. Quando se clica nesse link, o estilo é alterado e a página é rolada até o canto inferior até que se torne visível. Para evitar o envio de códigos HTML desnecessários, que tornariam a página mais lenta, é definida uma única caixa de comentários no canto inferior da página (em vez de uma caixa para cada mensagem). A Figura 3 mostra a caixa de comentários e os controles de entrada necessários.

 

image003.gif

Figura 3 Postando um Comentário

 

Como você especifica a mensagem para a qual o comentário é postado? Uma boa solução é armazenar a ID da mensagem-pai em um textbox hidden ASP.NET para quando o usuário clicar no link "Post your own comment". Quando o botão Post (Enviar) for pressionado, esse valor poderá ser recuperado do codebehind. Observe que você não pode usar a propriedade Visible para ocultar o controle, porque quando Visible estiver definido como False o controle será oculto e o código HTML não será enviado para o cliente. Você precisa usar o mesmo estilo de exibição utilizado para a DIV. A DIV e o controle textbox são declarados desta forma:

 

  

  

    runat="server" />

 

Observe que também foi usada uma âncora para assegurar que a caixa de comentários estará realmente visível quando a página for longa e a mensagem que o usuário desejar comentar estiver na parte superior da página. O link é declarado desta forma:

 

Post your own comment

 

A rotina ShowCommentBox JavaScript recebe a ID da mensagem a ser comentada e a utiliza como o valor do controle textbox oculto recém-declarado:

 

function ShowCommentBox(msgID)

{

    document.forms[0].ParentMessageID.value = msgID;

    ShowCommentBox2();

}

 

O código que realmente torna a caixa de comentários visível e rola a página para baixo está localizado em uma rotina separada (o código ShowCommentBox2). Chamaremos esse código novamente quando quisermos mostrar a caixa de comentários sem configurar o atributo de valor do controle textbox oculto:

 

function ShowCommentBox2()

{

    CommentBox.style.display = "";

    window.location.href = '#CommentBoxAnchor';

}

 

Agora, só nos resta criar o clique no botão Post (Enviar) para chamar o InsertComment da instância Business.Blog e vincular novamente os dados atualizados ao Repeater:

 

Private Sub PostComment_Click(...) Handles PostComment.Click

    m_BlogManager.InsertComment(Integer.Parse(ParentMessageID.Text), _

        Author.Text, Email.Text, Comment.Text)

    BindData()

 

    ' redefina o valor dos controles de entrada

    ParentMessageID.Text = ""

    ' redefina as outras textbox visíveis...

End Sub

 

Administrando o Blog

O código para as tarefas do usuário está quase completo. Os usuários podem ler as mensagens relacionadas ao intervalo selecionado e postar comentários. O proprietário do blog, por outro lado, precisa adicionar, inserir e excluir as mensagens e comentários diretamente no banco de dados. Para permitir esse acesso, a próxima etapa será desenvolver uma página de login e modificar a página principal do blog de modo que, quando o administrador estiver conectado, a página mostre os controles adicionais que permitem executar operações administrativas. A página de login é composta de caixas de texto para o nome e a senha do usuário, uma caixa de seleção para a opção "persistent login" (login persistente) e um botão de envio. Como você terá apenas um administrador e não precisará de segurança por função, armazenar as credenciais no arquivo web.config será suficiente. A Listagem 3 mostra a classe codebehind. Se as credenciais especificadas forem válidas, ele autenticará o usuário e o redirecionará para a página Default.aspx.

 

Listagem 3 Classe Codebehind da Página de Login

Imports System.Web.Security

 

Public Class Login

  Inherits System.Web.UI.Page

 

  ' Web Form Designer Generated Code here...

 

    Protected UserName As System.Web.UI.WebControls.TextBox

    Protected Password As System.Web.UI.WebControls.TextBox

    Protected Persistent As System.Web.UI.WebControls.CheckBox

    Protected WithEvents LoginUser As System.Web.UI.WebControls.Button

    Protected InvalidLogin As System.Web.UI.WebControls.Label

 

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As _

        System.EventArgs) Handles MyBase.Load

        If Not Page.IsPostBack Then

            ' se a querystring contiver um parâmetro "Action=logout", faça logout

            ' e exclua o cookie

            If Request.Params("Action") = "logout" Then

                FormsAuthentication.SignOut()

            End If

        End If

    End Sub

 

    Protected Sub LoginUser_Click(ByVal sender As System.Object, _

        ByVal e As System.EventArgs) Handles LoginUser.Click

        ' verifique o nome de usuário e senha

        If FormsAuthentication.Authenticate(UserName.Text, Password.Text)      

        Then

            ' se ok, salve o cookie

            FormsAuthentication.SetAuthCookie(UserName.Text, _

                Persistent.Checked)

            ' redirecione para Default.aspx

            Response.Redirect("Default.aspx", True)

        Else

            ' se as credenciais estiverem erradas, mostre a mensagem de erro

            InvalidLogin.Visible = True

        End If

    End Sub

End Class

 

Na página Default.aspx, adicionamos os controles de edição, que só estarão visíveis se o usuário for autenticado. Na parte superior da página, declare um painel (panel) com um link "logout" que aponte para Login.aspx com o parâmetro "action=logout" na seqüência de caracteres de consulta (query string), e uma tabela com as caixas de texto para especificar o título e o conteúdo da mensagem. Esse painel poderá ser exibido ou oculto no Page_Load, da seguinte forma:

 

MessageBox.Visible = User.Identity.IsAuthenticated

 

Quando o administrador preenche as caixas de texto e clica no botão Post (Enviar), é acionado o evento Click no lado servidor. Utilizamos a função InsertMessage para adicionar a nova mensagem ao banco de dados e chamar o BindData para carregá-la no Repeater.

Agora, é hora de adicionarmos a funcionalidade de edição. Para fazer isso, adicione um controle LinkButton ao ItemTemplate do Repeater:

 

    

 CommandName="Edit" CommandArgument='<%# Container.DataItem("MessageID")

       %>'

 Visible=''/>

 

O link só estará visível se o usuário tiver sido autenticado (ele funciona da mesma forma que no painel MessageBox, com a diferença de que, aqui, você o exibe ou oculta por meio de uma expressão data binding  no arquivo ASPX). A propriedade CommandArgument contém a ID da mensagem a ser editada, mas você também deve especificar CommandName como "Edit" porque existe outro LinkButton para excluir a mensagem, e é preciso ter certeza de qual dos dois botões foi selecionado. O código do evento ItemCommand do Repeater é mostrado na Listagem 4.

 

Listagem 4 Tratando Blog_ItemCommand

Private Sub Blog_ItemCommand(...) Handles Blog.ItemCommand

    Dim msgID = Integer.Parse(e.CommandArgument)

    If e.CommandName = "Edit" Then

        Dim msgTitle, msgText As String

        m_BlogManager.GetMessageData(msgID, msgTitle, msgText)

        MessageID.Text = msgID

        Title.Text = msgTitle

        Message.Text = msgText

    Else

        m_BlogManager.DeleteMessage(msgID)

        BindData()

    End If

End Sub

 

Primeiramente, o código recupera a ID da mensagem passada com a propriedade CommandArgument do botão selecionado. Em seguida, o administrador decide se deseja editar ou excluir a mensagem, de acordo com o CommandName do botão. Quando ele mostrar "Edit," os dados atuais da mensagem especificada serão recuperados e usados para preencher as caixas de texto do MessageBox, de modo que o administrador veja o texto atual e possa editá-lo. Quando clicar no botão Post (Enviar), se a textbox MessageID estiver vazia, significa que o administrador está postando uma nova mensagem; caso contrário, a textbox conterá a ID da mensagem que estiver sendo editada. Veja a seguir parte do evento Click:

 

Private Sub PostMessage_Click(...) Handles PostMessage.Click

    If Not User.Identity.IsAuthenticated Then _

        Response.Redirect("Login.aspx", True)

        If MessageID.Text.Trim().Length > 0 Then

            m_BlogManager.UpdateMessage(Integer.Parse(MessageID.Text), _

                Title.Text, Message.Text)

        Else

            m_BlogManager.InsertMessage(Title.Text, Message.Text)

        End If

        ' redefina as textbox para uma seqüência vazia e chame BindData...

    End If

End Sub

 

As funcionalidades Add New e Edit foram concluídas (a Figura 2 da parte I deste artigo na edição número 3 - janeiro/2004, mostra como ficará a aparência da página no modo administrator). Apenas mais um lembrete importante sobre a funcionalidade Delete. Na configuração atual, se o administrador clicar por engano no link Delete, a mensagem será imediatamente excluída, já que não é solicitada nenhuma confirmação. Adicionar uma caixa de diálogo pop-up de confirmação é simples — ela só precisa de alguns códigos JavaScript em resposta ao evento onClick do hyperlink. Isso é feito durante a criação do link, adicionando-se uma entrada à coleção Attributes do controle — ou seja, quando o item do Repeater for criado. Só precisamos tratar o evento ItemCreated do Repeater para os eventos pares e ímpares, obter uma referência ao Delete LinkButton e adicionar a caixa pop-up de confirmação do JavaScript:

 

Private Sub Blog_ItemCreated(...) Handles Blog.ItemCreated

    If e.Item.ItemType <> ListItemType.AlternatingItem AndAlso _

    e.Item.ItemType <> ListItemType.Item Then Exit Sub

    Dim lnkDelete As LinkButton = CType( _

        e.Item.FindControl("DeleteMessage"), LinkButton)

    lnkDelete.Attributes.Add("onclick", _

        "return confirm('Tem certeza de que deseja excluir esta" & _

        "mensagem?');")

End Sub

 

Se o administrador clicar em Cancel (Cancelar), o código JavaScript retornará false, a página não será enviada e a mensagem não será excluída.

A edição e a exclusão de comentários são implementadas da mesma forma, por isso não será necessário explicar tudo em detalhes novamente. No entanto, você encontrará a implementação completa no download do código deste artigo. Há apenas alguns detalhes que merecem ser explicados aqui. Sempre que precisei tratar um evento do Repeater, usei a palavra-chave Handle do Visual Basic® .NET, que permite associar um método ao evento de uma instância de controle declarada com o WithEvents. O mesmo já não pode ser feito com o DataList interno nos comentários porque ele é criado dinamicamente em tempo de execução e não existe nenhuma variável de controle WithEvents para ele. Porém, você pode especificar os tratadores (handlers) de evento diretamente na declaração do controle, conforme mostrado a seguir:

 

    OnItemCreated="Comments_ItemCreated"

    OnEditCommand="Comments_EditCommand" ...>

 

Outro pequeno detalhe é que, quando o administrador clica no link Edit de um comentário, a caixa de comentários deve ser exibida e a página rolada até em baixo, para assegurar que esteja visível. Essa ação foi executada anteriormente para o link "Post your own comment" (Publique seu próprio comentário), mas naquele caso a rotina JavaScript estava diretamente associada ao link e não era necessária nenhuma ida e volta ao servidor. Aqui, na página é feito primeiro o posted back para pré-preencher as caixas de texto de edição com o comentário, e a rotina JavaScript é chamada quando a página é reenviada para o navegador do cliente. Para fazer isso, foi enviado um código JavaScript para o cliente, o qual apenas chama a rotina ShowCommentBox2 escrita antes, desta forma:

Sub Comments_EditCommand(...)

    ' preencha as caixas de texto com a data atual para o comentário clicado

    •••

    Dim script As String = _

        " ShowCommentBox2(); "

    Me.RegisterStartupScript("ShowEditCommentBox", script)

End Sub

 

O RegisterStartupScript emite o bloco de JavaScript especificado imediatamente antes de a tag ser fechada no lado servidor da página, garantindo assim que a CommentBox já tenha sido criada (caso contrário, você poderia obter um erro de referência incorreta quando o container CommentBox não fosse encontrado).

 

Validação de Vários Formulários Virtuais

Sempre que houver textboxes ou outros controles de entrada, recomenda-se adicionar controles de validação, a fim de garantir que seja fornecido um valor e que este esteja dentro do formato e do intervalo apropriados. Neste aplicativo, você precisará executar diferentes validações, de acordo com a ação que o usuário desejar efetuar. Se o usuário pressionar o botão Post (Enviar) para enviar um comentário, você precisará assegurar-se de que ele tenha fornecido o nome e o texto do comentário. Se o usuário clicar no botão Load Blog, você precisará verificar se os formatos das datas de início e fim estão válidos. Ainda não é hora de adicionar os validadores porque precisamos solucionar um último problema.

Temos três "formulários virtuais" com controles de entrada: a caixa de comentários, a nova caixa de mensagens e a caixa de seleção de intervalo. Em uma página ASP.NET, você só pode ter um formulário no lado servidor. Isso significa que todos os controles de entrada, validadores e botões de envio estão no mesmo formulário. Depois que o usuário preencher corretamente as caixas de texto do intervalo e clicar no botão de envio, os validadores do controle textbox validarão a entrada. Os validadores das caixas de mensagens e comentários impedirão que no formulário seja o postback quando suas caixas de texto não contiverem um valor ou este valor não tiver sido informado corretamente.

Para solucionar o problema, os botões ASP.NET padrão foram substituídos por um controle personalizado desenvolvido por James M. Venglarik (o código deste controle está disponível para download no site da revista - Venglarik.exe). Esse controle cria botões que desativam uma lista especificada de validadores por meio de alguns códigos JavaScript no lado cliente e, dessa forma, possibilita o uso de botões que, antes do envio da página, validem alguns controles (mas não outros). Uma vez feita a referência ao controle na página, veja como declarar o botão Load Blog:

 

    NoFormValList="RequireAuthor,RequireComment,ValidateEmailFormat" />

 

A propriedade NoFormValList especifica uma lista de validadores separados por vírgula que deverão ser desativados quando clicados, que neste caso são todos os validadores das caixas de texto no comentário e nas caixas de mensagens.

 

Conclusão

O aplicativo criado neste artigo está agora totalmente funcional. Você poderá carregá-lo em seu servidor para escrever seu próprio blog ou poderá vê-lo ao vivo, em http://www.bytecommerce.com/blog. Os recursos de relacionamento do DataSet e a habilidade de criar DataLists e DataGrids aninhados facilitam o processamento de relatórios Master-Detail, o que, em última instância, é o que você faz neste aplicativo de blog. A flexibilidade dos template controls permite que você crie praticamente qualquer layout. Você pode agrupar vários itens de dados em tabelas e personalizar o comportamento da implementação padrão, como fez ao adicionar a caixa pop-up de confirmação para os botões Delete. Os controles combinados com um pouco de JavaScript no lado cliente, para documentar suas ações e agrupá-las, contribuem para um aplicativo em ASP.NET completo e rico em recursos.

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?