msdn09_capa.JPG

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

 

Personalização em ASP.NET 1.1

por Dino Esposito

Os aplicativos personalizáveis permitem que os usuários definam suas preferências de interface de usuário (UI) e armazenem essas configurações. Os aplicativos desktop geralmente armazenam as preferências do usuário em um arquivo local (arquivo XML ou de configuração) ou no Registro. Por outro lado, os aplicativos Web geralmente usam cookies para armazenar dados específicos ao usuário. Cookies são basicamente uma coleção de valores-chave baseados em texto enviada e recebida através de HTTP. Existem algumas desvantagens no uso de cookies — especificamente, no limite de tamanho (8 KB), na capacidade de o usuário recusar cookies e na natureza inerentemente insegura do texto sem formatação (embora você possa criptografar ou codificar o valor do cookie). Manter os dados personalizados no servidor é uma opção melhor, mas também tem seus problemas.

Analisarei algumas extensões ASP.NET 1.1 que fornecem recursos de personalização ao runtime atual. A API que pretendo descrever é inspirada na API de personalização do ASP.NET 2.0, mas não é uma cópia. A API de personalização do próximo ASP.NET 2.0, codename "Whidbey," não é excessivamente complexa mas é complexa demais para ser analisada em detalhes aqui.

 

Projetando uma camada de personalização

A camada de código que pretendo construir será destinada a trabalhar com um aplicativo que empregue um mecanismo de autenticação para autorizar seus usuários. No ASP.NET 1.1, você pode escolher entre vários modos de autenticação, tais como Passport e forms authentication. Se a camada de personalização detectar um usuário anônimo, ela não fará nada. O ASP.NET 2.0 fornecerá um recurso para lidar com situações em que tanto usuários anônimos como autorizados possam ser admitidos em um site.

A arquitetura da camada de personalização consiste principalmente em um módulo HTTP que precisa ser instalado e configurado pelo aplicativo. O módulo associa alguns eventos no nível do aplicativo — AuthorizeRequest e EndRequest — para carregar e salvar dados pessoais. A Figura 1 ilustra a arquitetura da API.

 

image001.gif

Figura 1 Minha API de personalização

 

Processar uma solicitação ASP.NET envolve várias etapas, e cada uma delas é caracterizada por um evento do aplicativo. Todos os eventos acionados durante o processamento de uma solicitação são associados com o objeto HttpApplication e não podem ser tratados pelo código em nível de página. Este código é apenas parte do ciclo de solicitação; conseqüentemente, nenhum código na página pode associar eventos do aplicativo. Para tratar eventos do aplicativo no nível do sistema, você precisa escrever um módulo HTTP.

O evento AuthorizeRequest é acionado quando a solicitação foi autenticada e a identidade por trás dela foi autorizada. Neste momento, o módulo usa o nome do usuário atual como um índice no data store — um banco de dados Microsoft® Access ou SQL Server™ — e recupera todas as informações pessoais relacionadas a ele. Essas informações deverão ser disponibilizadas de alguma maneira para o código da página. No ASP.NET 1.1, isso pode ser feito por meio da coleção Items do objeto HttpContext. No ASP.NET 2.0, foi adicionada uma nova propriedade à classe HttpContext, denominada Profile. A coleção Items da classe HttpContext é fornecida para que você possa preenchê-la com dados definidos pelo usuário. Programar a coleção Items é praticamente idêntico a programar o estado Session ou o objeto Cache:

 

Context.Items["BackColor"] = "gainsboro";

string color = (string) Context.Items["BackColor"];

 

Uma instância da classe HttpContext viaja com a solicitação durante todo o percurso e pode ser usada para compartilhar informações entre os módulos HTTP e a página. Assim, tudo que o módulo de personalização escreve no contexto é acessível à página e vice-versa. A página pode recuperar dados pessoais do contexto, processá-los e, se necessário, registrar as alterações. Quando o evento EndRequest é disparado, o módulo de personalização coleta os dados da coleção Items e os envia para a mídia.

