0 Comments

With my altogether too short break out of the way, its time for another post about something software related.

This weeks topic? Embedding websites into windows desktop applications for fun and profit.

Not exactly the sexiest of topics, but over the years I’ve run into a few situations where the most effective solution to a problem involved embedding a website into another application. Usually an external party has some functionality that they want your users to access and they’ve put a bunch of effort into building a website to do just that. If you’re unlucky, they haven’t really though ahead and the website is the only option for integration (what’s an API?), but all is not lost.

Real value can still be delivered to the users who want access to this amazing thing you have no control over.

So Many Options!

I’ve been pretty clear in the posts on this blog that one of things my team is responsible for is a legacy VB6 desktop application. Now, its still being actively maintained and extended, so its not dead, but we try not to write VB6 when we can avoid it. Pretty much any new functionality we implement is written in C#, and if we need to present something visual to the user we default to WPF.

Hence, I’m going to narrow the scope of this post down to those technologies, with some extra information from a specific situation we ran into recently.

Right at the start of the whole “hey, lets jam a website up in here” though process, the first thing you need to do is decide whether or not you can “integrate” by just shelling out to the website using the current system default browser.

If you can, for the love of all that is good and holy, do it. You will save yourself a whole bunch of pain.

Of course, if you need to provide a deeper integration than that, then you’re going to have to delve into the wonderful world of WPF web browser controls.

Examples include:

There are definitely other offerings, but I don’t know what they are. I can extrapolate on the first two (because I’ve used them both in anger), but I can’t really talk about the third one. I only included it because I’ve heard about it specifically.

Chrome Dome

CEFSharp is a .NET (both WPF and WinForms) wrapper around the Chromium Embedded Framework, and to be honest, its pretty great.

In fact, I’ve already written a post about using CEFSharp in a different project. The goal there was to host a website within a desktop application, but there were some tricksy bits around supplying data to the website directly (via shared Javascript context), and we actually owned the website being embedded, so we had a lot of flexibility around making it do exactly what we needed it to do.

The CEFSharp library is usually my first port of call when it comes to embedding a website in a desktop application.

Unfortunately, we we tried to leverage CEFSharp.WPF into our VB6/C# franken-application we ran into some seriously weird issues.

Our legacy application is at its core VB6. All of the .NET code is triggered from the VB6 via a COM interop, which essentially amounts to a message bus with handlers on the .NET side. VB6 raises event, .NET handles it. Due to the magic of COM, this means that you can pretty much do all the .NET things, including using the various UI frameworks like WinForms and WPF. There is some weirdness with windows and who owns them, but all in all it works pretty well.

To get to the point, we put a CEFSharp.WPF browser into a WPF screen, triggered it from VB6 and from that point forward the application would crash randomly with Access Violations any time after the screen was closed.

We tried the obvious things, like controlling the lifetime of the browser control ourselves (and disposing of it whenever the window closed), but in the end we didn’t get to the bottom of the problem and gave up on CEFSharp. Disappointing but not super surprising, given that that sort of stuff is damn near impossible to diagnose, especially when you’re working in a system built by stitching together a bunch of technological corpses.

Aiiiieeeeeee!

Then there is the built-in WPF WebBrowser control, which accomplishes basically the same thing.

Why not go with this one first? Surely built in components are going to be superior and better supported compared to third party components?

Well, for one, its somewhat dependent on Internet Explorer, which can lead to all sorts of weirdness.

A good example said weirdness if the following issue we encountered:

  • You try to load a HTTPS website using TL 1.2 through the WebBrowser control
  • It doesn’t work, giving a “page cannot be loaded error” but doesn’t tell you why
  • You load the page in Chrome and it works fine
  • You load the page in Internet Explorer and it tells you TLS 1.2 is not enabled
  • You go into the Internet Explorer settings and enable support for TLS 1.2
  • Internet Explorer works
  • Your application also magically works

The second pertinent piece of weirdness relates specifically to the controls participation in the WPF layout and rendering engine.

