Make our Gadget do Something Useful

I've added some functionality to the gadget we've been building in part 1 and part 2 but I think it's going to be very difficult to share this in a blog posting. Partly this is because I've tweaked things all over the place and to record the chronology of these changes in a blog post is tricky but mainly it's because my code formatter doesn't seem to work so posting code to my blog is broken (hence why I used screenshots in the previous posts). Aha, I now have a workaround for the code formatting (thanks to the Live Writer team) so we should be able to get somewhere with any luck...

Let's start by tidying our JavaScript a little. Using the principles of unobtrusive JavaScript (and I make no claim to be expert in JavaScript, nor do I suggest that everything I do follows current best practice, I just wanted to tidy things up a bit) we should try and separate behaviour from markup. So we can take our gadget.html and remove the embedded JavaScript:

 <html xmlns="https://www.w3.org/1999/xhtml" >
<head>
    <title>Gadget Template</title>
    <link rel="stylesheet" href="styles/default.css" type="text/css" />
    <script language="javascript" type="text/javascript" src="script/utils.js"></script>
</head>
<body>
    <div id="docked">
        <input id="search" type="text" />
        <input id="searchgo" type="button" value="Go" />
    </div>
    <div id="undocked"></div>
</body>
</html>

...to move the wiring up of event handlers into utils.js thus:

 window.onload = init;
 function init()
{
  // some stuff we don't need to worry about here
    
    document.getElementById("searchgo").onclick = flyout;    
    document.getElementById("search").onkeydown = check_for_enter;
}

window.onload fires after our document has loaded. I've wired that up to the init() function which is responsible for initialisation including the wiring of the other event handlers on the page. As well as handling the onclick event of the button, I've also added a handler for the onkeydown event of the textbox (input) so you can type a search term and just hit [Enter] rather than having to click on the button. The handler for onkeydown looks like this:

 function check_for_enter(e)
{
  // get event if not passed
  if (!e) var e = window.event;
   
  if (e.keyCode == 13)
  {
    flyout();
  }
}

Of course the fact we're running as a Vista gadget means we don't need to worry about things like cross-browser compatibility so it makes this sort of function much simpler. Here, for each keypress, we just check if the key pressed was the [Enter] key and if so, call the flyout function. The flyout function has now been reduced to just:

 function flyout()
{     
  System.Gadget.Flyout.show = true;      
}

...as I've moved the definition of both flyout and settings html files into the init() function:

 System.Gadget.settingsUI = "settings.html";
System.Gadget.Flyout.file = "flyout.html";

Setting the flyout html file in the flyout() function would make sense if we have multiple flyouts but as we only have the one, we can just define it upfront and leave it be. It might be a good idea to add a check to ensure that we don't show the flyout unless a minimum number of characters have been typed in the search box.

From this point there are various approaches you can take to handle the flyout itself. I think the MSDN article advocates handling the System.Gadget.Flyout.onShow event but TBH, I ran into a bit of trouble with this in my scenario so I opted for a different (and I think simpler) approach. No doubt someone will tell me this is doomed to failure but there you go.

Problems I ran into with handling onShow (IIRC) included some peculiarities with the flyout display and the fact that if I wanted to do a second search with the flyout already showing then of course the event doesn't fire and the query doesn't execute. I could probably have worked around these but I decided to go with a solution which encapsulates all the code required by the flyout into a separate js file. So we add a flyout.js and reference is in flyout.html:

 <html xmlns="https://www.w3.org/1999/xhtml">
<head>
  <title>Gadget Template Flyout</title>
  <link rel="stylesheet" href="styles/default.css" type="text/css" />
  <script language="javascript" type="text/javascript" src="script/flyout.js"></script>
</head>
<body style="width: 350px; height: 550px; background-image: url( '../images/flyout.png' );
  background-repeat: no-repeat;">
  <div id="flyoutBack">
    <span id="SearchUrl"></span>
    <div id="searchResults"></div>
  </div>
  <div id="loading">Loading...</div>
</body>
</html>