Minha API oferece suporte tanto a um banco de dados Access como ao SQL Server para enviar dados específicos ao usuário, mas você pode usar qualquer data store. O ASP.NET 2.0 vai mais além, tornando o provedor de dados genérico e ocultando-o através de uma interface de provedor (provider interface). Você pode escrever (ou usar) qualquer componente e fazê-lo funcionar com qualquer tipo de armazenamento, desde que ele exponha os métodos de uma interface específica. No build do PDC, o ASP.NET 2.0 fornece provedores internos para Access (o padrão) e SQL Server. É importante notar que, se for usada uma mídia baseada em arquivo (como o Access), você precisará assegurar que o processo de trabalho (worker process) do ASP.NET possui as permissões apropriadas para gravar em um sistema de arquivos do Web server e, especificamente, no arquivo de dados. Você pode conceder essas permissões a um arquivo ou pasta usando a ferramenta cacls.exe, que é um componente padrão do sistema operacional Windows®. Você usa a seguinte linha de comando, substituindo [user] pelo nome da conta de usuário real:

 

cacls.exe [path] /E /G [user]:F

 

No caso de aplicativos ASP.NET, a conta de usuário precisará coincidir com a conta do processo que opera fisicamente no bando de dados — geralmente, o ASPNET ou NetworkService se você usar o Windows Server™ 2003. A mesma atribuição de permissões também pode ser obtida selecionando-se a guia Security na caixa de diálogo Properties do arquivo de banco de dados do Access.

 

O módulo http de personalização

Os módulos HTTP são classes que implementam a interface IHttpModule para tratar os eventos de runtime. Além dos eventos de sistema acionados pelo objeto HttpApplication, o módulo também pode tratar eventos acionados por outros módulos HTTP. Por exemplo, o módulo session state, um dos módulos ASP.NET internos, fornece serviços de estado de sessão para um aplicativo. Ele aciona os eventos End e Start que podem ser tratados por outros módulos através das assinaturas Session_End e Session_Start familiares. Os módulos HTTP têm uma funcionalidade semelhante à funcionalidade dos filtros ISAPI — DLLs Win32® que estendem IIS — porém com um modelo de programação muito mais simples, orientado a objetos. Os módulos HTTP filtram os dados brutos dentro de uma solicitação e são configurados de acordo com o aplicativo dentro do arquivo web.config. Todos os aplicativos ASP.NET herdam vários módulos HTTP de sistema, configurados no arquivo machine.config.

A interface IHttpModule define apenas dois métodos — Init e Dispose. Init inicializa o módulo e o registra para tratar os eventos de aplicativo relevantes. O método Dispose desfaz-se dos recursos usados pelo módulo. As tarefas que você geralmente realiza dentro deste método incluem a liberação de conexões do banco de dados e o tratamentos de arquivos. Os métodos da interface IHttpModule têm as seguintes assinaturas:

 

void Init(HttpApplication app);

void Dispose();

 

O método Init recebe uma referência ao objeto HttpApplication que está atendendo à solicitação. Você usa essa referência para preparar seu aplicativo para receber eventos do sistema. O objeto HttpApplication também apresenta uma propriedade denominada Context que fornece acesso às propriedades intrínsecas do aplicativo ASP.NET. Desse modo, você ganha acesso aos objetos Response, Request e Session. Você encontrará detalhes da implementação do modulo na Listagem 1.

 

Listagem 1 Módulo HTTP de personalização

namespace MsdnMag

{

  public class PersonalizationModule : IHttpModule

  {

    public void Init(HttpApplication app)

    {

      app.AuthorizeRequest += new EventHandler(OnEnter);

      app.EndRequest += new EventHandler(OnLeave);

    }

 

    public void Dispose()

    {

    }

 

    private void OnEnter(object sender, EventArgs e)

    {

      HttpContext ctx = ((HttpApplication) sender).Context;

      LoadPersonalData(ctx);

    }

 

    private void OnLeave(object sender, EventArgs e)

    {

      HttpContext ctx = ((HttpApplication) sender).Context;

      SavePersonalData(ctx);

    }

 

    private void LoadPersonalData(HttpContext ctx)

    {

      if (!ctx.Request.IsAuthenticated)

         return;

 

      // Capture o nome do usuário autorizado atual

      string currentUser = ctx.User.Identity.Name;

 

      // Recupere as informações de personalização do usuário

      // e armazene-as na coleção Context.Items

      RetrievePersonalInfo(ctx, currentUser);

    }

       

