People often think of HTML 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 (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 <script runat="server"> syntax as long as it runs in a single page. You also need to use the appropriate <@Assembly> and <@Namespace> 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\test.aspx." You can also pass an optional querystring 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.
Listing 2 shows an example of a simple script page that is a TextRepeater?you type in a string on the form and the script page repeats the text on the form as many times as you specify in the querystring. Figure 1 shows the output form displayed in a Web Browser control.
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 you can then access 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");
Query strings are encoded key value pairs in the same format as Web page query strings and this example sends two keys?company and repeat. The script page uses the Repeat value:
<%
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 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) %>
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. SCRIPT_NAME and APPL_PHYSICAL_PATH, which do return useful values, you might easily use in your application.
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.
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 that you generate the HTML page that you want to render into the base path so that the image pathing works when rendering the form. I used:
loHost.cOutputFile =
Directory.GetCurrentDirectory() +
@"\WebDir\__Preview.htm";
to generate the HTML file into same directory 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 execute faster. The overhead 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 you'll have fast performance, but if you click Unload (which calls the Stop() method), the application unloads the runtime and you must reload it on the next hit which again incurs the 2-4 second startup time. Each time you unload the runtime, ASP.NET must recompile each page.
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 runtime must recompile the page so the first hit is a little slower again. You can also edit the script page in an external editor like VS.NET.
Once you've loaded a program or script into an AppDomain you cannot unload it unless you unload the AppDomain. 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?it takes a script page and turns it into a class that it compiles and runs 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 look at a more advanced and useful example later.
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 startpath and call it WebDir, or HTML or Templates. So while working on this project I'll use something like: D:\projects\AspNetHosting\bin\debug\WebDir\.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\test.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.
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 Querystring 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 Forloop 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 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.
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 Contextobject. Note that you must mark any objects and subobjects passed in this fashion 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, 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 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 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
Once you've loaded a program or script into an AppDomain you cannot unload it unless you unload the AppDomain.
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.
If you don't have an existing Visual Studio project you can still use the debugger against the Executable.
<VS Path>\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 scriptmaps 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.
The application works by selecting an assembly (a DLL or EXE) to import, which fills the Treeview on the left with data from the class. 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. wwReflection also performs post parsing of the retrieved values such as retrieving the XML documentation and formatting parameters and other text into formats suitable to display as documentation. The detailed method info in Figure 3 demonstrates some of the parsed information available.
The sample uses wwAspRuntimeHost 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 receive a passed object that contains the relevant information to display.
wwReflection handles all the member parsing for classes, methods, and properties, which pulls all members into local properties of objects. These local properties contain minimal formatting to display member information such as parameters, etc. TypeParser has an array of aObjects, each object has an array of aMethods and aProperties, and so on.
The ClassDocs form contains the LoadTree() method, and demonstrates the rather simple class behavior (shown in Listing 10).
The oParser.GetAllObjects() method parses the entire assembly into internal properties of the TypeParser class. aObjects[] is populated with all classes, and the aMethods, 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. 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 properties gets 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 when you select a node to retrieve the object and method to pass to the script page the code gets parsed out.
When the form loads, the code initializes the wwAspRuntimeHost class.
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();
It initializes and starts the ASP.NET runtime. Each click on a TreeNode then fires code to call a specific template. Listing 11 shows how to render a class method.
The code checks to see which type of member to call based on the ImageIndex?image 1 is a method. Next, the code creates a new DotnetObjectParameterData object to act as a parameter container for a number of other objects that you 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 DotnetObjectParameterData you can assign appropriate properties for each type of member and pass the information using the ParameterData object found in the wwAspRuntimeHost class. Inside the script page you can pick up the parameter and use it within the page. For the method script, Listing 12 shows the header/startup code. Note that I've modified and truncated this script significantly for brevity and clarity.
HTML scripting isn't only for Web-based applications.
To use the imported classes you must include the assembly and namespace. In this case, AspNetHosting.exe contains all classes referenced but with most real world solutions you'll likely have several assemblies that you need to reference. The code assigns a couple of local object references to set up the script page class?oObject and oMethod, which get assigned from the Context item.
DotnetObjectParameterData loData =
(DotnetObjectParameterData)
this.Context.Items["Content"];
This is the parameter that received the oHost.ParameterData member on the form and is now available to your script page. Once you have a reference to this object you can cast it and simply retrieve the properties you're interested in: the object, method, and project title.
To display each of these values you can now simply embed expressions into the ASP.NET page, such as <%= this.oMethod.cHelpText %>.
You can now use your 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. The solution provides clear separation of the user interface layer and the business logic layer while providing an attractive, extensible, and configurable user display for the user.
ASP.NET lets you use the full power of scripting in your pages. For example, you could have the Class page use 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.
The ASP.NET Runtime provides all the power of ASP.NET for plugging into your own applications without using IIS.
In addition, you can make your HTML display interface somewhat interactive if you handle hyperlinks in the HTML display to fire actions in your application, and you use the BeforeNavigate2 event of the Web Browser control. But that's a subject for a future article (as there are some problems with the COM imported Web Browser 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. At the same time, you can continue to use Windows Forms controls where they make the most sense?for data entry and validation, which HTML does not handle as well. For example, the real world application I built that this article's assembly documentation sample is based on uses templates to display help topic content and rendering the final HTML output that gets compiled into HTML Help files, but I do all the editing of the topic data 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.
I can think of 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?
As always, if you have any questions or comments about this article, please post them on the message board at: http://www.west-wind.com/wwThreads/default.asp?forum=Code+Magazine.