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/
rstrahl@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
Note: The wwASPRuntimeHost class has been changed slightly to
support .NET 2.0. Please review the updated samples for more detailed
information specifically on launching the runtime and managing
shared assemblies between the host app and the Web app.

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

 

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 good example. Coupled with a scripting/template mechanism you can build highly extendable applications that would be very difficult to build using standard Windows controls. In this article Rick introduces how to host the ASP.Net runtime in desktop applications and utilize this technology in a completely client side application using the Web Browser control.

 

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 subject and showing another more powerful mechanism that's built into the .Net framework to provide an ASP.Net style scripting host for your client applications.

Hosting the ASP.Net runtime

The .Net framework is very flexible especially in terms of the plumbing that goes into the various sub-systems that make up the core system services. So it should be no great surprise that the ASP.Net scripting runtime can be hosted in your own applications. This has several benefits over the ASP style parsing approach I showed in my last article.

 

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 good news is that calling the ASP.Net runtime from any .Net application is pretty straight forward. There are three major steps involved in this process:

 

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 object that is passed to the HttpRuntime's ProcessRequest method.

 

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: Normal Normal 10pt verdana">

<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 objects and retrieve data to display on an ASP.Net style form. We'll take a look at a more advanced and useful example later.

How it works

The 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 object reference.

 

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 page

So 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 good enough to be able to process code in templates, but you have to also be able to receive data from the calling application. In the examples above we've been limited by small text parameters that can be passed via QueryString. While you can probably use the QueryString to pass around serialized data from objects and datasets, this is really messy and requires too much code on both ends of the script calling mechanism.

 

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");


loHost.cOutputFile = Directory.GetCurrentDirectory() +

    @"\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 object that is accessible to your ASP.Net script pages and assign an object reference to it. Listing 5 shows an implementation of this class that takes the ParameterData property and stores into the Context object.     

 

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 object data. This subclass passes a single object – which for most purposes should be enough. If you need to pass more than one you can simply create a composite object and hang multiple object references off that one to pass multiple items. Of course you can also subclass this class on your own and add as many properties to the class to pass as needed into the Contextobject. Note that objects passed in this fashion including any subobjects must be marked as Serializable:

 

[Serializable()]

public class cCustomer

{

    public string cCompany = "West Wind Technologies";

    public string cName = "Rick Strahl";

    public string cAddress = "32 Kaiea Place";

    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: Normal Normal 10pt verdana;background:LightYellow">

<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 object to load the data to display, instead use the application to perform the load operation and simply pass the object to the page to be displayed. The main catch is that the object passed must be in some way serializable to pass over the AppDomain boundaries.

Providing POST data to the script page

Another 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?

Configuration

Setting 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 timeouts

So 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.Net

Sometimes 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 Documentation

As 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 object that contains the relevant information for display.

 

All the member parsing for classes, methods and properties is handled by wwReflection which pulls all members into local properties of objects. These local properties have fixed up information contained within them that include minimal formatting for display of member information such as parameters etc. The TypeParser has an array of aObjects, each object has an array of aMethods and aProperties and so on.

 

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)