    private void SavePersonalData(HttpContext ctx)

    {

      // Capture o nome do usuário autorizado atual

      string currentUser = ctx.User.Identity.Name;

 

      // Armazene as informações de personalização do usuário

      StorePersonalInfo(ctx, currentUser);

    }       

 

    // Mais código aqui...

    •••

  }

}

 

O método OnLeave trata o evento AuthorizeRequest. Ele captura uma referência ao contexto HTTP atual e verifica se a solicitação foi autenticada. Se a solicitação for anônima, o tratamento retorna imediatamente. Caso contrário, o método recupera o nome do usuário atual por meio da propriedade Identity no objeto User intrínseco do ASP.NET:

 

string currentUser = ctx.User.Identity.Name;

 

O nome de usuário é usado como chave para recuperar o registro específico no banco de dados de personalização que contém as preferências daquele usuário. Quando a solicitação estiver prestes a terminar, o registro do banco de dados referente àquele usuário será atualizado. Conforme mencionado, depois que a solicitação é autorizada, o módulo copia todos os dados recuperados na coleção Items do contexto da solicitação. No mesmo local, o módulo pega os dados que serão persistidos de volta à mídia de storage quando a atualização foi atendida e a operação estará então prestes a ser concluída. Vamos avançar mais um pouco e examinar as características da camada de storage dos dados e ver como os programadores podem definir por meio de declaração a estrutura dos dados de personalização.

 

A mídia de storage dos dados

No ASP.NET 2.0, o programador cria uma nova seção no arquivo web.config para definir o perfil do usuário. O perfil de usuário é uma classe que herda da classe HttpPersonalizationBase. A nova seção contém entradas que definem o nome e o tipo das propriedades personalizadas que serão adicionadas à classe. O módulo de personalização do ASP.NET 2.0 analisa o arquivo web.config do aplicativo, calcula a estrutura final da classe de personalização e emite seu código-fonte. Por fim, esse código-fonte é compilado dinamicamente em uma assembly e uma instância da classe de personalização é anexada à propriedade Profile, que faz sua estréia na classe HttpContext do Whidbey. Por exemplo, este script de configuração adiciona uma propriedade BackColor do tipo Color:

 

code01.jpg

 

A classe de perfil de usuário do aplicativo que contém o script anterior em seu arquivo web.config se parece com o seguinte pseudocódigo:

 

public class HttpPersonalization : HttpPersonalizationBase

{

   public Color BackColor;

}

 

O mecanismo geral é praticamente idêntico à compilação dinâmica das páginas ASP.NET. Se o assembly de personalização ainda não existir, será criado quando a primeira solicitação for feita ao aplicativo. O uso de um assembly compilado dinamicamente oferece uma dupla vantagem —é mais rápido e fornece acesso de tipificação forte aos dados de personalização.

O mecanismo de personalização analisado neste artigo é mais simples e fornece apenas acesso de tipificação fraca aos dados. Em outras palavras, o módulo de personalização lê os atributos de personalização desejados no arquivo web.config do aplicativo, mas não cria uma classe a partir deles. Uma vez recuperado, cada valor é copiado na coleção HttpContext.Items, que é uma instância da classe Hashtable. Como tal, ele é uma coleção não-tipificada de objetos. Isso fornece maior flexibilidade, mas traz custos de overhead ao runtime.

Para estender o ASP.NET 1.1 com personalização, você precisa executar duas etapas: recuperar as informações personalizadas do arquivo web.config e movê-las de/para a mídia de storage. No Whidbey, a seção é construída no novo esquema de configuração; no ASP.NET 1.1, você precisa adicioná-la através de um handler de seção personalizado. Veja a seguir um exemplo do esquema que criei para a seção personalizada:

 

code02.jpg
 

Abaixo do nó , você primeiro localiza um nó com um filho . O conteúdo do nó indica o tipo de mídia de storage que você planeja usar. Ele reconhece apenas dois valores: access e sqlserver. Repare que o código-fonte não faz uma verificação rígida no nome do data store. Ele procura a string "sqlserver"; fora isso, ele usa como padrão um banco de dados do Access.

