Forms authentication failed for the request. Reason: The ticket supplied has expired.

 administrador da rede onde trabalho me perguntou porque o Event Log do servidor Web está com várias entradas (do tipo Information), sempre com a mesma mensagem: Event message: Forms authentication failed for the request. Reason: The ticket supplied has expired.

Esta mensagem dá-se quando temos o recurso de Membership do ASP.NET 2.0 habilitado na aplicação. Há a possibilidade do cookie do FormsAuthentication ser persistido no cliente (“Lembrar Senha”), evitando assim, de que toda vez que o usuário entrar a aplicação, precisar redigitar o login e senha. Quando o usuário deseja fazer isso, o cookie é criado e, toda vez que o mesmo acessar a aplicação, o cookie é enviado do cliente para o servidor. Se o cookie já tiver expirado e o Health Monitoring estiver habilitado, automaticamente essa mensagem é logada no Event Log.

Isso é facilmente identificado quando você utiliza o Reflector, mais precisamente dentro do método privado ExtractTicketFromCookie da classe (módulo) FormsAuthenticationModule.

Livro de AJAX

Como muitos me pedem indicações sobre livros de AJAX, o Luis Abreu, autor de um dos melhores livros sobre ASP.NET 2.0, acaba de lançar um livro sobre AJAX com ASP.NET. O livro ainda é muito recente, mas acredito que a qualidade seja tão boa quanto o primeiro e, sendo assim, vale a pena adquirí-lo.

Formatando objetos utilizando IFormattable

Para o nosso cenário de exemplo, teremos dois objetos: Cliente e Cnpj. Cada cliente obrigatoriamente terá uma propriedade do tipo Cnpj. Este tipo por sua vez, implementará a interface IFormattable e deverá fornecer dois tipos de formatação: DF e PR. O primeiro significa “Documento Formatado” e retornará o valor do CNPJ formatado; já a segunda opção significa “PRefixo” e, se o usuário optar por este tipo de formatação, será retornado apenas o prefixo do CNPJ que, para quem não sabe, trata-se dos 9 primeiros dígitos. Vamos então montar a nossa arquitetura de classes que utilizaremos como exemplo:

public class Cliente
{
    private string _nome;
    private Cnpj _cnpj;

    public Cliente(string nome, string cnpj)
    {
        this._nome = nome;
        this._cnpj = new Cnpj(cnpj);
    }

    public string Nome
    {
        get
        {
            return this._nome;
        }
        set
        {
            this._nome = value;
        }
    }

    public Cnpj Cnpj
    {
        get
        {
            return this._cnpj;
        }
        set
        {
            this._cnpj = value;
        }
    }
}

public class Cnpj : IFormattable
{
    private string _numero;

    public Cnpj(string numero)
    {
        this._numero = numero;
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        if (format == "DF")
            return
                Convert.ToDouble(this._numero).ToString(@"000.000.000/0000-00");
        else if (format == "PR")
            return
                Convert.ToDouble(this._numero.Substring(0, 9)).ToString(@"000.000.000");

        return this._numero;
    }

    public override string ToString()
    {
        return this.ToString(null, null);
    }
}

Como podemos reparar, a classe Cliente tem uma propriedade chamada Cnpj do tipo Cnpj. O objeto Cnpj implementa a interface IFormattable e, dentro do método ToString, verifica qual o formato está sendo passado para ele. Baseando-se neste formato é que uma determinada formatação é aplicada ao número do CNPJ e, caso nenhuma formatação é especificada, somente o número é retornado, sem nenhuma espécie de formatação. Esse tipo de código nos permitirá fazer algo do tipo:

Cnpj c = new Cnpj("999999999999999");
Response.Write(string.Format("O documento formatado é {0:DF}.", c));
Response.Write(string.Format("O prefixo é {0:PR}.", c));
Response.Write(string.Format("O documento é {0}.", c));

// Output:

// O documento formatado é 999.999.999/9999-99.
// O prefixo é 999.999.999.
// O documento é 999999999999999.

Esse tipo de formatação torna tudo muito flexível e, podemos ainda especificar o tipo de formatação em controles DataBound, como por exemplo o GridView do ASP.NET, da mesma forma que fazemos para datas e números. As imagens abaixo ilustram isso:

 

Figura 1 – Configuração da formatação no GridView.

 

Contrato entre ASPX e ASCX

Nas versões anteriores do ASP.NET quando ainda não existia essa forma de compilação que é utilizada no ASP.NET 2.0, era facilmente possível dentro de um User Control (ASCX) invocar um método que está dentro de uma página ASPX, pois era somente fazer o cast da propriedade Page para o tipo da página e invocar o método desejado. Isso ainda tinha alguns problemas, já que o User Control nem sempre estaria em uma página que poderia ter o método implementado.

