Compiling an ASPX page into a standalone program
As I mentioned here, I'm trying to generate HTML from an ASPX page inside a WinForms.
I'm trying to compile the ASPX page directly into the EXE; I'd like to be able to write something like this:
var page = new ASP.MyPageName();
var stringWriter = new StringWriter();
using(var htmlWriter = new HtmlTextWriter(stringWriter))
page.RenderControl(htmlWriter);
I added an ASPX page开发者_StackOverflow中文版, set the Build Action to Compile, and put in the following Page
declaration:
<%@ Page Language="C#" ClassName="MyPageName" %>
The code compiles, and the properties that I defined in the ASPX are usable from the calling code, but the StringWriter remains empty. I tried calling htmlWriter.Flush
, and it didn't help.
The page
instance's Controls
collection is empty, and it probably shouldn't be.
What is the correct way to do this?
I ended up using ApplicationHost.CreateApplicationHost to run the entire application in the ASP.Net AppDomain. This is far simpler and more reliable than my attempt to fake the ASP.Net AppDomain.
Note: In order to do this, you must put a copy of your EXE file (or whatever assembly contains the type passed to CreateApplicationHost) in your ASP.Net folder's Bin directory. This can be done in a post-build step. You can then handle AssemblyResolve
to locate other assemblies in the original directory.
Alternatively, you can place the program itself and all DLLs in the ASP.Net's Bin
directory.
NOTE: WinForms' Settings feature will not work in an ASP.Net AppDomain.
I believe what you want to use is the SimpleWorkerRequest.
Unfortunately, however, it requires that the resource (I believe) live on disk. From your description it sounds like you prefered for the whole app to reside in your DLL. If that is the case you will most likely need to implement your own HttpWorkerRequest.
Warning
This does not work reliably, and I've given up on it.
I ended up copying the files to the output folder and initializing ASP.Net in same AppDomain, using the following code: (I tested it; it sometimes works)
static class PageBuilder {
public static readonly string PageDirectory = Path.Combine(Path.GetDirectoryName(typeof(PageBuilder).Assembly.Location), "EmailPages");
static bool inited;
public static void InitDomain() {
if (inited) return;
var domain = AppDomain.CurrentDomain;
domain.SetData(".appDomain", "*");
domain.SetData(".appPath", PageDirectory);
domain.SetData(".appVPath", "/");
domain.SetData(".domainId", "MyProduct Domain");
domain.SetData(".appId", "MyProduct App");
domain.SetData(".hostingVirtualPath", "/");
var hostEnv = new HostingEnvironment();//The ctor registers the instance
//Ordinarily, the following method is called from app manager right after app domain (and hosting env) is created
//Since CreateAppDomainWithHostingEnvironment is never called here, I need to call Initialize myself.
//Here is the signaature of the method.
//internal void Initialize(ApplicationManager appManager, IApplicationHost appHost, IConfigMapPathFactory configMapPathFactory, HostingEnvironmentParameters hostingParameters) {
var cmp = Activator.CreateInstance(typeof(HttpRuntime).Assembly.GetType("System.Web.Hosting.SimpleConfigMapPathFactory"));
typeof(HostingEnvironment).GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(hostEnv, new[] { ApplicationManager.GetApplicationManager(), null, cmp, null });
//This must be done after initializing the HostingEnvironment or it will initialize the config system.
SetDefaultCompilerVersion("v3.5");
inited = true;
}
static void SetDefaultCompilerVersion(string version) {
var info = CodeDomProvider.GetCompilerInfo("c#");
var options = (IDictionary<string, string>)typeof(CompilerInfo).GetProperty("ProviderOptions", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(info, null);
options["CompilerVersion"] = version;
}
public static TPage CreatePage<TPage>(string virtualPath) where TPage : Page {
return BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(TPage)) as TPage;
}
//In a base class that inherits Page:
internal string RenderPage() {
var request = new SimpleWorkerRequest("", null, null);
ProcessRequest(new HttpContext(request));
using (var writer = new StringWriter(CultureInfo.InvariantCulture)) {
using (var htmlWriter = new HtmlTextWriter(writer))
RenderControl(htmlWriter);
return writer.ToString();
}
}
InitDomain
must be called right when the program starts; otherwise, it throws an exception about the configuration system being already initialized.
Without the call to ProcessRequest
, the page's Controls
collection is empty.
UPDATE: The page is rendered during the call to ProcessRequest
, so that must be done after manipulating the Page
instance.
This code will not work if the program has a .config
file; I made a method to set the default C# compiler version without a .config
file using reflection.
Why dont you just look at hosting the ASP.NET runtime in your app?
There are several snippets online to show you how.
Here is one.
Most likely you are using wrong page class. You need to use not the actual nice-named class in code behind. During compilation ASP.NET generates page class, which inherits from class defined in code behind and within this class happens initialization of all the controls. Therefore you should use generated class (check its name using Reflector).
If you're looking for the MVC version of this answer, see: Is there a way to process an MVC view (aspx file) from a non-web application?
The code uses a separate AppDomain, but as far as I could tell, this is required as all the code generated from an ASPX file depends on HttpContext and HostingEnvironment.VirtualPathProvider.
public class AspHost : MarshalByRefObject
{
public string _VirtualDir;
public string _PhysicalDir;
public string AspxToString(string aspx)
{
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);
object view = BuildManager.CreateInstanceFromVirtualPath(aspx, typeof(object));
Page viewPage = view as Page;
if (viewPage == null)
{
UserControl viewUserControl = view as UserControl;
if (viewUserControl != null)
{
viewPage = new Page();
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/AspNetApplication", "/");
String rendered = host.AspxToString("~/Views/MyView.aspx");
you can use the ClienBuildManager class to compile ASPX files.
精彩评论