34

I would like to render a PartialView to an HTML string so I can return it to a SignalR ajax request.

Something like:

SignalR Hub (mySignalHub.cs)

public class mySignalRHub: Hub
{
    public string getTableHTML()
    {
        return PartialView("_MyTablePartialView", GetDataItems()) // *How is it possible to do this*
    }
}

Razor PartialView (_MyTablePartialView.cshtml)

@model IEnumerable<DataItem>

<table>
    <tbody>
        @foreach (var dataItem in Model)
        {
        <tr>
            <td>@dataItem.Value1</td>
            <td>@dataItem.Value2</td>
        </tr>
        }
    </tbody>
</table>

HTML (MySignalRWebPage.html)

<Script>
    ...      
    //Get HTML from SignalR function call
    var tableHtml = $.connection.mySignalRHub.getTableHTML();

    //Inject into div
    $('#tableContainer).html(tableHtml);
</Script>

<div id="tableContainer"></div>

My problem is that I can't seem to render a PartialView outside of a Controller. Is it even possible to render a PartialView outside of a Controller? It would be very nice to still be able to leverage the awesome HTML generating abilities that come with Razor.

Am I going about this all wrong? Is there another way?

Paolo Moretti
  • 54,162
  • 23
  • 101
  • 92
James
  • 7,877
  • 7
  • 42
  • 57
  • 1
    Also interested... Ideas? Can you render partials outside of controllers? – Samuel Fleming Jul 25 '12 at 20:14
  • It is not a convenient way to proceed. It is better to return just kson data and then to istantiate a client side tempate, that is already in the page. The tempate, in turn can be rendered witha partal view – Francesco Abbruzzese Aug 02 '12 at 14:17

6 Answers6

14

Here, this is what I use in Controllers for ajax, I modified it a bit so it can be called from method instead of controller, method returnView renders your view and returns HTML string so you can insert it with JS/jQuery into your page when you recive it on client side:

  public static string RenderPartialToString(string view, object model, ControllerContext Context)
        {
            if (string.IsNullOrEmpty(view))
            {
                view = Context.RouteData.GetRequiredString("action");
            }

            ViewDataDictionary ViewData = new ViewDataDictionary();

            TempDataDictionary TempData = new TempDataDictionary();

            ViewData.Model = model;

            using (StringWriter sw = new StringWriter())
            {
                ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(Context, view);

                ViewContext viewContext = new ViewContext(Context, viewResult.View, ViewData, TempData, sw);

                viewResult.View.Render(viewContext, sw);

                return sw.GetStringBuilder().ToString();
            }
        }

        //"Error" should be name of the partial view, I was just testing with partial error view
        //You can put whichever controller you want instead of HomeController it will be the same
        //You can pass model instead of null
        private string returnView()
        {
            var controller = new HomeController();
            controller.ControllerContext = new ControllerContext(HttpContext,new System.Web.Routing.RouteData(), controller);
            return RenderPartialToString("Error", null, new ControllerContext(controller.Request.RequestContext, controller));
        }

I didn't test it on a Hub but it should work.

formatc
  • 4,261
  • 7
  • 43
  • 81
  • I use a similar technique to render various views as strings for the purpose of sending them as email messages, and it's proven effective so far. The only thing is, it might get tricky calling it from outside the controller due to the ControllerContext in the parameter. – Forty-Two Jul 31 '12 at 20:01
  • @Forty-Two I think it should be good since I initilize instace of controller before calling this method. You can see instance of ControllerContext made from scratch.It would be nice if someone tested it just to let me know, since I am on vacation and don't have anywhere to test it. – formatc Aug 01 '12 at 11:13
  • Hmmm. This initially looked like it would do the trick, but when I tested fully, I keep running into problems: No route data available (as request is not routed via MVC routing). Added fake routing into to address this. Now getting nullreference exception when trying to find partialview... it continues to feel like this approach is swimming upstream. I am going to investigate some of the other options (such as Razor template engines) and see where that gets me. I will update this post as I find out more. – James Aug 06 '12 at 18:18
  • @James The controller can be any as long as your partial view is in shared folder, if it is not then you have to use instance of the controller which the partial view belongs to, does this solves the null reference? – formatc Aug 06 '12 at 19:01
  • @user1010609, it worked! I managed to get it working in the Controller folder also - just needed to adjust the faked routing a little. Thanks for sticking with it. – James Aug 07 '12 at 04:31
  • 1
    @James can you share how the route should look like? – formatc Aug 07 '12 at 11:06
  • 1
    I can usually hammer out these things but this one I just can't get to work. There has to be a better way to do this. Seems very hacky to me. – Dave Lawrence Oct 22 '12 at 13:40
  • when calling from signalr request it says : Response is not available in this context – pixparker Nov 28 '17 at 16:03
6

Probably the best choice is to use RazorEngine, as Wim is suggesting.

public class mySignalRHub: Hub
{
    public string getTableHTML()
    {
        var viewModel = new[] { new DataItem { Value1 = "v1", Value2 = "v2" } };

        var template = File.ReadAllText(Path.Combine(
            AppDomain.CurrentDomain.BaseDirectory,
            @"Views\PathToTablePartialView\_MyTablePartialView.cshtml"));

        return Engine.Razor.RunCompile(template, "templateKey", null, viewModel);
    }
}
Community
  • 1
  • 1
Paolo Moretti
  • 54,162
  • 23
  • 101
  • 92
  • 1
    Looks interesting. However, this library has issues with concurrent usage, which won't work well in a web environment. See this thread for details: http://stackoverflow.com/questions/6444277/using-razorengine-to-parse-razor-templates-concurrently. The author of this engine was doing a rewrite to address these problems, but there doesn't seem to have been any movement on this project for a while - might be a dead project now: https://github.com/Antaris/RazorEngine – James Aug 06 '12 at 18:22
  • 1
    @James RazorEngine v3 (the current [NuGet package](http://nuget.org/packages/RazorEngine)) should work fine in multi-threaded scenario. You just need to instantiate a `TemplateService`, rather than invoking the static method `Razor.Parse`. Look at [this post](http://www.fidelitydesign.net/?p=473) for more details. – Paolo Moretti Aug 06 '12 at 20:12
4

Further to the answer provided by @user1010609 above, I struggled through this as well and have ended up with a function that returns the rendered PartialView given a controller name, path to the view and model.

Takes account of the fact you don't have a controller and hence none of the usual state as coming from a SignalR event.

public static string RenderPartialView(string controllerName, string partialView, object model)
{
    var context = new HttpContextWrapper(System.Web.HttpContext.Current) as HttpContextBase;

    var routes = new System.Web.Routing.RouteData();
    routes.Values.Add("controller", controllerName);

    var requestContext = new RequestContext(context, routes);

    string requiredString = requestContext.RouteData.GetRequiredString("controller");
    var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
    var controller = controllerFactory.CreateController(requestContext, requiredString) as ControllerBase;

    controller.ControllerContext = new ControllerContext(context, routes, controller);      

    var ViewData = new ViewDataDictionary();

    var TempData = new TempDataDictionary();

    ViewData.Model = model;

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

        viewResult.View.Render(viewContext, sw);
        return sw.GetStringBuilder().ToString();
    }
}

You would call it with something similar to:

RenderPartialView("MyController", "~/Views/MyController/_partialView.cshtml", model);
JohnBarton
  • 115
  • 1
  • 6
  • `Response is not available in this context.` I am getting this exception on `var viewResult=ViewEngines.Engines.FindView(controller.ControllerContext,viewName,masterName)`. any help.? – Shahid Iqbal Apr 02 '18 at 14:31
1

Have you thought about using a razor template engine like http://razorengine.codeplex.com/ ? You can't use it to parse partial views but you can use it to parse razor templates, which are almost similar to partial views.

Wim
  • 1,967
  • 11
  • 19
  • Interesting, thanks. See comment on Paolo's post below. I may test out further, but there seem to be some potential issues with that project. – James Aug 06 '12 at 18:24
  • It doesn't support several of the basic features of Razor like string interpolation, Url.Action or Html.Raw – Savage Jul 15 '22 at 18:57
0

How about using the RazorEngineHost and RazorTemplateEngine. I found this nice article that might be what you're looking for. It's about hosting Razor outside of ASP.NET (MVC).

Tx3
  • 6,796
  • 4
  • 37
  • 52
-1

Based on the answers supplied to asimilar question below, I would suggest using

Html.Partial(partialViewName)

It returns an MvcHtmlString, which you should able to use as the content of your SignalR reponse. I have not tested this, however.

Stack Overflow Question: Is it possible to render a view outside a controller?

Community
  • 1
  • 1
Jason
  • 2,940
  • 2
  • 21
  • 34
  • The type of the "HTML" object is "HTMLHelper". To instantiate this, you need a ViewContext. To instantiate a ViewContext you need a ControllerContext... back to the same problem. No Controller. Thanks for looking though! – James Jul 26 '12 at 15:25