Using Virtual Earth in your WPF application

Technorati Tags: virtual earth, wpf, ve, geotagging

I wanted to experiment more with WPF, since I don't get to use it in my Real Job. Also, I love me some Virtual Earth, so I was looking to find a way to merge the two. What I came up with is an application which would allow you to tag your photos with geo-location information.

One of the difficulties was with this combination of VE and WPF; it's not an outta-the-box scenario, but it's definitely doable. The crux of the issue is that VE is a web application - it relies on HTML and AJAX to do its magic - but WPF doesn't ship with a full web browser control.

This dilemma is solved by the fact that WPF does support embedding standard Winforms controls, and Winforms does in fact ship with a web browser control.

Aside from that, the only other interesting issue I faced was that for this application to work, I needed two-way communication between the JavaScript running in the browser, and the C# code in my WPF app. Turns out this is also doable :)

I'll divide the rest of the post into the logical parts that I had to deal with.

Creating the Winforms control

This is the easy part. Add a project to your solution of type "Windows Forms Control Library". You'll get your typical Winforms canvas to play with, onto which you drag a WebBrowser control from the toolbox. That's essentially it for your control - it's just a web browser inside a container. You will need to call a few methods on the WebBrowser control from your WPF code. To do this, you can either wrap those methods in your user control, or directly expose the WebBrowser control as a property of your user control:

    1: public WebBrowser WebBrowserControl
    2: {
    3:     get { return browserMain; }
    4: }
Embedding the Winforms user control in your WPF app

In order to embed this user control, you'll need to do a few things in your WPF project:

  • Add a reference to System.Windows.Forms
  • Add a reference to WindowsFormsIntegration
  • Add a reference to the new user control project you created.

Once that's done, you can add a WPF WindowsFormsHost, which is a control that hosts Winforms controls. In your XAML, throw this where you want the map to appear:

    1: <WindowsFormsHost x:Name="hostWinForms" />

 

Now notice that this control is somewhat empty - nowhere is your user control mentioned. The WindowsFormsHost is just a husk in which you need to put your custom control. So, let's give this host control a child:

 

    1: browserControl = new WinFormsWebBrowserControl.WebBrowser();
    2: hostWinForms.Child = browserControl;

That's it. Now you can treat this browserControl object like a normal WPF control, and it will do your evil bidding. Let's talk more about that...

Getting the Winforms control to show a map

If you've ever done anything with VE in the browser before, you'll know this is a piece of cake. To get it working in this scenario, you write an HTML page just like you would for a normal website, and then you set the URL of the WebBrowser to point to this file. For example:

    1: browserControl.WebBrowserControl.Url = new Uri("VE.htm");

 

(I recommend getting the explicit path to the HTML file, since you can never trust what environment you'll be in. Better yet, perhaps embed it as a resource).

 

For the actual contents of VE.htm, see the interactive SDK for a working sample. It's really a trivial HTML page.

Talking from WPF to JavaScript

My geo-tagging app needed to be able to give various commands to the map. For example, telling the map div to resize in response to the WPF window resizing, or adding a push-pin. The concept is actually very simple: You can execute any JavaScript command you want, just by calling the WebBrowser.Document.InvokeScript method.

So the map resize functionality consisted of a JavaScript method in my VE.htm page:

    1: function Resize(width, height)
    2: {
    3:     map.Resize(width, height);
    4: }

(The map object is the typical one created by the call to the VEMap() constructor)

... Which was called from WPF like this:

    1: void hostWinForms_SizeChanged(object sender, SizeChangedEventArgs e)
    2: {
    3:     browserControl.WebBrowserControl.Document.InvokeScript("Resize", browserControl.Width, browserControl.Height);
    4: }

I'm basically telling the map to resize to the size of my user control. Notice how the arguments are passed through without casting or anything weird. It Just Works.

Talking from JavaScript to WPF

This is the final bit and a little bit more complex - but not much harder to implement. Javascript supports a system to call methods in the window that's hosting it. This is done using window.external.<method name> .

The catch is twofold:

  • You need to tell the browser control where to direct these method calls to.
  • The class that accepts these method calls need to be marked with an attribute that allows it to be visible to COM classes (which the browser control is).

Here's the minor issue: It's tempting to just make your main WPF page the go-to object and have it handle all the calls. The problem is that because it inherits from Window, it can't take this attribute.

We get around this by making an intermediate WPF class that can take the attribute and that knows about your main window class. This object can then call methods in your main window. Something like this:

    1: [ComVisible(true)]
    2: public class ObjectForScriptingHelper
    3: {
    4:     MainWindow m_Window;
    5:  
    6:     public ObjectForScriptingHelper(MainWindow w)
    7:     {
    8:         m_Window = w;
    9:     }
   10:  
   11:     public void MapPositionChange(double lat, double lon, int zoom)
   12:     {
   13:         m_Window.MapPositionChanged(lat, lon, zoom);
   14:     }
   15: }

Looking at the above, we have two methods:

  • Constructor: Takes an instance of my MainWindow class, and stores it for later reference.
  • MapPositionChange: This is a method which will be called by the JavaScript on the VE.htm page whenever the map wants to inform us that the user changed the map location. When this happens, this method calls a corresponding method in the MainWindow class, which simply records the Latitude/Longitude.

So the calling path is like this:

    JavaScript --> ObjectForScriptingHelper instance --> MainWindow instance.

How do we tell the JavaScript to call the ObjectForScriptingHelper class, rather than just the user control that's hosting the WebBrowser control? Like this, in our MainWindow constructor:

    1: ScriptHelper = new ObjectForScriptingHelper(this);
    2: browserControl.WebBrowserControl.ObjectForScripting = ScriptHelper;
Conclusion

That's it! Once you have this framework set up, you can add method calls back/forth between the VE.htm page and the WPF code. So it's a little complex, but there's no major code to be written. Once you get the concept it's not too bad - and the end result is gorgeous.

Avi