White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | Web Log | |
Using the ASP.Net Runtime for extending desktop applications with dynamic HTML Scripts
By Rick Strahl
http://www.west-wind.com/
Original Article: November 30, 2002 Last Update: September 16, 2012
Code for this
article: http://www.west-wind.com/presentations/aspnetruntime/aspnetruntime.zip Updated July 20, 2005
HTML is often thought of as the sole domain for Web
applications. But HTML's versatile display attributes are also very useful for
handling data display of all sorts in desktop applications. The Visual Studio.Net start page is a
A few issues back I
introduced the topic of dynamic code execution, which is not quite trivial in
.Net. That article garnered all sorts of interest and questions in how to
utilize this technology in your applications beyond the basics. Most of the
questions centered around the apparently intriguing
topic of 'executing' script pages that use ASP style syntax. Due to the size of
the article I didn't have enough room to add an extensive example of how to
apply this technology. I will do so this month, by rehashing this su Hosting the ASP.Net runtimeThe .Net framework is very flexible especially in terms of
the plumbing that First the ASP.Net runtime comes in the .Net framework and is a system component so you don't have to install anything separately. The runtime is also much more powerful than the simple script parser I introduced as it supports just about everything that ASP.Net supports for Web pages including all installed and registered languages and ASP.Net style Web form syntax. The runtime also includes the ability to determine if a page was previously compiled so it doesn't have to be recompiled each time. It handles updates to pages automatically and as an especially nice bonus you can debug your script pages using VS.Net debugger. As always with .Net internals though this power comes with a price and that price is overhead and complexity. There are number of non-obvious ways to accomplish seemingly simple tasks – such as passing parameters or leaving the runtime idle for a while – and I'll introduce a set of classes that greatly simplify this process down to a few lines of code you need to run while also showing you the key things that you need to known and implement. The 1. Setting up the
runtime environment This includes telling the runtime which directory to use as its base directory for a Web application (like a virtual directory on a Web server except here it will be all local files) and setting up a new AppDomain that the runtime can execute in. The ASP.Net runtime runs in another AppDomain and all information between your app and it run over the remoting features of .Net. 2. Creating the
script page This page is a single page that contains ASP.Net code. This means you can use pages that contain <% %>, <%= %> and <script runat="server"> syntax as long as it runs in a single page. You also need to make sure that you use the appropriate <@Assembly> and <@Namespace> inclusion tags. The current application directory and all assemblies accessible to the current application will also be accessible by the script pages 3. Calling the actual
script page to execute This step involves telling the runtime which page to execute
within the directory tree set up as a 'virtual' in the file system. ASP.Net
requires this to find its base directory and associated files. To make the
actual call you use the SimpleWorkerRequest class to
create a Request o Using the wwAspRuntimeHost Class
To simplify the process of hosting the ASP.Net runtime I created a class that wraps steps 1 and 3. With the class the code to run a single ASP.Net request from a disk file is: Listing 1 (Simplescript.cs): Using wwAspRuntimeHost class to execute an ASP.Net page loHost = new wwAspRuntimeHost(); /// *** Use WebDir beneath the application as the 'virtual' called 'LocalScript' loHost.cPhysicalDirectory = Directory.GetCurrentDirectory() + "\\WebDir\\"; loHost.cVirtualPath = "/LocalScript"; // Optional /// *** Store the output to this file on disk loHost.cOutputFile = loHost.cPhysicalDirectory + "__preview.htm"; /// *** Start the ASP.Net runtime in a separate AppDomain loHost.Start(); /// *** Run the actual page – can be called multiple times! loHost.ProcessRequest("TextRepeater.aspx","Text=Script+This&Repeat=3"); /// *** view the output file in Web Browser Control this.oBrowser.Navigate(loHost.cOutputHTML); /// *** Shutdown runtime – unload AppDomain loHost.Stop();
You start by instantiating the runtime object and setting the physical disk path where the ASP.Net application is hosted – this is the directory where scripts and other script content like images go. The start method then starts up the ASP.Net runtime in a new AppDomain. This process is delegated to a Proxy class – AspRuntimeHostProxy that actually performs all the work. The wwAspRuntimeHost class itself simply is a wrapper that has a reference to the proxy and manages this remote proxy instance by providing a cleaner class interface and error handling for any problems with the remoting required to go over AppDomain boundaries. Once the Start() method's been called you can make one or more calls to ProcessRequest() with the name of the page to execute in the local directory you set up in cPhysicalPath. Any relative path to an ASP.Net page can be used using syntax like "textRepeater.aspx" or "subdir\test.aspx". You can also pass an optional querystring made up of key value pairs that can be used be retrieved by the ASP.Net page. This serves as a simple though limited parameter mechanism. I'll show how to pass complex parameters later. In order to generate output from a page request you need to specify an output file with a full path in the cOutputFile property. This file will receive any parsed output that has been parsed in the ASP.Net runtime – in most cases the final HTML result from your script. Keep in mind that although you'll typically generate HTML for display in some HTML rendering format like a Web Browser or a Web Browser control (see figure one) you can generate output for anything. I often use templates for code and documentation generation which is not HTML. Listing 2 shows an example of simple script page that is a TextRepeater – you type in a string on the form and the script page repeats the form as many times as you specify in the querystring. Figure 1 shows the output from form displayed in a Web Browser control. Listing 2 (ASP.Net C#): A simple ASP.Net page executed locally <% string lcRepeat = Request.QueryString["Repeat"]; if (lcRepeat == null) lcRepeat = "1"; this.Repeat = Int32.Parse(lcRepeat); %> <html> <head> … omitted for brevity </head> <body style="font: <h2>Desktop
Scripting Sample</h2> <hr> Method RepeatText
value: <%=
this.RepeatText(Request.QueryString["Text"],this.Repeat) %> <hr> <small>Time is: <b><%=
System.DateTime.Now.ToString() %></b></small> </body> </html> <script runat="server" language="C#"> //*** Property added to Page class public int Repeat = 3; //*** Page Class method string RepeatText(string lcValue,int lnCount) { string lcOutput = ""; for (int x=0; x < lnCount; x++) { lcOutput = lcOutput + lcValue; } return lcOutput; } </script> This scripted page is pretty simple but it demonstrates the basic ASP style scripting behavior that you can perform on a page from embedding expressions (<%= %>) to executing code blocks (<% %>) to defining properties (Repeat) and methods (RepeatText) in the script block, which can then be accessed in the script or expressions. You can pass simple information to the page using a query string with code like this: this.oHost.ProcessRequest("Test.aspx","Text=Script This&Repeat=6"); Figure 1 – a simple ASP.Net client script exceuted locally and then displayed with the WebBrowser control. The repeat count (3 here) is passed as a querystring variable to the ASP.Net script page. Querystrings are encoded key value pairs in the same format as Web page query strings and this example sends two keys – company and repeat. The Repeat value is used in the script page using: <% string lcRepeat = Request.QueryString["Repeat"]; if (lcRepeat == null) lcRepeat = "1"; this.Repeat = Int32.Parse(lcRepeat); %> to retrieve the value and convert it into a numeric that can be used to pass to the RepeatText() method in the script. Just like ASP.Net pages you can create script pages that essentially contain properties and custom methods right inside of the script page with: <%= this.RepeatText(Request.QueryString["Text"],this.Repeat) %> In addition, all of the ASP.Net collections are available such as ServerVariables. But not all things that you might use in a Web app might be there, such as SERVER_NAME, REMOTE_CLIENT etc. since these don't apply to local applications. Others like SCRIPT_NAME and APPL_PHYSICAL_PATH on the other do return useful values that you might use in your application easily. If you want to embed images into your HTML you can do so via relative pathing in the Web directory relative to the output file. Just make sure you are generating the HTML page to be rendered into the base path so that the image pathing works when rendering the form. This is why I used: loHost.cOutputFile = Directory.GetCurrentDirectory() + @"\WebDir\__Preview.htm"; to generate the HTML into same directory used as the ASP.Net virtual path. The sample application shown in figure 1 lets you display any scripts in the WebDir directory of the sample. When you click on Execute you'll find that it takes 2-4 seconds for the ASP.Net runtime to start up for the first time, but subsequent calls to the same page are faster. The overhead you see here is both from the runtime loading for the first time as well as the script page being interpreted by the Just in Time Compiler. If you click Execute on the same page a few times performance is fast, but if you click on Unload (which calls the Stop() method) the runtime is unloaded and reloaded on the next hit which again incurs the 2-4 second startup time. Each time you unload the runtime each page executed must be recompiled. You can also edit scripts by clicking on the Edit tab, which contains a textbox with the script code. If you want to change a script simply make the change and click Save and then press the Execute button again to see the changes displayed in the Web Browser control. Note that when you make changes the page needs to be recompiled so the first hit is a little slow again. You can also edit the script page in an external editor like VS.Net of course. Once a program or script has been loaded into an AppDomain it can not be unloaded again unless you unload the AppDomain and each change made to a script page adds it to the existing type cache. Internally ASP.Net does something very similar to the process wwScripting introduced in my last article – taking a script page and turning it into a class that is compiled and run on the fly. In order to avoid having too much memory taken up by many scripts and the compilers you can unload the runtime using the Stop method of the wwAspRuntimeHost class. Obviously you can do more complex things in these dynamic
pages such as load business o How it worksThe wwAspRuntimeHost class is
based on a couple of lower level classes – wwAspRuntimeProxy
which acts as the remoting proxy reference for the
ASP.Net runtime and the wwWorkerRequest class which
is a subclass of the SimpleWorkerRequest class that
is used to handle parameter passing to script pages. Your application talks
only to the wwAspRuntimeHost class. This class acts
as a wrapper around the Proxy class to provide error handling for remoting problems since the proxy is actually a remote o Most of the work is performed by the wwAspRuntimeProxy class which performs the nuts and bolt operation of setting up and calling the ASP.Net runtime. The first step is to create a new Application Domain for the runtime to be hosted in. Microsoft actually provides a static method – ApplicationHost.CreateApplicationHost – that provides this functionality. Unfortunately this behavior is not very flexible and exactly what's required is sparsely document. For this reason and after a fair amount of searching a more flexible solution for me was to create my own AppDomain and load the runtime into it. This allows considerably more configuration of where the Runtime finds support files (in the code below in the main application's path) and how the host is configured as a custom class. Further it doesn't require copying the application's main assembly that hosts these classes into the virtual directory's BIN directory. Listing 1 shows the code to create an ASP.Net hosting capable AppDomain. The following class methods described are all part of the wwAspRuntimeProxy class which you can find in the code reference for this article. Listing 3 (wwAspRuntimeHost.cs): Creating the AppDomain ASP.Net can load in. public static wwAspRuntimeProxy CreateApplicationHost(Type hostType, string virtualDir, string physicalDir) { string aspDir = HttpRuntime.AspInstallDirectory; string domainId = "ASPHOST_" + DateTime.Now.ToString().GetHashCode().ToString("x"); string appName = "ASPHOST"; AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationName = appName; setup.ConfigurationFile
= "web.config"; AppDomain loDomain = AppDomain.CreateDomain(domainId, null, setup); loDomain.SetData(".appDomain", "*"); loDomain.SetData(".appPath", physicalDir); loDomain.SetData(".appVPath", virtualDir); loDomain.SetData(".domainId", domainId); loDomain.SetData(".hostingVirtualPath", virtualDir); loDomain.SetData(".hostingInstallDir", aspDir);
ObjectHandle oh = loDomain.CreateInstance( hostType.Module.Assembly.FullName, hostType.FullName);
wwAspRuntimeProxy loHost = (wwAspRuntimeProxy) oh.Unwrap(); // *** Save virtual and physical to tell where app runs
later loHost.cVirtualPath = virtualDir; loHost.cPhysicalDirectory = physicalDir; // *** Save Domain so we can unload later loHost.oAppDomain = loDomain;
return loHost; } public static wwAspRuntimeProxy Start(string PhysicalPath,string VirtualPath) { wwAspRuntimeProxy loHost = wwAspRuntimeProxy.CreateApplicationHost( typeof(wwAspRuntimeProxy), VirtualPath,PhysicalPath); return loHost; } public static void Stop(wwAspRuntimeProxy loHost) { if (loHost != null) { AppDomain.Unload(loHost.oAppDomain); loHost = null; } } These three methods represent the main management methods of the wwAspRuntimeProxy class. The code in CreateApplicationHost code essentially creates a new application domain (think of it as a separate process within a process) and assigns a number of properties to it that the ASP.Net runtime requires. The above code is the minimal configuration required to set up an AppDomain for executing ASP.Net. Once the AppDomain exists an instance of the runtime host class will be loaded into it loDomain.CreateInstance(). From thereon out the ASP.Net runtime host exists and can be accessed over AppDomain boundaries via .Net Remoting. Luckily several built-in helper classes help with this process. Note that these three methods are static – no instance is required to call them and they don't have access to any of the properties of the class. However the loHost instance created in CreateApplicationDomain is a full remote proxy instance and on it several properties are set to allow calling applications to keep track of where the environment was loaded via the virtual and physical path. The virtual path in a local application is nothing more than a label you'll see on error messages that ASP.Net will generate on script errors. The value should be in virtual directory format such as "/" or "/LocalScript". The physical path should point to a specific directory on your hard disk that will be the ASP.Net root directory for scripts. You can access scripts there by name or relative path. I like to use a physical path below the application's startpath and call it WebDir, or HTML or Templates. So while working on this project it's something like: D:\projects\RichUserInterface\bin\debug\WebDir\. The trailing backslash is important by the way. A class that can host ASP.Net is a pretty simple affair – it must derive from MarshalByRefObject in order to be accessible across domains and it should implement one or more methods that can call an ASP.Net request using the HttpWorkerRequest or SimpleWorkerRequest or subclasses thereof. Listing 4 shows the ProcessRequest method which takes the name of ASP.Net page in server relative pathing in the format of "test.aspx" or "subdir\test.aspx". which I'll talk about in a minute. As I mentioned to keep things simple both the static loader methods and the ProcessRequest method are contained in the same class. When the Start() method is called it returns a remote instance of the wwAspRuntimeProxy class, on which you can call the ProcessRequest() method. This method is the worker method that performs the pass through calls to the ASP.Net runtime. Listing 4 shows the implementation of this method. Listing 4 (wwAspRuntimeHost.cs): Calling an ASP.Net page once the runtime is loaded. public virtual bool ProcessRequest(string Page, string QueryString) { TextWriter loOutput; try { loOutput = File.CreateText(this.cOutputHTML); } catch (Exception ex) { this.bError = true; this.cErrorMsg = ex.Message; return false; }
SimpleWorkerRequest Request = new SimpleWorker(Page, QueryString, loOutput);
try { HttpRuntime.ProcessRequest(Request); } catch(Exception ex) { this.cErrorMsg = ex.Message; this.bError = true; return false; } loOutput.Close(); return true; } The class does two things: It creates an output file stream and uses the SimpleWorker class to create a request that is passed to the HTTP Runtime. The request is essentially similar to the way that IIS receives request information in a Web Server request, except here we're only passing the absolute minmal information to the ASP.Net processing engine: The name of the page to execute and a Querystring along with a TextWriter instance to receive the output generated. The new instance of the Request is then pass to the HttpRuntime for processing which makes the actual ASP.Net parsing call. It's important to understand that this code is being executed remotely in the created AppDomain that also hosts the ASP.Net runtime, so the call to this entire method (loHost.ProcessRequest()) actually runs over the AppDomain remoting architecture. This has some impact on error management. Any errors that occur within the script code itself will be returned as ASP.Net error pages just like you would see during Web development. Figure 2 shows an error in the for loop caused by not declaring the enumerating variable. Note that this is the only way you can get error information – there's no property that is set or error exception that is triggered on this failure, other than inside of the script code itself. This is both useful and limiting – the debug information is very detailed and easily viewable as HTML, but if your app needs this error info internally there's no way to get it except parsing it out of the HTML content. Figure 2 – Errors inside
of a client scripts bring up the detailed error messages in HTML format. No
direct error info is returned to the calling application however. Passing parameters to the ASP.Net pageSo far I've shown you how to do basic scripting which all of
itself is very powerful. However, when building template based applications
it's not The idea of a desktop application that utilizes scripts is that the application performs the main processing while the scripts act as the HTML display mechanism. In order to do this we need to be able to pass complex data to our script pages. My wwAspRuntimeProxy class provides an oParameterData property that you can assign any value to and it will pass this value to the ASP.Net application as a Context item named "Content" which can then be retrieved on a form. From the application code you'd do: Listing 5 (Simplescript.cs): Using the wwAspRuntime Class to execute an ASP.Net page loHost = AspRuntimeProxy.Start(Directory.GetCurrentDirectory() + @"\WebDir\","/LocalScript");
@"\WebDir\__Preview.htm"; … cCustomer loCust = new cCustomer(); loCust.cCompany = "West Wind Technologies"; loHost.ParameterData = loCust; this.oHost.ProcessRequest("PassObject.aspx",null); To get this to work a little more work and a few changes are
required. The SimpleWorkerRequest class doesn't
provide for passing properties or content to the ASP.Net page directly. However
we can subclass it and implement one of its internal methods that hook into the
HttpRuntime processing pipeline. Specifically we can
implement the SetEndOfSendNotification() method which
receives a reference to the HTTP Context o Listing 6 (wwAspRuntimeHost.cs): Implementing SimpleWorker request to pass objects to script pages. public class wwWorkerRequest : SimpleWorkerRequest { public object ParameterData = null; // object to pass
// *** Must implement
the constructor public wwWorkerRequest( string Page, string QueryString, TextWriter Output ) : base( Page, QueryString, Output ) {} public override void SetEndOfSendNotification( EndOfSendNotification callback, object extraData ) { base.SetEndOfSendNotification( callback, extraData ); if (this.ParameterData != null) { HttpContext context = extraData as HttpContext; if( context != null ) { /// *** Add any extra data
here to the context.Items.Add("Content",this.ParameterData); } } } } First we need to implement the constructor by simply forwarding the parameters to the base class. The SetEndOfSendNotification method gets fired just before processing is handed over to the ASP.Net page after any request data has been provided. The extraData parameter at this point contains an instance of the HttpContext object which you can access in your ASP.Net pages with: object loData = this.Context.Items["Content"]; And
voila you can now access o [Serializable()] public class cCustomer { public string cCompany = "West Wind Technologies"; public string cName = "Rick Strahl"; public string
cAddress = " public string cCity = "Paia"; public string cState = "HI"; public string cZip = "96779"; public string cEmail = "rstrahl@west-wind.com"; public cPhones oPhones = null; public cCustomer() { this.oPhones = new cPhones(); } } Alternately a class can derive from MarshalByRefObject to be able to be accessed over the wire as well: public class cPhones : MarshalByRefObject { public string Phone = "808 579-8342"; public string Fax = "808 579-8342"; } Before we can utilize this functionality we need to change a couple of things in the wwAspRuntimeProxy class. First we need to add a parameter called ParameterData which will hold the data we want to pass to the ASP.Net application. Next we need to change the code in the ProcessRequest method to handle our customer worker request class to use the wwWorkerRequest class instead of SimpleWorkerRequest: wwWorkerRequest Request = new wwWorkerRequest(Page, QueryString, loOutput); Request.ParameterData = this.ParameterData; We also need to pass the ParameterData property forward. To execute a script with the object contained with in it check out the PassObject.aspx script page shown in Listing 6. Listing 6 (PassObject.aspx): Receiving an object in script code from the calling application <%@assembly
name="AspNetHosting"%> <%@import
namespace="AspNetHosting"%> <%
this.oCust = (cCustomer) this.Context.Items["Content"]; %> <html><head> <style> H2 { background: Navy; color: White; font-size: 18pt; height: 24pt; } </style> </head> <body style="font: <h2>Object
Passing Demo</h2> <hr> Customer Name: <%= this.oCust.cName %><br> Company: <%= this.ReturnCustomerInfo() %><br> Address: <%=
this.oCust.cAddress %><br> City: <%= this.oCust.cCity %>, <%= this.oCust.cState %> <%=
this.oCust.cZip %> <p> Phone: <%= this.oCust.oPhones.Phone
%><br> Fax: <%=
this.oCust.oPhones.Fax %> <hr> <small>Physical
Path: <b><%=
Request.ServerVariables["APPL_PHYSICAL_PATH"] %></b><br> Script Name: <b><%=
Request.ServerVariables["SCRIPT_NAME"] %></b><br> Time is: <b><%= System.DateTime.Now.ToString()
%></b></small> </body> </html> <script runat="server" language="C#"> //*** Property added to Page class public cCustomer oCust = null; //*** Page Class method string ReturnCustomerInfo() { return this.oCust.cCompany; } </script> There are a couple of important points here. First notice that you need to import the assembly and namespace of any classes that you want to use. Since I declared the assembly in my main application (RichUserInterface.exe with a default namespace of RichUserInterface) I have to include the Exe file as an assembly reference. If you require any other non System namespaces or assemblies you will have to reference those as well. You should omit the .exe or .DLL extensions of any included assemblies. If you try to run with the extension you will get an error as the runtime tries to append the extensions as it searches for the file. Since we imported the namespace and assembly we can then reference our value by its proper type and add it to a property that I added to the script page (oCust). To assign the value we have to cast it to the proper cCustomer type. <% this.oCust = (cCustomer)
this.Context.Items["Content"]; %> Once this has been done, you can access this object as needed by using its property values. To embed it into the page you can then just use syntax like this: Customer Name: <%= this.oCust.cName %> You can also call methods this way. For example, if you add this method to the customer object: public string CityZipStateString() { return this.cCity + ", " + this.cState + " " + this.cZip; }
You can then call it from the script page like this: City: <%= this.oCust.CityZipStateString() %> This means that you can easily execute business logic right
within a script page. However, I would recommend you try to minimize the amount
of code you run within a script page, rather relying on it to provide the
dynamic and customizable interface for the application. So rather than passing
an ID via the querystring, then using the o Providing POST data to the script pageAnother useful interaction mechanism between the client and the script page is to provide POST data to the script page. This POST data can then be accessed just like on a true Web Form with the Request.Form collection. In fact with a little bit of trickery of using the the IE WebBrowser control you could easily create an offline viewer for a Web site that would not require a Web server at all. This could be handy for example for shipping a dynamic Web site on a CD.
The process to do this, like passing complex objects in the Context object, requires that you override methods in the SimpleWorkerRequest class. With POST a number of things should be set: The HTTP Verb, the content type and the content length all of which requires overriding of three separate methods.
To support POST operations on the runtime the SimpleWorkerRequest subclass must override three methods: GetHttpVerbName() which should return POST, GetKnownRequestHeader() which should return the content type and length, and the GetPreloadedEntityBody() which should return the actual POST buffer. In order for the client code to set the post buffer I added a AddPostKey() method to the wwAspRuntimeHost class, which uses internal PostData and PostContentType properties to hold the state of the post operations.
The implementation on the SimpleWorkerRequest subclass requires the code in Figure 6.5 which overrides three of the Http Pipeline’s methods.
Listing 6.5 – Adding POST support to the SimpleWorkerRequest subclass byte[] PostData = null; string PostContentType = "application/x-www-form-urlencoded"; … public override String GetHttpVerbName() { if (this.PostData == null) return base.GetHttpVerbName();
return "POST"; }
public override string GetKnownRequestHeader(int index) { If (index == HttpWorkerRequest.HeaderContentLength) { if (this.PostData != null) return this.PostData.Length.ToString(); } else if (index == HttpWorkerRequest.HeaderContentType) { if (this.PostData != null) return this.PostContentType; } return base.GetKnownRequestHeader (index);
}
public override byte[] GetPreloadedEntityBody() { if (this.PostData != null) return this.PostData;
return base.GetPreloadedEntityBody(); }
The value of PostData and PostContentType is passed down from the wwAspRuntimeProxy and ultimately from the wwAspRuntimeHost classes which both include these properties as well.
Post operations can then be performed using raw post buffers like this:
this.oHost.AddPostBuffer("Company=West+Wind&Name=Rick+Strahl");
The AddPostBuffer method supports both string and binary input (byte[]) and has an additional optional content type parameter which defaults to Urlencoded form content. Note that you must provide a raw POST buffer, which in the case of UrlEncode content looks like the snippet above. Other content types like multi-part and XML can be posted in raw form.
Once you’ve added this POST buffer to the application you can now retrieve that info using Request.Form inside of the ASP.Net page:
<%= Request.Form["Company"] %>
This is useful for a number of reason. First you can use this mechanism to pass complex parameters and XML to the application and use a standard mechanism to retrieve the input. If you have Web Forms you can use the ID values of controls that you want to set with specific values for example. So, the txtCompany form variable would update the Web Control with the ID txtCompany.
But secondly and more importantly this mechanism allows you to capture the POST buffer from a browser session in IE. So you can display a form using this mechanism in IE and then click the submit button, capture the BeforeNavigate2() event, grab the POST buffer and post it right back to the ASP.Net runtime. BeforeNavigate2 provides the URL and the POST buffer so you have all the info you need to provide a POST back to the ASP.Net page. This makes it possible to create applications that use all of ASP.Net’s features, but entirely without any Web Server at all. How’s that for cool? ConfigurationSetting up ASP.Net scripting is pretty straight forward. But you can configure the application handling even more by using a web.config file. Listing 7 (config.web): Configuring the ASP.Net environment with a configuration file <configuration> <system.web> <compilation debug="true"/> <httpHandlers> <add verb="*" path="*.wcs" type="System.Web.UI.PageHandlerFactory"/> <add verb="*" path="*.htm" type="System.Web.UI.PageHandlerFactory"/> </httpHandlers> </system.web> </configuration> There are two extremely useful settings that you can make. First you should set debug to true to allow you to debug your scripts. If you have this setting in your application you can debug your scripts right along with your application. Simply open the script in the VS environment set a breakpoint in the script, then run the application, hit the script and voila your script code can be debugged with all of the VS debugging features. If you don't have an existing VS project you can still use the debugger against the Executable with: <VS Path>\devenv /debugexe RichUserInterface.exe Let VS create a new solution for you, open the page to debug, set a breakpoint and off you go. This is a very cool feature as you can offer this feature to your customers as well, so they can more easily debug their scripts. Along the same lines you can use the VS editor to edit your scripts as well, although you should try and stay away from all of the Web form related stuff as that's meant for server side development. You can implement this but frankly you'll be much better off dealing with these sort of issues in your regular application code. When you build template based applications you might prefer to extensions other than ASPX for your scripts and you can do this by adding httpHandlers into the Config.Web file as shown above. Set each extension to the same System.Web.UI.PageHandlerFactory as ASPX files (set in machine.config) are sent and you can then process files with those extensions through the scripting runtime. Unlike ASP.Net you don't need scriptmaps to make this work because we're in control of the HttpRuntime here locally. Runtime timeoutsSo far I've shown the workings of the wwAspRuntimeProxy class and how it implements the code. You have to remember that the proxy is a remote object reference with all of its related issues. A remote reference is a proxy and if something happens to the remote object – it crashes unrecoverably or times out – the reference goes away. It's difficult to capture this sort of error because any access to the object could cause an error at this point. There are two issues here: Lifetime and error handling. Remote objects have a limited lifetime of 5 minutes by default. After 5 minutes remote object references are released regardless of whether the object has been called in the meantime. When I first ran into this I couldn't quite figure out what was happening – after 5 minutes or all of the sudden the pointers to the proxy where gone. It's difficult to detect this failure, because the reference the client holds is not Null, so you can't simply check for a non-Null value. The only way to detect this is with an exception handler, but wrapping every access to the Proxy into an exception handler isn't a good option from a code perspective and it doesn't allow for automatic recovery.
My inititial workaround was to create a wrapper class, that simply makes passthrough calls to the Proxy object. This is the main wwAspRuntimeHost class which wraps the calls to the Proxy into Exception handling blocks. Specifically each call to ProcessRequest() first checks to see if a property on the proxy is accessible and if it is not, it tries to reload the runtime automatically by calling the Start() method. Listing 7.5 shows the implementation of the wrapped ProcessRequest method. The first try/catch block performs the auto-restart of the runtime. Listing 7.5 (AspRuntimeHost.cs): The proxy wrapper code can catch proxy errors public virtual bool ProcessRequest(string Page, string QueryString) { this.cErrorMsg = ""; this.bError = false; try { string lcPath = this.oHost.cOutputFile; } catch(Exception ) { if (!this.Start()) return false; } bool llResult = false; try { this.oHost.ParameterData = this.ParameterData; llResult = this.oHost.ProcessRequest(Page,QueryString); } catch(Exception ex) { this.cErrorMsg = ex.Message; this.bError = true; return false; } return llResult; } The wrapper also simplifies the interface of the class by not using static members and setting properties of the assigned values internally, which makes all the information set more readily available to the calling application (See Listing 1). It also hides some of the static worker methods, so the developer method interface is much cleaner and easier to use resulting in much less code. Although I found a solution to my timeout problem creating this wrapper was definitely worthwhile. The timeout problem turns out to be related to remote object 'Lease'. The InitialLease for a remote object is for 5 minutes after which the object is release regardless of access. There are a number of ways to override this, but generically the easiest way to do it is to override the InitializeLifetimeService() method of the proxy object. To do this I added the code shown in Listing 7.7 which sets the value to a more reasonable number of minutes (15 by default) and allows the object to restart counting its Lease when a hit occurs. Listing 7.7 (AspRuntimeHost.cs): Setting the lifetime of the Proxy object using System.Runtime.Remoting.Lifetime; … //
implement on wwAspRuntimeProxy class public override Object InitializeLifetimeService() { ILease lease =
(ILease)base.InitializeLifetimeService(); if (lease.CurrentState == LeaseState.Initial) { lease.InitialLeaseTime = TimeSpan.FromMinutes(wwAspRuntimeProxy.nIdleTimeoutMinutes); lease.RenewOnCallTime = TimeSpan.FromMinutes(wwAspRuntimeProxy.nIdleTimeoutMinutes); lease.SponsorshipTimeout
= TimeSpan.FromMinutes(5); } return lease; } nIdleTimeoutMinutes is a private static member of the wwAspRuntimeProxy class and can't be set at runtime – you have to set this on the property, but it defaults to a reasonable value of 15 minutes that you can manually override on the class if necessary. The RenewOnCallTime property automatically causes the Lease to be renewed for the amount specified everytime a hit occurs which should be plenty of time. And if the runtime still should time out for some reason it will automatically reload because of the wrapper in wwAspRuntimeHost. Running under ASP.NetSometimes you may find it also useful to run a dynamic page under ASP.Net. For example, if you need to create an email confirmation for an order or you need to send a form letter to an client, you can do so by using an ASPX page to provide the 'textmerge' mechanism to perform this task. Even though you are already running under ASP.Net this process still requires creating a new Application domain and using the same mechanisms described here. There are a couple of extra considerations you need to think about.
In order to do so you will have to explicitly provide the cApplicationBase parameter of the wwAspRuntimeHost class and point it at the Web application's BIN directory or wherever the DLL/EXE is housed that contains the wwAspRuntimeHost class.
loHost.cPhysicalDirectory = Server.MapPath("/wwWebStore") + "\\"; loHost.cApplicationBase = loHost.cPhysicalDirectory + "bin\\";
Your other option in this regard is to create a separate DLL for the wwAspRuntimeHost classes, sign them and move the file to the Global Assembly Cache. By doing this ASP.Net can always find the DLL without extra hints and continue to load it.
Additionally you will need to have rights for the account that is running the ASP.Net application to be able to create a new application domain and create temporary files in the ASP.Net application cache where the compiled code is housed. An example: Assembly DocumentationAs an example how you can utilize the functionality of the wwAspRuntimeHost class and the ASP.Net runtime I created a small sample application that shows the content of assemblies by generating HTML output from the properties and methods as well as importing all the documentation from the XML comments if available. XML comments are available for C# applications that have an XML documentation export file set at Compile time. The sample will pick up a matching XML file to the assembly imported and parse out the documentation to the matching properties and generate HTML. The application acts as a viewer for the class hierarchy as well as the individual members of each class and you can export the entire thing into HTML pages to disk. Figure 4 shows an example of the running application that uses the Web Browser control to display each topic. Figure 4 – A sample application that demonstrates how to use the ASP.Net runtime to build a local application that uses HTML content for display in a desktop application. The application works by selecting an assembly (a DLL or EXE) for import which fills the Treeview on the left with data from the class. A while back I built a class called wwReflection that wraps the various type retrieval interfaces from Reflection to build an easy and COM exportable interface to export class information from assemblies. This class also performs post parsing of the retrieved values such as retrieving the XML documentation and formatting parameters and other text into formats suitable for display as documentation. The detailed method info in Figure 4 demonstrates some of the parsed information is available. The wwAspRuntimeHost class is used
in this sample to display each of the help topics for each method, property and
class by running a specific HTML template using ASP.Net code. Classes, methods
and properties each have a separate template – ClassHeader.aspx, ClassMethod.aspx
and ClassMethod.aspx respectively which are
passed an o All the member parsing for classes,
methods and properties is handled by wwReflection
which pulls all members into local properties of o The class behavior is rather simple and demonstrated by the LoadTree() method of the ClassDocs form as shown in Listing 8. Listing 8 (ClassDocs.cs): Using wwReflection to populate the treeview with class info private void LoadTree(string
lcAssembly) { File TypeParser
loParser = this.oParser; loParser.cFilename
= lcAssembly; this.txtName.Text = "Assembly Documentation " + loFile //*** If exists add XML documentation loParser.cXmlFilename
= wwUtils.ForceExtension(lcAssembly,"xml"); this.oParser.GetAllO this.oList.Nodes.Clear(); for (int x = 0; x
< loParser.nO { DotnetO TreeNode
loNode = new TreeNode(loO loNode.ImageIndex=0; loNode.SelectedImageIndex
= 0; ` loNode.Tag =
x.ToString() + ":-1"
; this.oList.Nodes.Add(loNode); //*** Add
Methods for (int y=0; y <
loO { O TreeNode
loNode2 = new TreeNode(loMethod.cName); loNode2.ImageIndex
= 1; loNode2.SelectedImageIndex
= 1; loNode2.Tag
= x.ToString() + ":"
+ y.ToString(); loNode.Nodes.Add(loNode2); } //*** Add
Properties for (int y=0; y <
loO { O TreeNode
loNode3 = new TreeNode(loProperty.cName); loNode3.ImageIndex
= 2; loNode3.SelectedImageIndex
= 2; loNode3.Tag
= x.ToString() + ":"
+ y.ToString(); loNode.Nodes.Add(loNode3); } } this.oStatus.Panels[0].Text = "Loaded Assembly: " + lcAssembly; this.cCurrentAssembly = lcAssembly; } The key features here are the oParser.GetAllO When the form loads the code initializes our wwAspRuntimeHost class with this code: this.oHost = new wwAspRuntimeHost(); this.oHost.cPhysicalDirectory
= Directory.GetCurrentDirectory() + this.oHost.cVirtualPath = "/Documentation"; this.oHost.cOutputFile = this.oHost.cPhysicalDirectory + this.oHost.Start(); which initializes and starts up the ASP.Net runtime. Each click on a treenode then fires code to call a specific template. The code in Listing 9 shows how the rendering of a class method is handled. Listing 9 (ClassDocs.cs): Rendering a method through a script page // *** Methods if (e.Node.ImageIndex == 1) { // *** Multiple o DotnetO loParameter.oO loParameter.oMethod = this.oParser.aO loParameter.cTitle = this.txtName.Text; this.oHost.ParameterData
= loParameter; this.oHost.ProcessRequest("ClassMethod.aspx",null); this.Navigate("file://" + this.oHost.cOutputFile); } The code first checks for which type of member is called
based on the ImageIndex – image 1 is a method. It
then creates a new DotnetO public class DotnetO { public string cTitle = "Assembly Documentation"; public DotnetO public O public O public O } Using this o Listing 10 (ClassMethod.aspx): Partial code from the method rendering script page. <%@assembly
name="AspNetHosting"%> <%@import
namespace="AspNetHosting"%> <%@import
namespace="Westwind.wwReflection"%> <script runat="server" language="C#"> DotnetObject oO ObjectMethod oMethod = null; string cTitle = ""; </script> <% DotnetO (DotnetObjectParameterData)
this.Context.Items["Content"]; this.oO this.oMethod = loData.oMethod; this.cTitle = loData.cTitle; %> <html> <head> <title><%= this.oMethod.cName %></title> … <h2><%= this.oO <p> <%=
this.oMethod.cHelpText %> <p> … <table …> … <tr> <td width="100" valign="top" align="right" class="labels">
</td> <td bgColor="#eeeeee" style="font: bold bold
10pt 'Courier New'"> </tr> </table> The assembly and namespace references are required to allow
use of the classes imported. In this case the RichUserInterface.exe contains
all classes referenced but with most real world solutions you'll likely have
several assemblies that you need to reference. Next the code assigns a couple
of local o DotnetO (DotnetO This is the parameter that was passed the oHost.ParameterData member on the form and is now available
to our script page. Once we have a reference to this o To display each of these values we can now simply embed
expressions into the ASP.Net page – such as <%= this.oMethod.cHelpText %>. The net effect of all of this is that we can use our business
o Because we are dealing with ASP.Net here we can also utilize the full power of scripting in our pages. For example, we could have the Class page utilize the oParser instance to run through all methods and properties and generate a simple table display summary by embedding the script. Using script provides a lot of flexibility for this sort of functionality. In addition you can make your HTML display interface
somewhat interactive by handling hyperlinks in the HTML display to fire actions
in your application by using the BeforeNavigate2 event of the WebBrowser control. But that's a su Scripts awayI hope this article has given you a better idea of how you can utilize dynamic content in your desktop applications. The ability to externalize your user interface into templates provides a powerful mechanism for creating rich user interfaces and provides the customization and extensibility that make your applications attractive to power users. Building display interfaces in HTML gives you display flexibility that you simply do not have with regular form controls. It's so much easier to build an engaging visual page including images and color with HTML than is possible with form controls. At the same time you can continue to use Windows Forms controls where they make most sense – for data entry and validation where HTML is sorely lacking. For example, the real world application that the assembly documentation sample is based on uses templates for displaying help topic content and rendering the final HTML output that gets compiled into HTML Help files, while all the editing of the topic data is accomplished through a standard tab based Windows Form interface. You get the instant real-time preview while still having a traditional and structured data entry mechanism. There are many more uses for scripting with ASP.Net – it's a tremendously powerful mechanism because you can basically create anything from batch scripts to code generators directly with the engine. So what's your next script?
Rick
|
By Rick Strahl
Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows Server Products, .NET, Visual Studio and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.
|
White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | |