devxlogo

Using the ASP.NET Runtime to Extend Desktop Applications with HTML Scripts

Using the ASP.NET Runtime to Extend Desktop Applications with HTML Scripts

he ASP.NET runtime in .NET provides the ability to create rich HTML content for your applications dynamically in your desktop applications. It also provides a powerful mechanism for extending the functionality of applications through script code.

A few issues back (CoDe Magazine, Nov/Dec 2002) I introduced the topic of dynamic code execution, which is not trivial in .NET. My article generated questions from CoDe readers about how to use this technology in more sophisticated applications. Most of the questions centered around the apparently intriguing topic of ‘executing’ script pages that use ASP-style syntax. My “Dynamically Executing Code in .NET” article was so long that 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 as I show you another more powerful mechanism that’s built into the .NET Framework to provide an ASP.NET-style scripting host for client applications.

Hosting the ASP.NET Runtime
Microsoft made the .NET Framework very flexible, especially in terms of the various sub-systems that make up the core system services. Did you know that you can host the ASP.NET scripting runtime in your own applications? This has several benefits over the ASP-style parsing approach I featured in my last article.

Microsoft ships the ASP.NET runtime in the .NET Framework and has made the ASP.NET runtime a system component so you don’t have to install anything separately. The runtime is much more powerful than the simple script parser I previously introduced because the runtime supports just about everything that ASP.NET supports for Web pages including all installed and registered languages and ASP.NET-style Web Forms syntax. You can use the runtime to determine if you’ve previously compiled a page so you don’t have to recompile it each time. The ASP.NET runtime handles updates to pages automatically, and as an especially nice bonus you can debug your script pages using the Visual Studio .NET debugger.

As always with .NET internals, though, this power comes with a price?overhead and complexity. Visual Studio .NET offers a number of non-obvious ways to accomplish seemingly simple tasks, including passing parameters or leaving the runtime idle for a while. I’ll introduce a set of classes that simplify this process down to a few lines of code. I’ll also show you the key things that you need to know and implement.

You’ll find that you can call the ASP.NET runtime from any .NET applications. Follow these three major steps:

1. Set up the runtime environment.

You’ll tell 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 you’ll set up a new AppDomain that the runtime can execute in.The ASP.NET runtime executes in another AppDomain and all information transmitted between your app and it run over the remoting features of .NET.

2. Create the script page.

You will create a single page that contains ASP.NET code. This means you can create pages that contain , , and syntax as long as it runs in a single page. You also need to use the appropriate and inclusion tags. The script pages can access the current application directory and all assemblies accessible to the current app.

3. Call the script page to execute.

You need to tell 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 you will pass 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. Listing 1 shows the code to run a single ASP.NET request from a disk-based script file.

You start by instantiating the runtime object and setting the physical disk path where you're hosting the ASP.NET application?you'll put scripts and other script content such as images into this same directory. The Start method launches the ASP.NET runtime in a new AppDomain. My class delegates this process to a Proxy class, AspRuntimeHostProxy, which actually performs all the work. My wwAspRuntimeHost class is simply 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 problems remoting over AppDomain boundaries.

Once you've called the Start() method 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. You can use any relative path to an ASP.NET page using syntax like "textRepeater.aspx" or "subdir est.aspx." You can also pass an optional query string made up of key value pairs that the ASP.NET page can retrieve. My example serves as a simple though limited parameter mechanism. I'll discuss 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 the ASP.NET runtime has parsed?in most cases the HTML result from your script. Although you'll typically generate HTML for display in some HTML rendering format like a Web browser or a Web Browser control (see Figure 1), you can generate output for anything. I often use templates for code and documentation generation that is not HTML.

How It Works
I've based my wwAspRuntimeHost class on a couple of lower level classes?wwAspRuntimeProxy which acts as the remoting proxy reference for the ASP.NET runtime, and wwWorkerRequest which is a subclass of the SimpleWorkerRequest class that I use to handle passing parameters to script pages. The application talks only to the wwAspRuntimeHost class, which acts as a wrapper around the Proxy class to provide error handling for remoting problems since the proxy is actually a remote object reference.

wwAspRuntimeProxy does most of the work, performing the nuts and bolts operation of setting up and calling the ASP.NET runtime. It first creates a new Application Domain for the runtime to be hosted in. Microsoft provides a static method, ApplicationHost.CreateApplicationHost, that provides this functionality. Unfortunately, this behavior is not very flexible and exactly what's required is not very well documented. For this reason and after a fair amount of effort spent searching for a more flexible solution, I decided to create my own AppDomain and load the runtime into it. This allows considerably more configuration concerning where the Runtime finds support files (in the code below in the main application's path) and how I can configure the host 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 3 shows the code to create an ASP.NET-capable AppDomain. The class methods I describe are all part of the wwAspRuntimeProxy class which you can find in the sample code for this article.

Three methods represent the main management methods of the wwAspRuntimeProxy class. CreateApplicationHost 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 code in Listing 3 shows the minimal configuration required to set up an AppDomain for executing ASP.NET. Once the AppDomain exists, an instance of the runtime host class called loDomain.CreateInstance() will load. Now the ASP.NET runtime host exists and you can access it over AppDomain boundaries via .NET Remoting. Luckily, several built-in classes help with this process.

These three methods are static?you don't need an instance 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 you set several properties on it to allow calling applications to keep track of where the environment was loaded via the virtual and physical path. A local application's virtual path is nothing more than a label you'll see on error messages that ASP.NET will generate on script errors. You should put the value in a virtual directory format such as "/" or "/LocalScript." Your physical path should point to a specific directory on your hard disk that ASP.NET uses as the 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 startup path and call it WebDir, or HTML or Templates. So while working on this project I'll use something like: D:projectsAspNetHostingindebugWebDir.The trailing backslash is important by the way.

