Dados dinâmicos do ASP.NET

Crie um site empresarial controlado por dados em 5 minutos

James E. Henry

Há anos, os desenvolvedores lutam com tarefas entediantes para criar camadas de dados com a funcionalidade CRUD (Criar, Ler, Atualizar e Excluir) a ser consumida pela interface do usuário. Pessoalmente, lembro-me de um projeto de há mais de 10 anos que exigiu que eu escrevesse código que gerava camadas de negócios e de dados automaticamente a partir de um modelo de objeto do Rational Rose. Foi um trabalho enorme.

Poucos anos depois, a Microsoft apresentou o Microsoft .NET Framework e o objeto DataSet. Com o designer do DataSet, você podia conectar um DataSet digitado com um banco de dados de back-end, como o SQL Server, e trabalhar com o DataSet em todo o aplicativo. Isso minimizava a necessidade de trabalhar diretamente com o SQL, mas ainda exigia a criação manual de páginas da Web para exibição de dados do banco de dados.

Passados mais alguns anos, a Microsoft introduziu o Entity Framework. Essa estrutura de ORM (Object Relational Mapping) usa LINQ para abstrair um esquema de banco de dados e o apresentar a um aplicativo como um esquema conceitual. Como o DataSet, essa tecnologia também minimizava a necessidade de trabalhar diretamente com o SQL. No entanto, ela ainda exigia a criação manual de páginas da Web para a exibição de dados.

Agora, a Microsoft introduziu os Dados Dinâmicos do ASP.NET, uma combinação do Entity Framework e do Roteamento do ASP.NET, que permite que um aplicativo responda a URLs que não existem fisicamente. Com esses recursos, você pode criar um site pronto para produção e controlado por dados em apenas alguns minutos. Neste artigo, mostrarei como fazer isso.

Introdução

Apenas para configurar o cenário, digamos que estou criando um site na intranet para uma empresa fictícia chamada Adventure Works. Esse site permite que a empresa gerencie informações de funcionários.

Os dados da empresa estão armazenados em um banco de dados do Microsoft SQL Server 2008. (O banco de dados pode ser baixado e instalado no msftdbprodsamples.codeplex.com.)

Agora, abra o Microsoft Visual Studio 2010 e crie um novo projeto C# do site de Entidades de Dados Dinâmicos do ASP.NET.

O tipo de projeto de site de Entidades de Dados Dinâmicos do ASP.NET tira proveito do Roteamento do ASP.NET e do Entity Framework para permitir criar sites controlados por dados rapidamente. Para obter essa funcionalidade, você precisa adicionar um modelo de dados do Entity Framework ao projeto. Para fazer isso, escolha Adicionar Novo Item no menu Site. Na caixa de diálogo Adicionar Novo Item, escolha Modelo de Dados de Entidade ADO.NET. Nomeie-o como HumanResources.edmx.

Em seguida, o Visual Studio perguntará se você deseja adicionar o modelo à pasta App_Code. Escolha Sim e permita que ele faça essa alteração. Quando você cria projetos de site, o Visual Studio compila dinamicamente todo o código que é colocado na pasta App_Code. No caso do Modelo de Dados de Entidade, o Visual Studio gera automaticamente uma classe parcial para o contexto de dados e classes parciais para as entidades. Neste exemplo, ele coloca o código em um arquivo denominado HumanResources.Designer.cs.

Em seguida, o Assistente de Modelo de Dados de Entidade é exibido, conforme mostrado na Figura 1. Escolha Gerar a Partir do Banco de dados e clique em Avançar.

image: Starting the Entity Data Model Wizard

Figura 1 Iniciando o Assistente de Modelo de Dados de Entidade

Agora, escolha uma conexão com o banco de dados Adventure Works. Se ainda não existir uma conexão, você precisará criar uma nova. A Figura 2 mostra uma conexão com o AdventureWorks em uma instância do SQL Server denominada Dev\BlueVision.

image: Configuring the Data Connection

Figura 2 Configurando a conexão de dados