Em seguida, você encontra uma seqüência de nós , cada um com pelos menos um nó-filho e possivelmente alguns outros itens. Caso não tenha percebido ainda, esse esquema coincide com o formato XML padrão de um DataSet que contém duas tabelas — Provider e Property. A tabela Provider tem um registro em uma coluna: datastore. A tabela Property possui vários registros, compostos de duas colunas — Name e Default. Como é uma seção personalizada no ASP.NET 1.1, você precisa fornecer um handler personalizado para ela que possa ser conectada ao mecanismo de leitura de configuração interno. Um handler de seção personalizado é simplesmente uma classe que implementa a interface IConfigurationSectionHandler. O código-fonte do handler personalizado que estou usando aqui é mostrado na Listagem 2.

 

Listagem 2 Handler de seção personalizado

using System;

using System.Data;

using System.Xml;

using System.Configuration;

using System.Web.Configuration;

 

namespace MsdnMag

{

  public class PersonalizationHandler : IConfigurationSectionHandler

  {

    // Construtor(es)

    public PersonalizationHandler()

    {

    }

   

    // IConfigurationSectionHandler.Create

    public object Create(object parent, object context, XmlNode section)

    {

      // Crie um DataSet temporário

      DataSet ds = new DataSet();

 

      // Leia esta seção no DataSet

      // (presumindo que a seção represente o form XML normal

      // de um DataSet—o que você obteria de WriteXml)

      XmlNodeReader nodereader = new XmlNodeReader(section);

      ds.ReadXml(nodereader);

 

      // Por design, você tem apenas uma tabela

      return ds

    }

  }

}

 

Para registrar um handler de seção personalizado para uma seção personalizada, use o script a seguir:

 

code03.jpg

 

O atributo name na seção deve coincidir com o nome do nó-raiz da seção real no arquivo web.config. O framework ASP.NET usará a classe especificada (no exemplo, a classe MsdnMag.MyHandler da assembly MsdnWebKit) para processar a sub-árvore XML e passar o objeto resultante para o aplicativo que chama. Nesse caso, o objeto resultante seria um objeto DataSet (consulte a Listagem 2). Cada registro contido na tabela Property faz referência a uma propriedade de personalização a ser armazenada na coleção Context.Items. Os nomes e valores existentes são recuperados de um banco de dados Access ou SQL Server conforme ditado pela tabela Provider.

 

Lendo os dados de personalização

Quando a solicitação é autorizada, o código da Listagem 3 é executado para recuperar as informações guardadas no banco de dados para o usuário atual. Para minimizar o overhead da E/S, todas as informações são agrupadas em um único objeto collection que é serializado e desserializado conforme necessário. O banco de dados auxiliar contém três campos: UserID (o campo-chave), PropertyValue e LastUpdatedData. UserID contém o nome do usuário autenticado. PropertyValue armazena a coleção de propriedades e valores, e LastUpdatedData contém a hora em que ocorreu a última atualização. Esse mesmo padrão — dados de coleção serializados para um stream exclusivo de dados — é usado pelo ASP.NET 1.1 para armazenar o estado de sessão em aplicativos fora de processo (out-of-process) e pelo ASP.NET 2.0 para armazenar os dados de personalização.

Em condições ideais, você usaria a serialização binária e, possivelmente, tipos simples (para minimizar a quantidade de dados serializada e, em última instância, aprimorar a performance da operação). No entanto, um banco de dados Access não pode aceitar um campo binário, então codifiquei o array de bytes no Base64 antes de armazená-lo no banco de dados. Note, no entanto, que essa escolha é totalmente arbitrária. Se você usar o SQL Server, esse problema não existirá e você poderá efetivamente armazenar os bytes em um campo BLOB. No entanto, em minha implementação eu não faço distinção entre o Access e o SQL Server e sempre codifico em Base64 os bytes gerados pelo processo de serialização binário.

