WebGrid do ASP.NET

Obtenha o máximo do WebGrid no ASP.NET MVC

Stuart Leeks

Baixar o código de exemplo

No início deste ano, a Microsoft lançou o ASP.NET MVC versão 3 (asp.net/mvc), além de um novo produto chamado WebMatrix (asp.net/webmatrix). O lançamento do WebMatrix incluiu vários auxiliares de produtividade para simplificar tarefas como renderização de gráficos e dados tabulares. Um desses auxiliares, o WebGrid, permite renderizar dados tabulares de forma muito simples com suporte à formatação personalizada de colunas, à paginação, à classificação e às atualizações assíncronas via AJAX.

Neste artigo, apresentarei o WebGrid e mostrarei como ele pode ser usado no ASP.NET MVC 3 e, depois, veremos como é possível realmente obter o máximo dele em uma solução ASP.NET MVC. (Para ter uma visão geral do WebMatrix — e da sintaxe do Razor que será usada neste artigo — consulte o artigo de Clark Sell, “Introdução ao WebMatrix”, da edição de abril de 2011 em msdn.microsoft.com/magazine/gg983489).

Este artigo analisa como encaixar o componente WebGrid em um ambiente ASP.NET MVC para permitir maior produtividade ao renderizar dados tabulares. Você se concentrará no WebGrid a partir de um aspecto do ASP.NET MVC: a criação de uma versão fortemente tipada do WebGrid com IntelliSense total, a ligação ao suporte do WebGrid para a paginação no lado do servidor e a adição da funcionalidade do AJAX que executa uma redução suave quando os scripts são desabilitados. As amostras de trabalho foram criadas sobre um serviço que fornece acesso ao banco de dados da AdventureWorksLT via Entity Framework. Se você tiver interesse no código de acesso aos dados, ele está disponível no download do código, e você talvez queira também dar uma olhada no artigo de Julie Lerman, “Paginação de servidor com o Entity Framework e o ASP.NET MVC 3” da edição de março de 2011 (msdn.microsoft.com/magazine/gg650669).

Introdução ao WebGrid

Para mostrar um exemplo simples do WebGrid, eu configurei uma ação do ASP.NET MVC que simplesmente passa um IEnumerable<Product> para a exibição. Estou usando o mecanismo de exibição do Razor para a maior parte deste artigo, mas depois também discutirei como o mecanismo de exibição WebForms pode ser usado. Minha classe ProductController tem a seguinte ação:

public ActionResult List()
  {
    IEnumerable<Product> model =
      _productService.GetProducts();
 
    return View(model);
  }

A exibição List inclui o seguinte código do Razor, que renderiza a grade mostrada na Figura 1:

    @model IEnumerable<MsdnMvcWebGrid.Domain.Product>
    @{
      ViewBag.Title = "Basic Web Grid";
    }
    <h2>Basic Web Grid</h2>
    <div>
    @{
      var grid = new WebGrid(Model, defaultSort:"Name");
    }
    @grid.GetHtml()
    </div>

A Basic Rendered Web Grid
(clique para ampliar)

Figura 1 Um WebGrid básico renderizado

A primeira linha da exibição especifica o tipo do modelo (por exemplo, o tipo da propriedade Model que acessamos na exibição) como IEnumerable<Product>. Dentro do elemento div, eu instancio um WebGrid, passando os dados do modelo; faço isso dentro de um bloco de código @{...} para que o Razor saiba que não deve tentar renderizar o resultado. No construtor, também defino o parâmetro defaultSort como “Name” para que o Web Grid saiba que os dados passados para ele já estão classificados por Nome. Finalmente, uso @grid.GetHtml() para gerar o HTML para a grade e renderizá-la na resposta.

Essa pequena quantidade de código fornece uma ótima funcionalidade de grade. A grade limita a quantidade de dados exibidos e inclui links de paginação para a movimentação pelos dados; títulos de colunas são renderizados como links para habilitar a paginação. Você pode especificar várias opções no construtor do WebGrid e no método GetHtml a fim de personalizar esse comportamento. As opções permitem desabilitar a paginação e a classificação, alterar o número de linhas por página, alterar o texto nos links de paginação e muito mais. A Figura 2 mostra os parâmetros do construtor do WebGrid, e a Figura 3 mostra os parâmetros do GetHtml.