Na próxima página, você poderá escolher todas as tabelas do esquema Human Resources. O nome do esquema é exibido entre parênteses. A Figura 3 mostra algumas das tabelas selecionadas.

image: Selecting Tables for the Human Resources Schema in Visual Studio

Figura 3 Selecionando tabelas do esquema Human Resources no Visual Studio

Quando você clica em Concluir, o Visual Studio gera automaticamente entidades das tabelas que você escolheu para o projeto. A Figura 4 mostra sete entidades que o Visual Studio gerou a partir do esquema do banco de dados. O Visual Studio usou as restrições de chave estrangeira do banco de dados para criar relações entre as entidades. Por exemplo, a entidade Employee participa de uma relação de um para muitos com a entidade JobCandidate. Isso significa que um funcionário pode ser um candidato a vários cargos dentro da empresa.

iamge: Entities Generated from Database Tables in Visual Studio

Figura 4 Entidades geradas a partir das tabelas do banco de dados no Visual Studio

Observe também que a entidade EmployeeDepartmentHistory é unida com as entidades Employee e Department. Se a tabela EmployeeDepartmentHistory contivesse apenas os campos necessários para unir as tabelas Employee e Department, o Visual Studio teria simplesmente omitido a entidade EmployeeDepartmentHistory. Isso teria permitido navegação direta entre as entidades Employee e Department.

Usando o Roteamento do ASP.NET

O Roteamento do ASP.NET permite que um aplicativo responda a URLs que não existem fisicamente. Por exemplo, duas URLs, http://mysite/Employee e http://mysite/Department, poderiam ser direcionadas para uma página em http://mysite/Template.aspx. Em seguida, a própria página poderia extrair informações da URL para determinar se deveria exibir uma lista de funcionários e uma lista de departamentos, onde as duas exibições usariam o mesmo modelo de exibição.

Uma rota é simplesmente um padrão de URL que é mapeado para um manipulador HTTP do ASP.NET. O manipulador é o responsável por determinar como a URL real é mapeada para o padrão interpretado. Os Dados Dinâmicos do ASP.NET usam um manipulador de rota denominado DynamicDataRouteHandler que interpreta espaços reservados denominados {table} e {action} em padrões de URL. Por exemplo, um manipulador poderia usar este padrão de URL:

    http://mysite/{table}/{action}.aspx

Esse padrão poderia então ser usado para interpretar a URL http://mysite/Employee/List.aspx.

Employee é processado como o espaço reservado {table} e List é processado como o espaço reservado {action}. O manipulador poderia então exibir uma lista de instâncias da entidade Employee. O DynamicDataRouteHandler usa o espaço reservado {table} para determinar o nome da entidade a ser exibido e usa o parâmetro {action} para determinar o modelo de página usado para exibir a entidade.

O arquivo Global.asax contém um método estático denominado RegisterRoutes que é chamado quando o aplicativo é iniciado pela primeira vez, conforme mostrado aqui:

public static void RegisterRoutes(RouteCollection routes) {
  // DefaultModel.RegisterContext(typeof(YourDataContextType), 
  //   new ContextConfiguration() { ScaffoldAllTables = false });
  routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
    Constraints = new RouteValueDictionary(
    new { action = "List|Details|Edit|Insert" }),
    Model = DefaultModel
  });
}

A primeira coisa que você precisa fazer é remover os comentários do código que chama o método RegisterContext da instância de DefaultModel. Esse método registra o contexto de dados do Entity Framework com os Dados Dinâmicos do ASP.NET. Você precisa modificar a linha de código, da seguinte maneira:

DefaultModel.RegisterContext(
    typeof(AdventureWorksModel.AdventureWorksEntities), 
    new ContextConfiguration() { ScaffoldAllTables = true });

O primeiro parâmetro especifica o tipo do contexto de dados, que é AdventureWorksEntities neste exemplo. Com a configuração da propriedade ScaffoldAllTables como true, você permite que todas as entidades sejam exibidas no site. Se você tivesse definido essa propriedade como false, seria necessário aplicar o atributo ScaffoldTable(true) para cada entidade que deveria ser exibida no site. Na prática, você deve definir a propriedade ScaffoldAllTables como false para evitar exposição acidental de todo o banco de dados para os usuários finais.