O esquema de serialização dos dados de personalização para um formato de texto é um bom plano se você usa o Access (o que não é recomendado para aplicativos de empresa na Internet). O ASP.NET 2.0 usa um novo tipo de objeto formatador que gera sua saída para o XML. Assim, por que não usar a classe XmlSerializer no ASP.NET 1.1? Você poderia fazer isso, mas lembre-se de que o serializador XML é uma ferramenta de Web service, que não foi projetada para serialização de objetos. Como resultado, ela não oferece suporte a dicionários nem à classe DataTable, e ambas são úteis aqui. A classe SoapFormatter é a sua única opção interna no ASP.NET 1.1 para serializar um objeto baseado em .NET para texto. Infelizmente, ela produz um texto realmente desnecessário e sem formatação. É por isso que escolhi a codificação Base64 e a razão por que o ASP.NET 2.0 é necessário para definir um novo formatador baseado em texto.

O esquema que escolhi para o banco de dados auxiliar é semelhante ao usado pelo ASP.NET 2.0. O ASP.NET 2.0 divide os nomes e valores em duas colunas distintas — PropertyName e PropertyValue — e faz referência aos usuários através de uma chave externa. A tabela externa é denominada ASP_Users e lista todos os usuários disponíveis. Isso faz muito sentido no ASP.NET 2.0 porque o banco de dados auxiliar é projetado para conter informações sobre personalização, membros, funções e contadores de site. Em um caso mais simples como este, ele representaria apenas uma complicação.

Na Listagem 3, uma consulta ao banco de dados retorna uma string Base64. Essa string é decodificada para um array de bytes e desserializada por meio do formatador binário. O resultado final é o objeto Hashtable. Esse objeto deve conter uma entrada pra cada propriedade declarada na seção . Observe que, se não houver nenhum registro para o nome de usuário especificado, será inserido um novo registro no banco de dados.

Por fim, o código na Listagem 3 recupera a lista de propriedades de personalização declaradas do arquivo web.config e adiciona uma entrada correspondente à coleção HttpContext.Items. O valor dessa entrada é o valor conforme ele é lido no banco de dados.

 

Listagem 3 Obtenha todas as informações

// RetrievePersonalInfo:  método auxiliar que recupera dados

private void RetrievePersonalInfo(HttpContext ctx, string currentUser)

{

  // Obtenha a lista de propriedade a partir do web.config

  DataSet ds = (DataSet)

        ConfigurationSettings.GetConfig("personalization");

  DataTable provider = ds.Tables["provider"];

  bool useSqlServer = false;

  if (provider.Rows[0]["datastore"].ToString() == "sqlserver")

     useSqlServer = true;

   

  // Obtenha a coleção name/value a partir do banco de dados Access

  Hashtable values = GetDataValues(currentUser, useSqlServer);

  if (values == null) return;

 

  // Preencha o contexto usando apenas os valores de leitura e

  // os nomes das propriedades no arquivo web.config

  DataTable data = ds.Tables["property"];

  foreach (DataRow row in data.Rows)

  {

    string prop = row["Name"].ToString();

    if (values[prop] != null)

        ctx.Items[prop] = values[prop];

    else

        ctx.Items[prop] = null;

  }

}

 

// GetDataValues: método auxiliar que lê os dados do store

private Hashtable GetDataValues(string currentUser, bool useSqlServer)

{

  IDbConnection conn;

  if (useSqlServer)

  {

     string connString = SqlConnectionStringBase;

     conn = new SqlConnection(connString);

  }

  else

  {

     string db = String.Format(AccessConnectionStringBase,

        HttpContext.Current.Server.MapPath(AccessPersonalization_DB));

     conn = new OleDbConnection(db);

  }

 

  string cmdText = String.Format(CmdQuery, currentUser);

  IDbCommand cmd = conn.CreateCommand();

  cmd.CommandText = cmdText;

 

  cmd.Connection.Open();

  string data = (string) cmd.ExecuteScalar();

  if (data == null)

  {

     // Adicione um novo registro (somente o nome do usuário)

     cmd.CommandText = String.Format(CmdInsert, currentUser);

     cmd.ExecuteNonQuery();

     cmd.Connection.Close();

     return null;

  }

  cmd.Connection.Close();

 

  // decodificação Base64 e desserialização binária

  byte[] bits = Convert.FromBase64String(data);

  MemoryStream mem = new MemoryStream(bits);

  BinaryFormatter bin = new BinaryFormatter();

  Hashtable values = (Hashtable) bin.Deserialize(mem);

  mem.Close();

 

  // Leia o cookie

  return values;

}

 