Com a nova forma de compilação do ASP.NET 2.0, fica difícil fazer o cast para o tipo da página, pois o nome somente será criado na compilação, ou seja, em design-time não conhecemos o tipo ainda. Se não estiver utilizando o Web Application Project (WAP) e quer ter essa possibilidade, então terá que criar um contrato (algo que já teria que ter feito nas versões anteriores :)) através de uma Interface. Esta Interface voce deverá implementá-la somente as páginas ASPX que deseja que o User Control invoque o(s) respectivo(s) método(s). Abaixo está o exemplo:

public interface IConnection
{
    void ExecuteProcedure();
}

public partial class _Default : System.Web.UI.Page, IConnection
public partial class Default2 : System.Web.UI.Page

[ ASCX ]
IConnection connection = this.Page as IConnection;
if (connection != null)
    connection.ExecuteProcedure();
else
    Response.Write(“The Page container isn’t a IConnection type!”);

Como podem reparar, a página Default.aspx implementa a Interface e a Default2.aspx não. Finalmente, dentro do UserControl, fazemos o cast para IConnection com o operador as que, se não for um tipo compatível retornará nulo. Se estiver utilizando o Visual Basic .NET 2005, então ao invés de as, utilize o operador TryCast, que tem a mesma finalidade.

HTTP Pipeline – Performance

Finalizando a leitura do livro do Luis Abreu, no último capítulo ele aborda o ciclo de vida de uma página ASP.NET e lembrei de um detalhe de performance que podemos aplicar nas aplicações ASP.NET com relação aos HttpModules e que implementei no site Projetando.NET.

No arquivo machine.config existe uma seção chamada httpModules onde temos vários módulos que, por padrão, estão relacionados para serem executados durante a requisição; aqui estão alguns exemplos: OutputCache, Session, FormsAuthentication, etc. Mas nem sempre esses módulos são utilizados na aplicação e, se este for o caso, podemos optar por remove-los da aplicação, efetuando a seguinte configuração no arquivo Web.Config:

<httpModules>
    <remove name=”Session” />
    <remove name=”WindowsAuthentication” />
    <remove name=”FormsAuthentication” />
    <remove name=”PassportAuthentication” />
    <remove name=”UrlAuthorization” />
    <remove name=”FileAuthorization” />
</httpModules>

Migrando Profiles

Baseando-se nisso, durante a transição de usuário anônimo para usuário autenticado é necessário transferirmos todo o profile, mais especificamente, a cesta de compras para o mesmo usuário, só que agora devidamente autenticado. Para satisfazer essa necessidade, o módulo ProfileModule (disponível no namespace System.Web.Profile) dispõe um evento chamado MigrateAnonymous, qual ocorre quando um usuário anônimo efetua o login na aplicação. É passado como parâmetro para este evento um objeto do tipo ProfileMigrateEventArgs que expõe as seguintes propriedades: AnonymousID e Context. A primeira delas, AnonymousID, retorna uma string que identifica o usuário anônimo na aplicação; já a segunda, Context, retorna a instância do objeto HttpContext que representa a requisição corrente.

Para exemplificar, suponha que existam duas propriedades (com o atributo allowAnonymous definido como True) na seção de profiles da aplicação: CompanyName e SelectedLanguage. Através do método GetProfile, informamos a identificação do usuário anônimo que desejamos recuperar, que por sua vez, é exposto pela propriedade AnonymousID do argumento ProfileMigrateEventArgs. Como esse evento é disparado depois que o usuário se autentica na aplicação, quando chamamos alguma das propriedades customizadas através da propriedade Profile, esse valor já está sendo atribuído ao usuário corrente/autenticado. Finalmente, invocamos o método estático ClearAnonymousIdentifier do módulo AnonymousIdentificationModule para remover a identificação do usuário anônimo que está associado com a sessão.

void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e)
{
    ProfileCommon anonymousProfile = Profile.GetProfile(e.AnonymousID);

    Profile.CompanyName = anonymousProfile.CompanyName;
    Profile.SelectedLanguage = anonymousProfile.SelectedLanguage;
    AnonymousIdentificationModule.ClearAnonymousIdentifier();
}

 

WebParts – Personalização e Provider

No decorrer do artigo vimos como configurar as WebParts, conectá-las, editá-las, etc.. Só que, até este momento, o principal gerenciador de tudo isso é o controle WebPartManager. Mas, até então, não sabemos como são mantidas as modificações entre as múltiplas sessões do usuário e, principalmente, como e quando esses processos acontecem. Neste capítulo veremos esses processos de “baixo-nível” um pouco mais detalhadamente, porém tudo isso já está encapsulado pela plataforma e não exige muita programação para configurá-lo. De qualquer forma, não deixa de ser interessante analisá-lo.

O primeiro conceito que temos que nos atentar se resume aos tipos de escopos de páginas possíveis: User e Shared. Veremos detalhadamente através da tabela abaixo os detalhes destes tipos de escopos:

Escopo Descrição
User

O escopo a nível de usuário significa que as mudanças e a personalização serão para um usuário específico.

Shared

O escopo compartilhado permitirá que todas as alterações feitas a nível de página sejam compartilhadas entre todos os usuários que a acessam.