At this stage we should really add a flyout.css file as well and move the styling on the body tag into that css file (you can't add it to default.css as it already has styling for the body tag for gadget.html). Flyout .html again wires things up for us so that things start happening when the flyout document loads:

window.onload = onloaded;

function onloaded()
{
  var searchstring = System.Gadget.document.getElementById("search").value;
  var searchUrl = "https://feeds.technorati.com/search/" + searchstring + "?language=en";

  addContentToFlyout(searchUrl,searchstring);
}

ie when the flyout document is loaded, get the value of the textbox from the "main" gadget document (exposed to use through System.Gadget.document), form a suitable search URL (in this case I just inspected the Technorati search URL structure - it's pretty simple). Then we call a function (addContentToFlyout) to do the work of performing the search and populating the flyout with the results.

Now I'm sorry if my naming conventions seem to be all over the place. This results from a combination of using Tim's template, writing my own code without adhering to the template conventions and then cribbing some of Tim's code from his MSDN gadget just to add to the confusion. :-) And I'm too lazy to go change it at this stage.

addContentToFlyout kicks off an asynchronous request using the Microsoft.XMLDOM XML parser. Much like an XHR request, we add a handler for the onreadystatechange event which checks the readyState. Call the load method and our XML document is loaded asynchronously. When it's done (successfully) we should find ourselves in the loadHandler function. The reason I've gone down the async route was simply that I wanted to display some simple "Loading" UI in the flyout and found this wouldn't work if I used the synchronous mode (I assume because it was blocking the UI thread before the flyout had been updated).

 function addContentToFlyout(url, searchstring)
{
  try
  {
    document.getElementById("loading").style.visibility = "visible";
              
    var results = new ActiveXObject("Microsoft.XMLDOM");
    results.onreadystatechange = function ()
    {
      if (results.readyState == 4 && results.parseError.errorCode == 0)
      {
        loadHandler(results,searchstring);
      }
    }

    results.load(url);
  }
  catch (e)
  {
    flyoutDoc.getElementById("loading").style.visibility = "hidden";
    flyoutDoc.getElementById("searchResults").innerHTML = "<b>" + e + "</b>";
  }
}

In loadHandler we navigate the results document (which is in RSS 2.0 format) using an XPath query to select the item nodes then iterating around that collection building the HTML as we go (adding the link, title and description). *** big red flag at this point - this is not the "right" way to do this. It is the simple way. See the end of this blog post for how this should be done "correctly" ***

There is also a "loading" element in the flyout html which I've styled to appear in the centre of the flyout pane. By default it's hidden but I change the visibility when we start the query and hide it again when we're done. This creates a (very simple) feedback mechanism for the user.

 function loadHandler (results,searchstring)
{
  var maxResults = 10;
  var maxDesc = 75;
  
  var items = results.selectNodes("/rss/channel/item");
  results = null;
  
  if (items.length < maxResults)
  {
      maxResults = items.length;
  }
  
  var feedBody = "";
  
  for (i = 0; i < maxResults; i++)
  {
    var link = items.item(i).getElementsByTagName("link").item(0).text;
    var title = items.item(i).getElementsByTagName("title").item(0).text;
    var desc = items.item(i).getElementsByTagName("description").item(0).text;
    //var desc = "";
    if (desc.length > maxDesc)
    {
      desc = desc.substring(0, maxDesc-3) + "...";
    }
    
    feedBody += "<div><div class='resultHeader'><a href='" + link + "'>" + 
                title + "</a></div><div class='resultDesc'>" + desc + "</div></div>";
    
    if (i < maxResults - 1)
    {
        feedBody += "<hr/>";
    }
  }
  
  document.getElementById("loading").style.visibility = "hidden";
  document.getElementById("searchResults").innerHTML = feedBody;
  document.getElementById("SearchUrl").innerHTML = 
    "Full results: <a href='https://technorati.com/search/" + searchstring + "?language=en'>" 
    + searchstring + "</a>";
}

There's some styling to go along with this too. Probably easiest if I point you to a download of the gadget files.

Note there is still work to be done, particularly around security. As it stands it would be possible for malicious code to be returned by our query which is then executed by our gadget. It is vital that input is validated and output coming from any untrusted source (such as our search query) is sanitized to ensure we do not inadvertently execute malicious code. See the excellent MSDN article Inspect Your Gadget.

Technorati Tags:
vista
,
sidebar
,
gadget