Escrevendo dados de personalização

Os dados de personalização são salvos de volta na mídia de storage ao término de cada solicitação. Somente uma operação de banco de dados é executada, independentemente do número de propriedades definidas nos dados de personalização. No entanto, se o usuário não alterou nada no contexto do HTTP, não há motivo para salvar os dados de volta na mídia de storage. No entanto, não existe nenhuma maneira imediata de verificar se os atributos de personalização foram alterados. Sem essa informação valiosa, você pode manter duas cópias dos dados de personalização — original e atual — para fazer uma comparação e descartar atualizações desnecessárias sempre que possível. No entanto, se você estiver lidando com uma quantidade muito grande de dados personalizados, essa não é uma boa opção.

Uma escolha melhor seria adicionar um valor booleano extra à coleção Items e denominá-lo "Modified", por exemplo. Em seguida, sempre que o aplicativo alterar um dos atributos de personalização, ela também poderá definir o atributo Modified como true e você só precisará verificar o valor booleano no handler EndRequest para evitar uma atualização desnecessária. Se você tentar isso, perceberá a importância de ter uma classe para empacotar todos os atributos do perfil do usuário. Tal classe, na verdade, poderia facilmente ocultar o suporte ao flag Modified nos acessores de conjunto de todas as propriedades. Se você planeja adicionar personalização ao seu aplicativo ASP.NET 1.1, recomendo-lhe que faça um esforço para empacotar todas as propriedades em uma classe.

Quando o evento EndRequest é acionado, o módulo de personalização age de forma inversa ao que faz quando a solicitação é autorizada (isto também é evidente na Figura 1). O tratador de eventos primeiro obtém um novo objeto de dicionário e, em seguida, preenche-o com um número de entradas equivalente ao número de atributos no arquivo web.config. Quando o objeto dictionary estiver pronto, ele será serializado pelo formatador binário e o array de bytes resultante será codificado em uma string Base64. Por fim, a string é armazenada no banco de dados por meio de um comando UPDATE. Por design, o registro existe porque o módulo o criou mediante uma autorização. A Listagem 4 mostra o código-fonte necessário para que o módulo de personalização envie as alterações para o data store.

 

Listagem 4 Envie as alterações para o Data Store

// StorePersonalInfo: método auxiliar que armazena os dados

private void StorePersonalInfo(HttpContext ctx, string currentUser)

{

  // Crie uma instância de um novo dicionário

  Hashtable values = new Hashtable();

 

  // Obtenha a lista de propriedades do web.config

  DataSet ds = (DataSet)

        ConfigurationSettings.GetConfig("personalization");

  DataTable data = ds.Tables["property"];

  foreach (DataRow row in data.Rows)

  {

    string prop = row["Name"].ToString();

    if (ctx.Items[prop] != null)

        values[prop] = ctx.Items[prop];

  }

 

  DataTable provider = ds.Tables["provider"];

  bool useSqlServer = false;

  if (provider.Rows[0]["datastore"].ToString() == "sqlserver")

        useSqlServer = true;

 

  // Armazene os dados de usuário no banco de dados do Access

  SetDataValues(values, currentUser, useSqlServer);

}

 

 

// SetDataValues: método auxiliar que grava os dados no store

private void SetDataValues(Hashtable values, string currentUser, bool useSqlServer)