Já o segundo conceito é a visibilidade dos controles. Isso irá determinar se um determinado controle está visível para um usuário ou para todos os usuários. Cada controle WebPart na página é um controle que é compartilhado, visível a todos os usuários que acessam a página; ou um controle per-user é visível somente a um usuário específico. A visibilidade é determinada de acordo com a forma que o controle é adicionado à página. Se o controle é adicionado via markup (diretamente no código HTML), ele sempre será um controle compartilhado. Se o controle é adicionado na aplicação via código ou se o usuário o seleciona em um catálogo (controles dinâmicos), a visibilidade é determinado pelo escopo corrente da personalização da página. Se a página estiver em escopo Shared, o controle dinamicamente criado será compartilhado; se a página estiver em escopo User, o controle será exclusivo daquele usuário que o criou.

O terceiro e último conceito é o escopo de propriedade. Quando você cria uma propriedade personalizada (denotada com o atribute Personalizable), você pode definir o escopo de personalização para a propriedade em questão para Shared ou User, sendo User o padrão. Isso fornecerá um controle detalhado sobre como podemos personalizar por todos os usuários e ainda podemos personalizar somente por usuários autorizados quando o escopo da página estiver definido como Shared.

Juntos, esses conceitos de escopo de personalização de página, visibilidade de controle e escopo de personalização de propriedade criam um conjunto de opções de como que os controles WebParts possam ser visualizados e personalizados pelos usuários. A tabela abaixo sumariza como estes controles se comportam quando os usuários personalizam esses controles em vários escopos:

Visibilidade de Controle Página em escopo compartilhado Página em escopo de usuário
Controle compartilhado

Um usuário autorizado pode personalizar propriedades em escopo User e Shared para todos os usuários.

No caso de controles adicionados dinamicamente, um usuário autorizado pode permanentemente fechar o controle para todos os usuários.

Já utilizando controles estáticos, eles não podem ser excluídos, embora possa ser fechado por um usuário autorizado para todos os usuários.

Usuários individuais não podem personalizar as propriedades definidas com o escopo compartilhado. Eles podem somente personalizar as propriedades definidas com o escopo de usuário.

Estes usuários podem fechar um controle compartilhado, que será adicionado à um Page Catalog, mas não poderá permanentemente excluí-lo.

Controle personalizado por usuário

Este controle não pode ser personalizado com a página em escopo compartilhado porque o controle não aparecerá toda vez na página, somente aparecerá quando a página estiver em escopo de usuário.

Usuários individualmente podem personalizar as propriedades dos controles em ambos escopos: User e Shared, porque a instância do controle é exclusiva/privada, podendo também excluir o controle permanentemente.

Depois de entendidos os conceitos, temos ainda dois componentes importantíssimos que possibilitam a personalização que são o WebPartManager e o WebPartPersonalization. O primeiro, WebPartManager, como já sabemos, gerencia todas as WebParts disponíveis na página, habilitando e gerenciando o ciclo de vida dos dados da personalização. Ele contém uma propriedade denominada Personalization, que expõe um objeto do tipo WebPartPersonalization, que disponibiliza as seguintes (auto-explicativas) propriedades: ProviderName, InitialScope e Enabled; já o WebPartPersonalization implementa a lógica necessária para realizar algumas ações referentes a personalização de “baixo-nível”.

A classe WebPartPersonalization armazena internamente um objeto do tipo PersonalizationProvider que, durante a execução, armazena uma referência ao provider especificado no arquivo Web.Config. Essa arquitetura faz parte de um padrão criado pela Microsoft denominado Provider Model, que já falamos sobre ele neste artigo. O ASP.NET fornece um provider para que os dados sejam salvos no banco de dados SQL Server. A classe concreta chama-se SqlPersonalizationProvider e, podemos ver através da imagem abaixo a hierarquia das classes, desde a abstrata ProviderBase até a classe concreta para SQL Server. Se quisermos criar um provider de personalização customizado basta herdarmos da classe abstrata PersonalizationProvider e customizarmos para o repositório desejado.

Figura 1 – Arquitetura de Provider Model para personalização.

Além das configurações que podem ser realizadas no controle WebPartManager, ainda há outras configurações, não menos importantes, a serem realizadas no arquivo Web.Config. Dentro do mesmo, mais especificamente dentro do elemento webParts, personalization há um sub-elemento chamado authorization, onde podemos especificar quais usuários (ou papéis) podem entrar e fazer parte de uma página que contém escopo compartilhado. Ainda no elemento authorization há um atributo denominado verbs, que permite definirmos dois valores:

Verbo Descrição
enterSharedScope

Indica se o usuário ou papel (role) pode acessar uma página definida com escopo compartilhado.

modifyState

Indica se o usuário ou papel (role) é capaz de modificar os dados de personalização para o escopo corrente.

Para exemplificar a configuração a ser realizada no arquivo Web.Config, analise o código abaixo. Repare que proibimos todos os usuários (deny) de entrar em um escopo compartilhado e de modificar os dados de personalização (note o atributo verbs). Depois disso, permitimos o acesso a estas funcionalidades apenas a usuários que estão contidos dentro do papel de Administradores.