O método Add da classe RouteCollection adiciona uma nova instância Route à tabela de rotas. Com os Dados Dinâmicos do ASP.NET, a instância Route é na verdade uma instância de DynamicDataRoute. A classe DynamicDataRoute usa DynamicDataRouteHandler internamente como seu manipulador. O parâmetro passado para o construtor de DynamicDataRoute representa o padrão que o manipulador deve usar para processar URLs físicas. Uma restrição também é definida para limitar os valores de {action} para List, Details, Edit ou Insert.

Teoricamente, isso é tudo o que você precisa fazer para usar os Dados Dinâmicos do ASP.NET. Se você compilar e executar o aplicativo, verá a página mostrada na Figura 5.

image: A Basic ASP.NET Dynamic Data Site

Figura 5 Um site básico de Dados Dinâmicos do ASP.NET

Dando suporte a metadados

Uma coisa que você deve observar é que os nomes de entidades são idênticos aos nomes das tabelas que representam. No código e no banco de dados isso pode ser normal, no entanto, esse não é o caso na interface do usuário.

Os Dados Dinâmicos do ASP.NET facilitam a alteração dos nomes de uma entidade exibida por meio da aplicação do atributo DisplayName à classe que representa essa entidade. Como as classes de entidades estão contidas em um arquivo que é regenerado automaticamente pelo Visual Studio, você deve fazer quaisquer alterações de código em classes parciais em um arquivo de código separado. Portanto, adicione um novo arquivo de código denominado Metadata.cs à pasta App_Code. Em seguida, adicione o seguinte código ao arquivo:

using System;
  using System.Web;
  using System.ComponentModel;
  using System.ComponentModel.DataAnnotations;

  namespace AdventureWorksModel {
    [DisplayName("Employee Addresses")]
    public partial class EmployeeAddress { }
  }

Em seguida, recompile e execute o aplicativo. Você deve observar que EmployeeAddresses agora é Employee Addresses.

De maneira semelhante, é possível alterar os nomes de EmployeeDepartmentHistories, EmployeePayHistories e JobCandidates para nomes mais apropriados.

Em seguida, clique no link Shifts. Esse link exibe uma lista de turnos de funcionários. Irei alterar os nomes StartTime e EndTime para Start Time e End Time, respectivamente.

Outro problema é que os valores Start Time e End Time mostram a data e a hora. Neste contexto, o usuário realmente precisa ver apenas a hora, portanto, formatarei os valores Start Time e End Time para que exibam apenas horas. Um atributo denominado DataType permite especificar um tipo de dados mais específico para um campo, como EmailAddress ou Time. Você aplica o atributo DataType ao campo ao qual ele se aplica.

Primeiro, abra o arquivo Metadata.cs e adicione as seguintes definições de classe:

[MetadataType(typeof(ShiftMetadata))]
public partial class Shift { }
public partial class ShiftMetadata { }

Observe que um atributo denominado MetadataType é aplicado à classe Shift. Esse atributo permite designar uma classe separada que contém metadados adicionais para os campos de uma entidade. Não é possível aplicar os metadados adicionais diretamente aos membros da classe Shift porque você não pode adicionar os mesmos membros da classe a mais de uma classe parcial. Por exemplo, a classe Shift definida em HumanResources.Designer.cs já tem um campo denominado StartTime. Portanto, você não pode adicionar o mesmo campo à classe Shift definida em Metadata.cs. Além disso, você não deve modificar HumanResources.Designer.cs manualmente porque esse arquivo é regenerado pelo Visual Studio.

O atributo MetadataType permite especificar uma classe totalmente diferente para que você possa aplicar metadados a um campo. Adicione o código a seguir à classe ShiftMetadata:

[DataType(DataType.Time)]
  [Display(Name = "Start Time")]
  public DateTime StartTime { get; set; }

