Click here to Skip to main content
15,885,947 members
Articles / Programming Languages / XML
Article

Giving desktop applications a Web look

Rate me:
Please Sign up or sign in to vote.
4.17/5 (23 votes)
14 May 200311 min read 189.4K   1.9K   95   36
This article provides some information on how to write standalone desktop applications with Web look. It also provides a framework to simplify the task.

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:

  1. Use standard Windows controls and mimic the behavior of web pages
  2. 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):

C#
using System;
using System.Windows.Forms;
using AxSHDocVw;
using mshtml;
namespace ShowHowToWriteToDocument

{ 
    public class ShowHowToWriteToDocument { 
        AxWebBrowser m_webBrowser = null; 
        // Here    goes the code that initializes the form and so on 
        // ............................
        // 
        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.

C#
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)
        {
            //Do something here
        }
        public bool HandleAnotherEvent(int someInformation)
        {
            //Do something here
            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;

        // Here goes the code that initializes the form and so on
        // ............................
        //

        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
<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:

C#
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:

C#
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:

C#
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:

XML
<?xml version="1.0" encoding="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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMsHtmHstInterop Pin
MunissoR22-May-03 0:28
MunissoR22-May-03 0:28 
GeneralRe: MsHtmHstInterop Pin
Stefan Popov22-May-03 21:44
Stefan Popov22-May-03 21:44 
GeneralRe: MsHtmHstInterop Pin
MunissoR22-May-03 22:17
MunissoR22-May-03 22:17 
GeneralI don't agree Pin
Chopper21-May-03 2:56
Chopper21-May-03 2:56 
GeneralRe: I don't agree Pin
c-smile4-Sep-03 9:16
c-smile4-Sep-03 9:16 
GeneralThanks Pin
ray turner20-May-03 16:12
sussray turner20-May-03 16:12 
Generalhosting MSHTML from the inside Pin
Ægidius Ahenobarbus20-May-03 0:39
Ægidius Ahenobarbus20-May-03 0:39 
GeneralRe: hosting MSHTML from the inside Pin
WorldMaker20-May-03 6:13
WorldMaker20-May-03 6:13 
That seems to be what Microsoft does... Have you seen the Outlook "start page" source code? (It is available on MSDN somewhere...) It uses OBJECTS and databinding to provide the components you see on the page.

I would really like to see your code and see if I can get any ideas from it. An article would be great.
GeneralI don't agree Pin
Lee Alexander16-May-03 22:40
Lee Alexander16-May-03 22:40 
GeneralRe: I don't agree Pin
Stephane Rodriguez.16-May-03 22:55
Stephane Rodriguez.16-May-03 22:55 
GeneralRe: I don't agree Pin
Stefan Popov17-May-03 1:33
Stefan Popov17-May-03 1:33 
GeneralRe: I don't agree Pin
User 24859020-May-03 2:55
User 24859020-May-03 2:55 
GeneralRe: I don't agree Pin
Anonymous20-May-03 7:04
Anonymous20-May-03 7:04 
GeneralRe: I don't agree Pin
Ægidius Ahenobarbus4-Dec-03 13:32
Ægidius Ahenobarbus4-Dec-03 13:32 
GeneralThere are many ways not only two... Pin
c-smile15-May-03 6:21
c-smile15-May-03 6:21 
GeneralRe: There are many ways not only two... Pin
Stefan Popov15-May-03 7:46
Stefan Popov15-May-03 7:46 
GeneralRe: There are many ways not only two... Pin
c-smile3-Sep-03 19:15
c-smile3-Sep-03 19:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.