<webParts>
  <personalization>
    <authorization>
      <allow roles="Administradores" verbs="enterSharedScope, modifyState" />
      <deny users="*" verbs="enterSharedScope, modifyState" />            
    </authorization>
  </personalization>
</webParts>

Definido no arquivo Web.Config os usuários ou papéis que terão as devidas permissões, podemos a qualquer momento fazer a transição entre o escopo de usuário (que é o padrão) para o escopo compartilhado. Tudo isso é possível através do método ToogleScope da classe WebPartPersonalization. Mas não podemos invocar este método sem antes verificar se o usuário corrente tem ou não permissão para entrar neste escopo compartilhado. O trecho de código abaixo ilustra esse processo de transição entre os escopos:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    if (this.WebPartManager1.Personalization.Scope == PersonalizationScope.User
        && WebPartManager1.Personalization.CanEnterSharedScope)
    {
        this.WebPartManager1.Personalization.ToggleScope();
    }
}

Como falamos acima, é importante verificar se o usuário tem ou não permissão para alternar entre os escopos. E para possibilitar essa verificação, a propriedade CanEnterSharedScope retorna um valor booleano indicando se o usuário está autorizado a entrar no escopo compartilhado.

Personalização de Propriedades

Como você já deve ter reparado, em um dos capítulos anteriores fizemos o uso de um atributo chamado PersonalizableAttribute em uma determinada propriedade de um UserControl. Esse atributo permite-nos especificar propriedades que devem ter seus valores persistidos através da personalização. Este atributo tem uma sobrecarga em seu construtor que permite informarmos um enumerador do tipo PersonalizationScope, indicando qual é o escopo da personalização. Este enumerador provê duas opções: Shared e User (padrão), acima explicadas. Abaixo é mostrado um exemplo de como criar um propriedade e denotá-la com o atributo Personalizable:

[
    WebBrowsable,
    WebDisplayName("Cor da Barra de Navegação"),
    WebDescription("Cor que aparecerá como Background da Barra de Navgegação"),
    Personalizable(PersonalizationScope.Shared)
]
public string CorBarraNavegacao
{
    get
    {
        return this.BarraNavegacao.BgColor;
    }
    set
    {
        this.BarraNavegacao.BgColor = value;
    }
}

Interface IPersonalizable

Se falarmos de personalização de WebParts, jamais poderíamos deixar de abordar a interface IPersonalizable. Esta interface fornece dois métodos e uma propriedade que nos permitirá ter um maior controle durante a persistência e o carregamento de dados a serem personalizados.

Como dissemos, esta interface nos fornece os seguintes membros: Load, Save e IsDirty. O método Load é invocado quando os dados de personalização são extraídos do repositório para a aplicação; o método Save permite-nos interceptar o processo de persistência dos dados no repositório; finalmente, a propriedade IsDirty retorna um valor booleano indicando se os dados do controle sofreram ou não alguma alteração.

Os métodos Load e Save tem em sua assinatura um parâmetro do tipo PersonalizationDictionary, que nada mais é do que uma coleção (key/value) de objetos do tipo PersonalizationEntry. Com isso podemos, em qualquer um dos momentos, acessar a coleção de dados a serem persistidos ou carregados e manipular da maneira que desejarmos. O uso desta técnica é geralmente utilizado quando não precisamos expor a propriedade com o atribute Personalizable já que o valor é definido internamente, ou seja, dentro do próprio controle. Através do exemplo abaixo, veremos como implementar esta interface em um UserControl:

private bool _isDirty;

public bool IsDirty
{
    get
    {
        return this._isDirty;
    }
}

public void Load(PersonalizationDictionary state)
{
    PersonalizationEntry entry = state["TextBoxValue"] as PersonalizationEntry;
    if (entry != null)
    {
        this.txtValor.Text = entry.Value.ToString();
    }
}

public void Save(PersonalizationDictionary state)
{
    state["TextBoxValue"] = 
        new PersonalizationEntry(
            this.txtValor.Text.Trim(), 
            PersonalizationScope.User);
}

protected void txtValor_TextChanged(object sender, EventArgs e)
{
    this._isDirty = true;
}

Analisando o código acima, podemos ver que no evento TextChanged de um determinado controle TextBox que está com a propriedade AutoPostBack definido como True, é definido True para o membro _isDirty, que é exposto pela propriedade IsDirty. O valor desta propriedade irá determinar se o método Save deverá ou não ser disparado para que possamos interceptar o processo de persistência e definirmos anexar valores na coleção fornecida como parâmetro para tal método.

Reinicializando o Estado da Página

A infra-estrutura ainda possibilita a reinicialização do estado das WebParts de uma determinada página, ou seja, permite que você volte ao ponto inicial, como ela foi inicialmente definida. Geralmente os métodos que possibilitam isso são baseados em escopos, onde você pode determinar qual dos escopos quer reinicializar. Para analisar os métodos disponíveis para reinicialização de estado, analise a classe estática PersonalizationAdministration e seus respectivos métodos.

