Introduction
When I first installed Windows Vista on my Virtual Server I noticed the sidebar and thought, "Ok, that's pretty cool". But when I found out that all you needed was HTML and Javascript to create one I said, "WOW, that's awesome!". My mind filled with the possibilities. One of the first that came to mind when I saw the Feed Headlines gadget in the Gadget Gallery was an image feed. I figured if the gadget could pull an RSS feed, which is essentially XML, from a remote location and refresh the data on an interval then I figured I could pull custom XML and display images located on a remote server as well.
The goal of this article will take you through the process of creating a Vista Sidebar Gadget for the first time. I have included just about all the steps you need to go through to create any gadget. Plus, I've tried to provide background on each step and point out the Gotchas that I ran into as I was creating this Sidebar Gadget for this article.
As a disclaimer, this article is a bit of a shameless plug for the company I work for because we sell professional sports photos online. But I figured it would be ok, because the point of the article is to show how you can integrate a Sidebar Gadget with your online content. For example, you could take this gadget and adapt it to consume an image feed of the current sale items, or best deals in your online catalog. If the user sees something they like they click the "buy" link and can purchase the item!
What is a Sidebar Gadget?
The Windows Vista Sidebar is an executable that runs on the desktop of the new Windows Vista OS. It hosts "Gadgets" which are small DHTML applications which have almost all the capabilities of a web page running in Internet Explorer. (I say almost all because this is a new technology and it's my first attempt at using it.)
Windows Vista comes with a small sampling of Sidebar Gadgets to give you an idea of what is possible. Some seem pretty useful:
- An analog clock
- An RSS news reader
- A CPU / memory monitor
"What's so useful about an analog clock" you say? Well, it's ok, but what makes it useful is that the Sidebar is capable of running multiple, independent instances of each gadget. And each instance has it's own settings. So if you work in a different time zone than your clients or compatriots you can run a separate instance of the clock for each time zone! Of course your desktop real-estate is limited, but the sidebar is capable of displaying multiple pages of gadgets, so you can navigate through the ones you have open.
A minimal Gadget application consists of the following items:
- An XML manifest file named gadget.xml (yes, the name is a requirement)
- An HTML file
- An "icon" (jpg, gif or png) file
A Gadget application may also include the following items:
- script files (.vbs or .js)
- stylesheet files (.css)
- a settings HTML file
- a "flyout" HTML file
- globalization files
- ActiveX components
Gadgets have two installation directories:
- %USER_DATA%\Local\Microsoft\Windows Sidebar\Gadgets - for user gadgets
- %SYSTEM_ROOT%\Program Files\Windows Sidebar\Gadgets - for global gadgets
Gadget Development Tools & Resources
What tools do you need to create a Sidebar Gadget? The short answer, "Notepad". Yup, if you are comfortable writing your HTML and Javascript or VBScript in a plain text editor then you can do just that. No compilation is needed. However, if you're planning on something more robust than your first "Hello, World!" gadget I'd recommend an IDE that supports script debugging. Because I do all my HTML in a text editor, I tried to get started with notepad on my first try. But since my window.alert()
attempts failed from the get go, I was dead in the water right out of the gate.
So I installed my IDE of choice, Visual Studio 2005, configured IE for script debugging and instantly determined the cause of my first problem and every problem thereafter. If you can do it all in notepad, more power to you. You're more of a man than I. But after my first experience, I don't recommend flying blind.
You don't need to be running Windows Vista either, but I don't recommend that either since you can't really test your gadget without it. I don't know about you, but I'm not quite ready to take that plunge. So I installed Vista as a virtual machine on Virtual Server. For this article, I am working with Windows Vista Ultimate Edition, RC2 Build 5744. The final release is out by now (or at least it's available on MSDN) so you can work with a more current version, but everything seemed to be working fine for me so there was no reason to download and install the entire OS all over again.
Gotcha #1
An important note about updating the installed gadget files. I found that if I'm just updating the .htm file for the Flyout or Settings windows, I can update the file and the next time the window loads it will display the updates. However, if I'm modifying a .js, .css or the main Gadget.htm file, then I have to close all running instances of the Gadget before I can open a new instance and view the changes.
Here are a list of resources I found helpful along the way:
Globalization
If you want to make your Sidebar Gadget localized the key is to use declared constants for all your text. If you have images with text on them, you'll need to create additional copies of the image in each language you intend to support. Then create a folder with the language code (en-US for english united states, for example). That folder should contain a duplicate folder structure for the resources (js, css and image files) and the localized resources. In the image of my solution explorer above, you'll see I have a folder named "en-US" with a subfolder named js which contains a javascript file named local.js. Local.js contains all my declared constants for error messages and other text. If I wanted to support other languages I would simply duplicate the contents of en-US with values in the language being supported.
Step 1: The manifest File
The manifest file describes your Sidebar Gadget and contains the settings required by Windows Sidebar to both display it in the Sidebar Gallery and create an instance of it in the Sidebar.
="1.0"="utf-8"
<gadget>
<name>MaxPreps Gallery Viewer</name>
<namespace>Developmentalmadness.Vista.Gadgets</namespace>
<version>1.0.0.0</version>
<author name="Mark J. Miller">
<info url="http://developmentalmadness.blogspot.com"<BR> text="Mark J. Miller's blog"/>
</author>
<copyright>© 2007</copyright>
<description>View high school sport's action photos!</description>
<icons>
<icon height="150" width="150" <BR> src="images/icons/MaxPreps_Blk_130w.gif" />
</icons>
<hosts>
<host name="sidebar">
<base type="HTML" apiVersion="1.0.0" src="gadget.htm" />
<permissions>Full</permissions>
<platform minPlatformVersion="0.3" />
<defaultImage src="images/icons/MaxPreps_Blk_130w.gif" />
</host>
</hosts>
</gadget>
There isn't really any documentation on the gadget.xml file schema, all the tutorials simply display an example of the manifest file. It's up to the reader to just copy & paste, then edit the sample. But I'll try and give you a summary here of what I've found.
Except for the hosts
element and it's children, most of the schema is used to describe your Sidebar Gadget for the Gadget Gallery. The elements name
and icons
are used to display an application icon above the name of your Sidebar Gadget in the Gadget Gallery. While version
, author
, info
, copyright
and description
all are used by the details pane to describe your Sidebar Gadget. The info
element has two attributes: url
and text
. url
is exactly what it says it is. The text
attribute is optional, but if you want to display something descriptive in place of the url you can use it. The only one I haven't been able to account for is namespace
, but by the time I realized it, I figured I had already come up with a namespace and entered it, so I decided to leave it.
Because of the lack of documentation, I'm guessing on some of this, but most of what is below the hosts
element stays as it is. However, the src
attribute of base
indicates the main HTML file for your Sidebar Gadget and defaultImage
allows you to specify a drag icon used when you drag your Sidebar Gadget from the Gadget Gallery to the Sidebar. This can be a transparent png and the transparency should be preserved as you drag the icon to the Sidebar. However, I am by no means a graphic artist, so I did not attempt to make my own graphic to test this.
Step 2: The Settings Page
As you create your gadget, it should save you some time if you create your settings page next. The reason I say this is because I did not, and I found that I would have to go back through much of my code that is already in place to accommodate each configuration setting I add. Because I found myself running out of time, I settled for keeping the configuration settings to a single simple option so I could have time to write this article. So unless you are disciplined enough to spec out your entire first project before starting it I recommend building your settings page first. Then as you build the rest of your Sidebar Gadget you can quickly add in settings as you write the application code and you won't have to go back and rewrite sections to accommodate new settings.
Each page (gadget, settings, flyout) is independent and does not share variables. So the Sidebar Gadget object model contains an System.Gadget.Settings
object which allows you to persist data while your Sidebar Gadget is running. It is very useful for saving state data and communicating between the different pages of your Sidebar Gadget. Your settings page is a means to allow users to make configuration changes to your Gadget and persist those during the current session.
A settings page is not required, but when you do create one you will see the above icon included below the close button next to your gadget. And when you click on it you'll get something like the image below. As I said before, I am not a graphic designer, so the part I appreciate about the settings page is it's simplicity. Even though you need to include a full HTML page (HTML
, HEAD
and BODY
elements) the only other thing you need is to add a STYLE
element to designate the height and width of the body and then drop a couple HTML controls onto the page. Everything around the page, including the "OK" and "Cancel" buttons and their functionality comes pre-built as part of the Sidebar Gadget package. I chose to go one step further and add a DIV
tag as a place holder for validation errors on the page.
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> Settings</title>
<script type="text/javascript" src="js/settings.js"> </script>
<style type="text/css">
body{
width:200px;
height:200px;
}
</style>
</head>
<body onload="loadSettings();">
List size (#):<input type="text" id="maxCount" size="3" />
<div id="errorMessage" <BR> style="color:Red;font-size:12pt;font-family:Calibri;"> </div>
</body>
</html>
Gotcha #2
A lesson I learned is that certain small details can cause a lot of headaches. When I created my first HTML page for this project I added a SCRIPT
tag to the HEAD
element to reference an external .js
file. But I couldn't figure out why the Sidebar Gadget wouldn't load. (This was when I decided I needed an IDE with script debugging capability if I wanted to continue with this endeavor). It turns out I could not use the following format for my script tag: <SCRIPT type="text/javascript" src="js/settings.js" />
. Unless you have a separate closing tag for your script reference, the Sidebar will not recognize it. So make sure your external SCRIPT
tags use this format: <script type="text/javascript" src="js/settings.js"></script>
In order to use the settings page you'll need the following two lines of code which will be called from your main page:
System.Gadget.settingsUI = "settings.htm";
System.Gadget.onSettingsClosed = settingsUpdated;
The first line should be called from your onload
event to tell the Sidebar you are using a settings page. This will add the settings button next to your gadget. When the user clicks this button, it will load your settings page. The second line indicates a method (in this case settingsUpdated
) to be called when the settings page successfully closes. It will allow you to read the new settings and update the behavior or layout of your Sidebar Gadget.
function loadSettings()
{
System.Gadget.onSettingsClosing = onClose;
var sMaxCount = System.Gadget.Settings.read(SETTING_MAX_GALLERY_COUNT);
if(sMaxCount != "")
gMaxCount = parseInt(sMaxCount);
maxCount.value = gMaxCount;
}
When your settings page loads, you'll need to do at least two things: set the onSettingsClosing
event handler, and read the current settings values to display them in the controls on the settings UI.
The first is straightforward, just create a function with the following signature and pass it to the onSettingsClosing delegate: function (parameter1)
. The name of the function and the parameter are up to you but here's what my function looks like:
function onClose(event)
{
if(event.closeAction == event.Action.commit)
{
if(isNaN(maxCount.value))
{
errorMessage.innerHTML = "Please enter an integer value.";
event.cancel = true;
return;
}
gMaxCount = parseInt(maxCount.value);
System.Gadget.Settings.write(SETTING_MAX_GALLERY_COUNT, gMaxCount);
event.cancel = false;
}
}
The parameter passed to your event handler is the event arguments. This is another case where I did not find documentation other than what was in the code samples I came across. If anyone finds documentation, I will gladly include it in my resource links. The event.Action
property has two possible values: commit
or cancel
. They correspond to the "Ok" and "Cancel" buttons on the settings UI.
In this case, I check to see if the user clicked the "OK" button (Action.commit
) and then validate the data to make sure the value is numeric. If it isn't, I display an error message and set event.cancel = true
so that the user will be returned to the settings page to either cancel the operation or correct the problem and resave the settings. If everything is valid, I save the settings and set event.cancel = false
so the settings page will close and the user will be returned to my Sidebar Gadget.
When you want to persist your settings you have two methods: write(string name, obj value)
and writeString(string name, obj value)
. When you use write
the Settings object will try and guess the type of your value. If your value is a string, use writeString
to eliminate the guesswork when you can.
This brings us to the second step I mentioned: reading the current settings. The write
and writeString
methods each have corresponding read methods: read(string name)
and readString(string name)
. When you read from the Settings object, make sure and check the value for an empty string (""
). If the setting is empty or has not been set, it will return and empty string instead of a null value. When using boolean values, a tip I learned from a tutorial I read was to cast the value to force it as boolean. You can cast it like this:
var valueFromSettings = System.Gadget.Settings.read("myValue");
var myBool = !!valueFromSettings;
Now lest move on to the actual Gadget functionality
Step 3: The Gadget UI
My goal in creating this gadget was to duplicate the RSS Reader gadget's functionality and pull an XML feed which I would use as a source to build a list of the most recent photo galleries on my site. Something I hadn't mentioned yet, is that because of the architecture of Sidebar Gadgets you can open up any Gadget on your system and view the source code, just like if you were viewing a web page in IE. No, I don't mean you can right-click and select "view source...". But you can navigate to the directory where the gadget is located and view the source files. So when I got started I opened up the RSS Reader and realized they were using the Feed Store built-in to IE. And since I would not be using RSS, Atom or any other standard schema for my XML I had to turn elsewhere.
After some playing around with XSLT, I opted to go with AJAX instead because it would make certain features easier to implement. There are some good AJAX tutorials on the site I used when I used AJAX for the first time, so do a search and read a few of them if you're not familiar with the technology yet.
The next decision I had to make was how to make the XML feed available to the gadget. So in order to simplify things and allow others to use the source code for this gadget on their local machines, I decided to create two static XML files and put them on my local web server. I have included these two xml files and Feed.xml and Feed2.xml in the source files included with the article. You can place them on your local web server or a remote one it doesn't matter. As I was building this gadget, I had the gadget installed on my Vista virtual machine and my feed files stored in my IIS virtual directory on Windows XP. When I wanted to test the auto update ability, I just swapped the files back and forth to imitate a dynamic process.
For brevity sake I haven't included the actual AJAX code here, just the pertinent stuff:
gadget.js
function loadMain()
{
var sMaxCount = System.Gadget.Settings.read(SETTING_MAX_GALLERY_COUNT);
if( sMaxCount != "" )
gListMax = parseInt(sMaxCount);
gResizeGadget = true;
makeXmlRequest(DATA_RESOURCE_URI, AJAX_TYPE_GALLERIES);
}
ajax.js
function getXmlContent()
{
if(checkReadyState(gRequest))
{
var ajaxDoc = gRequest.responseXML;
gXmlDoc = ajaxDoc;
if(gAjaxType == AJAX_TYPE_GALLERIES)
printGalleries();
setTimeout("refreshData()", 5 * 60000);
}
}
function refreshData()
{
if(gReadingXml)
{
setTimeout("refreshData()", 1 * 60000);
return;
}
var dt = new Date();
makeXmlRequest(DATA_RESOURCE_URI + "?t=" + dt.getTime(), <BR> AJAX_TYPE_GALLERIES);
}
To tie the missing stuff together here, onLoad
calls makeXmlRequest
which calls getXmlContent
when it completes. Then getXmlContent
stores the XML in a global variable and calls printGalleries
which uses the global variable to read the XML and build the list on the gadget UI. I haven't included it here because of it's length and there's nothing in printGalleries
which is unique to the Sidebar Gadget API. Then a timer is set to requery the remote server for updates to the feed by calling refreshData
.
Layout
A design aspect that you will be forced to deal with is the limited space you are given for your gadget. The width of the Sidebar is 130 pixels, which is not much space for anything. But certainly in our case there isn't much room for text. Because I'm pretty new to DHTML, my first attempt to deal with this was to count up the maximum number of characters I could fit using the current font, then use the substring
method to chop that string and append an ellipsis(...). But I couldn't get it to look as neat as the RSS Feed reader Gadget. So again, I opened up the code to see what they had done. Now those of you who are more experienced with CSS and DHTML may have seen this one coming a mile away, but bear with me here.
var newRow = table.insertRow(table.rows.length);
var cell = newRow.insertCell(0);
var html = "<DIV id=\"item" + i + "\" title=\"" + node.getAttribute("name")
html += "\" onClick=\"loadFlyout('" + node.getAttribute("id");<BR> html += "');this.blur();\"";
html += " onmouseover=\"this.style.color='Red'\"";<BR> html += " onmouseout=\"this.style.color=''\"";
html += " style=\"font-size:13px;margin-bottom:0px;margin-top:0px;\"> ";<BR> html += node.getAttribute("name") + "</DIV> ";
html += "<DIV id=\"sport" + i + "\" style=\"color:White;font-size:11px;";<BR> html += " color:#67788a;margin-top:0px;margin-bottom:0px;\"> " ;
html += node.getAttribute("sport") + "</DIV> ";
cell.innerHTML = html;
eval("item" + i).style.textOverflow = "ellipsis";
eval("item" + i).style.overflow = "hidden";
eval("item" + i).style.whiteSpace = "nowrap";
eval("item" + i).style.width = 115;
eval("sport" + i).style.textOverflow = "ellipsis";
eval("sport" + i).style.overflow = "hidden";
eval("sport" + i).style.whiteSpace = "nowrap";
eval("sport" + i).style.width = 115;
eval("sport" + i).style.borderBottom = "dotted 1px White";
There are two important steps here to point out. First, you must be sure to set the width of the HTML element which will act as the container for your text. In this case I am using a DIV
tag within a TD
element. So I have set the width of the DIV
tag. Then for each of the text elements, I set textOverflow
, overflow
and whiteSpace
style properties. You don't have to set them in any particular order, as you can see above, but they each need to be set to get the effect of the text running off the side of the page.
So now we have a Gadget which reads from a remote XML feed and then displays a list of the items in the feed. For navigation controls, I borrowed the graphics used by the RSS Reader Gadget. They include previous and next buttons, and a counter to display the start and end index of the items currently in the list. Let's move on now to what we can now do with that data.
Step 4: The Flyout
The flyout is an optional component of the Sidebar Gadget, but it's great when you need more room to display your application. The only limit to the size of your flyout is that of the user's display resolution. With the graphics capabilities required by Windows Vista in the first place it's probably safe to assume that your users are going to have a minimum of 1024x768 resolution.
To open your flyout only takes two lines of code. You need to specify the HTML file used by the flyout and set the show
property to true. Like this:
System.Gadget.Flyout.file = "flyout.htm";
System.Gadget.Flyout.show = true;
Where you place these commands isn't so important as the order they are in, before you display the flyout you must indicate the file to be used. You also have the option of registering a method with either or both of the onShow
and onHide
events. Keep in mind that the Gadget page and the Flyout are independent and cannot communicate directly with one another. This means that the onShow
and onHide
events are used for updating the Gadget UI or trigger some behavior in the main Gadget page based on those events. We'll come back to these events in a minute.
The best way to communicate with the flyout is to use the System.Gadget.Settings
object. In this case we use it to store the id of the gallery that was clicked on the Gadget so the flyout can read from the XML and display the images. Here's where we hit a bit of a snag.
Gotcha #3
The XML feed was too large to pass into the settings object. Whenever I tried to write the XML as a string, it was unsuccessful. There were no errors, the program just kept running as if everything was ok. But when the flyout tried to read the XML from System.Gadget.Settings.readString
the result was an empty string. I tried inspecting the settings object from the Gadget as soon as I had written the XML, but still the value wasn't getting stored. I knew I was doing everything correctly because I could read the gallery id value I had passed, just not the XML.
To work around this I decided to write the XML to disk, then I could read it in from the flyout. The next obstacle was that the Sidebar object model has no read and write capability without user interaction. If you want to open or save a file the System.Shell
object and it's children provide methods to ask the user where they want to save a file, or which file to open. And it has the ability to inspect the file system objects and to create and delete folders, but not to open files as text or write to them behind the scenes. However, this problem was easily solved by using the Scripting.FileSystemObject
.
ajax.js
function saveXmlDoc(xmlDoc)
{
gReadingXml = true;
var path = getDataPath();
var fso = new ActiveXObject("Scripting.FileSystemObject");
var output = fso.OpenTextFile(path,2,true);
output.WriteLine(xmlDoc.xml);
output.Close();
gReadingXml = false;
}
function deleteXmlDoc(path)
{
var fso = new ActiveXObject("Scripting.FileSystemObject");
fso.DeleteFile(path);
}
Now I was able to create save and open functions which used the Scripting.FileSystemObject
to allow the XML to be passed back and forth between the Gadget and the Flyout.
Which brings us to permissions. When writing files the Gadget can write files to the file system, but only within the application path and subdirectories. If you try to manipulate files out side that path you'll get permission denied errors. Fortunatelythe Sidebar Gadget object model has a convenient property to give you access to the application path: System.Gadget.path
.
But because we're writing files to be passed back and forth we've created two problems for ourselves. First, the System.Gadget.Settings
object is instance independent, so if there are multiple instances of your control in the sidebar they don't conflict. But part of that benefit, is that you have no way of knowing how many instances are open or what their settings are. So if I'm writing files back and forth, I need to make sure I'm not conflicting with other instances of my Gadget.
I decided the way around this was to use a file name that would be unique among all instances of my Gadget. And the easiest method available was the javascript Data.getTime
method which returns the number of milliseconds since Jan 1, 1970. Since the probability of a user being able to add multiple instances of my Gadget to the Sidebar is nil this works fine for our needs. Then in order to allow the Flyout to retrieve the file all I need to do is pass the path to the file to System.Gadget.Settings
.
Event Sequence
I want to backtrack for a moment to the Flyout events onShow
and onHide
. My first choice was to use these two events to write and clean up the XML file. The onShow
seemed a good choice because it was the perfect trigger to tell my Gadget when the file was needed. onHide
because I found that the Gadget has no onClose event to allow me to clean up files when I'm done. Because the System.Gadget.Settings
object does not persist data after a Gadget is closed, if I don't delete the files then eventually the files will pile up in the users directory over time. Disk space may be cheap, but it's up to the user to determine how to use that space, not me.
As it turns out, the onHide
event worked fine for my needs. Unfortunately, I found out that onShow
fires after the onLoad
event of the Flyout. So now I needed to figure out how to write the file before the Flyout tried to access it in the onLoad
event. I finally decided to use the onClick
event to first, write the XML file to disk, then open the flyout. This guarantees that the file exists before the Flyout loads.
gadget.js
function loadFlyout(galleryId)
{
storeXml();
System.Gadget.Flyout.show = true;
}
So, as I mentioned before, when the user clicks on the name of a gallery the onClick
event fires and calls loadFlyout
and passes the id of the gallery as an argument. The loadFlyout
function writes the XML stored in a global variable to disk, then opens the Flyout.
Gotcha #4
The next issue I ran into was a surprise. I had been using an external CSS file for the general formatting of my page, and I wanted to set the background color of the Flyout. But it remained white. The page could view the CSS file, because I could change the height and width of the body, but other settings had no effect. But when I used inline styles, I had no problem. I searched around for a workaround, but only found other complaints of the problem with no solutions. At this point I abandoned the external CSS and used inline styles.
External Links
The last important feature of note was that I wanted a user to be able to go to my website and purchase a print of the image. I thought, there might be a problem with putting a link to an external resource on my flyout page, but there wasn't. In fact it was easier than I imagined. If you put a link to an external resource, a new IE window will open to the resource in the link - exactly the way I wanted it to happen!
Step 5: Packaging Your Gadget
So now, we're finished. Or almost. To deploy our newly completed Sidebar Gadget you have two options. You can simply copy the files to a new folder in the Gadget directory, then rename the folder to include ".gadget" at the end. So if our folder is named "Photogallery", just rename it to "Photogallery.gadget".
The above method may work fine if you are creating your own personal gadget and then just dropping it in the folder locally. But if you have a gadget like ours, and you want to deploy it to other users, this second method is actually easier. Simply create a zip file containing all the files and subfolders (but not the root folder itself), then change the .zip extension to .gadget. And that's it! When a user tries to download or open your compressed .gadget file Windows Vista will automatically install it to their user gadgets folder.
The Finished Product
There you have it, the complete package, from start to finish. How to develop a functional Vista Sidebar Gadget. I struggled a bit towards the end with a list of features I would have liked to implement or some changes I would make now that I better understand the process. But they'll have to wait until version 2.0. I'd be happy to hear your ideas for different features that could make this Gadget better. Here's a few that I'd like to add when I get time to work on this again:
- Resize the main Gadget window when the user undocks the Gadget.
- Resize the Flyout window when the user clicks a thumbnail in order to make the enlarged view even bigger.
- Add more user configurable settings like the location of the feed, the size of the flyout, the number of thumbnails to display at a time, as well as color and font settings.