Implement Master-Detail layout with ASP.NET MVC

This is the first of posts that I am making to show out the things you can do with ASP.NET MVC. Also, it shows what I have done while building FlickrXplorer. I am bit lazy to write one article for it, so I thought rather to start it here.

In this post, I will show how you can implement a master-detail layout that invokes MVC controller to process its data and uses Ajax to do it in a non postback manner.

If you have looked though FlickrXplorer, you must have noticed that every list of images is tied with a detail view in a way that if user performs any action on the list ("click") , the detail view is updated accordingly.

Let's see the action flow below

image

It is almost clear that when you select an image it calls a Controller method or an Action method to be more precise. In this case, let's say that it is /Photo/Detail/112233. To map this a simple line in global.asax follows

routes.MapRoute("Detail", "Photo/Detail/{photoId}",
 new { controller = "Photo", action = "Detail" });

 

This will result in the call of PhotoController.Detail, when user hits the above url. Inside detail view its pretty simple.

[FilterResponse]
public ActionResult Detail(string photoId)
{
    try
    {
        PhotoDetail detail = model.GetPhoto(photoId); 
        return View(detail);
    }
    catch (Exception ex)
    {
        return View("Error", 
                    new ControllerException
      { ErrorUrl = HttpContext.Request.RawUrl, Message = ex.Message });
    }
}

As, expected it calls the model to get the photo and returns the ViewResult. Here, I haven't done any content related processing rather made a ViewPage named Detail.aspx which the MVC framework calls. By default, MVC framework looks for the ViewPage or ViewControl with name equals to the Action name, but View has other overrides that let me call other ViewPage/ViewControl as well. This is what done above when any error occurs, which calls Shared/Error.aspx. It is to be noted that if you have multiple controller and you want to access the same view from different controller, then you need to place it in the Shared folder, which will make it accessible by all controllers.

In the snippet, we also see that there is a FilterResponse attribute that will cache the response for same parameter set once the first call is made. FlickrResponse is inherited from MVC.ActionFilterAttribute attribute that provides a way to do some custom actions like caching and compression of http content.

So far, I have mapped the url and added the action method. Now, its time to get the result and show them in UI but I don't want any postbacks or redirects for it. So, I have used a callback model that did the work for me.

function renderContent(elementId, loadElementId, url, callback) {
    var element = $get(elementId);
    $get(loadElementId).style.display = "block";

    // Create the WebRequest object.
    var wRequest = new Sys.Net.WebRequest();

    // Set the request Url.  
    wRequest.set_url(url);
    // Set the request verb.
    wRequest.set_httpVerb("GET");
    // Set the Completed event handler, 
    // for processing return data
    wRequest.add_completed(function(sender, eventArgs) 
    {
        if (sender.get_responseAvailable()) 
        {
            element.innerHTML = sender.get_responseData();
            $get(loadElementId).style.display = "none";
            
            if (typeof callback != 'undefined')
                callback();
            
        }
    });
    // Make the request.
    wRequest.invoke();
}

 

Here, we see that renderContent takes in the Id of the element that will contain the rendered content and loading element that will be visible while the content is being processed. Optionally, we can pass in a parameter-less void callback which can be invoked after the content is processed. In that case, we might like to start the loading of comments after we have rendered the detail page. This renderContent is invoked while a user selects a photo from the list, this is a bit generic method which can be used for any callback scenarios. Therefore, for rendering photo detail, I wrapped it around so that it takes only a photoId.

function renderDeail(photoId, ignoreHash) {
    if (ignoreHash == 0) {
        if (window.location.hash == '') {
            // take the default one 
            window.location.hash = photoId;
        }
        else {
            photoId = getPhotoIdFromHash();
        }
    }
    else {
        window.location.hash = photoId;
    }
    var url = '<%= Html.ActionUrl("Photo", "Detail", new { photoId = "{0}" }) %>';
    url = String.format(unescape(url), photoId);
    renderContent("detailView", "loadingView", url, loadComments);
}

renderDetail  takes a photoId and ignoreHash := true/false. Inside, it uses Html.ActionUrl to process the url and then formats it with photoId and finally calls the renderContent. Html.ActionUrl is an extension method which is coded by me, the original is Html.ActionLink. So please dont get confused :-). I could have passed the url by hand but using ActionUrl  it creates the url on basis of the route mapping in global.asax. Also, for Photo controller if I change the url from /photo/detail/id to /p/detail/id, then I don't have to go everywhere in order to change the references.

Finally, while loading photo from url, I use window.hash to navigate to the selected photo, which is updated when a photo is clicked to show and thus, I have the master-detail layout yet url copy-paste facility.

That's it for now, I leave it on to the reader to explore it more in Codeplex. Also, hope that the introduction helps. Please note that some of the features like 2nd step loading of comments will be supported from 1.2 release (please check it out), so the JS might slightly differ to what is shown here.

Check it live at flickrmvc.net - send me feedbacks and updates.

kick it on DotNetKicks.com

No Comments