WebParts.zip (293.75 kb)

WebParts – Segurança

Assim como era de se esperar, as WebParts também fornecem um recurso bastante flexível para tratarmos da segurança das WebParts. Pode ocorrer, em algumas situações, que determinadas WebParts estarão visíveis somente aos usuários que se enquadrarem em um determinado papel, ou ainda, somente usuários específicos poderão visualizá-las.

Para podermos manipular a segurança das WebParts, temos: o evento AuthorizeWebPart da classe WebPartManager e o argumento WebPartAuthorizationEventArgs, que manda para o evento anteriormente citado informações para a customização da autorização. Além disso, ainda há uma propriedade, não menos importante, chamada AuthorizationFilter da classe WebPart, onde definimos em cada WebPart os usuários ou os papéis que terão acesso à WebPart em questão. A propriedade AuthorizationFilter e o evento AuthorizeWebPart trabalham em conjunto, ou seja, de nada adiantará se você apenas definir a propriedade AuthorizationFilter com os papéis ou os usuários que nada acontecerá. Além disso, você precisa adicionar a lógica necessária para tratar isso no evento AuthorizeWebPart.

Antes de vermos como codificar esse evento, vamos analisar as propriedades do argumento WebPartAuthorizationEventArgs que é passado para o evento AuthorizeWebPart:

Propriedade Descrição
AuthorizationFilter

Passa para o evento o valor que está definido nesta mesma propriedade da WebPart.

IsAuthorized

É através desta propriedade que informaremos se o usuário terá ou não permissão para a visualização da WebPart. Ela recebe um valor booleano, que você deverá informar através da lógica que implementar dentro do evento AuthorizeWebPart.

IsShared

Propriedade de somente leitura que especifica se a WebPart é visível/compartilhada em todos os usuários da aplicação.

Path

Propriedade de somente leitura que retorna o path do controle, caso ele seja um User Control – ASCX.

Type

Retorna o tipo do controle que está sendo autorizado.

Depois de entendermos cada uma das propriedades do argumento WebPartAuthorizationEventArgs, vamos ver como funciona isso na prática, ou seja, como devemos efetivamente validar o usuário para que ele possa ou não visualizar a WebPart. Em princípio, voltando um pouco atrás, na arquitetura das WebParts, sabemos que um server-control que está contido no interior de uma ZoneTemplate será sempre envolvido por uma GenericWebPart e, conseqüentemente, teremos a propriedade AuthorizationFilter à nossa disposição para definirmos os filtros necessários. Com isso, podemos analisar o código abaixo que ilustra esse ponto:

<html>
<body>
    <form id="form1" runat="server">
        <asp:WebPartManager ID="WebPartManager1" runat="server">
        </asp:WebPartManager>
        <table width="100%" align="center" cellspacing="0">
            <tr>
                <td width="75%" colspan="2" valign="top">
                    <asp:WebPartZone ID="WebPartZone1" runat="server">
                        <ZoneTemplate>
                            <uc1:Photos
                                ID="Photos1"
                                runat="server"
                                Title="Galeria"
                                AuthorizationFilter="Admins,Gerentes" />
                            <asp:Label
                                ID="lblNome"
                                runat="server"
                                Text="Seja bem-vindo Sr(a). Gerente."
                                AuthorizationFilter="Gerentes" />
                        </ZoneTemplate>
                    </asp:WebPartZone>
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

Assim como podemos ver, ao fazer o uso do ASCX Photos definimos na propriedade AuthorizationFilter os papéis de Admins e Gerentes. Mas como já falamos anteriormente, somente isso não faz com que a autorização seja executada. É necessário que você crie essa validação através do evento AuthorizeWebPart, que veremos mais tarde. O idéia de colocar o controle Label é somente para ilustrar que há a possibilidade de acessar a propriedade AuthorizationFilter para um server-control sem ele tê-la explicitamente.

Finalmente temos que codificar o evento AuthorizeWebPart para concluirmos o processo de autorização de uma determinada WebPart. Como no exemplo definimos papéis na propriedade AuthorizationFilter, é justamente isso que devemos validar, ou seja, teremos que verificar se o usuário corrente está ou não contido dentro de um dos papéis especificados. O código abaixo demonstra essa verificação:

protected void WebPartManager1_AuthorizeWebPart(object sender,
    WebPartAuthorizationEventArgs e)
{
    if (!string.IsNullOrEmpty(e.AuthorizationFilter))
    {
        string[] roles = e.AuthorizationFilter.Split(',');
        e.IsAuthorized = Array.Exists(roles, delegate(string role)
        {
            return User.IsInRole(role);
        });
    }
    else
    {
        e.IsAuthorized = true;
    }
}

Observação: o código muda ligeramente entre VB.NET e C# porque o VB.NET não suporta métodos anônimos que são uma característica exclusiva do C#.

WebParts.zip (293.75 kb)

WebParts – Exportação e Importação

