Is there a way to process an MVC view (aspx file) from a non-web application?

后端 未结 5 1278
执念已碎
执念已碎 2020-12-14 23:55

I have a background service running which sends out emails to users of my website. I would like to write the email templates as MVC views, to keep things consistent (so tha

相关标签:
5条回答
  • 2020-12-15 00:33

    Ended up answering my own question :)

    public class AspHost : MarshalByRefObject
    {
        public string _VirtualDir;
        public string _PhysicalDir;
    
        public string ViewToString<T>(string aspx, Dictionary<string, object> viewData, T model)
        {
            StringBuilder sb = new StringBuilder();
            using (StringWriter sw = new StringWriter(sb))
            {
                using (HtmlTextWriter tw = new HtmlTextWriter(sw))
                {
                    var workerRequest = new SimpleWorkerRequest(aspx, "", tw);
                    HttpContext.Current = new HttpContext(workerRequest);
    
                    ViewDataDictionary<T> viewDataDictionary = new ViewDataDictionary<T>(model);
                    foreach (KeyValuePair<string, object> pair in viewData)
                    {
                        viewDataDictionary.Add(pair.Key, pair.Value);
                    }
    
                    object view = BuildManager.CreateInstanceFromVirtualPath(aspx, typeof(object));
    
                    ViewPage viewPage = view as ViewPage;
                    if (viewPage != null)
                    {
                        viewPage.ViewData = viewDataDictionary;
                    }
                    else
                    {
                        ViewUserControl viewUserControl = view as ViewUserControl;
                        if (viewUserControl != null)
                        {
                            viewPage = new ViewPage();
                            viewPage.Controls.Add(viewUserControl);
                        }
                    }
    
                    if (viewPage != null)
                    {
                        HttpContext.Current.Server.Execute(viewPage, tw, true);
    
                        return sb.ToString();
                    }
    
                    throw new InvalidOperationException();
                }
            }
        }
    
        public static AspHost SetupFakeHttpContext(string physicalDir, string virtualDir)
        {
            return (AspHost)ApplicationHost.CreateApplicationHost(
                typeof(AspHost), virtualDir, physicalDir);
        }
    }
    

    Then, to render a file:

    var host = AspHost.SetupFakeHttpContext("Path/To/Your/MvcApplication", "/");
    var viewData = new ViewDataDictionary<SomeModelType>(){ Model = myModel };
    String rendered = host.ViewToString("~/Views/MyView.aspx", new Dictionary<string, object>(viewData), viewData.Model);
    
    0 讨论(0)
  • 2020-12-15 00:43

    We used Cassini web server for our Web application while it was offline. May be this approach will work for you too? Take a look here Cassini

    0 讨论(0)
  • 2020-12-15 00:44

    In a word, no -- ASP.NET view rendering is married to the web response cycle. Probably was quite necessary to get reasonable performance in the old days.

    Now, some other options exist, including the new razor view engine from Microsoft or the open-source Spark View Engine.

    0 讨论(0)
  • 2020-12-15 00:46

    This was my first attempt, and it failed. See above for the correct and working answer

    This is as close as I was able to get, but it still didn't work. Now it complains about get_Server causing a NullreferenceException.

    Just thought I'd post on here what I did and how far I got, in case anyone wants to continue the research.

    I modified my csproj file to generate an assembly with the precompiled ASPX files, as such:

    <PropertyGroup>
    ...
        <MvcBuildViews>true</MvcBuildViews>
        <AspNetMergePath>C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\NETFX 4.0 Tools\aspnet_merge.exe</AspNetMergePath>
    ...
    </PropertyGroup>
    <Target Name="AfterBuild" Condition="'$(MvcBuildViews)'=='true'">
        <AspNetCompiler PhysicalPath="$(ProjectDir)" TargetPath="$(ProjectDir)..\$(ProjectName)_CompiledAspx" Updateable="false" VirtualPath="$(ProjectName)" Force="true" />
        <Exec Command="%22$(AspNetMergePath)%22 %22$(ProjectDir)..\$(ProjectName)_CompiledAspx%22 -o %22$(ProjectName)_views%22" />
        <Copy SourceFiles="$(ProjectDir)..\$(ProjectName)_CompiledAspx\bin\$(ProjectName)_views.dll" DestinationFolder="$(TargetDir)CompiledAspx\" />
    </Target>
    

    This created a "MyProject_CompiledAspx.dll", which I then referenced from my application. This, however, caused a new NullReferenceException.

    It's a pitty that ASPX files, being as powerful as they are, are so tightly integrated with the ASP.NET server.

    0 讨论(0)
  • 2020-12-15 00:55

    The default asp.net view engine is tied to the asp.net engine. Its tied to the context, I think you can work around it but its definitely not simple.

    The issue is with the default view engine + asp.net engine combination, other view engines shouldn't have that issue. At the very least the spark view engine doesn't.


    Edit: OP solved with the last hints, but fwiw my version that uses the controller home index action of the default asp.net mvc project template:

    public class MyAppHost : MarshalByRefObject
    {
        public string RenderHomeIndexAction()
        {
            var controller = new HomeController();
            using (var writer = new StringWriter())
            {
                var httpContext = new HttpContext(new HttpRequest("", "http://example.com", ""), new HttpResponse(writer));
                if (HttpContext.Current != null) throw new NotSupportedException("httpcontext was already set");
                HttpContext.Current = httpContext;
                var controllerName = controller.GetType().Name;
                var routeData = new RouteData();
                routeData.Values.Add("controller", controllerName.Remove(controllerName.LastIndexOf("Controller")));
                routeData.Values.Add("action", "index");
                var controllerContext = new ControllerContext(new HttpContextWrapper(httpContext), routeData, controller);
                var res = controller.Index();
                res.ExecuteResult(controllerContext);
                HttpContext.Current = null;
                return writer.ToString();
            }
        }
    }
    

    ... from a separate project:

        [TestMethod]
        public void TestIndexAction()
        {
            var myAppHost = (MyAppHost)ApplicationHost.CreateApplicationHost(
                typeof(MyAppHost), "/", @"c:\full\physical\path\to\the\mvc\project");
            var view = myAppHost.RenderHomeIndexAction();
            Assert.IsTrue(view.Contains("learn more about"));
    
        }
    

    Some extra notes:

    • url in new HttpRequest doesn't matter, but needs to be a valid url
    • it isn't meant to be used from an asp.net app that already has a context / that said, I'm not sure if it'd actually spawn the new AppDomain and work
    • Controller type's constructor and specific instance is explicit in the code, could be replaced with something to be passed in the parameters, but need to deal with the restrictions of MarshalByRef / worst case some simple reflection could be used for it
    0 讨论(0)
提交回复
热议问题