Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

BeeMobile4.NET Gets Designer Support in Visual Studio for Windows Mobile Applications

I’ve just been in contact with the team at BeeMobile4.net and they told me that they now have full designer support within Visual Studio 2005 and Visual Studio 2008 for all their controls. When you purchase their iPack you not only get all their controls but the ability to use them from the toolbox in Visual Studio. Of course, since Windows Mobile application development isn’t supported in Visual Studio 2010 their controls don’t appear there!

Here’s a quick walk through of the experience. Firstly, love the installation process – good attention to detail through the install process.

image

Once installed I was amazed by the number of controls they now have on offer.

image

Rather than go through each of the controls in painful detail I downloaded their sample application that showcases all the controls:

image image image

Here you can see a splash screen with an on screen progress bar and a semi-transparent header. The next image includes two main regions including a list, all with transparency so you can see the background. The third image shows you their semi-transparent message box – this is configurable to control both the text, opacity and the buttons displayed.

imageimage

  image image

These four images just show some of the other controls in action.

If you’re doing Windows Mobile development I’d highly recommend these controls as a great starting point to make your application look amazing. Also, if you have other specific control requirements, contact the team at BeeMobile4.Net as they are able to build controls to suit your requirements.

Windows Azure, .NET Services from a Windows Mobile Widget

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

That’s right, if you hadn’t guessed what I was working on with my previous posts on .NET Services (Tokens, Queues I, II, III, Routers I & II) I’ve been working towards accessing .NET Services from within a widget. I must apologise for the following as it’s just a code dump of a sample widget that shows how to create a queue and subscribe to an existing router.  Note that if you are going to use this on a number of devices you need to create a unique queue on each device, otherwise you’ll end up subscribing to the router multiple times – this will cause duplicate messages in your queue and contention between the devices dequeuing the messages.

To get this widget to work, simply create a widget (Developing Widgets for Windows Mobile 6.5) and then copy the following code into your html file.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml" >
<head>
    <title>Widget Chat</title>

      <script type="text/javascript">
          var theToken;
          var theRouter = "myrouter"
          var widgetQueue = "widget";

          function connect() {
              var solutionName = document.getElementById("SolutionNameText");
              var solutionPassword = document.getElementById("SolutionPasswordText");

              alert('Solution Name: ' + solutionName.value);
              alert('Solution Password: ' + solutionPassword.value);
              httpGetAuthenticationToken(solutionName.value, solutionPassword.value, createQueue);
          }

          function createQueue() {
              alert('Creating Queue');
              httpCreateQueue(theToken, widgetQueue, subscribeToRouter);
          }

          function subscribeToRouter() {
              alert('Subscribing to Router');
              httpSubscribeQueueToRouter(theToken, theRouter, widgetQueue, pollForMessage);
          }

          function pollForMessage() {
              httpDequeueMessage(theToken, widgetQueue, messageReceived, pollForMessage);
          }

          function messageReceived(message) {
              alert('Message Received: ' + message);
              pollForMessage();
          }

          function send() {
              var messageBox = document.getElementById("Message");
              httpRouteMessage(theToken, theRouter, messageBox.value);
          }

          function httpGetAuthenticationToken(username, password, callback) {
              try {
                  http = new XMLHttpRequest();
                  http.open("GET", "
https://accesscontrol.windows.net/IssueToken.aspx?u=" + username + "&p=" + password);
                  http.onreadystatechange = function() {
                      if (http.readyState == 4) {
                          if (http.status == 200) {
                              theToken = http.responseText;
                              alert('Authenticated:' + theToken);
                              callback();
                          }
                          else {
                              alert('Authentication failed');
                          }
                      }
                  }
                  http.send();
              }
              catch (e) {
                  alert(e.message);
              }
          }

          function httpCreateQueue(token, queue, callback) {
              var policy = "<entry xmlns=\"
http://www.w3.org/2005/Atom\">" +
  "<QueuePolicy xmlns=\"
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect\">" +
    "<ExpirationInstant>2009-09-03T01:00:00.0000000Z</ExpirationInstant>" +
  "</QueuePolicy>" +
"</entry>";

              http = new XMLHttpRequest();
              http.open("POST", "
https://blogsample.servicebus.windows.net/" + queue, false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.setRequestHeader("Content-type", "application/atom+xml;type=entry;charset=utf-8");
              http.onreadystatechange = function() {
                  if (http.readyState == 4) {
                      if (http.status == 201) {
                          var created = http.responseText;
                          alert('queue created: ' + created);

                      }
                      else {
                          alert('queue not created - ' + http.status);
                      }
                      callback();
                  }
              }
              http.send(policy);
          }

          function httpGetServiceList(token) {
              http = new XMLHttpRequest();
              http.open("GET", "
https://blogsample.servicebus.windows.net/", false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.onreadystatechange = function() {
                  if (http.readyState == 4 && http.status == 200) {
                      output = http.responseText;
                      alert(output);
                      // Do something with the service list
                      httpRenewQueue(token, "mobilequeue", output);
                  }
              }
              http.send();
          }

          function httpRenewQueue(token, queue, getResponse) {
              xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
              xmlDoc.async = "false";
              xmlDoc.loadXML(getResponse);

              var entry;
              var entries = xmlDoc.getElementsByTagName("title");
              var i;
              for (i = 0; i < entries.length; i++) {
                  if (entries[ i ].childNodes[0].nodeValue == queue) {
                      entry = entries[ i ].parentNode;
                  }
              }
              entry.getElementsByTagName("ExpirationInstant")[0].childNodes[0].nodeValue = "2009-08-31T09:56:25.2951184Z";

              http = new XMLHttpRequest();
              http.open("PUT", "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue)", false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.setRequestHeader("Content-type", "application/atom+xml;type=entry;charset=utf-8");
              http.onreadystatechange = function() {
                  if (http.readyState == 4 && http.status == 200) {
                      var output = http.responseText;
                      alert('renew: ' + output);
                      httpEnqueueMessage(token, queue, "Hi Everyone");
                  }
              }
              http.send(entry.xml);
          }

          function httpDeleteQueue(token, queue) {
              http = new XMLHttpRequest();
              http.open("DELETE", "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue)", false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.onreadystatechange = function() {
                  if (http.readyState == 4 && http.status == 200) {
                      var deleted = http.responseText;
                      alert('deleted');
                  }
              }
              http.send();
          }

          function httpEnqueueMessage(token, queue, message) {
              http = new XMLHttpRequest();
              http.open("POST", "
https://blogsample.servicebus.windows.net/" + queue, false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.setRequestHeader("Content-type", "text/plain;charset=utf-8");
              http.onreadystatechange = function() {
                  if (http.readyState == 4 && http.status == 202) {
                      var output = http.responseText;
                      alert('enqueue: ' + output)
                      httpDequeueMessage(token, queue);
                  }
              }
              http.send(message);
          }

          function httpDequeueMessage(token, queue, callback, noMessageCallback) {
              http = new XMLHttpRequest();
              http.open("DELETE", "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue/head)?encoding=asreply&maxmessages=1&timeout=30", true);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.onreadystatechange = function() {
                  if (http.readyState == 4) {
                      if (http.status == 200) {
                          var output = http.responseText;
                          callback(output);
                      }
                      else {
                          noMessageCallback();
                      }
                  }
              }
              http.send();
          }

          function httpRouteMessage(token, router, message) {
              http = new XMLHttpRequest();
              http.open("POST", "
https://blogsample.servicebus.windows.net/" + router, false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.setRequestHeader("Content-type", "text/plain;charset=utf-8");
              http.onreadystatechange = function() {
                  if (http.readyState == 4) {
                      if (http.status == 202) {
                          var output = http.responseText;
                          alert('Send ok!');

                      }
                      else {
                          alert('Unable to send');
                      }
                  }
              }
              http.send(message);
          }

          function httpSubscribeQueueToRouter(token, router, queue, callback) {
              var policy = "<entry xmlns=\"
http://www.w3.org/2005/Atom\">" +
              "<link rel='alternate' href='
https://blogsample.servicebus.windows.net/" + queue + "' />" +
  "<HttpHeaders xmlns=\"
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect\">" +
    "<HttpHeader name='X-MS-Identity-Token' value='" + token + "' />" +
  "</HttpHeaders>" +
"</entry>";

              http = new XMLHttpRequest();
              http.open("POST", "
https://blogsample.servicebus.windows.net/" + router + "/!(router/subscriptions)", false);
              http.setRequestHeader("X-MS-Identity-Token", token);
              http.setRequestHeader("Content-type", "application/atom+xml;type=entry;charset=utf-8");
              http.onreadystatechange = function() {
                  if (http.readyState == 4) {
                      if (http.status == 200) {
                          var created = http.responseText;
                          alert('subscribed ok:' + created);
                          callback();
                      }
                      else {
                          alert('not subscribed ok' + http.status);
                      }
                  }
              }
              http.send(policy);
          }

      </script>
</head>
<body>
    <p>   
    Solution Name:<br />
    <input id="SolutionNameText" type="text" />
        <br />
    Solution Password:<br />
    <input id="SolutionPasswordText" type="password" /><br />
    <Button onclick="connect()">Connect</Button>
    </p>

    <p>   
    Message:
        <br />
    <input id="Message" type="text" /><br />
    <Button onclick="send()">Send</Button>
    </p>

</body>
</html>

Windows Azure, Microsoft .NET Services – Working with Routers (II)

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

In the previous post on Working with Routers I showed how you can create a router using a Http Post.  A router is only really useful when it can actually route messages. To do this it needs one or more subscribers.  As you can imagine creating a subscriber (as with other creates) involves a Http Post.  When we created the router one of the links that was returned was the subscriptions url. All you really need to do to create a subscriber is send a Http Post to this url. Of course you need to send the url where you want messages to be routed to, and any other information that is required for the router to route messages.  The upshot is that you end up sending an entry that looks similar to the following:

<entry xmlns="http://www.w3.org/2005/Atom">
  <link rel="alternate" href="
https://blogsample.servicebus.windows.net/myqueue" />
  <HttpHeaders xmlns="
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">
    <HttpHeader name="X-MS-Identity-Token" value="zpleGHYQzEiRUXkdFd7UN+gBbBzSIuW5Bc2hIA==" />
  </HttpHeaders>
</entry>

Of course, this needs to be sent as a Http Post:

private static string SubscribeQueueToRouter(string token, string router, string queue)
{
    string subscriptionsUri = "
https://blogsample.servicebus.windows.net/" + router + "/!(router/subscriptions)";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(subscriptionsUri);
    request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "POST";
    request.ContentType = "application/atom+xml;type=entry;charset=utf-8";

    XElement content = new XElement(XName.Get("entry", "http://www.w3.org/2005/Atom"),
                            new XElement(XName.Get("link", "
http://www.w3.org/2005/Atom"),
                                new XAttribute("rel", "alternate"),
                                new XAttribute("href", "
https://blogsample.servicebus.windows.net/" + queue)),
                            ServicesElement("HttpHeaders",
                                ServicesElement("HttpHeader",
                                    new XAttribute("name","X-MS-Identity-Token"),
                                    new XAttribute("value",token))));

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
        writer.Write(content);
        writer.Flush();
    }

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}

In this example the link url provided is the tail of the queue (ie the url where you enqueue messages).  The token is included in a HttpHeader element so that it can be routed as part of sending the message to the queue. You should expect a 200, Ok response. If you want to get a list of subscribers you can simply issue a Http Get to the subscriptions url.  This will return a feed of the subscribers.

<feed xmlns="http://www.w3.org/2005/Atom">
   <title type="text">Subscriptions</title>
   <id>uuid:eb256797-2832-4f2b-afd9-ba3e4cfeeb7b;id=1869</id>
   <updated>2009-09-01T06:20:00Z</updated>
   <link rel="self" href="https://blogsample.servicebus.windows.net/myrouter/!(router/subscriptions)"></link>
   <entry>
     <id>uuid:eb256797-2832-4f2b-afd9-ba3e4cfeeb7b;id=1870</id>
      <title type="text">urn:uuid:90d62d9e-2a13-446c-951a-72046101fa17</title>
      <updated>2009-09-01T06:20:00Z</updated>
      <link rel="alternate" href="https://blogsample.servicebus.windows.net/myqueue"></link>
      <link rel="self" href="https://blogsample.servicebus.windows.net/myrouter/!(router/subscriptions/urn:uuid:90d62d9e-2a13-446c-951a-72046101fa17)"></link>
      <content type="text"></content>
      <Expires xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">2009-09-02T06:18:56.9899631Z</Expires>
      <NotifyTo xmlns="http://schemas.xmlsoap.org/ws/2004/08/eventing">
         <Address xmlns="http://www.w3.org/2005/08/addressing">https://blogsample.servicebus.windows.net/myqueue</Address>
      </NotifyTo>
      <HttpHeaders xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">
         <HttpHeader name="X-MS-Identity-Token" value="zpleGHYQzEiRUXkdFd7UN+gBbBzSIuW5Bc2hIA=="></HttpHeader>
      </HttpHeaders>
   </entry>
   <entry>
      <id>uuid:eb256797-2832-4f2b-afd9-ba3e4cfeeb7b;id=1871</id>
      <title type="text">urn:uuid:d27a29ab-46e4-49ca-9d1e-cd05bdc24d42</title>
      <updated>2009-09-01T06:20:00Z</updated>
      <link rel="alternate" href="https://blogsample.servicebus.windows.net/myqueue"></link>
      <link rel="self" href="https://blogsample.servicebus.windows.net/myrouter/!(router/subscriptions/urn:uuid:d27a29ab-46e4-49ca-9d1e-cd05bdc24d42)"></link>
      <content type="text"></content>
      <Expires xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">2009-09-02T06:01:01.0635156Z</Expires>
      <NotifyTo xmlns="http://schemas.xmlsoap.org/ws/2004/08/eventing">
         <Address xmlns="http://www.w3.org/2005/08/addressing">https://blogsample.servicebus.windows.net/myqueue</Address>
      </NotifyTo>
      <HttpHeaders xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">
         <HttpHeader name="X-MS-Identity-Token" value="J+rsqHMQzEgxQ3yu3JwyDv3J8wNgIQybBoxtng=="></HttpHeader>
      </HttpHeaders>
   </entry>
</feed>

Now the last thing to do is to send a message to the router. In the above example a message sent to the router will get routed through to the subscribed queue. As we did with enqueuing a message on a queue, to send a message to the router you do a Http Post to the alterate url of the router (this is also the original url that you used to create the router).

private static string SendMessageToRouter(string token, string router, string message)
{
    string routerUri = "
https://blogsample.servicebus.windows.net/" + router;
    // send
    HttpWebRequest request = HttpWebRequest.Create(routerUri) as HttpWebRequest;
    request.Method = "POST";
    request.Headers.Add("X-MS-Identity-Token", token);
    request.ContentType = "text/plain;charset=utf-8";

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
        writer.Write(message);
        writer.Flush();
    }

    using (var response = request.GetResponse())
    {
        return (response as HttpWebResponse).StatusCode.ToString();
    }
}

Again you should expect a 202, Accepted response. To retrieve the message off the destination queue, simply send a Http Delete message as discussed in Working with Queues (III).

Windows Azure, Microsoft .NET Services – Working with Routers (I)

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

The last couple of posts have covered working with .NET Services queues (parts I, II and III). Now we move on to cover routers. Like I did previously I’ll cover creating and working with routers, before we move on to working with them in conjunction with queues.

To create a router you send a Http Post to the url that you want the router to reside at, supplying a RouterPolicy entry.  The url has to belong to your solution url, so for example https://blogsample.servicebus.windows.net/myrouter. An example RouterPolicy would look like the following:

<entry xmlns="http://www.w3.org/2005/Atom">
  <RouterPolicy xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">
    <Discoverability>Public</Discoverability>
    <ExpirationInstant>2009-09-01T05:18:06.9677603Z</ExpirationInstant>
    <TransportProtection>None</TransportProtection>
    <MaxSubscribers>2147483647</MaxSubscribers>
    <MessageDistribution>AllSubscribers</MessageDistribution>
  </RouterPolicy>
</entry>

Now to send this:

private static string HttpCreateRouter(string token, string router)
{
    string routerUri = "
https://blogsample.servicebus.windows.net/" + router;

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(routerUri);
    request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "POST";
    request.ContentType = "application/atom+xml;type=entry;charset=utf-8";

    XElement content = new XElement(XName.Get("entry", "http://www.w3.org/2005/Atom"),
                            ServicesElement("RouterPolicy",
                                ServicesElement("Discoverability","Public"),
                                ServicesElement("ExpirationInstant", DateTime.UtcNow.AddMinutes(30)),
                                ServicesElement("TransportProtection", "None"),
                                ServicesElement("MaxSubscribers", "2147483647"),
                                ServicesElement("MessageDistribution", "AllSubscribers")));

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
            writer.Write(content);
            writer.Flush();
    }

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}

private static XElement ServicesElement(string element, params object[] content)
{
    return new XElement(XName.Get(element, "
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect"), content);
}

A note of warning… for some reason the order of the xml elements for the RouterPolicy does matter.  Submitting them in a different order appears to cause a duplicate RouterPolicy entry. This will most likely be resolved for the production release but in the meantime if you are seeing weird RouterPolicy entries being returned this may be the cause.

When you run this code the response (assuming it is successful) should be a 201, Created. Subsequent Posts to this url will result in a 202, Accepted – note however this does not update or alter the router. In addition to the 201 status, the Post also returns information about the created router:

<entry xmlns="http://www.w3.org/2005/Atom">
   <id>uuid:0ae5cae9-6f3f-4fcc-8377-83f34c106124;id=1546</id>
   <title type="text">myrouter</title>
   <updated>2009-09-01T04:48:41Z</updated>
   <link rel="alternate" href="https://blogsample.servicebus.windows.net/myrouter/"/>
   <link rel="self" href="https://blogsample.servicebus.windows.net/myrouter/!(router)"/>
   <link rel="subscriptions" href="https://blogsample.servicebus.windows.net/myrouter/!(router/subscriptions)"/>
   <RouterPolicy xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <Discoverability>Public</Discoverability>
      <ExpirationInstant>2009-09-01T05:18:06.9677603Z</ExpirationInstant>
      <TransportProtection>None</TransportProtection>
      <MaxMessageSize>61440</MaxMessageSize>
      <BufferTimeout>PT10S</BufferTimeout>
      <MaxBufferLength>0</MaxBufferLength>
      <MaxBufferCapacity>0</MaxBufferCapacity>
      <MaxSubscribers>50</MaxSubscribers>
      <MessageDistribution>AllSubscribers</MessageDistribution>
      <PushDeliveryRetries>3</PushDeliveryRetries>
      <PushDeliveryTimeout>PT30S</PushDeliveryTimeout>
   </RouterPolicy>
</entry>

As with when you create a queue, the response contains a number of links.

alternate – This is the url you will use when placing new messages on the router. 

self – This is the url you will now use to update, renew and delete the router.

subscriptions – This is the url that you use when subscribing to the router.

As you can see the process of creating a router is roughly the same as for a queue, with the only big difference being the RouterPolicy you need to supply.  Querying, renewing and deleting a router is done exactly the same way as for a queue, with the exception you need to use the self url returned when you create the router (which ends in (router) instead of (queue)).

Windows Azure, Microsoft .NET Services – Working with Queues (III)

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

In my previous posts, Working With Queues (I) & Working with Queues (II), I showed how you can create, query, renew and delete queues with the Windows Azure, Microsoft .NET Services. Now for the interesting stuff – let’s enqueue and dequeue messages.

As you can imagine enqueuing equates to Post(ing) a message to the tail of the queue, whilst dequeuing equates to delete(ing) from the head of the queue. In code, this is:

private static string EnqueueMessage(string token, string queue, string message)
{
    string queueUri = "
https://blogsample.servicebus.windows.net/" + queue;

    // send
    HttpWebRequest request = HttpWebRequest.Create(queueUri) as HttpWebRequest;
    request.Method = "POST";
    request.Headers.Add("X-MS-Identity-Token", token);
    request.ContentType = "text/plain;charset=utf-8";

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
        writer.Write(message);
        writer.Flush();
    }

    using (var response = request.GetResponse())
    {
        return (response as HttpWebResponse).StatusCode.ToString();
    }
}

private static string DequeueMessage(string token, string queue)
{
    string queueUri = "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue/head)?encoding=asreply&maxmessages=1&timeout=30";

    HttpWebRequest request = HttpWebRequest.Create(queueUri) as HttpWebRequest;
    request.ConnectionGroupName = "dequeue";
    request.Method = "DELETE";
    request.Headers.Add("X-MS-Identity-Token", token);
    request.ContentLength = 0;

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}

To enqueue a message you send a Http Post to the alternate link (otherwise known as the tail of the queue), ie https://blogsample.servicebus.windows.net/myqueue. You should get back a 202, Accepted response. When you are dequeuing a message you send a Http Delete to the queuehead link (otherwise known as the head of the queue), ie https://blogsample.servicebus.windows.net/myqueue/!(queue/head). You should get back a 200, Ok response, with the message in the content of the response.

As part of the Http Delete you can specify a timeout, in this case 30 seconds. This determines how long the request will wait for a message to appear on the queue before returning.  If a message appears within this timeout you will get the message and a 200 response.  If not, you will get a 204, No Content response.

And now to cover enqueuing and dequeuing in javascript:

function httpEnqueueMessage(token, queue, message) {
    http = new XMLHttpRequest();
    http.open("POST", "
https://blogsample.servicebus.windows.net/" + queue, false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.setRequestHeader("Content-type", "text/plain;charset=utf-8");
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 202) {
            var output = http.responseText;
            // Do something…
        }
    }
    http.send(message);
}

function httpDequeueMessage(token, queue) {
    http = new XMLHttpRequest();
    http.open("DELETE", "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue/head)?encoding=asreply&maxmessages=1&timeout=20", false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 200) {
            var output = http.responseText;
            // Do something...
        }
    }
    http.send();
}

Windows Azure, Microsoft .NET Services – Working with Queues (II)

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

In my previous post, Working With Queues (I), I showed how you can create a queue using a Http Post message against the Windows Azure, Microsoft .NET Services. Now that we’ve created a queue, let’s look at how you can query, renew and delete your queue.

To query for the existence of a queue (and remember that when a queue expires it is no longer available) you just need to send a Http Get to the parent url. So for example if you had a queue https://blogsample.servicebus.windows.net/application1/myqueue you would send the Get to https://blogsample.servicebus.windows.net/application1. In our case because the queue we created previously was https://blogsample.servicebus.windows.net/myqueue we need to send the Http Get to https://blogsample.servicebus.windows.net/.

public static string GetServicesListing(string token)
{
    string listingUri = "
https://blogsample.servicebus.windows.net/";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(listingUri);
    request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "GET";
    request.ContentLength = 0;

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        var output=  reader.ReadToEnd();
        return output;
    }
}

When you run this code the response (assuming it is successful) should be a 200, Ok. In addition to the 200 status, the Get also returns information about services available under the url that the Get was sent to:

<feed xmlns="http://www.w3.org/2005/Atom">
   <title type="text">Publicly Listed Services</title>
   <subtitle type="text">This is the list of publicly-listed services currently available</subtitle>
   <id>uuid:c285faa1-b963-4668-918f-af7b99ec19ea;id=3944</id>
   <updated>2009-08-31T00:30:31Z</updated>
   <generator>Microsoft® .NET Services - Service Bus</generator>
   <entry>
      <id>uuid:c285faa1-b963-4668-918f-af7b99ec19ea;id=3945</id>
      <title type="text">myqueue</title>
      <updated>2009-08-31T00:30:31Z</updated>
      <link rel="alternate" href="https://blogsample.servicebus.windows.net/myqueue/"/>
      <link rel="self" href="https://blogsample.servicebus.windows.net/myqueue/!(queue)"/>
      <link rel="queuehead" href="https://blogsample.servicebus.windows.net/myqueue/!(queue/head)"/>
      <link rel="queuecontrol" href="https://blogsample.servicebus.windows.net/myqueue/!(queue/control)"/>
      <QueuePolicy xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
         <ExpirationInstant>2009-08-31T03:56:25.2951184Z</ExpirationInstant>
         <MaxMessageSize>61440</MaxMessageSize>
         <EnqueueTimeout>PT10S</EnqueueTimeout>
         <MaxConcurrentReaders>10</MaxConcurrentReaders>
         <MaxDequeueRetries>3</MaxDequeueRetries>
         <MaxMessageAge>PT10M</MaxMessageAge>
         <MaxQueueLength>2147483647</MaxQueueLength>
         <MaxQueueCapacity>10485760</MaxQueueCapacity>
      </QueuePolicy>
   </entry>
</feed>

And for the javascript version:

function httpGetServiceList(token) {
    http = new XMLHttpRequest();
    http.open("GET", "
https://blogsample.servicebus.windows.net/", false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 200) {
            output = http.responseText;
            // Do something with the service list
        }
    }
    http.send();
}

As you will have noticed, queues have an ExpirationInstant which is the instance when your queue will expire and be unavailable.  In most cases this is not that desirable so you will want to periodically renew it.  You do this by sending a Http Put to the self link address (ie https://blogsample.servicebus.windows.net/myqueue/!(queue)).  Instead of just sending a new QueuePolicy you need to get the appropriate entry out of the service feed (retrieved using the Http Get we just discussed), update the QueuePolicy with the new expiration instant and then submit the whole entry in the content of the Http Put.

 

public static string RenewQueue(string token, string queue)
{
    string listing = GetServicesListing(token);
    XElement feed = XElement.Parse(listing);
    var content = (from entry in feed.Descendants(XName.Get("entry", "
http://www.w3.org/2005/Atom"))
                  where entry.Element(XName.Get("title", "http://www.w3.org/2005/Atom")).Value == queue
                  select entry).FirstOrDefault();
    if (content == null)
    {
        return null;
    }

    // Update the expiration time
    content.Descendants(ServicesElementName("ExpirationInstant")).FirstOrDefault().SetValue(DateTime.UtcNow.AddMinutes(30));

    string queueUri = "https://blogsample.servicebus.windows.net/" + queue + "/!(queue)";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(queueUri);
    request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "PUT";
    request.ContentType = "application/atom+xml;type=entry;charset=utf-8";

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
        writer.Write(content);
        writer.Flush();
    }

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}
private static XName ServicesElementName(string element)
{
    return XName.Get(element, "
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect");
}

Here you should expect a 200, Ok response with the updated entry being returned in the response (you can use this to double-check the queue attributes were updated correctly). And now the same but in javascript:

function httpRenewQueue(token, queue, getResponse) {
    xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
    xmlDoc.async = "false";
    xmlDoc.loadXML(getResponse);

    var entry;
    var entries = xmlDoc.getElementsByTagName("title");
    var i;
    for (i = 0; i < entries.length; i++) {
        if (entries[i].childNodes[0].nodeValue == queue) {
            entry = entries[i].parentNode;
        }
    }
    entry.getElementsByTagName("ExpirationInstant")[0].childNodes[0].nodeValue="2009-08-31T03:56:25.2951184Z";

    http = new XMLHttpRequest();
    http.open("PUT", "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue)", false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.setRequestHeader("Content-type", "application/atom+xml;type=entry;charset=utf-8");
    http.onreadystatechange = function() {
    if (http.readyState == 4 && http.status == 200)
        {
            var output = http.responseText;
            // Do something…
        }
    }
    http.send(entry.xml);
}

Note that in these examples there is NO error handling and the expiration is hard coded – you will need to adjust this otherwise you will get a 500 Internal Server error if the expiration is in the past.

Ok, so finally you need to be able to delete your queue.  This is done by sending a Http Delete message to the Self link, as follows:

public static string DeleteQueue(string token, string queue)
{
    string queueUri = "
https://blogsample.servicebus.windows.net/" + queue + "/!(queue)";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(queueUri);
    request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "DELETE";
    request.ContentLength = 0;
    using (var response = request.GetResponse())
    {
        return (response as HttpWebResponse).StatusCode.ToString();
    }
}

One important point here is that you must set the ContentLength to 0.  You should expect a 200, Ok if the queue is deleted.  For both the renew (ie Http Put) and delete (ie Http Delete) if the queue has expired, you can expect a 404, Not found exception to be raised.  Lastly, let’s see the delete in javascript:

function httpDeleteQueue(token, queue) {
    http = new XMLHttpRequest();
    http.open("DELETE", "
https://blogsample.servicebus.windows.net/" + queue +"/!(queue)", false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 200) {
            var output = http.responseText;
            // Do something….
        }
    }
    http.send();
}

Windows Azure, Microsoft .NET Services – Working With Queues (I)

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

In my previous post on Acquiring an Authentication Token I demonstrated how you can use a Http Get operation to acquire an Authentication Token.  This token can be used to create, modify, delete and send messages to both queues and routers as part of the Windows Azure, Microsoft .NET Services. Again, whilst there are .NET libraries for working with both queues and routers in this post we’ll still to the basic Http messages so that you can see what’s going on under the hood.

The first place to start is to create a queue. To do this you send a Http Post to the url that you want the queue to reside at, supplying a QueuePolicy entry.  The url has to belong to your solution url, so for example https://blogsample.servicebus.windows.net/myqueue. There are a number of attributes you can set in the QueuePolicy you supply but as a minimum you need to set the ExpirationInstant, which as you can imagine is the instant that the queue will expire and self-destruct (unless renewed). An example QueuePolicy would look like the following:

<entry xmlns="http://www.w3.org/2005/Atom">
  <QueuePolicy xmlns="http://schemas.microsoft.com/netservices/2009/05/servicebus/connect">
    <ExpirationInstant>2009-08-30T23:56:25.2951184Z</ExpirationInstant>
  </QueuePolicy>
</entry>

Now to send this:

public static string HttpCreateQueue(string token, string queue)
{
    string queueUri = "
https://blogsample.servicebus.windows.net/" + queue;

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(queueUri);
   request.Headers.Add("X-MS-Identity-Token", token);
    request.Method = "POST";
    request.ContentType = "application/atom+xml;type=entry;charset=utf-8";

    XElement content = new XElement(XName.Get("entry", "http://www.w3.org/2005/Atom"),
                            ServicesElement("QueuePolicy",
                                ServicesElement("ExpirationInstant", DateTime.UtcNow.AddMinutes(30))));

    using (var requestStream = request.GetRequestStream())
    using (var writer = new System.IO.StreamWriter(requestStream))
    {
            writer.Write(content);
            writer.Flush();
    }

    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}

private static XElement ServicesElement(string element, object content)
{
    return new XElement(XName.Get(element, "
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect"), content);
}

Note that you need to supply the authentication token using the X-MS-Identity-Token header.  Here we have built up the QueuePolicy as an XElement, you can of course use the .NET library to generate this for you. When you run this code the response (assuming it is successful) should be a 201, Created. Subsequent Posts to this url will result in a 202, Accepted – note however this does not update or alter the queue. In addition to the 201 status, the Post also returns information about the created queue:

<entry xmlns="http://www.w3.org/2005/Atom">
   <id>uuid:11cdc611-62fb-49d1-8dee-e47bf9cc93d8;id=1407</id>
   <title type="text">myqueue</title>
   <updated>2009-08-30T23:44:13Z</updated>
   <link rel="alternate" href="
https://blogsample.servicebus.windows.net/myqueue/"/>
   <link rel="self" href="
https://blogsample.servicebus.windows.net/myqueue/!(queue)"/>
   <link rel="queuehead" href="
https://blogsample.servicebus.windows.net/myqueue/!(queue/head)"/>
   <link rel="queuecontrol" href="
https://blogsample.servicebus.windows.net/myqueue/!(queue/control)"/>
   <QueuePolicy xmlns="
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <ExpirationInstant>2009-08-31T00:14:13.2742033Z</ExpirationInstant>
      <MaxMessageSize>61440</MaxMessageSize>
      <EnqueueTimeout>PT10S</EnqueueTimeout>
      <MaxConcurrentReaders>10</MaxConcurrentReaders>
      <MaxDequeueRetries>3</MaxDequeueRetries>
      <MaxMessageAge>PT10M</MaxMessageAge>
      <MaxQueueLength>2147483647</MaxQueueLength>
      <MaxQueueCapacity>10485760</MaxQueueCapacity>
      </QueuePolicy>
</entry>

The important elements to note from this are the links.  These correspond to the urls where you will perform actions on the queue.

alternate – This is the url you will use when placing new messages on the queue.  You can think of this as the tail of the queue.

self – This is the url you will now use to update, renew and delete the queue.

queuehead – This is the url where you will pop messages off the queue.  As the name implies, this is the head of the queue

queuecontrol – Currently unused, it is anticipated that this will be used for queue message control.

We’ll come back to these in a minute but let’s first see what this would look like in javascript (again remember this will only work in the context of Gadgets and Widgets where the cross site scripting rules are relaxed):

function httpCreateQueue(token, queue) {
    var policy = "<entry xmlns=\"
http://www.w3.org/2005/Atom\">" +
  "<QueuePolicy xmlns=\"
http://schemas.microsoft.com/netservices/2009/05/servicebus/connect\">" +
    "<ExpirationInstant>2009-08-31T03:56:25.2951184Z</ExpirationInstant>" +
  "</QueuePolicy>" +
"</entry>";

    http = new XMLHttpRequest();
    http.open("POST", "
https://blogsample.servicebus.windows.net/" + queue, false);
    http.setRequestHeader("X-MS-Identity-Token", token);
    http.setRequestHeader("Content-type", "application/atom+xml;type=entry;charset=utf-8");
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 201) {
            var response = http.responseText;
            // Do stuff with the response message
        }
    }
    http.send(policy);
}

One observation is that the service bus is quite sensitive when it comes to the formatting and contents of the request, and like all secure services if you get something wrong it will give you a 500 Internal Server error with no additional information to diagnose what’s going wrong.  In my case the number one thing that was biting me was the ExpirationInstant.  This needs to be UTC time and in the future :-)

Windows Azure, Microsoft .NET Services – Acquiring an Authentication Token

[This post is based on the Windows Azure Tools for Microsoft Visual Studio July 2009 CTP]

Before we jump into looking at authentication tokens, why you need them and how to acquire them, let’s take a quick refresher on how to get up and running with the Windows Azure, Microsoft .NET Services. To begin with you have to request an account; once you receive your welcome email you need to sign in to the Windows Azure portal where you can then view all your .NET Services solutions and subscriptions.

image

To start working with .NET Services you need to create a solution by selecting the Add Solution button. You will be prompted to enter a name for your solution. It’s important to think about the name of your solution as you can’t (currently at least) change it. The solution name becomes the first segment in the url that you will use to access elements of the .NET Services - https://blogsample.servicebus.windows.net/. After entering a solution name you should use the Validate Name link to see whether the name you have entered is in use.

image

Once you have successfully created your solution you will be returned to the main portal screen.  Initially your new solution will appear in the list of solutions with “Activating…” alongside it. If you hit the refresh button after a few seconds (perhaps a couple of minutes depending on how busy the server is) you will see that your solution is available for use.

image

You can use the three links alongside your newly created solution to manage permissions to your solution (Access Control Service), access the Service Bus Registry or manage the Credentials for your solution.  Before we go on you will need to go into Credentials and specify a Solution Password – this is required for you to programmatically access your .NET Services solution.

image

Now that your solution is setup, lets get into working with your .NET Services solution. Nearly anything you do with your solution requires you to supply an Authentication Token. An Authentication Token can be requested from the Microsoft .NET Access Control Service by supplying a set of credentials in a number of different forms.  For the purpose of this post I’ll be using the username/password option which will mean that you can authenticate using whatever technology stack you feel most comfortable with. Using the .NET library this can be done in just a few lines of code. However, sometimes it’s useful to see what’s going on under the covers, so let’s look at how you can acquire a token using just Http messages.

Essentially acquiring a Authentication Token can be done by sending a Http Get to http://accesscontrol.windows.net/issuetoken.aspx, supplying the solution name and password as query parameters. For example I could enter https://accesscontrol.windows.net/issuetoken.aspx?u=BlogSample&p=<password> into a web browser and would get back a page containing a token “ELp6ymQPzEiKbwgyTg+cQKvvhGVm0oOnGqO1kA==”. To do this in code you just need to create a webrequest to this url.

public static string HttpGetAuthenticationToken(string username, string password)
{
    string requestUri = string.Format("
https://accesscontrol.windows.net/issuetoken.aspx?u={0}&p={1}",
                                    username, Uri.EscapeDataString(password));

    var request = WebRequest.Create(requestUri);
    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        return reader.ReadToEnd();
    }
}

You can make a similar request from javascript (note that due to cross-domain scripting this will only work from Windows Gadgets or Windows Mobile Widgets where the normal restrictions don’t apply).

function httpGetAuthenticationToken(username, password) {
    http = new XMLHttpRequest();
    http.open("GET", "
https://accesscontrol.windows.net/IssueToken.aspx?u=" + username + "&p=" + password, false);
    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status == 200) {
            var token = http.responseText;
            // Do something with the token....
        }
    }
    http.send();
}

WCF on Windows Mobile and .NET Compact Framework

I was just listening to Don, Dave and James on the second of the Jumpstart series for the Codemasons’ Guild and the topic of communicating via WCF came up.  Now typically when I build mobile apps I don’t go through all the pain of using WCF, I simply use a regular asmx web service and then use Add Web Reference to add it to my mobile project.  To secure it, I just communicate over SSL. If you do want/need to use WCF on the server side, there are a couple of options to do this.

Before we jump into how you use WCF, let me point out a couple of useful powertoys:

The Power Toys for .NET CF include NetCFSvcUtil.exe which is a device equivalent of SvcUtil.exe and is needed in order generate the WCF proxy.

Firstly, you need to be aware that the .NET CF has some severe limitations when it comes to WCF.  Unfortunately the only binding that is supported (excluding the much over-hyped WCF via Exchange) is basicHttpBinding. For the WCF service you want to consume you need to change it from using the default wsHttpBinding.  This can be done by launching the Tools > WCF Service Configuration Editor from Visual Studio. Open the web.config file for the WCF Service project.  Under Endpoints, adjust the Binding to basicHttpBinding.

image

Save this change and run the WCF Service.

Now to the options…..

1) The first option is to use Add Web Reference.  This is by far the simplest approach as you can click Browse to: Web services in this solution.  Select your service and click Add Reference. 

image

Once you have added the reference you can call your service method the same way you would from a regular desktop application:

localhost.Service1 service = new localhost.Service1();
service.Url = service.Url.Replace("localhost", "192.168.1.2");
return service.GetData(5, true);

Note: You have to change the “localhost” host name to something that can be resolved by the device.  I typically just use the ip address of the development machine.  Clearly for production you will want to specify this in a configuration file or make it a configurable setting within your application.

2) The second option is to use NetCFSvcUtil.exe to generate the appropriate WCF proxy information. Start by opening up the Visual Studio command prompt (Start > Microsoft Visual Studio 2008 > Visual Studio Tools > Visual Studio 2008 Command Prompt) and adding the path to the Compact Framework power toys:

>> set path=%path%;C:\Program Files\Microsoft.NET\SDK\CompactFramework\v3.5\bin

Navigate to the folder where you want the proxy files to be created and then use NetCFSvcUtil.  I figured this would be quite simple but it appears that somewhere between Vista SP1 and SP2 (and there are reports of this problem on Windows 7 too) a bug in NETCFSvcUtil surfaced preventing it from working.

image

As you can see the error message is really helpful:

Error: An error occurred in the tool.
Error: Error in the application.

Currently, there doesn’t seem to be a workaround for this.  Some people have had varied success by changing the parameters and return types of the service methods.  The one strategy I used that appears to work is to use a combination of SvcUtil and NetCFSvcUtil.

>> svcutil.exe c:\temp\WindowsMobileServices\MyDataServices\bin\MyDataServices.dll

Microsoft (R) Service Model Metadata Tool
[Microsoft (R) Windows (R) Communication Foundation, Version 3.0.4506.2152]
Copyright (c) Microsoft Corporation.  All rights reserved.

Generating metadata files...
C:\temp\WindowsMobileServices\WindowsMobileServices\tempuri.org.wsdl
C:\temp\WindowsMobileServices\WindowsMobileServices\tempuri.org.xsd
C:\temp\WindowsMobileServices\WindowsMobileServices\schemas.microsoft.com.2003.1
0.Serialization.xsd

>> netcfsvcutil.exe tempuri.org.wsd tempuri.org.xsd
Microsoft (R) .NET Compact Framework Service Model Metadata Tool
[Microsoft (R) Windows (R) Communication Foundation, Version 3.5.0.0]
Copyright (c) Microsoft Corporation.  All rights reserved.

Generating files...
C:\temp\WindowsMobileServices\WindowsMobileServices\tempuri.org.cs
C:\temp\WindowsMobileServices\WindowsMobileServices\CFClientBase.cs

Now, include all the generated file into your mobile project.  You will also need to add references to System.ServiceModel and System.Runtime.Serialization.

In order to call your service method you can now write the following:

var binding = System.ServiceModel.BasicHttpBinding();
var endpoint = System.ServiceModel.EndpointAddress("http://192.168.1.2:6323/Service1.svc");
Service1Client client = new Service1Client(new binding, new endpoint
);
return client.GetData(5);

So, the question is why would you go the second option?  Well if you actually look at the generated code, adding the WCF service using Add Web Reference adds a bunch of unnecessary fields.  When calling the method GetData there is a second parameter called “valueSpecified” which is little more than a flag to indicate if the first parameter was specified or not.  This is not required if you use the second option to generate the proxy information.

More Windows Mobile Controls for .NET Compact Framework with Mirabyte

I noticed Frank’s post on the new Touch Controls Suite 1.0 from Mirabyte.  It’s hard to do the UI justice, particularly because the YouTube video they have on the page uses such horrible colours.  I do like how it appears in Visual Studio and I think the nice touch responsiveness (in WM5 and WM6 apparently) looks great.

image image

It’s great to see more and more controls being built to help Windows Mobile developers build rich user interfaces.

Another bee4Mobile.Net Control

In my previous post I talked about the ColourPicker, the ProgressBar and the OpenSaveDialog controls.  Now, in an effort to assist Windows Mobile developers build stylish applications they have released another swag of controls.

First is the iWheel.  Pictured below, the iWheel is similar to the wheel control seen in a number of iPhone applications.  Specifically designed to take advantage of touch input, the iWheel is not limited to just picking dates.  The developer can control the number of items and the number of cylinders on the wheel.

image

The other controls they have produced are a set of transparency enabled controls:

  • TPictureBox
  • TCheckBox
  • TLabel
  • TForm
  • TImageButton

These controls make use of the alpha channel of images in order to give a transparent effect.  The TCheckbox also allows the developer to specify a custom image for the checkbox itself.

The controls that ship with the .NET Compact Framework are in a word “Grey”.  Without heavily customising these controls it’s almost impossible to build an attractive user interface.  I would encourage mobile application developers to consider either building their own controls (often a painstaking process to get right) or using the controls from bee4Mobile.Net

Accessing Data with Windows Mobile 6.5 Widgets, Ajax 4, Entity Framework and ADO.NET Data Services.

Step 1: Creating Your Projects

Create a new ASP.NET Web Application (new solution) called DataWidgetDemo.  You can delete the Default.aspx page as this is not required.

Add new web site to your solution.  Select the Empty Website template, change the location to be nested within your solution folder (by default it goes under documents and settings) and call the website ProductWidget.  The location should look like c:\<solution root>\DataWidgetDemo\ProductWidget. You can delete the web.config file as this is not required.

Your solution should look similar to the following:

image 

Step 2: Adding Data

There are two components to exposing data (done in the DataWidgetDemo project):

a) Create an Entity Data Model which will interact with the AdventureWorksLT database– To do this, select Add New Item and then select the ADO.NET Entity Data Model from the Add New Item dialog. In our example we have named the new item AdventureWorksModel.edmx.

Work through the Entity Data Model Wizard, specifying the connection string for your database instance.  When prompted, select the Product and ProductCategory tables. You don’t need to change any of the other default values in this wizard. Once complete this should yield the entity designer with the two tables displayed.

image 

b) In order to consume this from our widget we need to expose the AdventureWorks data via an ADO.NET Data Service. To do this select Add New Item and pick the ADO.NET Data Service template.  Give the new item a name: AdventureWorksData.svc and select Add.

Once this item has been added you just need to specify which data set you want to expose. The text marked in red are the components that have been updated in order to expose all the tables (ie *) in the AdventureWorksLTEntities data model

public class AdventureWorksData : DataService<AdventureWorksLTEntities>
   {
       // This method is called only once to initialize service-wide policies.
       public static void InitializeService(IDataServiceConfiguration config)
       {
           // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
           // Examples:
          config.SetEntitySetAccessRule("*", EntitySetRights.All);
           // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
       }
   }

 

Step 4: Add Ajax 4 JS Libraries

This step requires referencing the ASP.NET Ajax 4 javascript libraries.  As these are still pre-release the location of these files is not well defined but you should be able to locate them on your machine in order to copy them into the ProductWidget web site.

image

Step 5: Building the Products page

The products page is just going to be a simple html page.  Windows Mobile 6.5 can’t render ASP.NET pages (ie aspx) pages as part of widgets as this would require having the equivalent of the ASP.NET server engine on the device.  As such widgets are constrained to html, css and javascript.  To generate the products page select Add New Item on the ProductWidget web site. Select the HTML Page template and give the new page a name, Products.htm, and click Add.

Here we are only going to use HTML! There is not a single of javascript in the following code snippet.  There is javascript, but it’s all included in the ASP.NET Ajax 4 library.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml" >
<head>
    <title>AdventureWorks - Products</title>
    <style type="text/css">
        .sys-template {display: none;}
        .rowSelected  {background-color: yellow;}
    </style>

    <script src="MicrosoftAjax/MicrosoftAjax.debug.js" type="text/javascript"></script>
    <script src="MicrosoftAjax/MicrosoftAjaxTemplates.debug.js" type="text/javascript"></script>
    <script src="MicrosoftAjax/MicrosoftAjaxAdoNet.debug.js" type="text/javascript"></script>

</head>
<body xmlns:sys="javascript:Sys"
      xmlns:dataview="javascript:Sys.UI.DataView"
      xmlns:datacontext="javascript:Sys.Data.AdoNetDataContext"
      sys:activate="*">

    <br /><br />

    <table
        sys:attach="datacontext"
        datacontext:sys-key="dataContext"
        datacontext:serviceuri ="
http://192.168.0.102:18886/AdventureWorksData.svc"
        >
    <thead>
        <tr>
            <th>Select Category</th>
        </tr>
    </thead>
    <tbody
        id="dvCategories"
        class="sys-template"
        sys:attach="dataview"
        dataview:sys-key="categoriesKey"
        dataview:dataprovider="{{ dataContext }}"
        dataview:autofetch="true"
        dataview:selecteditemclass="rowSelected"
        dataview:fetchoperation="ProductCategories"
        dataview:fetchparameters="{{ {$expand: 'Products'} }}"
        >
        <tr sys:command="select">
            <td>{{ Name }}</td>
        </tr>
    </tbody>
    </table>

    <br /><br />

    <table>
    <thead>
        <tr>
            <th>Name</th>
        </tr>
    </thead>
    <tbody
        class="sys-template"
        sys:attach="dataview"
        dataview:sys-key="moviesKey"       
        dataview:data="{binding selectedData.Products, source={{categoriesKey}} }">   
        <tr>
            <td>{binding Name}</td>
        </tr>
    </tbody>
    </table>

    </body>
    </html>

One point to note about this html: The url marked in dark blue needs to be updated to point to the fully qualified url of your ADO.NET Data Service.  As this code will be running on the mobile device or emulator you can’t use localhost as it won’t resolve.  Instead use the IP address of your computer.  This example will work whether you are using Cassini or IIS to host your web application but if you are using Cassini you will need to specify the port number.

Step 6: Add a config.xml file

In order for Windows Mobile 6.5 to install your widget it needs to be able to locate a config.xml file within your widget.  This specifies properties such as the name and author of the widget – sort of a manifest for your widget. Again to add this we select Add New Item on the ProductWidget web site.  Select the XML File template and name the file config.xml.

<?xml version="1.0" encoding="utf-8" ?>
<widget xmlns="
http://www.w3.org/ns/widgets"
        id="http://www.builttoroam.com/ProductWidget"
        version="1.0">

  <name>AdventureWorks Product Widget</name>

  <description>
    Basic product widget based on ajax 4
  </description>

  <author href="https://blogimages.builttoroam.com/nick"
            email="[email protected]">
    Nick Randolph @ Built To Roam
  </author>

  <content src="Products.htm" type="text/html"/>

  <access network="true"/>

  <icon src="BTRLogo.png" />

  <license>
    Copyright (c) 2009 Built To Roam.  All rights reserved.
  </license>
</widget>

You will notice that this config file specifies BTRLogo.png for its icon.  In this case this is a 90x90 png file that I have added to root of the the ProductWidget web site.

Step 7: Zip and Deploy Your Widget

The last step in building your widget is to zip up the contents of the ProductWidget web site.  Don’t zip up the ProductWidget folder itself, just the contents.  Rename the zip file to have the .widget extension:

image

Deployment is as simple as copying the widget file to the emulator and clicking on the widget file on the device.  Note that this deployment mechanism is not going to be supported out of the box by real Windows Mobile 6.5 devices.  Instead there is a registry key that needs to be set to enable this functionality (which is set by default on the emulator images).

image

With this registry key set the widget file will be identified within File Explorer on the device

image

Clicking the Products widget will give you the option to install and run the widget on the device.

image

If you click Yes the widget will be installed onto the device.  After installation your application will immediately be run.  In this case because we are accessing data from across the network you will see the following prompt.

image 

Selecting continue will launch your widget.  Don’t be alarmed if it takes a while to download the data! For a production application you would of course want some animation to indicate that it’s downloading data in the background.

Disclaimer: The UI is not fantastic but this post is around exposing data rather than building the UI around it. As it’s just html and css you can customise the UI as much as you want.

image

Once you have closed your widget you can run it again by selecting its icon from the Start menu.

image

Widgets can be removed in the same way as normal programs.  From the Start menu select Settings –> System –> Remove Programs.

image

Note: You will need to remove your widget if you want to install an updated version.  During development to save you from doing this you can simply copy over updated files.  Your widget will be installed to \Program Files\Widgets\User\<id> where id is a numeric value assigned to your widget.

And there you have it – a simple widget that exposes data from your sql database!

Working with the Wave API for Windows and Windows Mobile via the .NET Framework

There are a number of samples out there that show how to work with the Wave API (ie sound recording and playback) on both Windows and Windows Mobile via either the .NET Framework or the .NET Compact Framework.  Unfortunately because of subtle differences between the platforms it is difficult to build a single library that works across both platforms. Over a series of posts I’m going to show you how to work with the api to build a library that will work across both platforms.

The starting point is to look at the api calls themselves.  There is plenty of documentation out there that discusses what each of the calls are supposed to do but essentially the api calls are the same between the platforms with the exception that they reside in different assemblies.  As they are native calls they need to be imported using a DllImport.  Here are most of the Windows (Desktop suffix) and Windows Mobile (CF suffix) imports – there are some additional api calls that you can include if you require them following a similar pattern.

[DllImport("coredll.dll", EntryPoint = "waveInGetNumDevs")]
internal static extern int waveInGetNumDevs_CF();

[DllImport("winmm.dll", EntryPoint = "waveInGetNumDevs")]
internal static extern int waveInGetNumDevs_Desktop();

[DllImport("coredll.dll", EntryPoint = "waveInOpen")]
internal static extern MMSYSERR waveInOpen_CF(out IntPtr phwi, uint uDeviceID, ref WAVEFORMATEX pwfx, IntPtr callback, uint dwInstance, uint fdwOpen);
[DllImport("winmm.dll", EntryPoint = "waveInOpen")]
internal static extern MMSYSERR waveInOpen_Desktop(out IntPtr phwi, uint uDeviceID, ref WAVEFORMATEX pwfx, IntPtr callback, uint dwInstance, uint fdwOpen);

[DllImport("coredll.dll", EntryPoint = "waveInPrepareHeader")]
internal static extern MMSYSERR waveInPrepareHeader_CF(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveInPrepareHeader")]
internal static extern MMSYSERR waveInPrepareHeader_Desktop(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveInUnprepareHeader")]
internal static extern MMSYSERR waveInUnprepareHeader_CF(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveInUnprepareHeader")]
internal static extern MMSYSERR waveInUnprepareHeader_Desktop(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveInClose")]
internal static extern MMSYSERR waveInClose_CF(IntPtr hwi);

[DllImport("winmm.dll", EntryPoint = "waveInClose")]
internal static extern MMSYSERR waveInClose_Desktop(IntPtr hwi);

[DllImport("coredll.dll", EntryPoint = "waveInReset")]
internal static extern MMSYSERR waveInReset_CF(IntPtr hwi);

[DllImport("winmm.dll", EntryPoint = "waveInReset")]
internal static extern MMSYSERR waveInReset_Desktop(IntPtr hwi);

[DllImport("coredll.dll", EntryPoint = "waveInStart")]
internal static extern MMSYSERR waveInStart_CF(IntPtr hwi);

[DllImport("winmm.dll", EntryPoint = "waveInStart")]
internal static extern MMSYSERR waveInStart_Desktop(IntPtr hwi);

[DllImport("coredll.dll", EntryPoint = "waveInStop")]
internal static extern MMSYSERR waveInStop_CF(IntPtr hwi);

[DllImport("winmm.dll", EntryPoint = "waveInStop")]
internal static extern MMSYSERR waveInStop_Desktop(IntPtr hwi);

[DllImport("coredll.dll", EntryPoint = "waveInAddBuffer")]
internal static extern MMSYSERR waveInAddBuffer_CF(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveInAddBuffer")]
internal static extern MMSYSERR waveInAddBuffer_Desktop(IntPtr hwi, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveInGetDevCaps")]
internal static extern MMSYSERR waveInGetDevCaps_CF(uint uDeviceId, byte[] pwic, uint cbwic);

[DllImport("winmm.dll", EntryPoint = "waveInGetDevCaps")]
internal static extern MMSYSERR waveInGetDevCaps_Desktop(uint uDeviceId, byte[] pwic, uint cbwic);

[DllImport("coredll.dll", EntryPoint = "waveOutGetNumDevs")]
internal static extern int waveOutGetNumDevs_CF();

[DllImport("winmm.dll", EntryPoint = "waveOutGetNumDevs")]
internal static extern int waveOutGetNumDevs_Desktop();

[DllImport("coredll.dll", EntryPoint = "waveOutOpen")]
internal static extern MMSYSERR waveOutOpen_CF(out IntPtr phwo, uint uDeviceID, ref WAVEFORMATEX pwfx, IntPtr dwCallback, uint dwInstance, uint fdwOpen);
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]        internal static extern MMSYSERR waveOutOpen_Desktop(out IntPtr phwo, uint uDeviceID, ref WAVEFORMATEX pwfx, IntPtr dwCallback, uint dwInstance, uint fdwOpen);        /// <summary>

[DllImport("coredll.dll", EntryPoint = "waveOutPrepareHeader")]
internal static extern MMSYSERR waveOutPrepareHeader_CF(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
internal static extern MMSYSERR waveOutPrepareHeader_Desktop(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveOutWrite")]
internal static extern MMSYSERR waveOutWrite_CF(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
internal static extern MMSYSERR waveOutWrite_Desktop(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveOutUnprepareHeader")]
internal static extern MMSYSERR waveOutUnprepareHeader_CF(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
internal static extern MMSYSERR waveOutUnprepareHeader_Desktop(IntPtr hwo, IntPtr pwh, uint cbwh);

[DllImport("coredll.dll", EntryPoint = "waveOutClose")]
internal static extern MMSYSERR waveOutClose_CF(IntPtr hwo);

[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
internal static extern MMSYSERR waveOutClose_Desktop(IntPtr hwo);

[DllImport("coredll.dll", EntryPoint = "waveOutReset")]
internal static extern MMSYSERR waveOutReset_CF(IntPtr hwo);

[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
internal static extern MMSYSERR waveOutReset_Desktop(IntPtr hwo);

[DllImport("coredll.dll", EntryPoint = "waveOutGetDevCaps")]
internal static extern MMSYSERR waveOutGetDevCaps_CF(uint uDeviceID, byte[] pwoc, uint cbwoc);

[DllImport("winmm.dll", EntryPoint = "waveOutGetDevCaps")]
internal static extern MMSYSERR waveOutGetDevCaps_Desktop(uint uDeviceID, byte[] pwoc, uint cbwoc);

You will notice that the Windows Mobile calls all reference coredll.dll, whilst the Windows calls reference winmm.dll.  In order to build a common library we need a way to abstract these calls so that after initialising the library we never have to worry about whether it’s running on a device or a desktop machine. To do this we just need to define a series of delegates that map to each of these functions, and then create instances of them that point to the relevant functions.

public delegate int HardwareDeviceCount();
public delegate MMSYSERR HardwareOpen(out IntPtr hardwareInterface, uint deviceId, ref WAVEFORMATEX waveFormatSettings, IntPtr callbackFunction, uint callbackData, uint callbackType);
public delegate MMSYSERR HardwareGetDeviceCaps(uint deviceId, byte[] data, uint dataSize);
public delegate MMSYSERR HardwarePrepareHeader(IntPtr hardwareInterface, IntPtr headerReference, uint headerSize);
public delegate MMSYSERR HardwareAddBuffer(IntPtr hardwareInterface, IntPtr bufferReference, uint bufferSize);
public delegate MMSYSERR HardwareStart(IntPtr hardwareInterface);
public delegate MMSYSERR HardwareStop(IntPtr hardwareInterface);
public delegate MMSYSERR HardwareUnprepareHeader(IntPtr hardwareInterface, IntPtr headerReference, uint headerSize);
public delegate MMSYSERR HardwareClose(IntPtr hardwareInterface);
public delegate MMSYSERR HardwareReset(IntPtr hardwareInterface);
public delegate MMSYSERR HardwareGetPosition(IntPtr hardwareInterface, ref MMTIME timeReference, uint timeSize);
public delegate MMSYSERR HardwareGetId(IntPtr hardwareInterface, ref uint deviceOutput);

private static HardwareDeviceCount waveInGetNumDevs;
private static HardwareOpen waveInOpen;
private static HardwareGetDeviceCaps waveInGetDevCaps;
private static HardwarePrepareHeader waveInPrepareHeader;
private static HardwareUnprepareHeader waveInUnprepareHeader;
private static HardwareAddBuffer waveInAddBuffer;
private static HardwareStart waveInStart;
private static HardwareClose waveInClose;
private static HardwareStop waveInStop;
private static HardwareReset waveInReset;

private static HardwareDeviceCount waveOutGetNumDevs;
private static HardwareOpen waveOutOpen;
private static HardwareReset waveOutReset;

private static HardwareGetDeviceCaps waveOutGetDevCaps;
private static HardwarePrepareHeader waveOutPrepareHeader;
private static HardwareUnprepareHeader waveOutUnprepareHeader;
private static HardwareClose waveOutClose;
private static HardwareWrite waveOutWrite;

private static void SetupDesktopDelegates()
{
    waveOutGetNumDevs = NativeMethods.waveOutGetNumDevs_Desktop;
    waveOutOpen = NativeMethods.waveOutOpen_Desktop;
    waveOutReset = NativeMethods.waveOutReset_Desktop;

    waveOutGetDevCaps = NativeMethods.waveOutGetDevCaps_Desktop;
    waveOutPrepareHeader = NativeMethods.waveOutPrepareHeader_Desktop;
    waveOutUnprepareHeader = NativeMethods.waveOutUnprepareHeader_Desktop;
    waveOutClose = NativeMethods.waveOutClose_Desktop;
    waveOutWrite = NativeMethods.waveOutWrite_Desktop;

    waveDataCallback = QueryOutgoingData;
    waveDataCallbackPointer = Marshal.GetFunctionPointerForDelegate(waveDataCallback);
    CALLBACK_TYPE = Wave.CALLBACK_FUNCTION;
}

private static void SetupCompactDelegates()
{
    waveOutGetNumDevs = NativeMethods.waveOutGetNumDevs_CF;
    waveOutOpen = NativeMethods.waveOutOpen_CF;
    waveOutReset = NativeMethods.waveOutReset_CF;

    waveOutGetDevCaps = NativeMethods.waveOutGetDevCaps_CF;
    waveOutPrepareHeader = NativeMethods.waveOutPrepareHeader_CF;
    waveOutUnprepareHeader = NativeMethods.waveOutUnprepareHeader_CF;
    waveOutClose = NativeMethods.waveOutClose_CF;
    waveOutWrite = NativeMethods.waveOutWrite_CF;

    WaveLibrary.Native.CF.SoundMessageWindow window = Wave.SetupMessageWindow();
    window.WaveOutDoneMessage += new WaveDoneEventHandler(MessageWindow_WaveDoneMessage);
    waveDataCallbackPointer = window.Hwnd;
    CALLBACK_TYPE = Wave.CALLBACK_WINDOW;
}

In this code we have declared the set of delegates, a set of static instances and then have a two methods that create instances pointing to either the Windows or Windows Mobile imported functions.  Note that it’s important to have these abstracted into separate methods as the library will throw an exception when it attempts to reference a dll that can’t be located.  Having these calls in a separate function means that only the called function will be JIT’d meaning the exception won’t be thrown.

if (useDesktop)
            {
                SetupDesktopDelegates();
            }
            else
            {
                SetupCompactDelegates();
            }

You can simply call the relevant setup function based on whether you are running on the desktop or a device.

beeMobile4.net Controls

I’ve been meaning to take a look at the set of controls available from www.beemobile4.net for a while now.  Here’s a brief getting started:

There are three control sets to pick from: ColorPicker, ProgressBar and OpenSaveDialog.  Each of these has a trial download. There is also a free set of utils, which mainly just a set of code samples for wrapping native calls.  It includes things like resetting the idle timeout, resetting the device and getting device (eg IMEI) and application information (eg application directory).

Unfortunately they have elected to go with a zip deployment for each of the trial downloads.  Whilst for most of us this is fine I don’t believe it’s the best way to deliver developer tools – building an installer with Wix isn’t that difficult after all. Once you have downloaded the zip file you need to extract it; you will notice there are versions of the controls for both v2 and v3.5 of the .NET Compact Framework.

image

Once you have extracted the zip file you need to manually add the controls to the toolbox.  I’d start by creating a new tab to hold these controls together.

image

Next use the Choose Items…. dialog to browse to the extracted folder, select the version of the controls you want to use and then select the dll.

image

Once the controls are in the toolbox you can simply drag them onto the designer

image

Here you can see the static colour palette and the colour picker.  The Palette has a series of predefined colours but can also contain one or two rows of custom colours.

The colour picker has two modes, Office and Professional, that determine how the colour selector is laid out.  Both the palette and the picker have a SelectedColorChanged event that you can listen for.

The OpenSaveDialog control provides a reusable control (ie not a separate window) that you can host in your own forms.

image

Unfortunately I’m not able to comment on the ProgressBar as it wasn’t available for download as a trial.  Well, the link was there, but all I got was a second instance of the OpenSaveDialog control :-(

Updated: This link has now been fixed and the ProgressBar is available from the site for you to try.

Using the right controls is just part of building a full featured application.  I’d suggest giving these controls a bit of a whirl as they can save you time.

All CC.NET Builds are Green

One of the things that has been bugging me over the last month is that one of our over 40+ build projects was failing.  Earlier this week Andrew brought in his build board – similar concept to the Build Bunny.    

image

Yesterday it was connected and what was amazing to watch was the efficiency with which the one broken build was fixed.  Now perhaps this was just to see what the board would read when all the builds were ok but I suspect that just having the build more visible is a key factor. 

What’s interesting is that most of the crew use cctray so they know that one of the builds was failing.  So why does the build board make a difference?  I think the key difference is that with cctray because it’s somewhat private everyone can choose to ignore it.  With it in the open there’s no escaping it and someone has to put their hand up and fix it.

Visual Studio 2010, .NET Framework 4.0 CTP and a Competition

Across on the Professional Visual Studio blog I posted earlier today that the first public CTP for Visual Studio 2010 and the .NET Framework 4.0 is available for download.  While I was there I noted that I have been remiss in not covering the “Win a Free Copy of Professional Visual Studio 2008” competition.  Essentially all you have to do is write a comment on any post of the Professional Visual Studio website or link to the site from a post you have made on your sit – the best entry each month wins a copy of the book!

ASP.NET Virtual Earth Control (Windows Live Tools for VS2008 CTP3)

Last week CTP3 of the Windows Live™ Tools for Microsoft® Visual Studio® 2008 was made available.  This includes an ASP.NET Virtual Earth control that makes working with Virtual Earth as easy as drag-n-drop.

In this video I introduce this control and show you just how easy it is to integrate Virtual Earth functionality into both client and server side code on you site.

The Windows Live Tools also includes an ASP.NET control for Silverlight Streaming.  Dr Neil goes through getting started with this control in his post from last week.

If you have any feedback on this, or any part of these tools, I would love to hear from you.  Providing feedback to Microsoft on these tools is the best way to ensure they continue to deliver the best developer tools to help us do our jobs!

 

Advanced Graphics with .NET Compact Framework

Building managed applications for Windows Mobile devices that have a rich user interface is almost impossible if you restrict yourself to the primitive controls that ship with the .NET Compact Framework.  Luckily building your own custom controls isn't that difficult.  You typically have to override the painting of the control to render the content the way you want it.  Unfortunately if you want to do some more complex rendering you will still run into problems because the Graphics object that is exposed by the OnPaint event is a significantly reduced subset of the desktop Graphics object.  The main things it misses are the ability to transform (scale, rotate and translate) the graphics.

Lets take an example, say you have a method that draws a cross:

Private Sub DrawCross(ByVal g As Graphics, 
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, -size, -size, size, size)
    g.DrawLine(mForegroundPen, size, -size, -size, size)
End Sub

As you can see from this method it draws the cross based around the origin.  This is great but the likelihood of you wanting to draw a cross at the origin is virtually 0 since only a quarter of it would be visible.  There are a couple of options in terms of positioning the cross.  Firstly, you can modify the DrawCross method to accept a third parameter that identifies the centre of the cross:

Private Sub DrawCross(ByVal g As Graphics, _
                               ByVal centre As Point, _
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, centre.X - size, centre.Y - size, _
                                           
centre.X + size, centre.Y + size)
    g.DrawLine(mForegroundPen, centre.X + size, centre.Y - size, _
                                            centre.X - size, centre.Y + size)
End Sub

Now all of a sudden the method has become significantly less readable and you can imagine how it would get if the rendering was more complex than just a cross. The second way to do this, and my preference, is to translate the centre of the graphics canvas.  This way the DrawCross method doesn't change - it still things it's drawing at the origin - just the canvas that you are drawing on does.  A way to visualise this is to imagine a pen suspended in mid air ready to draw a cross on the canvas below it.  What we want to do is reposition the canvas underneath so that when the pen draws the cross, the cross is actually at the required position in the canvas.  When we are done, we have to remember to reset the canvas so that other drawing is done at the right place.  We can do this as follows:

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    MyBase.OnPaint(pe)

    Dim g As Graphics = pe.Graphics
   
g.Clear(Me.BackColor)
   
    g.TranslateTransform(Me.Width / 2, Me.Height / 2)
    DrawCross(g, 10)
    g.ResetTransform()
End Sub

As you can see it is clear from here what is going on - we are moving to the centre of the control, drawing the cross with size 10 and then resetting the canvas (just in case other methods are added).

Going back to the original discussion around the .NET Compact Framework you will notice that the TranslateTransform method doesn't exist on the Graphics object.  I'm guessing that part of the reason for this is the lack of support from the underlying rendering engine but here's quite a simple way to get around this issue (be warned though, calculations involved in doing these operations can quickly become CPU intensive which can make your application slow and consume battery power!).  You need to create a wrapper graphics class that is capable of doing the layout changes you want:

Public Class TranformGraphics
    Private mGraphics As Graphics
    Private mMatrix As TMatrix

    Public Sub New(ByVal g As Graphics)
        Me.mGraphics = g
    End Sub

    Public Sub DrawLine(ByVal pen As Pen, ByVal x1 As Integer, _
                                                        ByVal y1 As Integer, _
                                                        ByVal x2 As Integer, _
                                                        ByVal y2 As Integer) 
        Me.mGraphics.DrawLine(pen, ConvertedX(x1, y1), _
                                                ConvertedY(x1, y1), _
                                                ConvertedX(x2, y2), _
                                                ConvertedY(x2, y2))
    End Sub

    Private Function ConvertedX(ByVal x As Integer, _
                                           ByVal y As Integer) As Integer
        If Me.mMatrix Is Nothing Then Return x
        Return CInt(mMatrix.R1C1 * x + mMatrix.R1C2 * y + mMatrix.DX)
    End Function

    Private Function ConvertedY(ByVal x As Integer, _
                                           ByVal y As Integer) As Integer
        If Me.mMatrix Is Nothing Then Return y
        Return CInt(mMatrix.R2C1 * x + mMatrix.R2C2 * y + mMatrix.DY)
    End Function

    Public Sub ResetTransform()
        mMatrix = Nothing
    End Sub

    Public Sub TranslateTransform(ByVal dx As Single, ByVal dy As Single)
        Dim trans As New TMatrix(1, 0, 0, 1, dx, dy)
        If mMatrix Is Nothing Then
            mMatrix = trans
        Else
            mMatrix = mMatrix.Multiply(trans)
        End If
    End Sub
End Class

I've left the implementation of TMatrix to your imagination but the important method that you need to get right is the matrix multiplication:

Public Function Multiply(ByVal M1 As TMatrix) As TMatrix
    Dim M2 As TMatrix = Me

    Return New TMatrix(M2.R1C1 * M1.R1C1 + M2.R1C2 * M1.R2C1, _
                        M2.R1C1 * M1.R1C2 + M2.R1C2 * M1.R2C2, _
                        M2.R2C1 * M1.R1C1 + M2.R2C2 * M1.R2C1, _
                        M2.R2C1 * M1.R1C2 + M2.R2C2 * M1.R2C2, _
                        M2.R1C1 * M1.DX + M2.R1C2 * M1.DY + M2.DX, _
                        M2.R2C1 * M1.DX + M2.R2C2 * M1.DY + M2.DY)
End Function

There might be an easier way to do this with existing classes, so if you come across something please let me know.  Now back to our code - instead of using the Graphics.TranslateTransform (which doesn't exist for the .NET CF), we can now use our newly created TransformGraphics.TranslateTransform:

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    MyBase.OnPaint(pe)

    Dim g As new TransformGraphics(pe.Graphics)
   
g.Clear(Me.BackColor)
   
    g.TranslateTransform(Me.Width / 2, Me.Height / 2)
    DrawCross(g, 10)
    g.ResetTransform()
End Sub

Private Sub DrawCross(ByVal g As TransformGraphics, 
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, -size, -size, size, size)
    g.DrawLine(mForegroundPen, size, -size, -size, size)
End Sub

Note we have still had to modify the DrawCross method so that it accepts a TransformGraphics but overall we haven't really affected the readability of the code.  Please feel free to comment on how you get around the challenges of complex rendering using the .NET Compact Framework!

What do Victorian .NET SIG attendees and Ben Cousins have in Common?

A: They're both "users" in the eyes of the government....

Ok, so not a great joke but I had to post about this as it is a true sign that political correctness has gone a step too far.  When Victoria .NET was established, the Melbourne .NET User Group and the Australian Developers .NETwork combined and came in under their banner.  For better or worse it was done to help the user groups function, providing venue and funds for them do continue to operate.  However, they decided to change the name to Victoria .NET Dev SIG.  Apparently one of the reasons it wasn't called a "user group" was that it has connotations relating to drug using - go figure huh!

I mentioned this earlier today to a friend of mine and his response was

... so the attendees now wear protective helmets and badges that say they are "special" ...

Power Tools for .NET Compact Framework v3.5

Microsoft have just released the Power Tools for the .NET CF v3.5 B2 that can be download here, which includes:

  • Remote Performance Monitor and GC Heap Viewer
  • NETCF CLR Profiler
  • App Configuration Tool
  • NETCF ServiceModel Metadata Tool
  • Remote Logging Configuration Tool
  • NETCF Network Log Viewer

There are a few known issues (like 25+) but most of them are with the CLR Profiler.