Um recurso bastante interessante que as WebParts fornecem é a Importação e Exportação de WebParts. Esse recurso utiliza um arquivo com extensão *.webpart onde, em seu interior, o conteúdo é XML onde teremos informações a respeito de uma determinada WebPart. Este arquivo conterá as propriedades, tipos e valores de uma WebPart.

Para efetuar a exportação, há duas formas: através da utilização do verbo Export ou através de um método chamado ExportWebPart da classe WebPartManager. Já a importação podemos fazer através do catálogo ImportCatalogPart ou também programaticamente, através do método Import da classe WebPartManager. Veremos no decorrer desta seção essas duas opções.

Exportação

Antes de mais nada, necessitamos habilitar a possibilidade de exportação de WebParts no arquivo Web.Config:

<webParts enableExport="true">

Depois desta configuração realizada no arquivo Web.Config é necessário definirmos a propriedade ExportMode das WebParts que pretendemos exportar com um dos valores fornecidos pelo enumerador WebPartExportMode que, por sua vez, fornece três opções, a saber: None (padrão), All e NonSensitiveData. O primeiro deles, None faz com que os dados de uma WebPart não sejam exportados. Já o segundo, All, como o próprio nome diz, permite que todas as propriedades, sem excessão, de uma determinada WebPart sejam exportadas e, finalmente, a opção NonSensitiveData faz com que dados personalizáveis (propriedades denotas com o atributo Personalizable e que também definem o valor True à propriedade IsSensitive) não sejam exportados. A propriedade CorBarraNavegacao que criamos anteriormente mostra como configurar, ou seja, como definir a propriedade IsSensitive para que a mesma possa ser ocultada na exportação da WebPart:

[
    WebBrowsable,
    WebDisplayName("Cor da Barra de Navegação"),
    WebDescription("Cor que aparecerá como Background da Barra de Navgegação"),
    Personalizable(PersonalizationScope.User, true)
]
public string CorBarraNavegacao
{
    get
    {
        return this.BarraNavegacao.BgColor;
    }
    set
    {
        this.BarraNavegacao.BgColor = value;
    }
}

Para exemplificar, veremos abaixo o conteúdo de um arquivo que contém a WebPart e, por motivos de testes, vou permitir a visualização da propriedade acima neste arquivo:

<?xml version="1.0" encoding="utf-8"?>
<webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
    <metaData>
      <type src="~/UserControls/Personagens.ascx" />
      <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
    </metaData>
    <data>
      <properties>
        <property name="CorBarraNavegacao" type="string" />
      </properties>
      <genericWebPartProperties>
        <property name="AllowClose" type="bool">True</property>
        <property name="Width" type="unit" />
        <property name="AllowMinimize" type="bool">True</property>
        <property name="AllowConnect" type="bool">True</property>
        <property name="ChromeType" type="chrometype">Default</property>
        <property name="TitleIconImageUrl" type="string" />
        <property name="Description" type="string" />
        <property name="Hidden" type="bool">False</property>
        <property name="TitleUrl" type="string" />
        <property name="AllowEdit" type="bool">True</property>
        <property name="Height" type="unit" />
        <property name="HelpUrl" type="string" />
        <property name="Title" type="string">Personagens</property>
        <property name="CatalogIconImageUrl" type="string" />
        <property name="Direction" type="direction">NotSet</property>
        <property name="ChromeState" type="chromestate">Normal</property>
        <property name="AllowZoneChange" type="bool">True</property>
        <property name="AllowHide" type="bool">True</property>
        <property name="HelpMode" type="helpmode">Navigate</property>
        <property name="ExportMode" type="exportmode">All</property>
      </genericWebPartProperties>
    </data>
  </webPart>
</webParts>

Analisando o arquivo XML gerado acima, podemos destacar algumas seções (elementos e atributos) do arquivo:

Elemento/Atributo Descrição
webParts

O elemeto root do arquivo. Pode existir somente um por arquivo.

webPart

Define informações do controle que será exportado.

metaData

Contém informações sobre o tipo da WebPart e também define uma mensagem de erro que será exibida aos usuários caso algum erro ocorra durante o processo de importação desta WebPart.

type

Lista os tipos, incluindo o full-name do controle e, no caso de um arquivo, terá o path até o controle.

importErrorMessage

Mensagem de erro que será exibida aos usuários caso algum erro ocorra durante o processo de importação desta WebPart.

data

Contém as propriedades e os respectivos valores.

properties

Contém as propriedades customizadas da WebPart.

genericWebPartProperties

Contém as propriedades de uma WebPart quando o controle não herda da classe WebPart.

Além desta forma declarativa, temos a possibilidade de efetuar a exportação programaticamente. Para isso, como já vimos acima, devemos utilizar o método ExportWebPart da classe WebPartManager em conjunto com um objeto do tipo XmlTextWriter, devidamente inicializado, que será responsável por recuperar e, através de algumas classes auxiliares, salvar fisicamente o conteúdo XML gerado pelo método ExportWebPart. O código abaixo mostra um exemplo:

using System.IO;
using System.Text;

