Inside F#

Brian's thoughts on F# and .NET

An RSS Dashboard in F#, part five (Javascript, COM interop, and WCF services – oh my!)

Posted by Brian on February 15, 2010

Previous posts in this series:

Last time I covered using an F# agent, along with the .Net speech synthesis libraries, to provide an easy API to communicate via the computer speakers without having to worry about threading issues.  Today I’m switching gears again, with the oh-so-lofty goal of finding out whether URL links should be colored blue or purple in my UI.  The implementation will use COM interop and Javascript (two technologies I had almost zero previous experience with), as well as a WCF service (which I know well).  Fortunately, my tools will make this easy on me; F#+.Net is a winning combination to get most any job done.

The goal

Let me quote myself from the introductory entry of this blog series:

I really wanted the links in my app to be the same color as the links in my browser.  But I also wanted my entire UI to be hand-coded WPF (I didn’t want to create HTML and host that in a browser, that is not fun/interesting to me).  So I needed a way to get IE to tell me what color each link should be.  Perhaps there is a simple(r) way to do this, but I did find a way that involved hosting an invisible IE control that browses some HTML-and-Javascript my app serves to get the info it needs.  It’s one of those hacks that you know is awful, but you nevertheless grin about, because it works.  (I’d never written a line of Javascript before this – more unexpected new technology.  Score!)

That about sums it up.  You can see from the colors in the screenshot from part one what I desired, but this requires knowing which links are ‘visited’ and which are not, so I can color each link appropriately.  It turns out, this information can be had via unusual means…

The strategy

I recalled a while back seeing a trick that made the rounds as a brief internet meme, where you’d visit a web site and the site would tell you information about your own browsing history.  You can find a good description of the underlying technique here.  The idea is you have some Javascript on your page that runs in the client browser and discovers whether the browser thinks a link is visited or not.  I don’t know any Javascript, but I have friends who do, who helped me hack together just enough to do what I needed.  In the end I had a Javascript function that takes a URL as a parameter and returns true or false depending on if the link is ‘visited’.  (I don’t know if the Javascript is portable, but it works on my machine at work in IE8.  Which is sufficient, this was all about hacking a useful app for my own selfish interests.)

I originally tried just saving the HTML-with-Javascript as a local “file:///blah…” URI, but for whatever reason (internet security options? I never spent enough effort trying to diagnose it) that didn’t work.  Fortunately, as a veteran developer from the WCF team, I can write a quickie HTTP server in 10 lines of code, and sure enough, serving my page as “http://localhost…” worked.  So I took the path of least resistance, even though it’s total overkill.

Finally, I needed a way to run the Javascript programmatically from my app (rather than in an actual instance of a browser).  Once again, the platform has APIs for all of this; I had to poke around a bit, but found that adding COM References (in Visual Studio, “Add Reference”, then go to the “COM” tab) to “Microsoft Internet Controls” and “Microsoft HTML Object Library” gave me access to some ugly COM objects going by names like “SHDocVw.InternetExplorerClass” and “mshtml.IHTMLDocument”, which had the APIs to create a browser control, navigate to my document, get the Javascript running in the context of the browser, and call it from my app.

The code

Here’s all the code for today, followed by some commentary.

open System
open System.Reflection 
open System.Runtime.InteropServices 
open System.ServiceModel
open System.ServiceModel.Web
open System.IO 