Figura 2 Parâmetros do construtor do WebGrid

Nome Tipo Observações
origem IEnumerable<dynamic> Os dados a serem renderizados.
columnNames IEnumerable<string> Filtra as colunas que são renderizadas.
defaultSort string Especifica a coluna padrão para classificação.
rowsPerPage int Controla quantas linhas são renderizadas por página (o padrão é 10).
canPage bool Habilita ou desabilita a paginação de dados.
canSort bool Habilita ou desabilita a classificação de dados.
ajaxUpdateContainerId string A ID do elemento que contém a grade, que habilita o suporte ao AJAX.
ajaxUpdateCallback string A função no lado do cliente a ser chamada quando a atualização do AJAX estiver concluída.
fieldNamePrefix string Prefixo para que os campos de cadeia de caracteres de consulta deem suporte a várias grades.
pageFieldName string Nome do campo de cadeia de caracteres de consulta para número de página.
selectionFieldName string Nome do campo de cadeia de caracteres de consulta para número da linha selecionada.
sortFieldName string Nome do campo de cadeia de caracteres de consulta para coluna de classificação.
sortDirectionFieldName string Nome do campo de cadeia de caracteres de consulta para direção de classificação.

Figura 3 Parâmetros do WebGrid.GetHtml

Nome Tipo Observações
tableStyle string Classe de tabela para definição de estilo.
headerStyle string Classe de linha de cabeçalho para definição de estilo.
footerStyle string Classe de linha de rodapé para definição de estilo.
rowStyle string Classe de linha para definição de estilo (apenas linhas ímpares).
alternatingRowStyle string Classe de linha para definição de estilo (apenas linhas pares).
selectedRowStyle string Classe de linha selecionada para definição de estilo.
caption string Uma cadeia de caracteres exibida como a legenda da tabela.
displayHeader bool Indica se a linha de cabeçalho deve ser exibida.
fillEmptyRows bool Indica se a tabela pode adicionar linhas vazias para garantir a contagem de linhas de rowsPerPage.
emptyRowCellValue string Valor usado para popular linhas vazias; usado apenas quando fillEmptyRows está definida.
columns IEnumerable<WebGridColumn> Modelo de coluna para personalizar a renderização de colunas.
exclusions IEnumerable<string> Colunas a serem excluídas ao popular colunas automaticamente.
mode WebGridPagerModes Modos para a renderização da paginação (o padrão é NextPrevious e Numeric).
firstText string Texto para um link para a primeira página.
previousText string Texto para um link para a página anterior.
nextText string Texto para um link para a próxima página.
lastText string Texto para um link para a última página.
numericLinksCount int Número de links numéricos a ser exibido (o padrão é 5).
htmlAttributes object Contém os atributos HTML a serem definidos para o elemento.

O código do Razor anterior renderizará todas as propriedades de cada linha, mas convém limitar quais colunas são exibidas. Existem várias maneiras de se conseguir isso. A primeira (e mais simples) é passar o conjunto de colunas para o construtor do WebGrid. Por exemplo, este código renderiza apenas as propriedades Name e ListPrice:

var grid = new WebGrid(Model, columnNames: new[] {"Name", "ListPrice"});

Você também pode especificar as colunas na chamada para GetHtml em vez de no construtor. Embora isso demore um pouco mais, a vantagem é que você pode especificar informações adicionais sobre como renderizar as colunas. No exemplo a seguir, especifiquei a propriedade header para tornar a coluna ListPrice mais amigável:

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name"),
 grid.Column("ListPrice", header:"List Price")
 )
)