{

  if (currentUser == "") return;

 

  // Serialização binária e codificação Base64

  BinaryFormatter bin = new BinaryFormatter();

  MemoryStream mem = new MemoryStream();

  bin.Serialize(mem, values);

 

  IDbConnection conn;

  IDbDataParameter parm1, parm2;

  string commandText = "";

  if (useSqlServer)

  {

    string connString = SqlConnectionStringBase;

    conn = new SqlConnection(connString);

    parm1 = new SqlParameter("@PropertyValue", SqlDbType.NVarChar);

    parm2 = new SqlParameter("@LastUpdatedData", SqlDbType.DateTime);

    commandText = String.Format(CmdUpdateSql, currentUser);

  }

  else

  {

    string db = String.Format(AccessConnectionStringBase,

        HttpContext.Current.Server.MapPath(AccessPersonalization_DB));

    conn = new OleDbConnection(db);

    parm1 = new OleDbParameter("PropertyValue",

                OleDbType.LongVarChar);

    parm2 = new OleDbParameter("LastUpdatedData", OleDbType.Date);

    commandText = String.Format(CmdUpdateAccess, currentUser);

  }

 

  IDbCommand cmd = conn.CreateCommand();

  cmd.CommandText = String.Format(commandText, currentUser);

  parm1.Value = Convert.ToBase64String(

  mem.GetBuffer(), 0, (int) mem.Length);

  parm2.Value = DateTime.Now;

  cmd.Parameters.Add(parm1);

  cmd.Parameters.Add(parm2);

 

  cmd.Connection.Open();

  cmd.ExecuteNonQuery();

  cmd.Connection.Close();

  mem.Close();

 

  return;

}

 

Usando personalização em um aplicativo

Até aqui, analisei apenas os fundamentos do módulo de personalização e a camada de armazenamento dos dados. Desta maneira, cada página ASP.NET dentro de um aplicativo personalizável encontra os valores que correspondem aos atributos declarados na configuração da coleção HttpContext.Items. Por padrão, esses valores são nulos, embora você também possa oferecer suporte a valores padrão.

Um aplicativo personalizável tem duas características principais. Primeiro, ele suporta autenticação e autorização. Em segundo lugar, ele inclui alguns elementos de interface de usuário para permitir que estes alterem os valores dos atributos de personalização.

No ASP.NET 1.1, a ativação da autenticação e da autorização é principalmente uma questão de se ajustar o arquivo web.config. Nele, você declara o tipo de autenticação desejado bem como os usuários que quer autorizar. Na maioria dos cenários do mundo real, a escolha mais razoável é a forms authentication, e a autorização é realizada comparando-se as credenciais do usuário àquelas armazenadas no banco de dados. O fragmento de código a seguir mostra como modificar o web.config para a forms authentication:

 

code04.jpg

 

O atributo Mode da seção seleciona o tipo de autenticação desejado dentre várias opções pré-definidas. A autenticação Forms consiste em uma página definida pelo usuário (login.aspx no exemplo) que reúne as credenciais do usuário e as compara com um banco de dados ou com qualquer outro data store persistente que você pretenda usar (como o Active Directory® ou Passport). Na prática, a autenticação forms lhe fornece uma infra-estrutura interna para proteger o acesso ao aplicativo, mas também o torna responsável pela implementação real. Geralmente, o form de login contém um botão para fazer o login. Quando selecionado, o botão executará um código que se parecerá com o seguinte:

 

string user = userName.Text;

string pswd = passWord.Text;