The WebBrowser control does not follow the same rendering logic as a normal WPF control, likely because its just a wrapper around something else. It works very similar to a WinForms control hosted in WPF, which is a nice way of saying it works pretty terribly.

For example, it renders on top of everything regardless of how you think you organised it, which can lead to all sorts of strange visual artefacts if you tend to use the Z-axis to create layered interfaces.

Conclusion

With CEFSharp causing mysterious Access Violations that we could not diagnose, the default WPF WebBrowser was our only choice. We just had to be careful with when and how we rendered it.

Luckily, the website we needed to use was relatively small and simple (it was a way for us to handle sensitive data in a secure way), so while it was weird and ugly, the default WebBrowser did the job. It didn’t exactly make it easy to craft a nice user experience, but a lot of the pain we experienced there was more the fault of the website itself than the integration.

That’s a whole other story though.

In the end, if you don’t have a horrifying abomination of technologies like we do, you can probably just use CEFSharp. Its solid, well supported and has heaps of useful features, assuming you handle it with a bit of care.

0 Comments

I did a strange thing a few weeks ago. Something I honestly believed that I would never have to do.

I populated a Single Page Javascript Application with data extracted from a local database via a C# application installed using ClickOnce.

But why?

I maintain a small desktop application that acts as a companion to a service primarily offered through a website. Why do they need a companion application? Well, they participate in the Australian medical industry, and all the big Practice Management Software (PMS) Systems available are desktop applications, so if they want to integrate with anything, they need to have a presence on the local machine.

So that’s where I come in.

The pitch was that they need to offer a new feature (a questionnaire) that interacted with installed PMS, but wanted to implement the feature using web technologies, rather than putting it together using any of the desktop frameworks available (like WPF). The reasoning behind this desire was:

  1. The majority of the existing team had mostly web development experience (I’m pretty much the only one that works on the companion application), so building the whole thing using WPF or something similar wouldn’t be making the best use of available resources, 
  2. They wanted to be able to reuse the implementation in other places, and not just have it available in the companion application.
  3. They wanted to be able to reskin/improve the feature without having to deploy a new companion application

Completely fair and understandable points in favour of the web technology approach.

Turtles All The Way Down

When it comes to running a website inside a C# WPF desktop application, there are a few options available. You can try to use the native WebBrowser control that comes with .NET 3.5 SP1, but that seems to rely on the version of IE installed on the computer and is fraught with all sorts of peril.

You’re much better off going with a dedicated browser control library, of which the Chromium Embedded Framework is probably your best bet. For .NET that means CEFSharp (and for us, that means its WPF browser control, CEFSharp.WPF).

Installing CEFSharp and getting it to work in a development environment is pretty straightforward, just install the Nuget packages that you need and its all probably going to start working.

While functional, its not the greatest experience though.

  • If you’re using the CEFSharp WPF control, don’t try to use the designer. It will crash, slowly and painfully. Its best to just do the entire thing in code behind and leave the browser control out of the XAML entirely.
  • The underlying C++ library (libcef.dll) is monstrous, weighing in at around 48MB. For us, this was about 4 times the size of the entire application, so its a bit of a change.
  • If you’re using ClickOnce to deploy your application, you’re in for a bit of an adventure.

The last point is the most relevant to this post.

Because of the way ClickOnce works, a good chunk of the dependencies that are required by CEFSharp will not be recognised as required files, and thus will not be included in the deployment package when its built. If you weren’t using ClickOnce, everything would be copied correctly into the output folder by build events and MSBuild targets, and you could then package up your application in a sane way, but once you pick a deployment technology, you’re pretty much stuck with it.