Muitas vezes, ao renderizar uma lista de itens, você deseja permitir que os usuários cliquem em um item para navegar para a exibição de Detalhes. O parâmetro format do método Column permite personalizar a renderização de um item de dados. O código a seguir mostra como alterar a renderização de nomes para gerar a saída de um link para a exibição de Detalhes de um item e gera a saída do preço de lista com duas casas decimais, da forma geralmente esperada para valores de moeda; a saída resultante é mostrada na Figura 4.

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name", format: @<text>@Html.ActionLink((string)item.Name,
            "Details", "Product", new {id=item.ProductId}, null)</text>),
 grid.Column("ListPrice", header:"List Price", 
             format: @<text>@item.ListPrice.ToString("0.00")</text>)
 )
)

A Basic Grid with Custom Columns

Figura 4 Uma grade básica com colunas personalizadas

Embora pareça haver algo de mágico acontecendo quando especifico o formato, o parâmetro format é, na verdade, um Func<dynamic,object> — um delegado que pega um parâmetro dinâmico e retorna um objeto. O mecanismo do Razor pega o trecho de código especificado para o parâmetro format e o transforma em um delegado. Esse delegado pega um item indicado pelo parâmetro dinâmico, e essa é a variável de item que é usada no trecho de código de format. Para obter mais informações sobre o funcionamento desses delegados, consulte a postagem de blog de Phil Haack em bit.ly/h0Q0Oz.

Como o parâmetro item é do tipo dinâmico, não ocorre nenhuma verificação do IntelliSense ou do compilador durante a escrita do código (consulte o artigo de Alexandra Rusina sobre tipos dinâmicos na edição de fevereiro de 2011 em msdn.microsoft.com/magazine/gg598922). Além do mais, não há suporte para a chamada de métodos de extensão com parâmetros dinâmicos. Isso significa que, ao chamar métodos de extensão, é preciso garantir que você esteja usando tipos estáticos — é por isso que item.Name é convertido em uma cadeia de caracteres quando chamo o método de extensão Html.ActionLink no código acima. Com a variedade de métodos de extensão usados no ASP.NET MVC, esse choque entre métodos dinâmicos e de extensão pode se tornar uma verdadeira chatice (ainda mais se você usar algo como T4MVC: bit.ly/9GMoup).

Adicionando a tipagem forte

Embora a tipagem dinâmica seja provavelmente uma boa ideia para o WebMatrix, existem benefícios nas exibições fortemente tipadas. Uma maneira de se obter isso é criar um tipo derivado WebGrid<T>, como mostra a Figura 5. Como você pode ver, esse é um wrapper bem leve!

Figura 5 Criando um WebGrid derivado

public class WebGrid<T> : WebGrid
  {
    public WebGrid(
      IEnumerable<T> source = null,
      ... parameter list omitted for brevity)
    : base(
      source.SafeCast<object>(), 
      ... parameter list omitted for brevity)
    { }
  public WebGridColumn Column(
              string columnName = null, 
              string header = null, 
              Func<T, object> format = null, 
              string style = null, 
              bool canSort = true)
    {
      Func<dynamic, object> wrappedFormat = null;
      if (format != null)
      {
        wrappedFormat = o => format((T)o.Value);
      }
      WebGridColumn column = base.Column(
                    columnName, header, 
                    wrappedFormat, style, canSort);
      return column;
    }
    public WebGrid<T> Bind(
            IEnumerable<T> source, 
            IEnumerable<string> columnNames = null, 
            bool autoSortAndPage = true, 
            int rowCount = -1)
    {
      base.Bind(
           source.SafeCast<object>(), 
           columnNames, 
           autoSortAndPage, 
           rowCount);
      return this;
    }
  }

  public static class WebGridExtensions
  {
    public static WebGrid<T> Grid<T>(
             this HtmlHelper htmlHelper,
             ... parameter list omitted for brevity)
    {
      return new WebGrid<T>(
        source, 
        ... parameter list omitted for brevity);
    }
  }

Então, o que isso nos dá? Com a nova implementação do WebGrid<T>, eu adicionei um novo método Column que pega um Func<T, object> para o parâmetro format, o que significa que a conversão não é necessária ao se chamar métodos de extensão. Além disso, você agora tem a verificação do IntelliSense e do compilador (supondo-se que o MvcBuildViews esteja ativado no arquivo do projeto; ele fica desativado por padrão).