O atributo DataType especifica o valor enumerado Time para indicar que o campo StartTime deve ser formatado como um valor de hora. Outros valores enumerados incluem PhoneNumber, Password, Currency e EmailAddress, para citar alguns. O atributo Display especifica o texto que deve ser exibido como a coluna do campo quando o campo for exibido em uma lista, bem como o rótulo do campo quando for exibido em modo de edição ou somente leitura.

Agora, recompile e execute o aplicativo. A Figura 6 mostra o resultado de um clique no link Shifts.

image: Revised Shifts Page Using MetadataType Definitions

Figura 6 Página Shifts revisada por meio de definições de tipos de metadados

É possível adicionar código semelhante para alterar a aparência do campo EndTime.

Agora, vamos dar uma olhada no link JobCandidates. A coluna Employee exibe valores para o campo NationalIDNumber. Isso talvez não seja útil. Embora a tabela Employee do banco de dados não tenha um campo para o nome de um funcionário, ela tem um campo para o logon de um funcionário, que é LoginID. Esse campo talvez forneça informações mais úteis para um usuário desse site.

Para fazer isso, modificarei novamente o código dos metadados para que todas as colunas de Employee exibam o valor do campo LoginID. Abra o arquivo Metadata.cs e adicione a seguinte definição de classe:

[DisplayColumn("LoginID")]
public partial class Employee { }

O atributo DisplayColumn especifica o nome do campo de uma entidade que deve ser usado para representar instâncias daquela entidade. A Figura 7 mostra a nova lista com os LoginIDs.

image: Identifying Employees with LoginIDs

Figura 7 Identificando funcionários com LoginIDs

Barra lateral: Alterações de atributos no .NET Framework 4

A partir do Microsoft .NET Framework 4, é recomendável usar o atributo Display em vez do atributo DisplayName do .NET Framework 3.5. Obviamente, DisplayName ainda está na estrutura, mas não é recomendável sempre que o atributo Display puder se usado.

Display tem preferência sobre DisplayName por dois motivos. Primeiro, o atributo Display dá suporte à localização, enquanto o atributo DisplayName não dá.

Segundo, o atributo Display permite que você controle todos os tipos de coisas. Por exemplo, você pode controlar o texto para as várias maneiras como um campo pode ser exibido (prompt, cabeçalho, etc.), quer o campo seja mostrado como um filtro ou seja mostrado no scaffold (AutoGenerate=false o desativa).

Portanto, embora o código mostrado nos exemplos deste artigo seja totalmente válido, é recomendável substituir DisplayName e ScaffoldColumn pelo atributo Display. Você ainda precisa usar os atributos ScaffoldTable e DisplayName no nível de classe.

É melhor seguir essas recomendações porque outras equipes da Microsoft dão suporte ao namespace DataAnnotations (Serviços WCF RIA), e seguir essas práticas garantirá que o código funcione com ele.

O campo ContactID da entidade Employee na verdade faz referência a uma linha da tabela Contact, que faz parte do esquema Person no banco de dados. Como eu não adicionei nenhuma tabela do esquema Person, o Visual Studio permitirá a edição direta do campo ContactID. Para reforçar a integridade relacional, evitarei editar esse campo embora ainda permita que ele seja exibido para referência. Abra o arquivo Metadata.cs e modifique a classe Employee com a aplicação do seguinte atributo MetadataType:

[DisplayColumn("LoginID")]
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee { }

Em seguida, defina a classe EmployeeMetadata da seguinte maneira:

public partial class EmployeeMetadata {
  [Editable(false)]
  public int ContactID { get; set; }
}

O atributo Editable especifica se um campo é editável na interface do usuário.

Em seguida, adicionarei metadados à entidade EmployeePayHistory para exibir o campo Rate como Hourly Rate, bem como formatarei o valor do campo como moeda. Adicione as seguintes definições de classes ao arquivo Metadata.cs:

[MetadataType(typeof(EmployeePayHistoryMetadata))]
public partial class EmployeePayHistory { }
public partial class EmployeePayHistoryMetadata {
  [DisplayFormat(DataFormatString="{0:c}")]
  [Display(Name = "Hourly Rate")]
  public decimal Rate { get; set; }
}

Personalizando modelos