You’ll find a lot of guidance on the internet about how to make ClickOnce work with CEFSharp, but I had the most luck with the following:

  1. Add Nuget references to the packages that you require. For me this was just CEFSharp.WPF, specifically version 49.0.0 because my application is still tied to .NET 4.0. This will add references to CEFSharp.Common and cef.redist.x86 (and x64).
  2. Edit the csproj file to remove any imports of msbuild targets added by CEFSharp. This is how CEFSharp would normally copy itself to the output folder, but it doesn’t work properly with ClickOnce. Also remove any props imports, because this is how CEFSharp does library references.
  3. Settle on a processor architecture (i.e. x86 or x64). CEFSharp only works with one or the other, so you might as well remove the one you don’t need.
  4. Manually add references to the .NET DLLs (CefSharp.WPF.dll, CefSharp.Core.dll and CefSharp.dll). You can find these DLLs inside the appropriate folder in your packages directory.
  5. This last step deals entirely with the dependencies required to run CEFSharp and making sure they present in the ClickOnce deployment. Manually edit the csproj file to include the following snippet:

    <ItemGroup>    
        <Content Include="..\packages\cef.redist.x86.3.2623.1396\CEF\*.*">
          <Link>%(Filename)%(Extension)</Link>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <Visible>false</Visible>
        </Content>
        <Content Include="..\packages\cef.redist.x86.3.2623.1396\CEF\x86\*.*">
          <Link>%(Filename)%(Extension)</Link>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <Visible>false</Visible>
        </Content>
        <Content Include="..\packages\CefSharp.Common.49.0.0\CefSharp\x86\CefSharp.BrowserSubprocess.*">
          <Link>%(Filename)%(Extension)</Link>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <Visible>false</Visible>
        </Content>
        <Content Include="lib\*.*">
          <Link>%(Filename)%(Extension)</Link>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <Visible>false</Visible>
        </Content>
        <Content Include="$(SolutionDir)packages\cef.redist.x86.3.2623.1396\CEF\locales\*">
          <Link>locales\%(Filename)%(Extension)</Link>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <Visible>false</Visible>
        </Content>
    </ItemGroup>
    This particularly brutal chunk of XML adds off the dependencies required for CEFSharp to work properly as content files into the project, and then hides them, so you don’t have to see their ugly faces whenever you open your solution.

After taking those steps, I was able to successfully deploy the application with a working browser control, free from the delights of constant crashes.

Injected With A Poison

Of course, opening a browser to a particular webpage is nothing without the second part; giving that webpage access to some arbitrary data from the local context.

CEFSharp is pretty good in this respect to be honest. Before you navigate to the address you want to go to, you just have to call RegisterJsObject, supplying a name for the Javascript variable and a reference to your .NET object. The variable is then made available to the Javascript running on the page.

The proof of concept that I put together used a very simple object (a class with a few string properties), so I haven’t tested the limits of this approach, but I’m pretty sure you can do most of the basic things like arrays and properties that are objects themselves (i.e. Foo.Bar.Thing).

The interesting part is that the variable is made available to every page that is navigated to in the Browser from that point forward, so if there is a link on your page that goes somewhere else, it will get the same context as the previous page did.

In my case, my page was completely trivial, echoing back the injected variable if it existed.

<html>
<body>
<p id="content">
Script not run yet
</p>
<script>
if (typeof(injectedData) == 'undefined')
{
    document.getElementById("content").innerHTML = "no data specified";
}
else 
{
    document.getElementById("content").innerHTML = "name: " + injectedData.name + ", company: " + injectedData.company;
}
</script>
</body>
</html>

Summary

It is more than possible to have good integration between a website (well, one featuring Javascript at least) and an installed C# application, even if you happen to have the misfortune of using ClickOnce to deploy it. Really, all of the congratulations should go to the CEFSharp guys for creating an incredibly useful and flexible browser component using Chromium, even if it does feel monstrous and unholy.

Now, whether or not any of the above is a good idea is a different question. I had to make some annoying compromises to get the whole thing working, especially with ClickOnce, so its not exactly in the best place moving forward (for example, upgrading the version of CEFSharp is going to be pretty painful due to those hard references that were manually added into the csproj file). I made sure to document everything I did in the repository readme, so its not the end of the world, but its definitely going to cause pain in the future to some poor bastard.

This is probably how Dr Frankenstein felt, if he were a real person.

Disgusted, but a little bit proud as well.