O método de extensão Grid permite tirar proveito da inferência de tipos do compilador para parâmetros genéricos. Portanto, neste exemplo, você pode escrever Html.Grid(Model) em vez do novo WebGrid<Product>(Model). Em ambos os casos, o tipo retornado é WebGrid<Product>.

Adicionando paginação e classificação

Você já viu que o WebGrid oferece funcionalidades de paginação e classificação sem que seja necessário qualquer esforço de sua parte. Também viu como configurar o tamanho da página usando o parâmetro rowsPerPage (no construtor ou por meio do auxiliar Html.Grid) para que a grade mostre automaticamente uma única página de dados e renderize os controles de paginação para permitir a navegação entre as páginas. No entanto, o comportamento padrão pode não ser bem aquele que você deseja. Para ilustrar isso, adicionei um código para renderizar o número de itens da fonte de dados depois que a grade for renderizada, como mostra a Figura 6.

The Number of Items in the Data Source

Figura 6 O número de itens da fonte de dados

Como você pode ver, os dados que estamos passando contêm toda a lista de produtos (295 deles neste exemplo, mas não é difícil imaginar cenários com ainda mais dados sendo recuperados). À medida que aumenta a quantidade de dados retornados, você sobrecarrega cada vez mais seus serviços e bancos de dados, mesmo renderizando a mesma única página de dados. Mas existe uma abordagem melhor: a paginação no lado do servidor. Nesse caso, você traz apenas os dados necessários para exibir a página atual (por exemplo, apenas cinco linhas).

A primeira etapa da implementação da paginação no lado do servidor para o WebGrid é limitar os dados recuperados da fonte. Para fazer isso, você precisa saber qual página está sendo solicitada para que possa recuperar a página de dados correta. Quando o WebGrid renderizar os links de paginação, ele reutilizará a URL da página e anexará um parâmetro de cadeia de caracteres de consulta com o número da página, como http://localhost:27617/Product/DefaultPagingAndSorting?page=3 (o nome do parâmetro de cadeia de caracteres de consulta pode ser configurado nos parâmetros do auxiliar, o que é útil se você quiser dar suporte à paginação de mais de uma grade em uma página). Isso significa que você pode pegar uma parâmetro chamado page em seu método de ação e ele será populado com o valor da cadeia de caracteres de consulta.

Se você simplesmente modificar o código existente para passar uma única página de dados para o WebGrid, ele verá apenas uma única página de dados. Como ele não sabe que há mais páginas, não renderizará mais os controles de paginação. Felizmente, o WebGrid tem outro método, chamado Bind, que pode ser usado para especificar os dados. Além de aceitar os dados, o método Bind tem um parâmetro que pega a contagem total de linhas, permitindo que ele calcule o número de páginas. Para usar esse método, a ação List precisa ser atualizada para recuperar as informações adicionais a serem passadas para a exibição, como mostra a Figura 7.

Figura 7 Atualizando a ação List

public ActionResult List(int page = 1)
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products = productService.GetProducts(
    out totalRecords, pageSize:pageSize, pageIndex:page-1);
            
  PagedProductsModel model = new PagedProductsModel
                                 {
                                   PageSize= pageSize,
                                   PageNumber = page,
                                   Products = products,
                                   TotalRows = totalRecords
                                 };
  return View(model);
}

Com essas informações adicionais, a exibição pode ser atualizada para usar o método Bind do WebGrid. A chamada a Bind fornece os dados a serem renderizados e o número total de linhas, além de definir o parâmetro autoSortAndPage como falso. O parâmetro autoSortAndPage diz ao WebGrid que ele não precisa aplicar a paginação, porque o método de ação List está tomando conta disso. O código a seguir ilustra isso:

    <div>
    @{
      var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
        defaultSort:"Name");
      grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
    }
    @grid.GetHtml(columns: grid.Columns(
     grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
       "Details", "Product", new { id = item.ProductId }, null)</text>),
      grid.Column("ListPrice", header: "List Price", 
        format: @<text>@item.ListPrice.ToString("0.00")</text>)
      )
     )
     
    </div>

