Introduction
Many recent programs have a web look. Take for example the login screen of Windows XP, the Control Panel in category view on XP, the User Accounts manager again on XP. Or the the start page of the Visual Studio .NET or its wizards. They all look like HTML pages (some of them, the last 2 for example, actually are). The reason for that is because HTML pages can look and feel really cool. Much better than the traditional dialogs.
How are such applications made?
There are 2 ways:
- Use standard Windows controls and mimic the behavior of web pages
- Use Internet Explorer (IE from now on) as an embedded ActiveX control
The first one is no good at all. It turns out that you spend most of the time writing code that aligns the controls, pictures, etc. A small change in the user interface design results in vast changes in the source code done by hand (no designer available when you align the controls by yourself). Microsoft has tried to solve this problem trough anchoring and docking. In my opinion they have failed.
So this leads us to the second choice - use the IE as an ActiveX control embedded in your application.
Using the IE control
There are 2 tasks to solve so that we can use the IE control: How to fill it with HTML and how to handle events from the Web page. Microsoft provides a solution for both called HTML dialogs. Unfortunately, it has many drawbacks. First, it is only for C++ and MFC. Second, the web page and its resources are located in the Win32 resources of the exe/DLL. This is not easy achievable in .Net. Another issue is that the event handling does not always work. Sometimes you press a button and it does not generate any events. In addition, the dialogs have a fixed look. You create them at design time and they do not change anymore. In conclusion: it is better to find some other method.
Filling the control with HTML
I will address the problem of generating HTML later. For now I will assume that the HTML for the dialog has somehow been generated. To fill the HTML in the control it is sufficient to get it's Document
object, and than use it's Write
function. Like this (see the WriteToDocument
function):
using System;
using System.Windows.Forms;
using AxSHDocVw;
using mshtml;
namespace ShowHowToWriteToDocument
{
public class ShowHowToWriteToDocument {
AxWebBrowser m_webBrowser = null;
public void WriteToDocument(string aString)
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.write(aString);
}
void ClearContent()
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.write("");
doc.close();
doc.write("");
}
bool OnClickHandler(IHTMLEventObj o)
{
MessageBox.Show("click");
return true;
}
public void AttachEvents()
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.writeln("<body><button name='button1'>button1</button>"
+ "<button name='button1'>button2</button></body>");
object button = doc.all.item("button1", 0);
HTMLButtonElementEvents2_Event buttonEvents =
(HTMLButtonElementEvents2_Event)button;
buttonEvents.onclick += new
HTMLButtonElementEvents2_onclickEventHandler(OnClickHandler);
}
}
}
Note: you have to navigate to about:blank prior to using the write
function of the document. Otherwise the Document
object will be null
. To clear the document's content use the ClearContent
.
Handling events
is more complicated. Pieces of .NET code have to be attached to events of controls on the web page. This can be achieved using the mshtml.IHTMLDocument2
interface. See the AttachEvents
and OnClickHandler
functions in the example above. This is the mechanism used to capture events in the HTML dialogs I was talking about previously. It has some serious drawbacks. I have noticed that it takes a lot of time (near 2 seconds on my machine) to attach the first event handler. Extended testing has showed that there is a bug in the MSHTML library and sometimes the events are not attached. Another thing is that you have to refer to the element by name. Usually this name is hard-coded and once you write the routine that installs the event handler you can not easily change the name of the HTML element on the page - you have to change it your C# code as well. Fortunately there is another way. It is the window.external
object. Microsoft has provided a way of JavaScript code in the HTML page to invoke methods of the hosting application (the application which hosts the IE control). With it the programmer no longer has to hardcode the names of the controls in his program. Instead he provides functions to the window.external
object and they get called by JavaScripts attached to events of the controls. See the example below.
using System;
using System.Windows.Forms;
using AxSHDocVw;
using mshtml;
using MsHtmHstInterop;
using System.Runtime.InteropServices;
namespace ShowHowToUseWindowExternal
{
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TheWindowExternalInterface
{
[DispId(0)]
void HandleSomeEvent(string someInformation);
[DispId(1)]
bool HandleAnotherEvent(int someInformation);
}
public class TheWindowExternalImplementation : TheWindowExternalInterface
{
public TheWindowExternalImplementation() {}
public void HandleSomeEvent(string someInformation)
{
}
public bool HandleAnotherEvent(int someInformation)
{
return true;
}
}
public class DemonstrateWindowExternal
{
public class DocHostUIHandlerImpl : IDocHostUIHandler
{
object m_external;
public DocHostUIHandlerImpl(object aExternal)
{
m_external = aExternal;
}
public void EnableModeless(int fEnable) { }
public void GetOptionKeyPath(out string pchKey, uint dw)
{ pchKey = null; }
public void TranslateAccelerator(ref MsHtmHstInterop.tagMSG lpmsg,
ref System.Guid pguidCmdGroup, uint nCmdID) { }
public void FilterDataObject(MsHtmHstInterop.IDataObject pDO,
out MsHtmHstInterop.IDataObject ppDORet) { ppDORet = null; }
public void OnFrameWindowActivate(int fActivate) { }
public void UpdateUI() { }
public void ShowContextMenu(uint dwID,
ref MsHtmHstInterop.tagPOINT ppt,
bject pcmdtReserved, object pdispReserved)
{
throw new COMException("", 1);
}
public void TranslateUrl(uint dwTranslate, ref ushort pchURLIn,
System.IntPtr ppchURLOut)
{
throw new COMException("", 1);
}
public void ShowUI(uint dwID,
MsHtmHstInterop.IOleInPlaceActiveObject pActiveObject,
MsHtmHstInterop.IOleCommandTarget pCommandTarget,
MsHtmHstInterop.IOleInPlaceFrame pFrame,
MsHtmHstInterop.IOleInPlaceUIWindow pDoc) { }
public void GetExternal(out object ppDispatch)
{
ppDispatch = m_external;
}
public void ResizeBorder(ref MsHtmHstInterop.tagRECT prcBorder,
MsHtmHstInterop.IOleInPlaceUIWindow pUIWindow, int fRameWindow)
{ }
public void GetDropTarget(MsHtmHstInterop.IDropTarget pDropTarget,
out MsHtmHstInterop.IDropTarget ppDropTarget)
{ ppDropTarget = null; }
public void GetHostInfo(ref
MsHtmHstInterop._DOCHOSTUIINFO pInfo) { }
public void HideUI() { }
public void OnDocWindowActivate(int fActivate) { }
}
AxWebBrowser m_webBrowser = null;
public void InstallWindowExternal()
{
TheWindowExternalImplementation exObj;
exObj = new TheWindowExternalImplementation();
ICustomDoc custDoc = (ICustomDoc)m_webBrowser.Document;
custDoc.SetUIHandler(new DocHostUIHandlerImpl(exObj));
}
}
}
Again - to use the InstallWindowExternal
function you have to navigate to some web page first - for example about:blank. See the MSDN if you are interested in more details of the IDocHostUIHandler
and ICustomDoc
interfaces. To use them you should have the Interop assembly MsHtmHstInterop.dll which can be found in the example. The IDocHostUIHandler
is a legacy COM interface. And it expects an HRESULT
to be returned from it's methods. Normally the .NET returns S_OK
. In the case of TranslateUrl
and ShowContextMenu
you need to return S_FALSE
. This is done by throwing a COMException
. Below is an example of how to use the window.external
object from the HTML page.
<html>
<body>
<a href="javascript:window.external.HandleSomeEvent('someInfo')">
Invoke window.external.HandleSomeEvent('someInfo')
</a>
<a href="javascript:if(window.external.HandleAnotherEvent(5))
alert('true');">
Invoke window.external.HandleAnotherEvent(5)
</a>
</body>
</html>
Some unresolved issues
Until now we haven't talked about the HTML generation. One way is to store it somewhere, read it and fill it in when needed. This works for some applications but the HTML pages displayed in this way will be static. It would be nice to use some kind of framework for generating dynamic WEB pages. Some other issues:
- All links in the HTML have to be absolute as the page doesn't really exist. This means that your program will have to fix all links before filling in the HTML.
- All your content (pictures, styles ...) have to exist physically (on some file system). You will not be able to generate pictures dynamically or show resources that are for example in the embedded resources of your application.
- You can not use the browser's history capabilities.
The solution
What this class library offers as a solution is a small web server. It solves all the problems described above.
Architecture of the server
The web server has a plug-in architecture. The server acts as a container and it is up to you to provide it with the plug-ins. All the work is done by them. The plug-ins should provide 3 functions: Resolves
, Answer
and GetResourceAsStream
. When the web server receives a request it starts walking all installed plug-ins. It invokes their Resolves
function with the received request as a parameter. Should this function return true, the web server invokes the plug-in's Answer
function which is responsible for generating (or reading from somewhere) the HTML and sending it back to the client. There is one more function that these plug-ins should implement - GetResourceAsStream
. This function is used to access resources using their virtual addresses (/view/stf.html is a virtual address for example). GetResourceAsStream
is used only for static content - such that that depends only on the file name and not on the request parameters. It is somewhat similar to the Server.MapPath
in ASP. If you need some resource in your application you can access it through the GetResourceAsStream
function of the web server. It works by traversing all installed plug-ins and calling their GetResourceAsStream
function until one of them returns a value different from null
. From now on I will call these plug-ins, resolvers.
Predefined resolvers
There are some resolvers at your disposition that you can use. They fall into 2 categories : Dynamic content resolvers and static content resolvers. The static resolvers are used to serve the HTML server for static content. Their work is to map virtual addresses to physical ones. There are 2 static resolvers in the library: One for resources located on physical file systems and one for resources embedded in the application. There is only one dynamic content resolver. It serves as a container for user defined servlets. I will come back later on it. Below is an example of how to start the server and add 2 resolvers to it:
using System;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace ShowHowToUseServer
{
class ShowHowToUseServer
{
WebServer m_server;
public void Start()
{
m_server = new WebServer(8080);
ContentLocationResolver clr = new
ContentLocationResolver("c:\temp", "phys");
m_server.AddResolver(clr, 10, -1);
EmbeddedLocationResolver elr;
elr = new EmbeddedLocationResolver(
Assembly.GetExecutingAssembly(),
"stf.res", "emb");
m_server.AddResolver(elr, 10, 20);
m_server.Start();
}
}
}
After you execute the Start
method you will have a running web server. You can start IE and connect to the server. Like this: http://localhost:8080/phys/stf.htmlfor example. Assume that there is a subdirectory of temp called view and there is a file in it called stf.html. If you type http://localhost:8080/phys/view/stf.html, this file will be displayed in the Explorer. The request will be handled by the first resolver. If you call m_server.GetResourceAsStream("/phys/view/stf.html") you will get a read only stream pointing to c:\temp\view\stf.html. Now assume you have an embedded resource with the name stf.res.anotherview.file.html. If you request http://localhost:8080/emb/anotherview/file.html this resource will be displayed in the browser. In the example above when adding a resolver to the server you specify 2 numbers. They are the number of pooled threads and the maximum number of threads. The server is multi threaded. Once it finds a resolver that can handle a request it starts it's Answer
function in a different thread. There are a number of threads that are created when the server starts and are put to sleep. I call them pooled threads. When a request comes, the server wakes one of them and passes it the request. If all of them are currently busy (none is sleeping) the server creates a new thread and answers the request in it. After that the thread is destroyed. The number of pooled threads + the number of free threads can not exceed the maximum number of threads. If all pooled threads are busy and the server can not create any extra threads, the request is queued and is processed as soon as a thread becomes available. If you want to run some resolver in single threaded specify 1, 1 as parameters to AddResolver
.
Dynamic resolvers and servlets
There is currently only one dynamic resolver in the framework. It serves as a container for servlets. It is up to the programmer to write them. A servlet is a class implementing the IServletPage
interface. Usually you derive your servlet classes from the ServletPageBase
abstract class. When writing a servlet you should implement the Address
property, and the Answer
method. Writing HTML from servlets is usually messy. That is why ASP pages are used for example. This engine does not support ASP pages but it provides you with an alternative : XSLT servlet pages. To use them you should override the XsltServletPageBase
or the XsltServletPage
class (the first class gives you some extra freedom). You should implement the getXML
function and provide the file name of transformation (as a virtual address). When the user tries to see your page, the server will first get the XML through the getXML
function and than transform it to HTML using the supplied XSL transformation. The advantage is that the data becomes completely separated from the visualization. You write and test your getXML
function first and after you are sure it does what it should, you write the XSLT. Another advantage is that you can change the XSLT while the server is running and see the changes without having to restart it. Below are 2 examples: one for a plain servlet and one for a XSLT servlet.
The plain servlet:
using System;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace PlainServletExample
{
class PlainServletExample : ServletPageBase
{
public PlainServletExample() {}
public override string Address
{
get
{
return "/test/test.sfrm";
}
}
public override void Answer
(Vitamin.Research.WebFramework.WebRequest aRequest)
{
aRequest.Response.WriteLine("<html><body>"
+ "A plain servlet example</body></html>")
}
}
}
The XSLT servlet:
using System;
using System.Xml;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace XsltServletExample
{
class XsltServletExample : XsltServletPage
{
public XsltServletExample() {}
public override string XslTransformName
{
get
{
return "/view/XsltTest.xslt";
}
}
public override XmlDocument getXML(WebRequest aRequest)
{
XmlDocument xdoc = new XmlDocument();
XmlElement elPage = xdoc.CreateElement("page");
xdoc.AppendChild(elPage);
for(int i = 0; i < 100; i++)
{
XmlElement el = xdoc.CreateElement("number");
XmlAttribute attr = xdoc.CreateAttribute("value");
attr.Value = i.ToString();
el.Attributes.Append(attr);
elPage.AppendChild(el);
}
return xdoc;
}
public override string Address
{
get
{
return "/view/XsltTest.xfrm";
}
}
}
}
The XsltTest.xslt file:
="1.0" ="UTF-8"
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<body>
<xsl:for-each select="page/number">
<xsl:value-of select="@value"/>
<br/>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Note that in order for the above example to work you should have some static resolver that resolves /view/XsltTest.xslt to the file above. To add servlets to the dynamic resolver use the AddPage
and AddAllPages
functions. The second function searches the assembly passed as parameter for classes marked with the ServletPage
attribute, creates an instance of each of them and then adds them to the resolver via the AddPage
function.
Tips on debugging servlets
Plain servlets are debugged just as you debug a program. This is not the case with XSLT servlets. I still haven't found a good XSL transformation debugger so the technique I use is to write messages in the output. You could eventually ease your self if you write an extension object for the XSLT with a function that prints messages to the debug console via Debug.Write
. Another problem is that you can not see the XML generated by your getXML
function. To be able to do so you can set XsltServletPageBase.XMLDumpPath
to some path (on the hard disk) and the server will dump the generated XML there. Set XsltServletPage.DebugReload
to true to have the server reload your XSLT file every time before transformation. Otherwise it will be loaded only once and kept in memory. You can debug parts (certain pages) of your program using a standalone explorer.
Drawbacks and possible improvements
One of the things that has not been thought of is security. In this release anyone from any computer can connect to the running server. A good improvement would be to add ASP support. The hardest part of it is to create the servlet (source code) from the asp. Compiling it afterwards is easy using the compilers provided with .NET.