You need a class that can host ASP.NET?you must derive it from MarshalByRefObject in order to make it accessible across domains, and your derived class 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 an ASP.NET page in server relative pathing in the format of "test.aspx" or "subdir est.aspx."

To keep my description of this process simple, I've put both the static loader methods and the ProcessRequest method into the same class. When you call the Start() method 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.

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.

My wwAspRuntimeProxy class does two things: It creates an output file stream and it uses the SimpleWorker class to create a request that it can pass 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 minimal information to the ASP.NET processing engine: the name of the page to execute and a Query string along with a TextWriter instance to receive the output generated.

You want to pass the new instance of the Request to the HttpRuntime for processing, which, in turn, makes the actual ASP.NET parsing call. It's important to understand that you're executing this code 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.

Your application will return any errors that occur within the script code itself 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?no property gets set or error exception triggers 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.

Passing Parameters to the ASP.NET Page
So far I've shown you how to do basic scripting. However, when you build template-based applications it's not good enough to just process code in templates. You have to also be able to receive data from the calling application. In the previous examples you've been limited by small text parameters that you can pass via Query string. While you can probably use the Query string 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.

My idea of a desktop application that utilizes scripts dictates that the application performs the main processing while the scripts act as the HTML display mechanism. To do this I need to pass complex data to my script pages.

wwAspRuntimeProxy provides a ParameterData 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 you can then retrieve on a form. To call a script page with a parameter you do this:

   // *** 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);

But, I first need to do a little more work and make a few changes. SimpleWorkerRequest doesn't provide a way to pass properties or content to the ASP.NET page directly. However, I can subclass it and implement one of its internal methods that hook into the HttpRuntime processing pipeline. Specifically, I can implement the SetEndOfSendNotification() method to receive a reference to the HTTP Context object that is accessible to my ASP.NET script pages and assign an object reference to it. Listing 5 shows an implementation of SimpleWorkerRequest that takes the ParameterData property and stores into the Context object.

I'll 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 that you can access in your ASP.NET pages with:

   object loData = this.Context.Items["Content"];

And voil?! 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 object you can simply create a composite object and hand multiple object references off to the composite object to pass multiple items. Of course, you can also create a more complex class and add as many properties as you need to pass into the Context object. Note that you must mark any objects and sub-objects passed in this fashion 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 = "[email protected]";      public cPhones oPhones = null;      public cCustomer()       {         this.oPhones = new cPhones();      }   }

Alternately, you can derive a class from MarshalByRefObject to make it accessible over the wire:

   public class cPhones : MarshalByRefObject   {      public string Phone = "808 579-8342";      public string Fax = "808 579-8342";   }

Before you can utilize this functionality you need to change a couple of things in the wwAspRuntimeProxy class. First, you need to add a parameter called ParameterData that will hold the data you want to pass to the ASP.NET application. Next, you need to change the code in the ProcessRequest method to handle a customer worker request class to use the wwWorkerRequest class instead of SimpleWorkerRequest.

   wwWorkerRequest Request = new       wwWorkerRequest(Page, QueryString, loOutput);   Request.ParameterData = this.ParameterData;

You should also pass the ParameterData property forward. To execute a script with the object contained within it, check out the PassObject.aspx script page shown in Listing 6.

Please note a few important points here. Notice that you need to import the assembly and namespace of any classes that you want to use in the script. Since I declared the assembly in my main application (AspNetHosting.exe with a default namespace of AspNetHosting), 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 you imported the namespace and assembly, you can reference your value by its proper type and add it to a property that I added to the script page (oCust). To assign the value you must cast it to the proper cCustomer type.

   <%  this.oCust = (cCustomer)        this.Context.Items["Content"]; %>

Once I've done this, you can access this object as needed by using its property values. To embed it into the page you can 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() %>

You can easily execute business logic right within a script page! However, I recommend that you try to minimize the amount of code you run within a script page, rather than rely on it to provide the dynamic and customizable interface for the application. So, rather than passing an ID via the query string 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 that you want to display. You must make the object you want to pass in some way serializable to pass over the AppDomain boundaries.

Configuration
You'll find it easy to set up ASP.NET scripting. You can configure the application handling even more by using a web.config file as shown in Listing 7.

I suggest 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 Visual Studio environment, set a breakpoint in the script, then run the application. You'll hit the script and voil?, you can debug your script code with all of the Visual Studio debugging features.

Once you've loaded a program or script into an AppDomain you cannot unload it unless you unload the AppDomain.

If you don't have an existing Visual Studio project you can still use the debugger against the Executable.

   devenv /debugexe AspNetHosting.exe

Let Visual Studio create a new solution for you! Open the page to debug, set a breakpoint, and off you go. This is a very cool feature that you can offer to your customers as well, so they can more easily debug their scripts. Along the same lines, you can use the Visual Studio editor to edit your scripts as well, although you should try and stay away from all of the Web Forms-related stuff because that's meant for server side development. You can implement this, but frankly I think you'll be much better off dealing with these issues in your regular application code.

Second, when you build template-based applications you might prefer to use extensions other than ASPX for your scripts. 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 set, then you can process those files with those extensions through the scripting runtime. Unlike ASP.NET, you don't need script maps to make this work because you're in control of the HttpRuntime 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. 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 initial workaround was to create a wrapper class that simply makes passthrough calls to the Proxy object. This is the main wwAspRuntimeHost class that 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 8 shows the implementation of the wrapped ProcessRequest method. The first try/catch block performs the auto-restart of the runtime.

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 released 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 9.

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 every time 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.

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 sample application that documents 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. My sample application 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 3 shows an example of the running application that uses the Web Browser control to display each topic.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist