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.
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: }
In order to embed this user control, you'll need to do a few things in your WPF project:
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" />
1: browserControl = new WinFormsWebBrowserControl.WebBrowser();
2: hostWinForms.Child = browserControl;
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");
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)
3: map.Resize(width, height);
(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)
3: browserControl.WebBrowserControl.Document.InvokeScript("Resize", browserControl.Width, browserControl.Height);
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.
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:
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:
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;
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