Com essas alterações realizadas, o WebGrid volta à vida, renderizando os controles de paginação, porém com a paginação ocorrendo no serviço em vez de na exibição! No entanto, com autoSortAndPage desativado, a funcionalidade de classificação é interrompida. O WebGrid usa parâmetros de cadeia de caracteres de consulta para passar a coluna e a direção de classificação, mas nós o instruímos a não fazer a classificação. A correção é adicionar os parâmetros sort e sortDir ao método e passá-los pelo serviço para que ele possa executar a classificação necessária, como mostra a Figura 8.

Figura 8 Adicionando parâmetros de classificação ao método de ação

public ActionResult List(
           int page = 1, 
           string sort = "Name", 
           string sortDir = "Ascending" )
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products =
    _productService.GetProducts(out totalRecords,
                                pageSize: pageSize,
                                pageIndex: page - 1,
                                sort:sort,
                                sortOrder:GetSortDirection(sortDir)
                                );
 
  PagedProductsModel model = new PagedProductsModel
  {
    PageSize = pageSize,
    PageNumber = page,
    Products = products,
    TotalRows = totalRecords
  };
  return View(model);
}

AJAX: alterações no lado do cliente

O WebGrid dá suporte à atualização assíncrona do conteúdo da grade usando o AJAX. Para se beneficiar disso, basta assegurar que o div que contém a grade tenha uma ID e passar essa ID no parâmetro ajaxUpdateContainerId para o construtor da grade. Você também deve fazer referência a jQuery, mas isso já está incluído na exibição de layout. Quando ajaxUpdateContainerId é especificado, o WebGrid modifica seu comportamento para que os links de paginação e classificação usem o AJAX para as atualizações:

    <div id="grid">
     
    @{
      var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
      defaultSort: "Name", ajaxUpdateContainerId: "grid");
      grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
    }
    @grid.GetHtml(columns: grid.Columns(
     grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
       "Details", "Product", new { id = item.ProductId }, null)</text>),
     grid.Column("ListPrice", header: "List Price", 
       format: @<text>@item.ListPrice.ToString("0.00")</text>)
     )
    )
     
    </div>

Embora a funcionalidade interna para uso do AJAX seja boa, a saída gerada não funciona se o scripts estiverem desabilitados. O motivo para isso é que, no modo AJAX, o WebGrid renderiza as marcas de âncora com href definido como “#” e injeta o comportamento do AJAX por meio do manipulador onclick.

Sempre gosto de criar páginas que são reduzidas suavemente quando os scripts são desabilitados, e geralmente descubro que a melhor forma de se fazer isso é por meio do aprimoramento progressivo (basicamente, ter uma página que funcione sem scripts e que é enriquecida com a adição de scripts). Para conseguir isso, você pode fazer a reversão para o WebGrid não AJAX e criar o script da Figura 9 para reaplicar o comportamento do AJAX:

Figura 9 Reaplicando o comportamento do AJAX

$(document).ready(function () {
 
  function updateGrid(e) {
    e.preventDefault();
    var url = $(this).attr('href');
    var grid = $(this).parents('.ajaxGrid'); 
    var id = grid.attr('id');
    grid.load(url + ' #' + id);
  };
  $('.ajaxGrid table thead tr a').live('click', updateGrid);
  $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
 });

Para permitir que o script seja aplicado apenas a um WebGrid, ele usa seletores jQuery para identificar elementos com a classe ajaxGrid definida. O script estabelece manipulares click para os links de classificação e paginação (identificados por meio do cabeçalho ou rodapé da tabela dentro do contêiner de grade) usando o método live do jQuery (api.jquery.com/live). Isso configura o manipulador de eventos para elementos existentes e futuros que correspondam ao seletor, o que é útil, já que o script substituirá o conteúdo.