{

    FileInfo loFileInfo = new FileInfo(lcAssembly);

 

    TypeParser loParser = this.oParser; 

    loParser.cFilename = lcAssembly;

    this.txtName.Text = "Assembly Documentation " + loFileInfo.Name;

 

    //*** If exists add XML documentation

    loParser.cXmlFilename = wwUtils.ForceExtension(lcAssembly,"xml");

 

    this.oParser.GetAllObjects(); // parse all objects

 

    this.oList.Nodes.Clear();

    for (int x = 0; x < loParser.nObjectCount; x++)

    {

          DotnetObject loObject = this.oParser.aObjects[x];

         

          TreeNode loNode = new TreeNode(loObject.cName);

          loNode.ImageIndex=0;

          loNode.SelectedImageIndex = 0;

 

`   loNode.Tag = x.ToString() + ":-1" ;

 

          this.oList.Nodes.Add(loNode);

 

          //*** Add Methods

          for (int y=0; y < loObject.nMethodCount; y++)

          {

                ObjectMethod loMethod = loObject.aMethods[y];

 

                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 < loObject.nPropertyCount; y++)

          {

                ObjectProperty loProperty = loObject.aProperties[y];

 

                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.GetAllObjects() method which parses the entire assembly into internal properties of the TypeParser class. aObjects[] is populated with all classes and the aMethods and aProperties and aEvents members all are filled with the appropriate subobjects. Each of those arrays contains custom objects that contain all of the required class information. Note that each of these classes (defined in wwReflection.cs) is defined as MarshalByRefObject which is important in order to be passed to our script pages for rendering. Each of the node's tag property is set with a string key value pair that identifies the class indexer and the member indexer: 0:3 means class index 0 and member index 3 for example and this code is parsed out when a node is selected to retrieve the object and method to pass to the script page.

 

When the form loads the code initializes our wwAspRuntimeHost class with this code:

this.oHost = new wwAspRuntimeHost();

this.oHost.cPhysicalDirectory = Directory.GetCurrentDirectory() +
                                "\\WebDir\\";

this.oHost.cVirtualPath = "/Documentation";

this.oHost.cOutputFile = this.oHost.cPhysicalDirectory +
                         "__preview.htm";

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 object by parameter by using a container object

    DotnetObjectParameterData loParameter = new DotnetObjectParameterData();

    loParameter.oObject = this.oParser.aObjects[lnClassIndex];

    loParameter.oMethod = this.oParser.aObjects[lnClassIndex].aMethods[lnMemberIndex];

    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 DotnetObjectParameterData object which acts as a parameter container for a number of other objects that we want to pass to the script page. It's defined like this:

 

public class DotnetObjectParameterData : MarshalByRefObject

{

    public string cTitle = "Assembly Documentation";

    public DotnetObject oObject = null;

    public ObjectMethod oMethod = null;

    public ObjectProperty oProperty = null;

    public ObjectEvent oEvent = null;

}

 

Using this object we can assign properties appropriate for each type of member and pass the information using the wwAspRuntimeHost class' ParameterData 'parameter' object. Inside of the script page we can then pick up the parameter and use it within the page. For the method script the header/startup code for the script is shown in Listing 10. Note that this script is modified and truncated significantly for brevity and clarity.

 

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 oObject = null;

  ObjectMethod oMethod = null;

  string cTitle = "";

</script>

<%

DotnetObjectParameterData loData =

  (DotnetObjectParameterData) this.Context.Items["Content"];

 

this.oObject = loData.oObject;

this.oMethod = loData.oMethod;

this.cTitle = loData.cTitle;

%>

<html>

<head>

<title><%= this.oMethod.cName %></title>

<h2><%= this.oObject.cName %> :: <%= this.oMethod.cName %></h2>

<p>

<%= this.oMethod.cHelpText %>

<p>

<table …>

<tr>

  <td width="100" valign="top" align="right" class="labels">
     <
p align="right">Syntax:

  </td>

  <td bgColor="#eeeeee" style="font: bold bold 10pt 'Courier New'">
      <
b>o.<%= this.oMethod.cName%>(<%= this.oMethod.cParameters %>)</b></td>

</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 object references that have been set up for the script page class – oObject and oMethod. These are assigned from the Context item with:

DotnetObjectParameterData loData =

  (DotnetObjectParameterData) this.Context.Items["Content"];

 

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 object we can cast it and simply retrieve the properties we're interested in which are the object, method and project title.

 

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 object in the desktop application to perform all logic and use only a couple of lines of code in the script to retrieve the appropriate parameter data and then retrieve the data to display; clear separation of the user interface layer and the business logic layer while providing an attractive, extensible and configurable user display for the user.

 

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 subject for a future article (as there are some problems with the COM imported WebBrowser control).

Scripts away

I 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 Strahl 

 

Comments? Questions? Suggestions?

As always, if you have any questions or comments about this article please post them on our message board at:
http://www.west-wind.com/wwThreads/default.asp?forum=Code+Magazine.

 

Code for this article:

The source code for this article includes the wwAspRuntimeHost, wwAspRuntimeProxy and wwWorkerRequest classes in a separate project. All the samples are also included including the wwReflection classes and several support classes in the sample project. The samples run with version 1.0 and 1.1 of the .Net framework. You can download the files from:

http://www.west-wind.com/presentations/aspnetruntime/aspnetruntime.zip

 

References

Much of the material in this article was compiled by searching various online articles on the Web since the MSDN documentation on this subject is very slim at best. Although this list is not complete by any means the following resources were of the most help.
 

The following article was the best starting resource on ASP.Net hosting as it combined information from various sources into a short document:

http://radio.weblogs.com/0105476/stories/2002/07/12/
executingAspxPagesWithoutAWebServer.html

by Jim Klopfenstein

  

The following article discusses issues on hosting a Web Server without IIS and hooking in the ASP.Net runtime:
http://www.clrgeeks.com/Papers/HostingASPNET/HostingASPNET.html
by Ted Neward

 

The Reflector  Assembly viewer and Decompiler from Lutz Roeder has been extremely useful in finding more info on how CreateApplicationHost works.
http://www.aisto.com/roeder/dotnet

 

Document Updates:
07/20/2005
Updated the code for support of .NET 2.0. Reverted back to using stock ASP.NET's CreateApplicationHost() to start runtime, and added a ShadowCopyAssemblies property to allow auto-copying of assemblies into the Web application's bin directory. Note this has changed the interface of the class slightly. Please see samples for updated code.

07/23/2003
Updated document and code for ASP.Net operation by adding cApplicationBase directory. Set this property to the BIN directory of the ASP.Net application and include the wwASPRuntimeHost.dll file there or alternately move the DLL into the GAC.

02/04/2004
Updated to show running inside of ASP.Net and a few other special situations

05/23/2003
Updated document and code for support for POST operations.

11/30/2002
Original document posted.

 

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

 

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 |