Simple ASP.NET MVC CRUD views opening/closing in JavaScript UI dialog

mare picture mare · Dec 4, 2009 · Viewed 7k times · Source

I have various simple ASP.NET MVC views for CRUD operations which work fine on their own as a simple webpage. I will now integrate them into the website itself (into the content) and have for instance links like "Create new post" which would fire up the view in a chosen Lightbox clone (don't know yet which one, maybe Colorbox or Thickbox but that doesn't matter).

What I want to achieve is that the view itself somehow detects that it was opened in a JavaScript UI dialog so that the Form action (most commonly POST using a simple Submit button) will be rendered with a logic that would close the UI dialog after the action has performed.

The way views work now is POST/Redirect/GET. The view should still support this simple pattern when opened directly thru URL in a web browser but should render some additional logic when opened through JavaScript dialog.

Hopefully you understand my question. Any help appreciated

Answer

John Farrell picture John Farrell · Dec 4, 2009

Your in luck, I've done this!

So the first thing you need is a new ViewEngine to handle rendering a page without all the normal header/footer stuff that will get in the way of your modal windows. The simpliest way to do that is to use a mostly empty master page for your modal windows. You want the master page switching logic out of the way and in a custom ViewEngine because otherwise each controller method is going to have to have if() else() all over the place detecting IsAjaxRequest(). I like dry, sahara dry.

With this technique I also have the advantage of degrading very gracefully. My site functions without javascript just perfectly. Links are fine, forms work, zero code changes to go from "modal aware site" to plain old html form submits.

All I did was subclass the default engine and add some MasterPage selection bits:

The View Engine:

public class ModalViewEngine : VirtualPathProviderViewEngine 
{
    public ModalViewEngine()
    {                
     /* {0} = view name or master page name 
      * {1} = controller name      */  

     MasterLocationFormats = new[] {  
         "~/Views/Shared/{0}.master"  
     };  

     ViewLocationFormats = new[] {  
         "~/Views/{1}/{0}.aspx",  
         "~/Views/Shared/{0}.aspx"
     };  

     PartialViewLocationFormats = new[] {  
         "~/Views/{1}/{0}.ascx",               
        }; 
    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        throw new NotImplementedException();
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {

        return new WebFormView(viewPath, masterPath );
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        //you might have to customize this bit
        if (controllerContext.HttpContext.Request.IsAjaxRequest())
            return base.FindView(controllerContext, viewName, "Modal", useCache);

        return base.FindView(controllerContext, viewName, "Site", useCache);
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        return base.FileExists(controllerContext, virtualPath);
    }        
}

So my Modal.Master page is very simple. All I have is a div wrapper so I know when something is rendered inside the modal window. This will be helpful when you need to select certain elements with jquery only when the elements are in "modal mode".

Modal.Master

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<div id="modalcontent"><asp:ContentPlaceHolder ID="MainContent" runat="server" /></div>

The next step is to create your form. I use the default property name = input name so I can model bind easily and keep things simple. Nothing special about the form. I looks just like you'd do it normally. ( Note I'm using MVC 2 and EditorFor() in my code but that shouldn't matter ) Here is my final HTML:

HTML Output

<div id="modalcontent">
    <h2>EditFood</h2>
    <div id="form">
        <form method="post" action="/edit/food?Length=8">
            <input id="CommonName" class="text-box single-line" type="text" value="" name="CommonName"/>
            <input class="button" type="submit" value="Edit Food"/>
        </form>
    </div>
</div>

Besides model binding really nicely you can also use the Jquery.Form plugin to have seemless and easy ajax capabilities layered into your application with minimal code. Now I've chosen ColorBox as my modal window script purely because I wanted Facebookesque transparent corners and I like the extensibility points the author added.

Now with these scripts combined you get a really nice combination that makes this technique really stupid easy to do in javascript. The only javascript I had to add was ( inside document.ready ):

Javascript/Jquery

    $("a.edit").colorbox({ maxWidth: "800px", opacity: 0.4, initialWidth: 50, initialHeight: 50 });

    $().bind('cbox_complete', function () {
        $("#form form").ajaxForm(function () { $.fn.colorbox.close() });
    });

Here I'm telling ColorBox to open a modal window for my edit links ( Edit Food ). Then binding go the colorbox's complete event to wire up your ajaxform stuff with a success callback to tell ColorBox to close the modal window. Thats it.

This code was all done as a proof of concept and thats why the view engine is really lite and there is no validation or other standard form bling. ColorBox and JQuery.Form have tons of extensibility support so customizing this should be easy.

Note this was all done in MVC 2 but here is my controller code anyway just to show how easy this is to do. Remember my proof of concept goal was to get modal windows working in such a way that I didn't have to make any code changes other than set up some basic infrastructure.

    [UrlRoute(Path="edit/food")]
    public ActionResult EditFood()
    {            
        return View(new Food());
    }

    [HttpPost][UrlRoute(Path = "edit/food")]
    public ActionResult EditFood(Food food)
    {          
        return View(food);
    }