Saturday, January 29, 2011

Grid view in MVC using mvcContrib

Last post I showed a simple way to display images in your html using an MVC controller Displaying images from a database - MVC style.

But then it got me thinking...what if I have a lot of records ? what if I want to allow the users to search for a specific record?
So the answer is of course - adding some sort of page/sort/filter control/s
in ASP.NET we just needed to pick one from millions..what about MVC?

Googling through this topic I found all sort of HTML helpers some free & some commercial, the 1st one to catch my eye and later on proven to be a good catch was mvcContrib.

I will show you a small sample I built based on Raj Kaimal blog using mvcContrib.

Lets start from a few semi-new concepts when developing in MVC:

1. ViewModels:
The architectural hype these days is definitely ORM tools like EntityFramework, NHibernate etc - These frameworks and others were invented to decouple the database structure from the logical entities defined in our application and map between them to allow transferring data back and forward.
The thing is that before these tools came we were working with smaller sets of data, retrieving only the data we needed for display and sometimes modeling it for smoother work in a specific view.
For this type of work we will use ViewModel, view-model is a portion of our model built specific for a single view (or more if same that is display in more than one way), it decouple the view from the model, allows us to work with a smaller set of data in a structure adjusted to the view but still keep it strongly typed.

2. Controllers
Controllers as their name suggests - control, they are the main MVC concept, Controllers are the gateway of the view to the application & data - building a wall between them - a real wall - not an imaginary one as in webform ASP.NET - this "wall" will allow us to serve the same services for different types of views (web applications, webservices, smart phones etc).

Let's see an implementation of these concepts.

In every grid view there are 3 different views:
1. The filter view.
2. The main view.
3. The paging view.



Every view has its own viewModel.
The filter will need a viewModel which describes the filter, a list of items for a combo filter, a datetime for a datepicker etc
The main view will probably need columns from all sorts of entities, some of them could be "translated" using some sort of lookup table, some maybe formatted differently for this view etc.
The paging view will need a viewModel that describes the paging ruler, in the case of mvcContrib - implementation of IPagination.
The main view will also need to implement GridSortOptions to allow sorting.

Sounds kinda complicated, but actually it couldn't be easier...

Filter's view model will contain a list that will be represented as a dropdown in the view:
public class AlbumFilterViewModel
    {
        public AlbumFilterViewModel()
        {
            SelectedArtistId = -1;
        }

        public int SelectedArtistId { get; set; }

        public List Artists { get; set; }
    }

The main view view-model, this will contain the columns we want to display in the gridview, notice that we can change the way the columns are displayed using simple attributes, this could be implemented for a specific view or at the entity level exploiting the fact that EF entities are define as partial classes (using DataAnnotations: [MetadataType(typeof(AlbumMetaData))]):

public class AlbumListViewModel
    {
        [ScaffoldColumn(false)]  //this columns won't be shown in view
        public int ArtistId { get; set; }

        [ScaffoldColumn(false)]
        public int AlbumId { get; set; }

        [DisplayName("Artist Name")] //change the display name of the column
        public string ArtistName { get; set; }

        [DisplayName("Album Name")]
        public string Name { get; set; }

        [DisplayName("Cover")]
        public byte[] Picture { get; set; }

        [DisplayName("Last Updated Date")]
        [DisplayFormat(DataFormatString = "{0:g}")] //format the date
        public DateTime LastUpdatedDate { get; set; }

    }

As I mentioned before the main view implements the acutal grid and also the paging and sorting, the viewModel will contain the previous one plus the paging and sort definition. it will look like this:
public class AlbumListContainerViewModel
    {
        //paging
        public IPagination AlbumPageList { get; set; }
        //main view
        public AlbumFilterViewModel AlbumFilterViewModel { get; set; }
        //sort
        public GridSortOptions GridSortOptions { get; set; }
    }

Now lets go to the view.
We'll have 3 different partial views (ascx):
1. The filter.
2. The main view.
3. The paging ruler that we'll had before and after the main view.

The container will look something like this:
<% Html.RenderPartial("SearchFilter", Model.AlbumFilterViewModel); %>
    <% Html.RenderPartial("Pager", Model.AlbumPageList); %>
    <% Html.RenderPartial("SearchResult", Model); %>
    <% Html.RenderPartial("Pager", Model.AlbumPageList); %>