//

WebPart tempWebPart = this.WebPartManager1.WebParts[2];

StringBuilder sb = new StringBuilder();
XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));

xmlWriter.Indentation = 4;
xmlWriter.Formatting = Formatting.Indented;
this.WebPartManager1.ExportWebPart(tempWebPart, xmlWriter);

using (StreamWriter writer = new StreamWriter("C:\Personagens.WebPart"))
{
    writer.Write(sb.ToString());
    writer.Close();
}

Importação

Depois da exportação realizada, precisamos saber como devemos proceder para que seja possível importarmos um determinado arquivo de WebPart para a página. Para isso também temos duas opções: através do catálogo ImportCatalogPart e programaticamente.

Como já sabemos, para adicionar um catálogo do tipo ImportCatalogPart na página é necessário incluir, primeiramente, uma zona do tipo CatalogZone que, por sua vez, serve de PlaceHolder para todos os tipos de catálogos, assim como vimos anteriormente. É importante dizer também que este catálogo somente aparecerá se o estado da página estiver definido como CatalogDisplayMode.

Depois do catálogo devidamente configurado, ao rodar a aplicação teremos o mesmo sendo exibido, pronto para ser utilizado. A partir deste momento, o usuário pode selecionar um arquivo com extensão *.WebPart, que conterá o conteúdo XML referente a WebPart a ser importada pelo controle. Quando clicar no botão Upload, o arquivo é carregado para dentro do catálogo, porém ainda não é adicionado a coleção de WebParts da página. A título de curiosidade, o arquivo não é salvo fisicamente no disco após o Upload. O ASP.NET apenas lê o seu conteúdo através de um Stream, como já foi mostrado aqui. Note na imagem abaixo os dois estágios do catálogo, ou seja, a do lado esquerdo contém o catálogo em seu estágio inicial; já a do lado direito é exibida após o upload e, como pode ver, já consta o controle que importamos do arquivo.

Figura 1 – Utilizando o catálogo ImportCatalogPart.

A outra alternativa que temos para importar uma WebPart é via programação, onde utilizamos o método ImportWebPart da classe WebPartManager. Dado um arquivo *.WebPart – XML, este método recupera o conteúdo do arquivo e gera um server control, retornando um objeto do tipo WebPart. Informamos a este método um objeto do tipo XmlTextReader, devidamente inicializado com o arquivo *.WebPart e um outro parâmetro obrigatório do tipo string, que armazenará a mensagem de erro de importação, caso ela venha a acontecer. O trecho de código abaixo ilustra o processo de importação via programação:

XmlTextReader reader = new XmlTextReader("C:\Personagens.WebPart");
string mensagemErro;
WebPart part = this.WebPartManager1.ImportWebPart(reader, out mensagemErro);

if (string.IsNullOrEmpty(mensagemErro) && part != null)
{
    // ...
}

A importação de WebParts pode trazer alguns riscos à aplicação como, por exemplo, um usuário malicioso pode incluir em uma propriedade do tipo string um script que pode ser executado durante a importação da mesma e, para evitar esse problema, todas as strings devem ser codificadas.

WebParts.zip (293.75 kb)

WebParts – Conexões

As conexões entre as WebParts permitem-nos estabelecer ligações entre elas e, conseqüentemente, gerar um conteúdo baseado em um valor que a mesma receberá como uma espécie de parâmetro. Essas ligações utilizam um conceito de produtor e consumidor. O produtor é quem disponibiliza o valor para que as WebParts possam fazer o uso; já o consumidor são as WebParts que consumirão o conteúdo e, baseando-se nele, gerará um conteúdo ou tratará da forma que achar conveniente.

Existem dois tipos de conexões: estáticas e dinâmicas. A primeira delas, estáticas, são criadas em tempo de desenvolvimento e colocadas dentro da seção StaticConnections do controle WebPartManager, ou seja, temos as conexões já pré-definidas; as conexões dinâmicas permitem ao usuário final da aplicação criar as conexões automaticamente, através de um catálogo do tipo ConnectionsZone, que veremos mais tarde, ainda nesta seção. O controle WebPartManager também é responsável por gerenciar todas as conexões, estáticas e dinâmicas.

Para que seja possível termos qualquer uma dessas conexões, precisamos definir as WebParts que serão as produtoras e as WebParts que serão as consumidoras. Para efetuarmos essa “amarração”, é necessário criar uma Interface pública em que o produtor deverá implementá-la. Essa Interface será utilizada para aqueles consumidores que estão interessados em resgatar os dados do produtor.

Podemos ver em nosso exemplo que há uma WebPart em que dentro dela há um controle do tipo DropDownList que dispõe todas as temporadas disponíveis da série 24 Horas. A idéia aqui é, selecionada uma dessas temporadas, todas as WebParts da página (DVD, Personagens e Galeria de Fotos) deverão customizar seu conteúdo baseando-se na temporada escolhida. Como já falamos acima, devemos inicialmente criar a Interface pública que será utilizada para a troca de informações:

public interface IConnection
{
    int TemporadaId { get; }
}

Com a Interface criada precisamos, neste momento, customizar o produtor e os consumidores. Inicialmente, devemos implementar essa Interface dentro da WebPart que disponibilizará o conteúdo. Como temos um WebUserControl (ASCX) que disponibiliza a temporada selecionada, vamos implementar essa Interface nele e expor o valor selecionado do controle DropDownList. O resultado dessa implementação ficará da seguinte forma:

public partial class UserControls_Episodios : 
    System.Web.UI.UserControl, IConnection 
{
    // removido para poupar espaço

    public int TemporadaId
    {
        get
        {
            return Convert.ToInt32(this.ddlTemporadas.SelectedValue);
        }
    }

    [ConnectionProvider("Identificação da Temporada")]
    public IConnection GetInterfaceConnection()
    {
        return this;
    }
}

Analisando o código acima, podemos reparar que além da propriedade TemporadaId exposta pela Interface também precisamos criar um método denotado com o atributo ConnectionProvider que retornará uma referência do produtor para que o controle WebPartManager possa extrair os dados e, conseqüentemente, mandar aos consumidores do mesmo.

Com o produtor finalizado, precisamos, neste momento construir o(s) consumidor(es) que fará uso do valor exposto pelo produtor. Nas WebParts, que serão as consumidoras desses valores, apenas deverá conter um método, denotado com o atributo ConnectionConsumer e, como parâmetro obrigatório, deverá receber um elemento do tipo IConnection, que é a Interface utilizada para estabelecer a conexão. A implementação é mostrada através do código abaixo onde, depois de verificado se não é uma instância nula, passamos o Id da temporada para se efetuar uma busca na base de dados para retornar o DVD correspondente:

public partial class UserControls_DVDs : 
    System.Web.UI.UserControl
{
    // removido para poupar espaço

    <ConnectionConsumer("Identificação da Temporada")]
    public void DefineTemporada(IConnection temporada)
    {
        if(temporada != null)
        {
             this.FindDVDsInDB(temporada.TemporadaId);
        }
    }
}

Conexões Estáticas

Como falamos um pouco acima, a conexão estática é definida no interior da seção StaticConnections do controle WebPartManager. Para exemplificar essa conexão, abaixo iremos configurar o controle WebPartManager para possibilitar a conexão entre as WebParts que configuramos acima:

<asp:WebPartManager ID="WebPartManager1" runat="server">
    <StaticConnections>
        <asp:WebPartConnection 
            ID="Conexao1" 
            ProviderID="Episodios1" 
            ConsumerID="DVDs1" />
        <asp:WebPartConnection 
            ID="Conexao2" 
            ProviderID="Episodios1" 
            ConsumerID="Personagens1" />
    </StaticConnections>
</asp:WebPartManager>

Como podemos ver, definimos nos atributos ProviderID e ConsumerID o ID do produtor e consumidor respectivamente. Neste tipo de conexão, tudo é feito de forma automática, ou seja, assim que o valor for alterado, automaticamente o ASP.NET notifica os consumidores para que eles possam se adequar de acordo com o valor selecionado.

Conexões Dinâmicas

As conexões dinâmicas permitem ao usuário final da aplicação criar as conexões quando achar conveniente, ou seja, ao invés de você deixar as conexões explícitas no código, dentro da seção StaticConnections do controle WebPartManager, você adiciona na sua página um controle do tipo ConnectionsZone. É importante lembrar que mesmo nas conexões dinâmicas é necessário a criação do produtor e consumidor acima descritos.

Um outro ponto importante é que esta zona/controle somente estará disponível quando a propriedade DisplayMode do controle WebPartManager estiver definida como ConnectDisplayMode, como é mostrado na seção de Manipulação e Configuração. Depois do controle adicionado à página e a propriedade DisplayMode devidamente configurada, quando rodarmos a aplicação um verbo chamado Connect (Imagem 1) estará disponível para as WebParts produtoras e consumidoras. Sendo assim, ao clicar em algum deles, o controle ConnectionsZone será exibido para você optar em qual WebPart quer se conectar.

Figura 1 – Verbo Connect.

Quando o controle ConnectionsZone é exibido são exibidas duas possibilidades, onde uma delas permite criar a conexão para um produtor e a outra permite criar a conexão para um consumidor. Quando clicamos no verbo Connect de um determinado consumidor são listados dentro do controle ConnectionsZone todos produtores em que o consumidor pode se conectar; já quando clicamos no mesmo verbo, agora de um produtor, teremos a disposição no controle ConnectionsZone todos os consumidores que podem conectar-se a este produtor. A imagem abaixo mostra o passo-à-passo de como gerar uma conexão entre o consumidor e o produtor:

Figura 2 – Gerando as conexões a partir do controle ConnectionsZone.

Depois de gerada a conexão, o controle ConnectionsZone exibe a lista das conexões efetuadas, como é mostrado na imagem abaixo:

Figura 3 – Conexões geradas.

 WebParts.zip (293.75 kb)