2

I tried to simply new up a different controller and then calling the action method. The return value is of type ViewResult and the model was created, but the View is null. I guess the context in which the runtime tries to find the correct view is wrong.

How can I achieve this?

Background

Trying to see if a template controller can use a view to both render HTML e-mail to the browser as well as render to a string to be appended to the body of an MailMessage object. The second scenario will happen in a different controller, hence the problem that calling the template controller's action does not resolve the view.

This may be similar to a unit test environment where you want to check if the view result contains the content you expected.

Thanks!

Michiel van Oosterhout
  • 22,839
  • 15
  • 90
  • 132
  • I'm confused on what your asking, can you post some code? – John Farrell Oct 26 '10 at 12:29
  • 1
    I think the problem is with context. When you just "new up" a controller, the new controller instance has no context to use. This means that most of the MVC factories will fail to perform their job as you expect them to, like the view factory, which takes into account the route values in the context. The same problem occurs if you try to call another action in the same controller which chooses its view by any other means than directly specifying the name of the view. You need to look into the `Controller.Execute(HttpContext)` function to get around this problem. – Nick Larsen Oct 26 '10 at 12:41
  • michielvoo - in-house, we've adopted an approach whereby we define a 'tasks' set of classes (a kind of superset of related objects similar to viewmodels). these classes both generate html against existing business logic, as well as perfoming exactly the scenario that you describe. of course, they don't use as(pc)x views as they are built using the tag builder. this may therefore not be appropriate for your use but it's certainly an alternative approach if needed. if it's merely outputting views as strings, then i've included some of my helpers below.. – jim tollan Oct 26 '10 at 13:08
  • @NickLarsen I might be able to execute a controller, but trying to create a controller through the ControllerFactory requires quite a lot of informataion (i.e. RouteData, RequestContext, HttpContext). Do you know of an example online? – Michiel van Oosterhout Oct 26 '10 at 14:19
  • @NickLarsen Controller.Execute seems like the way to go, but it's not easy since it requires a lot of context (up to and possibly including the HttpResponse). Still, I'd mark it as answer if you turned your comment into an answer (not sure why you didn't in the first place). – Michiel van Oosterhout Oct 27 '10 at 15:21
  • @michielvoo, I wasn't sure the problem was with context without looking it up, but I didn't have time to look it up when I posted, so I made it a comment instead of an answer with the hope someone else would look it up. Setting up a call to `Execute` can be done in just a few lines however. Check out step 2 from this answer to another question: http://stackoverflow.com/questions/619895/2577095#2577095 – Nick Larsen Oct 27 '10 at 16:12

1 Answers1

1

michielvoo - as mentioned in the comment on OT, a few methods that may be useful if you also need to output your views or partialviews as strings (this is from my BaseController):

public static class ExtensionMethods
{
    // usage
    /*
        var model = _repository.Find(x => x.PropertyID > 3).FirstOrDefault();
        var test = this.RenderViewToString("DataModel", model);
        return Content(test);
     */
    public static string RenderViewToString<T>(this ControllerBase controller, string viewName, T model)
    {
        using (var writer = new StringWriter())
        {
            ViewEngineResult result = ViewEngines
                      .Engines
                      .FindView(controller.ControllerContext, viewName, null);

            var viewPath = ((WebFormView)result.View).ViewPath;
            var view = new WebFormView(viewPath);
            var vdd = new ViewDataDictionary<T>(model);
            var viewCxt = new ViewContext(controller.ControllerContext, view, vdd, new TempDataDictionary(), writer);
            viewCxt.View.Render(viewCxt, writer);
            return writer.ToString();
        }
    }

    public static string RenderPartialToString<T>(this ControllerBase controller, string partialName, T model)
    {
        var vd = new ViewDataDictionary(controller.ViewData);
        var vp = new ViewPage
        {
            ViewData = vd,
            ViewContext = new ViewContext(),
            Url = new UrlHelper(controller.ControllerContext.RequestContext)
        };

        ViewEngineResult result = ViewEngines
                                  .Engines
                                  .FindPartialView(controller.ControllerContext, partialName);

        if (result.View == null)
        {
            throw new InvalidOperationException(
            string.Format("The partial view '{0}' could not be found", partialName));
        }
        var partialPath = ((WebFormView)result.View).ViewPath;

        vp.ViewData.Model = model;

        Control control = vp.LoadControl(partialPath);
        vp.Controls.Add(control);

        var sb = new StringBuilder();

        using (var sw = new StringWriter(sb))
        {
            using (var tw = new HtmlTextWriter(sw))
            {
                vp.RenderControl(tw);
            }
        }
        return sb.ToString();
    }
}

may or may not be of use in this scenario, but worth a wee look..

jim tollan
  • 22,305
  • 4
  • 49
  • 63
  • Looks good, but the action I want to call already does all the work to get the model, so I'd be duplicating that logic in the calling method, since I would need to pass the model to this RenderViewToString method. – Michiel van Oosterhout Oct 26 '10 at 14:07
  • michielvoo - no worries, tho see a similar question here on SO from a few months back. it proposed a similar scenario to what i described in my comment in the OT re task/service layer: http://stackoverflow.com/questions/1296680/net-mvc-call-method-on-different-controller#1304449 also, it's an old one, but you could check this out too: http://stephenwalther.com/blog/archive/2008/07/21/asp-net-mvc-tip-22-return-a-view-without-creating-a-controller-action.aspx – jim tollan Oct 26 '10 at 14:36
  • Thanks for the links, but still, moving this to the service layer means that using the ViewEngine to render templates is no longer possible. – Michiel van Oosterhout Oct 27 '10 at 15:19