O projeto do Visual Studio contém uma pasta denominada FieldTemplates. Essa pasta contém controles de usuário para edição de campos de vários tipos de dados. Por padrão, os Dados Dinâmicos do ASP.NET associam um campo a um controle de usuário que tem o mesmo nome que o tipo de dados associado ao campo. Por exemplo, o controle de usuário Boolean.ascx contém a interface do usuário para exibir campos Boolianos, embora o controle de usuário Boolean_Edit.ascx contenha a interface do usuário para edição de campos Boolianos.

Como alternativa, você pode aplicar o atributo UIHint a um campo para garantir que um controle de usuário diferente seja usado. Adicionarei um modelo de campo personalizado para exibir um controle de Calendário para edição do campo BirthDate da entidade Employee.

No Visual Studio, adicione um novo item de campo de dados dinâmicos ao projeto e chame-o de Date.ascx. O Visual Studio adiciona automaticamente um segundo arquivo denominado Date_Edit.ascx à pasta FieldTemplates. Primeiro, substituirei o conteúdo da página Date_Edit.ascx pela seguinte marcação:

<asp:Calendar ID="DateCalendar" runat="server"></asp:Calendar>

Em seguida, modificarei o arquivo Date_Edit.ascx.cs file com a definição completa da classe mostrada na Figura 8.

Figura 8 Classe Date_EditField personalizada

public partial class Date_EditField : System.Web.DynamicData.FieldTemplateUserControl {
  protected void Page_Load(object sender, EventArgs e) {
    DateCalendar.ToolTip = Column.Description;
  }

  protected override void OnDataBinding(EventArgs e) {
    base.OnDataBinding(e);

    if (Mode == DataBoundControlMode.Edit && 
        FieldValue != null) {
      DateTime date = DateTime.MinValue;
      DateTime.TryParse(FieldValue.ToString(), out date);
      DateCalendar.SelectedDate = 
        DateCalendar.VisibleDate = date;
    }
  }
    
  protected override void ExtractValues(
    IOrderedDictionary dictionary) {
    dictionary[Column.Name] = ConvertEditedValue(
      DateCalendar.SelectedDate.ToShortDateString());
  }

  public override Control DataControl {
    get {
      return DateCalendar;
    }
  }
}

Eu substituo o método OnDataBinding para definir as propriedades SelectedDate e VisibleDate do controle Calendar como o valor do campo FieldValue. O campo FieldValue é herdado de FieldTemplateUserControl e representa o valor do campo de dados que está sendo renderizado. Eu também modifico o método ExtractValues substituído para salvar as alterações na propriedade SelectedDate em um dicionário de pares de valores de nomes de campos. Os Dados Dinâmicos do ASP.NET usam os valores desse dicionário para atualizar a fonte de dados subjacente.

Em seguida, preciso instruir os Dados Dinâmicos do ASP.NET a usar os modelos de campo Date.ascx e Date_Edit.ascx para o campo BirthDate. Posso realizar isso de duas maneiras. Primeiro, posso aplicar o atributo UIHint da seguinte maneira:

[UIHint("Date")]
public DateTime BirthDate { get; set; }

Como alternativa, posso aplicar o atributo DateType da seguinte maneira:

[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }

O atributo DataType fornece mapeamento automático por meio da correspondência do nome do tipo de dados com o nome do controle de usuário. O atributo UIHint oferece maior controle em situações onde o tipo de campo não corresponde ao nome do controle de usuário. A Figura 9 mostra o resultado da edição de um funcionário.

image: Customized Employee Edit Form

Figura 9 Formulário de edição de funcionário personalizado

Se você alterar a data de nascimento do funcionário selecionado e clicar em Atualizar, os novos dados serão salvos no banco de dados.