The filter view html is quite simple, notice that the view is strongly typed:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
.
.
            
<%:Html.DropDownList("ArtistId", Model.Artists, "-- All --", htmlAttributes)%>
. .

The search result view code, you will see the mvcContrib's html helpers does everything, the only two place I wrote my own code was to specify a different behavior for two columns, one is the "More.." column which is linked to a detail view of the chosen row and a the image column which is linked to the controller that retrieve the image from the database:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="MvcContrib.UI.Grid" %>
<%@ Import Namespace="DiegJukeboxRemote.Models" %>
<%@ Import Namespace="DiegJukeboxRemote.HtmlHelpers" %>
<%= Html.Grid(Model.AlbumPageList).AutoGenerateColumns()
    .Columns(column => {
        column.For(a => Html.ActionLink("More..", "ThumbChooserEdit", new { id = a.AlbumId })).InsertAt(4).Encode(false);
    })
    .Columns(column=> {
        column.For(a => Html.Image(a.AlbumId, "RetrieveImage", "MusicInfo", 
            new Dictionary() { 
                                                { "Height", "100" }, { "Width", "100" },
                                                {"onerror", "onImgErrorSmall(this)"}
            })).InsertAt(3).Encode(false);
    })
    .Sort(Model.GridSortOptions)
    .Attributes(@class => "table-list")
%>

The pager couldn't be simpler:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="MvcContrib.UI.Pager" %>

<%= Html.Pager(Model) .First("First") .Last("Last") .Next("Next") .Previous("Previous") %>


Finally the controller, using LINQ we retrieve the data we need from artist/album repositories, you will see here two special points:
1. Using System.Web.Mvc.SelectListItem for the dropdownlist.
2. Using MvcContrib.UI.Grid.GridSortOptions & MvcContrib.Pagination.PaginationHelper so mvcContrib helpers can do the rest :-)
public ActionResult ThumbChooser(int? artistId, GridSortOptions gridSortOptions, int? page, bool? MissingCoverOnly)
        {
            IQueryable albumList = null;

            if (!MissingCoverOnly.HasValue || !MissingCoverOnly.Value)
            {
                albumList = _BL.GetAlbumListView();
            }
            else
            {
                albumList = _BL.GetAlbumListViewMissingCover();
            }

            if (string.IsNullOrWhiteSpace(gridSortOptions.Column))
            {
                gridSortOptions.Column = "ArtistId";
            }

            if (artistId.HasValue)
            {
                albumList = albumList.Where(a => a.ArtistId == artistId.Value);
            }

            var albumFilter = new AlbumFilterViewModel();
            albumFilter.SelectedArtistId = artistId ?? -1;

            albumFilter.Artists = _BL.GetArtistList().OrderBy(art=>art.Name)
                                    .Select(a => new { a.ArtistId, a.Name })
                                    .ToList().Select(a =>
                                        new SelectListItem
                                        {
                                            Text = a.Name.Length > 20 ? string.Concat(a.Name.Substring(0,20),"...") : a.Name,
                                            Value = a.ArtistId.ToString(),
                                            Selected = a.ArtistId == albumFilter.SelectedArtistId
                                        }).ToList();

            var albumPageList = albumList
                                .OrderBy(gridSortOptions.Column, gridSortOptions.Direction)
                                .AsPagination(page ?? 1, 10);

            var albumListContainer = new AlbumListContainerViewModel
            {
                AlbumFilterViewModel = albumFilter,
                AlbumPageList = albumPageList,
                GridSortOptions = gridSortOptions
            };

            return View(albumListContainer);
        }

That's it!

Till next time...
Diego

8 comments:

  1. Hello!
    did you finish this project?
    i need this code project.can you give it to me.
    my email:qhovnpl@gmail.com

    ReplyDelete
  2. Please share the complete code.
    My emailid: srinivas0906@gmail.com

    Regards,
    Srinivas Alwala

    ReplyDelete
  3. can i have the source code for reference??
    Email :CHAN838@hotmail.com

    Thx

    ReplyDelete
  4. Hi , can u send me this code on :jhs0011.anil@gmail.com

    Thanks
    Anil

    ReplyDelete
  5. golden.rinom@gmail.com

    ReplyDelete
  6. Good day everyone! The article is very usefull!
    But how can use the Ajax during sorting or paging?

    ReplyDelete