[<ServiceContract>]
type MyContract() =
    // Javascript that can be used in IE8 to determine if a Uri is visited
    let magicHtml = @"
    <!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 4.0 Transitional//EN"">
    <html><head><script type=""text/javascript"" language=""javascript"">
            function IsUriVisited(uri) {
                var iframe = document.createElement(""iframe"");
                iframe.style.visibility = ""hidden"";
                document.body.appendChild(iframe);
                iframe.doc = iframe.contentWindow.document;
                iframe.doc.open();
                iframe.doc.write('<style>');
                iframe.doc.write(""a{color: #000000; display:none;}"");
                iframe.doc.write(""a:visited {color: #0000FF; display:inline;}"");
                iframe.doc.write('</style>');
                iframe.doc.close();
                var a = iframe.doc.createElement(""a"");
                a.href = uri;
                a.innerHTML = ""blah"";
                iframe.doc.body.appendChild(a);
                var didVisit = a.currentStyle[""display""] != ""none"";
                iframe.parentNode.removeChild(iframe);
                return didVisit;
            }
        </script></head><body></body></html>"
    [<OperationContract>]
    [<WebGet(UriTemplate="*")>]
    member this.Get() : Stream =
        upcast new MemoryStream(System.Text.Encoding.UTF8.GetBytes(magicHtml))

type IEVisitedLinksDiscoverer() =
    let mutable resultType = null
    let mutable htmlDoc = null
    let mutable script = null
    let mutable host = null
    do
        // We need to serve that Javascript HTML on a web page (rather than local file) 
        // to get the script to run.  So host a local web service to serve the HTML.
        let javascriptHtmlLocation = "http://localhost:64385/"  // TODO ideally this would pick an open port or let user configure it
        host <- new WebServiceHost(typeof<MyContract>, new Uri(javascriptHtmlLocation))
        host.AddServiceEndpoint(typeof<MyContract>, new WebHttpBinding(), "") |> ignore
        host.Open()
        // COM Reference: Microsoft Internet Controls
        let ie = new SHDocVw.InternetExplorerClass()
        ie.Navigate(javascriptHtmlLocation)
        while (ie.Busy) do
            System.Threading.Thread.Sleep(500);
        // COM Reference: Microsoft HTML Object Library
        htmlDoc <- ie.Document :?> mshtml.IHTMLDocument
        script <- htmlDoc.Script
        resultType <- script.GetType()
    // must be called on STAThread
    member this.IsUriVisited(uri:string) =
        resultType.InvokeMember("IsUriVisited", BindingFlags.InvokeMethod, null, script, [| box uri |]) :?> bool
    interface IDisposable with
        member this.Dispose() =
            Marshal.ReleaseComObject(script) |> ignore
            Marshal.ReleaseComObject(htmlDoc) |> ignore
            try host.Close() with e -> ()

The big red string constant is the HTML document I need to serve.  It’s just a document skeleton with a Javascript function in it.  The Javascript does some black magic to compute if a particular URL is a visited link or not.

I can serve this document over HTTP GET on localhost with a tiny WCF web service.  To describe what I want to serve, I created a tiny MyContract class, with a single Get() method that returns the document as a Stream.  I mark up the class with the appropriate WCF attributes.  That’s just 6 lines of code in addition to my giant string constant.

Now for the API I want clients to use.  I create a class called IEVisitedLinksDiscoverer.  It just exposes one method, IsUriVisited().  When you construct an instance of this class, it starts a WCF server (3 more lines of code – the ones starting with “host”) on a crazy (likely-to-be-unused) port number.  Then it news up an instance of the IE control, navigates to the page, grabs the document, and gets the script out of the document.  Now we have a handle by which we can call the Javascript via reflection.  (To figure out these few lines of code, I did a web search, found some code that looked promising, and then tweaked it until it worked.  I was wearing my Mort hat.)

The IsUriVisited() function uses the .Net reflection APIs to call the Javascript function stored inside the script object.  I don’t know exactly what magic is happening under the covers here, but it works.  Hurray.

The ‘discoverer’ object implements IDisposable so we can be good citizens and clean up when we’re done.  This includes releasing the COM objects we created and shutting down the WCF service.  I’m ignoring any error code or exceptions that happen here (since I’m only going to Dispose() when the app finishes running).

Here’s some tiny client code that demonstrates how this can be used to find out which links are visited:

let Main() =
    use disc = new IEVisitedLinksDiscoverer()
    printfn "%A" (disc.IsUriVisited("http://stackoverflow.com/questions/tagged/f%23"))
    printfn "%A" (disc.IsUriVisited("http://www.google.com/"))

[<System.STAThread>]
do
Main()

which, of course, prints “true” and “false”.  I hereby pronounce this code “good enough”!

Summing up

There are times when you want to do thing “the right way”, “elegantly”, etc… and then there are times when you are happy to be quick-and-dirty, and just get something working.  F# is a good tool in both of these roles.  Today’s topic was all about quick-and-dirty; I knew what I wanted done, I envisioned one way to achieve that goal, and I just plowed through, combining three technologies in just one screenful of code.  I used Javascript to do a little browser-magic, COM interop to talk to some browser controls, and a WCF service to serve up my document over HTTP.

A great thing about F# is that the .Net platform integration makes it so easy to combine all these various technologies to knock up some code that gets the job done.

Next time

We’re almost done!  At this point in the blog series, we’ve walked through all the ‘guts’ of the application.  The only remaining thing to do is assemble all the pieces and slap a dazzling WPF UI on top of it.  App ahoy!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: