Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return Partial View and JSON from ASP.NET MVC Action

I'm introducing KnockoutJS into an existing app. My plan is to modify/utilize the existing partial views that we've already created and bind them to the JS view models with Knockout's declarative attributes. When I make an AJAX call to an action, ideally I'd like the action to return both the HTML of the partial view and JSON object. Then I can fill a div with the HTML, convert the JSON to a Knockout object and bind it to the HTML. But I can't figure out how to return both from the action.

I need the full view model because I'll be updating it and eventually sending it back to the server.

I thought about having the action return the partial view (already bound to the model), and within the partial view, include javascript to convert the .Net model into a Knockout object. But I feel that scattering the JS around like that is messy and unmaintainable. I'd rather have everything close to the original ajax call.

I guess another alternative is to make two action calls. One for the JSON, and another for the partial view. But there has to be a slicker way.

Any ideas on how best to do this?

like image 265
nthpixel Avatar asked Sep 06 '13 22:09

nthpixel


2 Answers

I'm sure there are a variety of ways to do this. I manually render the view from the controller, and then pass the rendered view back as part of my JSON response.

This preserves the responsibilities of each entity. Views are still located using the view engine and they can be reused. The controller knows little or nothing about the view beyond its name and model type.

Manual Rendering

public static class RenderHelper
{
    public static string PartialView( Controller controller, string viewName, object model )
    {
        controller.ViewData.Model = model;

        using( var sw = new StringWriter() )
        {
            var viewResult = ViewEngines.Engines.FindPartialView( controller.ControllerContext, viewName );
            var viewContext = new ViewContext( controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw );

            viewResult.View.Render( viewContext, sw );
            viewResult.ViewEngine.ReleaseView( controller.ControllerContext, viewResult.View );

            return sw.ToString();
        }
    }
}

In your action method:

object model = null; // whatever you want
var obj = new { 
    someOtherProperty = "hello", 
    view = RenderHelper.PartialView( this, "_PartialName", model ) 
};

return Json( obj );

Note that I'm returning an anonymous type. You can return any (serializable) type you want, as long as it has a string property for the rendered view.

Testing

Testing an action that uses manual rendering requires a slight modification. This is due to rendering the view a bit earlier than it would be rendered in the MVC pipeline.

Manual Rendering

  1. Enter action method
  2. Render view explicitly <-- this will make it difficult to test the calling action
  3. Exit action method

Automatic Rendering

  1. Enter action method
  2. Create a view result
  3. Exit action method
  4. Process view result (thus rendering the view)

In other words, our manual rendering process kicks off a variety of other operations that make it difficult to test (such as interacting with the build manager to compile the view).

Assuming you wish to test the action method and not the actual contents of the view, you can check whether or not the code is executing in a hosted environment.

    public static string PartialView( Controller controller, string viewName, object model )
    {
        // returns false from a VS 2013 unit test, true from IIS
        if( !HostingEnvironment.IsHosted )
        {
            // return whatever you want here
            return string.Empty;
        }

        // continue as usual
     }

Checking HostingEnvironment.IsHosted is inexpensive (under the hood, it is simply a null check).

like image 171
Tim M. Avatar answered Nov 16 '22 00:11

Tim M.


You could create a hidden <input> on the partial with a value set to the JSON string of the ViewModel. Then before you render the partial view, grab the JSON value from that field, and parse it. Then remove it from the partial view, insert it into your page, and do ko.applyBindingsToDescendants(viewModel, $("#parentElement")[0])

I'm not entirely sure how I feel about this approach, and it's just a theory. I haven't tested this out but I suspect it would work. One booty trap you'd have to look out for is the browser trying to cache your GET request. In your ajax request you'd want to do:

$.ajax({
  url: "/",
  type: 'GET',
  cache: 'false'
});

Or just do a $.post request. (reference)

So that's one option.

like image 25
nwayve Avatar answered Nov 15 '22 22:11

nwayve