O método updateGrid é definido como o manipulador de eventos, e a primeira coisa que ele faz é chamar preventDefault para suprimir o comportamento padrão. Depois, ele obtém a URL a ser usada (a partir do atributo href ou da marca anchor) e faz uma chamada ao AJAX para carregar o conteúdo atualizado para o elemento contêiner. Para usar essa abordagem, certifique-se de que o comportamento padrão AJAX do WebGrid esteja desabilitado, adicione a classe ajaxGrid ao div do contêiner e inclua o script da Figura 9.

AJAX: alterações no lado do servidor

Um ponto adicional a ser destacado é que o script usa a funcionalidade do método load do jQuery para isolar um fragmento do documento retornado. A simples chamada load(‘http://example.com/someurl’) carregará o conteúdo da URL. No entanto, load(‘http://example.com/someurl #someId’) carregará o conteúdo da URL especificada e retornará o fragmento com a ID “someId”. Isso espelha o comportamento padrão AJAX do WebGrid e significa que não é preciso atualizar seu código do servidor para adicionar comportamento parcial de renderização; o WebGrid carregará toda a página e removerá a nova grade dela.

No que se refere à obtenção rápida da funcionalidade do AJAX, isso é ótimo, mas significa que você está enviando mais dados pela conexão do que necessário e, possivelmente, procurando por mais dados no servidor do que precisa. Felizmente, o ASP.NET MVC simplifica bem isso. A ideia básica é extrair a renderização que você deseja compartilhar nas solicitações AJAX e não AJAX em uma exibição parcial. A ação List no controlador pode, então, renderizar apenas a exibição parcial para as chamadas do AJAX ou toda a exibição (que, por sua vez, usa a exibição parcial) para as chamadas não AJAX.

A abordagem pode ser tão simples quanto testar o resultado do método de extensão Request.IsAjaxRequest de dentro de seu método de ação. Isso pode funcionar bem se houver apenas algumas diferenças muito pequenas entre os caminhos de código AJAX e não AJAX. No entanto, geralmente existem diferenças mais significativas (por exemplo, a renderização total requer mais dados do que a parcial). Nesse cenário, você provavelmente escreveria um AjaxAttribute para poder escrever métodos separados e, depois, faria com que a estrutura do MVC escolhesse o método certo, dependendo de a solicitação ser AJAX (da mesma forma como funcionam os atributos HttpGet e HttpPost). Para ver um exemplo disso, dê uma olhada em minha postagem de blog em bit.ly/eMlIxU.

O WebGrid e o mecanismo de exibição do WebForms

Até o momento, todos os exemplos descritos usaram o mecanismo de exibição do Razor. No caso mais simples, não é preciso alterar nada para usar o WebGrid com o mecanismo de exibição do WebForms (além das diferenças na sintaxe do mecanismo de exibição). Nos exemplos anteriores, mostrei como você pode personalizar a renderização dos dados de linha usando o parâmetro format:

grid.Column("Name", 
  format: @<text>@Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)</text>),

O parâmetro format é, na verdade, um Func, mas o mecanismo de exibição do Razor oculta isso de nós. Mas você fica livre para passar um Func — por exemplo, pode usar uma expressão lambda:

grid.Column("Name", 
  format: item => Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)),

Munido dessa transição simples, agora você pode facilmente tirar proveito do WebGrid com o mecanismo de exibição do WebForms!

Conclusão

Neste artigo, mostrei como alguns ajustes simples lhe permitem tirar proveito da funcionalidade que o WebGrid oferece sem sacrificar a tipagem forte, o IntelliSense ou a paginação eficiente no lado do servidor. O WebGrid tem algumas ótimas funcionalidades para ajudá-lo a se tornar produtivo quando você precisar renderizar dados tabulares. Espero que este artigo tenha lhe dado uma ideia de como tirar o máximo proveito dele em um aplicativo ASP.NET MVC.

Stuart Leeks é gerente de desenvolvimento de aplicativos da equipe de Suporte a desenvolvedores no Reino Unido. Ele é totalmente obcecado por atalhos de teclado. Ele mantém um blog em blogs.msdn.com/b/stuartleeks, onde fala sobre os assuntos técnicos de seu interesse (incluindo, sem limitação, ASP.NET MVC, Entity Framework e LINQ).

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Simon Ince e Carl Nolan