Wow. Where do I start? Basically the idea here is to get MapMarker to work as the geocoding engine behind Bing! maps. This allows you to leverage an excellent geocoding engine with some beautiful, full-featured map data as well as leverage a NATIVE .net framework. So what's the down side? First, MapMarker XML objects are not native to .net. However, that's pretty easy to overcome because MapInfo provides some excellent classes that will do all this heavy lifting for you. The real trouble starts when you realize that the system.net.webclient object is a 'friend' of Silverlight, so you can't add the custom headers you need to initiate the web request . Next, Silverlight doesn't support synchronous web requests anymore, so that's out too. Also, Silverlight requires it's own server-side security settings to access your (existing!) MapMarker web service. Did I mention that MapMarker runs on tomcat?? Yeah that was fun. Even after you solve all these problems, you still have to get your information from the web request back to your UI somehow.
But I solved it all for you monkeys. So without further ado, here is how to use MapMarker with Bing! maps. Or more accurately, how to asynchronously retrieve an XML request from MapMarker using Silverlight.
- How to add the MapMarker XML API to your silverlight application
- How to use httpwebrequest/response instead of webclient
- How to confgure clientaccesspolicy.xml and tomcat
- How to encode your data
- How to update your UI asynchronously
MapMarker and Silverlight
What we want to do here is compile the MapMarker XML API and use it in our silverlight application as a reference. If you are writing natively in C#, you can skip this if you want. However, if you're particularly awesome like me and you scoff at those C# douchebags, here's an easy way to add the API to your VB project.
If you are programming in C#, you can skip this procedure and simply add the classes to your silverlight project.
- On your mapmarker CD, browse to the following directory:
In this directory is a C# Project. Load this into Visual Studio. I'm using Visual Studio 2010, and I was able to convert this project without a single issue.
- Once you have this project loaded, go ahead and compile it and play around with the application. You may need to reference this later
- Open a new Project of the type Visual C# Silverlight Class Library
- There are two files you will need from the sample project:
Load these into your new project and compile them. (note: I compiled them in 'release' configuration so I would get the best performance.)
Now you're ready to start building your silverlight application
- Create a new Silverlight project
- In the Solution Explorer, right-click the project and click 'add reference...'
- Click the 'Browse' tab and find the DLL you created in the previous procedure
- Add it to your project.
The Death of WebClient
If you look over the sample code from MapMarker, you'll notice that they create a webclient object to initiate the XML transaction. However, because Silverlight runs in a web page and is not actually a web page itself, it is restricted from accessing the properties of some objects. WebClient is one of these. So what we have to do is go one level deeper and manage the web request ourselves using the System.Net.HttpWebRequest and System.Net.HttpWebResponse objects Rather than call the webclient.headers.add() property, we add our custom headers as follows:
Private Sub Button1_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles Button1.Click Dim geocoder As New Uri("http://localhost:8095/mapmarker40/servlet/mapmarker", UriKind.Absolute) Dim request As HttpWebRequest = HttpWebRequest.Create(geocoder) request.ContentType = "text/xml" request.Method = "POST" request.Headers("MI_XMLProtocolRequest") = "GeocodeRequest" request.Headers("MI_XMLProtocolVersion") = "MI_XML_Protocol_GeocodeRequestAndResponse_1_0" request.Headers("MI_XMLProtocolTransactionId") = "0000" Dim result As IAsyncResult = CType(request.BeginGetRequestStream(AddressOf RequestCallback, request), IAsyncResult) Do Loop While result.IsCompleted = False End Sub
Furthermore, Silverlight will not allow you to manage these calls synchronously anymore, so you need to create some asynchronous requests to manage both the request and the response. More information is available here:
The Request Callback
Private Sub RequestCallback(ByVal asynchronousResult As IAsyncResult) Dim request As HttpWebRequest = CType(asynchronousResult.AsyncState, HttpWebRequest) Dim result As IAsyncResult = CType(request.BeginGetResponse(AddressOf ResponseCallback, request), IAsyncResult) End Sub
The Response Callback
Private Sub ResponseCallback(ByVal asynchronousResult As IAsyncResult) Dim request As HttpWebRequest = CType(asynchronousResult.AsyncState, HttpWebRequest) Dim response As HttpWebResponse = CType(request.EndGetResponse(asynchronousResult), _ HttpWebResponse) Dim streamResponse As Stream = response.GetResponseStream() Dim streamRead As New StreamReader(streamResponse) Dim responseString As String = "" responseString = streamRead.ReadToEnd streamResponse.Close() streamRead.Close() response.Close() End Sub
Client Access Policies and tomcat
- Silverlight looks for a server-side XML file called clientaccesspolicy.xml when it is attempting to access a web service that is outside it's own domain. On your mapmarker server, create the file in the $CATALINA_HOME/webapps/ROOT/ directory. (note: On my system this is D:\Program Files\mapinfo\MapMarker_USA_v24\sdk\tomcat\webapps\ROOT)
- use the following settings
<?xml version="1.0" encoding="utf-8"?> <access-policy> <cross-domain-access> <policy> <allow-from http-request-headers="*"> <domain uri="*"/> </allow-from> <grant-to> <resource path="/" include-subpaths="true"/> </grant-to> </policy> </cross-domain-access> </access-policy>
- reload tomcat
Encoding your XML BitStream
Something else that's changed is that Silverlight does not encode in ASCII anymore, but the compiler still counts your characters in 8 bits. So, we need to change the sample code a little bit so that it our bitstream is encoded in Unicode (16 bit) and the entire bitstream is sent:
Dim byteArray As Byte() = Encoding.Unicode.GetBytes(xmlRequest) postStream.Write(byteArray, 0, xmlRequest.Length * 2)
Getting the Data back to the UI
Finally, we've got to get some kind of data back to our UI or else what was the point? The simplest way I've found to do this is by using the Dispatcher object in the ResponseCallback() function:
Dispatcher.BeginInvoke(Sub() TextBlock1.Text = responseString)
But by the best way is to use a Synchronization context:
Public Class MainPage ... Dim syncContext As SynchronizationContext Private Sub MainPage_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded syncContext = SynchronizationContext.Current End Sub Private Sub ResponseCallback(ByVal asynchronousResult As IAsyncResult) ... syncContext.Post(AddressOf getResponse, response) End Sub Private Sub getResponse(ByVal state As Object) Dim response As HttpWebResponse = CType(state, HttpWebResponse) Dim streamResponse As Stream = response.GetResponseStream() If streamResponse.CanRead = True Then 'Read your response stream here End If End sub
All that's really left is to tear up the sample code according to whether it's part of the request or part of the response. Source code is available on request.