asp.netasp.net-mvcrazorprecompiledrazorgenerator

RazorViewEngine.FindView can't find the precompiled view


App.Web and App.Views are my projects in one solution, I put my views in App.Views and precompiled with RazorGenerator. It's working well if I used App.Web like,

~/Views/Index.cshtml is virtual path of my view in App.View

It can successfully render this view in App.Web

public ActionResult Index() {
  return View("~/Views/Index.cshtml");
}

But when I try to RenderViewToString, it returns null.

class FakeController : ControllerBase
{
    protected override void ExecuteCore() { }
    public static string RenderViewToString(string controllerName, string viewName, object viewData)
    {
        using (var writer = new StringWriter())
        {
            var routeData = new RouteData();
            routeData.Values.Add("controller", controllerName);
            var fakeControllerContext = new ControllerContext(new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null), new HttpResponse(null))), routeData, new FakeController());
            var razorViewEngine = new RazorViewEngine();
            var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
            var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
            razorViewResult.View.Render(viewContext, writer);
            return writer.ToString();
        }
    }
}

And this is how can all it,

FakeController.RenderViewToString("FakeName", "~/Views/Index.csthml", MessageModel);

enter image description here

This is discussed and probably solved in asp.net core, but I'm working with asp.net mvc 5.

Could you please help me to figure out, why it's not working?


Solution

  • You are trying to use Razor View Engine for getting precompiled views. This is a mistake. Razor engine produces views based on cshtml files. However in case of precompiled views, cshtml files have been already compiled by RazorGenerator to a set of classes derivied from System.Web.Mvc.WebViewPage. These classes override method Execute() (autogenerated by RazorGenerator based on input cshtml) that write html to output TextWriter. Original view files (cshtml) are not required anymore and thus are not deployed with the application. When you call Razor that tries to locate cshtml and build view based on it, it expectedly returns null view.

    ASP.NET MVC supports multiple view engines (the classes that implement System.Web.Mvc.IViewEngine). RazorViewEngine is one of them. RazorGenerator.Mvc NuGet package adds its own view engine (PrecompiledMvcEngine) that works based on precompiled views. Registered view engines are stored in ViewEngines.Engines collection. When you install RazorGenerator.Mvc NuGet package, it adds RazorGeneratorMvcStart class that registers instance of PrecompiledMvcEngine:

    [assembly: WebActivatorEx.PostApplicationStartMethod(typeof(App.Views.RazorGeneratorMvcStart), "Start")]
    
    namespace App.Views {
        public static class RazorGeneratorMvcStart {
            public static void Start() {
                var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) {
                    UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
                };
    
                ViewEngines.Engines.Insert(0, engine);
    
                // StartPage lookups are done by WebPages. 
                VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
            }
        }
    }
    

    You should use this instance of PrecompiledMvcEngine instead of RazorViewEngine for accessing precompiled views. Here is adjusted code of RenderViewToString method:

    public static string RenderViewToString(string controllerName, string viewName, object viewData)
    {
        using (var writer = new StringWriter())
        {
            var routeData = new RouteData();
            routeData.Values.Add("controller", controllerName);
            var fakeControllerContext = new ControllerContext(new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null), new HttpResponse(null))), routeData, new FakeController());
            var viewEngine = ViewEngines.Engines.OfType<PrecompiledMvcEngine>().FirstOrDefault();
            if (viewEngine == null)
            {
                throw new InvalidOperationException("PrecompiledMvcEngine is not registered");
            }
            var viewResult = viewEngine.FindView(fakeControllerContext, viewName, "", false);
            var viewContext = new ViewContext(fakeControllerContext, viewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
            viewResult.View.Render(viewContext, writer);
            return writer.ToString();
        }
    }
    

    One important note: you should install RazorGenerator.Mvc NuGet package into project with your views (App.Views), not into Web application project, because PrecompiledMvcEngine takes current assembly as the source for the precompiled views. Aslo make sure that RazorGeneratorMvcStart was not added to App.Web project. It happened for me when I have added reference to RazorGenerator.Mvc.dll assembly.

    Sample Project on GitHub