A pasta PageTemplates contém modelos de página que renderizam as exibições adequadas para as entidades. Por padrão, há suporte para cinco modelos de página: List.aspx, Edit.aspx, Details.aspx, Insert.aspx e ListDetails.aspx. O modelo de página List.aspx renderiza uma interface do usuário para exibição das entidades como dados tabulares. O modelo de página Details.aspx renderiza uma exibição somente leitura de uma entidade, enquanto o modelo de página Edit.aspx abre uma exibição editável de uma entidade. A página Insert.aspx renderiza uma exibição editável com valores de campo padrão. O modelo de página ListDetails.aspx permite exibir uma lista de entidades, bem como os detalhes da entidade selecionada em uma única página.

Conforme mencionei anteriormente neste artigo, os Dados Dinâmicos do ASP.NET roteiam automaticamente solicitações de URL para a página apropriada por meio de exame do valor do parâmetro {action} definido para a rota. Por exemplo, se os Dados Dinâmicos do ASP.NET avaliarem um parâmetro {action} como List, o modelo de página List.aspx será usado para exibir uma lista de entidades. Você pode alterar os modelos de páginas existentes ou adicionar um novo modelo à pasta. Se você adicionar um novo modelo, deverá garantir que ele seja adicionado à tabela de rotas no arquivo Global.asax.

A pasta EntityTemplates contém modelos para exibição de instâncias de entidades em modos somente leitura, de edição e de inserção. Por padrão, essa pasta contém três modelos denominados Default.ascx, Default_Edit.ascx e Default_Insert.ascx, que exibem instâncias de entidades em modo somente leitura de edição e de inserção, respectivamente. Para criar um modelo que se aplique a apenas uma entidade específica, basta adicionar um novo controle de usuário à pasta e dar a ele o nome do conjunto de entidades. Por exemplo, se você adicionar um novo controle de usuário denominado Shifts.ascx à pasta, os Dados Dinâmicos do ASP.NET usarão esse controle de usuário para renderizar o modo somente leitura de uma entidade Shift (conjunto de entidades Shifts). De maneira semelhante, Shifts_Edit.ascx e Shifts_Insert.ascx renderizam os modos de edição e de inserção da entidade Shift, respectivamente.

Para cada lista de entidades, os Dados Dinâmicos do ASP.NET usam os campos de chave estrangeira da entidade, os campos Boolianos e os campos de enumeração para criar uma lista de filtros. Os filtros são adicionados como controles DropDown à página de lista, conforme mostrado na Figura 10.

image: Including Data Filters on the Page

Figura 10 Incluindo filtros de dados na página

Para filtros Boolianos, o controle DropDownList simplesmente contém três valores: All, True e False. Para filtros de enumeração, o controle DropDownList contém todos os valores enumerados. Para filtros de chave estrangeira, o controle DropDownList contém todos os valores distintos de chaves estrangeiras. Os filtros são definidos como controles de usuário na pasta Filters. Por padrão, existem apenas três controles de usuário: Boolean.ascx, Enumeration.ascx e ForeignKey.ascx.

Despache-o!

Embora esse seja um cenário fictício, você viu que é totalmente possível criar um site de Recursos Humanos operacional em apenas alguns minutos. Em seguida, continuei a melhorar a interface do usuário com a adição de metadados e um modelo de campo personalizado.

Os Dados Dinâmicos do ASP.NET fornecem funcionalidade pronta para uso que permite que você tenha um site ativo e em execução rapidamente. No entanto, ele é totalmente personalizável, o que permite que as necessidades de desenvolvedores e organizações individuais sejam atendidas. O suporte dos Dados Dinâmicos do ASP.NET para Roteamento do ASP.NET permite que você reutilize modelos de páginas para operações CRUD. Se você se sente frustrado por precisar executar continuamente tarefas entediantes para implantar páginas CRUD em cada projeto de aplicativo Web, os Dados Dinâmicos do ASP.NET vão tornar sua vida muito mais fácil.         

James Henry é um desenvolvedor de software independente da BlueVision LLC, uma empresa especializada em consultoria de tecnologias da Microsoft. Ele é o autor do “Developing Business Intelligence Solutions Using Information Bridge and Visual Studio .NET” (Blue Vision, 2005) e do “Developing .NET Custom Controls and Designers Using C#” (Blue Vision, 2003). Entre em contato com ele pelo email msdnmag@bluevisionsoftware.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Scott Hunter