if (AuthenticateUser(user, pswd)

   FormsAuthentication.RedirectFromLoginPage(user, false);

 

AuthenticateUser é uma função personalizada que compara as credenciais com o repositório de seu aplicativo de usuários autorizados. O nó no script de configuração serve para negar o acesso aos usuários anônimos. Um aplicativo configurado dessa maneira pode apresentar uma caixa de login semelhante à mostrada na Figura 2. Quando o usuário digita seu nome e senha e essas credenciais são confirmadas, o objeto User na classe Page é configurado de acordo com elas, e a coleção HttpContext.Items é preenchida com os dados de personalização específicos ao usuário armazenados no banco de dados. Por exemplo, suponha que todas as páginas no aplicativo permitam que o usuário escolha a cor de plano de fundo e os hyperlinks a serem exibidos na barra superior. A Listagem 5 mostra o código de inicialização que pode ser usado para esse tipo de página.

 

image002.gif

Figura 2 Caixa LogIn de AuthenticateUser

 

Listagem 5 Inicialização

private void Page_Load(object sender, System.EventArgs e)

{

  ApplyPersonalization();

}

 

private void ApplyPersonalization()

{

  // O aplicativo possui uma propriedade BackColor do tipo Color

  if(Context.Items["BackColor"] != null)

  {

     TheBody.Attributes["bgcolor"] = Context.Items["BackColor"].ToString();

  }

 

  // O aplicativo possui uma propriedade Links do tipo StringCollection

  StringCollection coll = (StringCollection) Context.Items["Links"];

  if (coll != null)

  {

    foreach(object o in coll)

    {

      HyperLink h = new HyperLink();

      h.Font.Name = "verdana";

      h.Font.Size = FontUnit.Point(8);

      h.Text = o.ToString();

      h.NavigateUrl = o.ToString();

      Favorites.Controls.Add(h);

      Favorites.Controls.Add(new LiteralControl ("   "));

    }

  }

 

  LinkButton link = new LinkButton();

  link.Font.Name = "verdana";

  link.Font.Size = FontUnit.Point(8);

  link.Text = "Log out";

  link.Click += new EventHandler(OnLogOut);

  StdLinks.Controls.Add(link);

}

 

private void OnLogOut(object sender, System.EventArgs e)

{

  FormsAuthentication.SignOut();

    Server.Transfer("login.aspx?ReturnUrl=webform1.aspx");

}

 

Para alterar o plano de fundo da página por meio de programação, o aplicativo pode adicionar um atributo runat à tag e atribuir uma ID a ele. Assim, a tag body se transforma em um elemento programável e expõe a interface de programação da classe HtmlGenericControl. A interface é simples e fornece uma coleção de strings Attributes. No entanto, como pode ser visto no código a seguir, ela é suficiente para alterar a cor de plano de fundo de uma página:

 

TheBody.Attributes["bgcolor"] = Context.Items["BackColor"].ToString();

 

A propriedade Links nos dados de personalização consiste em uma coleção de strings e seus elementos estão organizados em uma faixa de hyperlinks criados dinamicamente. Os hyperlinks são então vinculados a um controle de espaço reservado (placeholder). Para obter mais detalhes, faça o download do código-fonte deste artigo. Em resumo, qualquer tipo de objeto baseado no .NET pode encontrar seu caminho para o stream de personalização e o aplicativo pode usufruir da vantagem desses recursos da forma que lhe for mais conveniente. A Figura 3 mostra que o mesmo aplicativo pode ser exibido de forma diferente quando executado por diferentes usuários. No que se refere ao tipo a ser usado nos atributos personalizáveis, o único requisito é que ele seja um tipo serializável.

 

image003.gif

Figura 3 Alterando atributos personalizáveis

 

O aplicativo de exemplo na Figura 3 também apresenta um link button Personalize que permite que os usuários alterem o valor dos atributos personalizáveis. O aplicativo é totalmente responsável por criar e gerenciar a interface do usuário (UI) no que se refere à personalização. O aplicativo de exemplo faz isso usando um controle que coleta novos valores e os armazena na coleção Items do contexto (veja a Figura 4).

 

image004.gif

Figura 4 Coletando atributos

 

Conclusão

A personalização torna o aplicativo mais flexível e agradável de usar. No ASP.NET 1.1, a implementação dessa camada de código fica totalmente por conta do desenvolvedor. No ASP.NET 2.0, você encontrará uma infra-estrutura pronta excelente, que lê as declarações no arquivo web.config e cria uma classe. A classe é então compilada dinamicamente em uma assembly, proporcionando acesso de tipificação rígida e melhor performance.

Como você codificaria isso no ASP.NET 1.1? Crie uma classe base e inclua todos os recursos que deseje que sejam suportados. Em seguida, estenda o módulo de personalização para ler as propriedades adicionais e seus tipos e crie o código-fonte de uma classe C# ou Visual Basic® .NET usando a API gerenciada do CodeDOM. Quando você terminar, a mesma API do CodeDOM lhe permitirá compilar a classe em uma assembly.

Parece simples? Bem, não completamente. A parte mais difícil é determinar como detectar as alterações no modelo de dados do usuário e invalidar a assembly criada anteriormente. A única abordagem em que consigo pensar é monitorar o arquivo web.config em busca de alterações. Se você tiver alguma idéia, envie-a e eu poderei incluí-la em um de meus futuros artigos.