Introduction
In this article, I would like to help you make your second gadget. As you have probably already found out, the Gadget APIs are not so well documented yet (and not very rich), there are some bugs here and there, and not everything works the way you want it to the first time around. Moreover, being an experienced .NET or C++ developer does not spare you from some perfidy in JavaScript and DHTML. I've decided to share my experience of getting somewhat further into gadgets and I hope I will save you some unnecessary moments...
What You Will Need
- An idea what sidebar gadget is and how to create it
Here are a few well-known tutorials you might find useful:
- Gadget Development Overview - at MSDN, using RC1 build.
This is a very first introduction to gadget development. You get quick insight of the possibilities and create Hello, World! gadget.
- Using a Vista Sidebar Gadget to Consume an Image Feed - at Code Project, using RC2 build.
Quite advanced gadget step-by-step guide. I definitely recommend you to take a look at it. The gadget I will be discussing today is at the similar level.
- Sidebar Gadget Tutorial - at Microsoft Gadgets, using RC1 build.
Also more advanced, detailed guide and documentation for the Virtual Earth Map gadget.
- An operating system to test your gadgets on
In this article, I'm working with the final, RTM version of Windows Vista (32-bit).
- Motivation and never-ending patience
Too many surprises are behind the door. Build your gadgets for fun! (Or for the Vista Gadgets Competition) ;-)
What You Will Not Find Here
- Step-by-step guide
If you want a detailed walk-through, go ahead and read the great article Using a Vista Sidebar Gadget to Consume an Image Feed. I expect you to have some basic knowledge about what a gadget is and how to create it.
- 100% facts
I don't have any connection with the gadget team, and all thoughts I write are based on my trial and error and my understanding of the technology. If I'm 'way off somewhere, please let me know. I also read Sidebar Gadget Development MSDN forum and found some answers there. Jonathan Abbott is leading the answers, so I would like to thank him for his effort. Some things I will mention are things that I found in his posts.
What You Get
- My personal tips regarding what you can do, what you can not and how
I don't believe my way is necessarily the best way. If you find any of the information useful, cool. If you don't, share a better way with us, please.
- Garfield daily comics gadget
This gadget is the basis for the article. It is my second gadget and yes, I tried to make it better than my first one. :-) I hope you'll enjoy it!
Before You Begin the Second Gadget
How long ago did you create your first gadget? A month ago? Half a year? This will determine what changes you will need to make to it so that your gadget will work with the sidebar in the release version of Windows Vista. Working with beta versions of software has a disadvantage: breaking changes may be made.
First of all, I would strongly recommend that you read the known bugs list managed by Jonathan. It may help you with some issues you could run into. Also, please report any bugs you find, either to the MSDN forums, or to Jonathan's list if you have an aeroxp.org account.
Sidebar Gadget Manifest
To make our gadgets discoverable by the sidebar, we need to make a Gadget.xml manifest file with all necessary metadata about the gadget. The structure of this file has been changed a bit with latest RC releases; however, most of the older manifests register just fine.
What if the sidebar 'does not work'
The bad thing with the manifest is that if something is messed up inside it, sidebar will silently overlook your gadget even if it is in the gadgets directory, and will remain quiet even if you try to install it. If this happens to you, check the gadgets directory for the current user (usually C:\Users\[user name]\AppData\Local\Microsoft\Windows Sidebar\Gadgets). Directories with names ending with .~0000 and similar remain in this folder after installing a gadget with an invalid manifest, so it's a good idea to clean it up when a gadget is messed up.
TIP: Successful installation ends with the gadget shown on the sidebar.
Check to be sure that the file is a valid XML file. If you can't find the mistake, I would recommend that you start over by modifying any manifest which obviously works (perhaps try some from C:\Program Files\Windows Sidebar\Gadgets) or use the template I made below.
Encoding
If you come across a message during the installation that says that the manifest is invalid ("this is not a valid gadget package"), first check that you have the file saved using the encoding specified in the XML document element. This should be UTF-8, but if you started your manifest from scratch in Notepad, it will have been saved using ANSI encoding by default. This should not be a problem, though, unless you use Unicode characters. But if you use UTF-16, you may have problems. The easiest way to find out how a text file is encoded is to open it in Notepad and click Save As.... The encoding selected in the Save dialog box is the one that is currently used. If there are no problems there, look for the problem somewhere else or start over.
Schema
The (informally-defined) schema has changed slightly from the Beta 2 version. The very bad decision at the Microsoft side (at least in my opinion) was to not declare any formal schema/namespace for the gadget manifests to use. This means that no formal validation is available - see above. This also means that I cannot give you a schema that you can use in Visual Studio for Intellisense.
Our Garfield's gadget manifest looks like this:
="1.0"="utf-8"
<gadget>
<name>Garfield Comics</name>
<font color="gray"><namespace>UAM.InformatiX.Windows.Gadgets</namespace>
<version>2.5</version>
<author name="Jan Kuèera">
<info url="http://www.codeproject.com/" text="The Code Project" />
<logo src="Logo.png"/>
</author>
<copyright>© Pawn, Inc. since 1978</copyright>
<description>Watch Garfield daily comics!</description>
<icons>
<icon height="48" width="48" src="Garfield128.png" />
</icons>
<hosts>
<host name="sidebar">
<base type="HTML" src="Garfield.html" />
<permissions>full</permissions>
<platform minPlatformVersion="1.0" />
<defaultImage src="Garfield128.png" />
</host>
</hosts>
</gadget>
It has all possible elements and attributes that are supported (I believe), so feel free to copy this manifest and modify it for your purposes (don't forget to save in UTF-8). The elements marked in bold are required by the sidebar. The grey elements are not used by sidebar version 1.0 and are reserved for future use. Here are some notes on usage that you might find useful:
name
: This string is said to be displayed also on the Windows Sidebar page in the control panel, and on the Sidebar window on the desktop (as well as the Gadget Gallery), but I've never seen anything like that. It is also used as a caption for the settings dialog box of your gadget. I don't know about you, but I prefer when the entire gadget name appears in the Gadget Gallery, so try to keep the name short enough to fit. version
: The Sidebar uses this value during gadget installation. If another gadget with the same name has already been installed, Sidebar does a version comparison. If the versions differ, the user is prompted to select the appropriate version. Valid version strings are of the form major.minor and each of these substrings can contain 0 to 4 digits between the values of 0 and 9, inclusive. The gadgets shipped with Vista have been versioned 1.0.0.0 since the beginning, however, so it doesn't seem this string is being critically parsed (at least right now). permissions
: Unfortunately, only full
trust is currently supported. I hope this will change, since not all gadgets need to run in full trust - not all gadgets will be from trusted sources. logo
: Contains a link to the graphics file to be displayed next to the author's name in the Gadget Gallery's expanded details area. The image is proportionally scaled to 48x48 pixels. icons
: This tag can contain multiple icon
tags so you can specify different images for different sizes. The width
and height
attributes are optional, and instead of specifying the actual dimensions of the image files, they specify which dimensions the image should be used for. The Sidebar will use an icon closest in size to the one required for a particular purpose. If you have multiple icons specified and you omit the size attributes in only some of them, these will be treated as infinity (in other words, the actual dimensions aren't determined, and these icons will not get chosen at all in any case). Images for the Gadget Gallery icons are scaled to 48x48 pixels. If you do not specify any icon, the default will be used. defaultImage
: This image will be displayed when the user drags the gadget from the Gadget Gallery, before the gadget is instantiated. If you do not specify any, the icon for Gadget Gallery will be used, at its normal size. This is the reason why I explicitly specified Garfield128.png, because its actual dimensions are 128x128 and it will be loaded again, unscaled, for dragging.
In all image-related tags, you can specify any image stored in format supported by GDI+ 1.0: PNG, GIF, JPEG, BMP, TIFF, EMF, WMF and icons (ICO).
Some of these statements are from Sidebar Gadget Manifest at MSDN, where you can get more (but not much more) detailed info. In the Image Feed article, you can find a screenshot showing how these tags are used in the Gadget Gallery.
Gadget Script Changes
Depending on how long ago you made the first gadget, some changes apply to the Gadget APIs and script behaviour. Some gadgets written for Vista Beta 2 release may fail to function correctly.
alert() is gone
Note to experienced JavaScript developers: just ignore this paragraph. :-)
Probably the most deceitful change you find out very quickly is that alert() message boxes are being suppressed now. I don't want to speculate on whether this change is good or bad, but it's there and we have to work with it. If you used the message box to show a message to the user, use some non-modal way for notifying users instead. If you used it to watch variables, then you have to add a status element or use some kind of tooltip. But using a debugger would be easier and more desirable. And now, the important question: How do I break in the debugger if I cannot put the magic alert(xx);
there?
TIP: Attach to the debugger using the debugger; statement.
If you already have the debugger attached, you can use System.Debug.outputString("Reached this line!")
to check what's happening. The output will be written to the Visual Studio Output window.
Dropped APIs
Here is a quick list of the functionality that was removed in RC1. The functions should be accessible from Windows Management Instrumentation, through Shell or using FileSystemObject
scripting. I don't know what the actual recommended replacements are because I haven't needed them, but if you are interested, let me know, and we will try to find that out. Be sure to take a look at RC1 Changes to the Sidebar APIs at Gadget Corner for the full list of changes.
System.Net.NetworkInterface
System.Net.RecycleBin.percentFull
System.Net.Sound
The Development Environment
If you did the last gadget in Notepad, consider switching to an IDE, most likely Visual Studio. Not only do you get the syntax highlighting (which I found quite useful as the code grew), but most importantly, you get comfortable debugging capabilities - you can easily watch every object's state and properties (and events and methods in Orcas as well), which is invaluable when working with DHTML.
Browser vs. Sidebar
If we are going to create HTML stuff, should we test it in the web browser at first? I would say not necessarily. The things you would likely have troubles with are sidebar-specific. You don't have the dimensions you will have in the sidebar, you can't test docking, dragging, flyouts, settings, etc. Moreover, if your favorite browser is not Internet Explorer, you will probably face some layout differences, since the sidebar engine uses IE, of course, and I'm not so sure you are able to change it. On the other hand, that means that you can use some IE-specific stuff, like filters or expressions in CSS.
There is one situation where you will find Internet Explorer useful, though. If you have problems with the HTML/layout part of your gadget, you may want to use the Internet Explorer Developer Toolbar, which displays your HTML structure, and even allows you to make changes to the document. So you can try things to find out what would work and then include it in your code.
If you want to work in the web browser, keep in mind that the Gadget APIs are not available. It makes sense to track this state in the code to avoid script errors and retain functionality:
var IsGadget = (window.System != undefined);
Edit and Continue
There are two common ways of installing the gadget. You can either execute the .gadget ZIP archive or simply copy the files to the sidebar folder. As a developer, you should try both of them. First, pack your manifest into a ZIP archive, rename it to .gadget and see what happens. If everything is ok, you are ready to write the code.
In order to maximize the comfort of development, work directly in the gadget's folder. Just use the Open web site command from Visual Studio and point it to the user's gadget folder, C:\Users\[you]\AppData\Local\Microsoft\Windows Sidebar\Gadgets\Garfield.Gadget in our case. Here, you can edit the manifest, create HTML, stylesheets, and scripts, and organize folders or run the gadget in the browser.
As you probably know, edit and continue is not supported for scripting. If you find a mistake in your code, you have to stop debugging to be able to correct it. What should you do next? Reinstall the gadget? Restart the sidebar?
You don't have to. After changing:
Gadget manifest | Main gadget files | Gadget settings | Gadget flyouts |
If you change some strings like author or copyright, clicking your gadget icon in the Gadget Gallery is enough. If you change the icon, you need to close and reopen the gallery. If you change the base code link, re-instantiate your gadget. | You need to instantiate the gadget again. You can leave the Gadget Gallery open the whole time and just drag the gadget to the desktop again and again. Don't forget to close the old ones from time to time, or else you will get lost easily. By switching to the Gadget Gallery, you don't bring the gadgets on top; you have either to click the sidebar system tray icon () or press Windows key + Spacebar. I don't think you can do that much easier. | The settings page is loaded every time you display the settings dialog box. So clicking on the gadget settings icon () will do. | Flyouts are also reloaded every time user tries to display them. As with settings, you can work with the same instance of your gadget the whole time. |
Enable Script Debugging
If you have for any reason disabled script debugging in Internet Explorer, you will have to enable it for the debugger to work. You can find this option in Internet Options, Advanced tab, Browsing group. Uncheck Disable script debugging (Internet Explorer) to debug your gadget in IE, and uncheck Disable script debugging (Other) to debug your gadget in the sidebar. You may also find it useful to check the Display a notification about every script error option, if you find it hard to notice the little exclamation mark in the IE's status bar.
Sinking Deeper
Before I give you a few general hints I discovered, I'll say a few things about the attached source code.
...And This Is My Cat, Garfield.
The purpose of my gadget is to display daily Garfield comics. I have to say that in the middle of writing this article, Rajesh Lal posted his article Daily Dilbert 1.0 - A simple sidebar gadget, with pretty much the same aim. I have decided, however, to finish and post this article, so I hope that you aren't too bored with the idea, and I will bring you some new or helpful things. I believe that examples are a very efficient way of learning, and I have commented the code as much as I could, so take it as an important part of the article.
Hints and Tips
General
Security
This paragraph is here only to note that some security precautions may impact the functionality. Gadgets:
- are running in a 'zone' similar to HTML Applications or the Local Machine Zone;
- are allowed to initialize and script ActiveX controls not marked safe for scripting;
- are allowed to access data sources across domains;
- are not allowed to download and install new ActiveX controls (signed or unsigned);
- run with standard privileges and operations, which requires UAC permission fails without prompting;
- are subject to parental control restrictions;
- are not subject to Internet Explorer's Protected Mode.
I made this list from the Sidebar Security post at Gadget Corner, where you can find more details on this topic.
Docking and Undocking Practices
You can supply only one shared HTML page for both the docked and the undocked state of your gadget. The bad thing is that you have to manage the layout changes yourself. The good thing is that you don't need to synchronize variables between these two states. So the normal solution is to put the two layouts into containers and display only one of them at a time:
<body>
<div id="DockedModeDisplayArea"> ... </div>
<div id="UnDockedModeDisplayArea"> ... </div>
</body>
You do this in the handlers for the System.Gadget.onDock
and System.Gadget.onUndock
events and to find out what's happening, check the value of the System.Gadget.docked
property.
The second approach is to create the document content by setting the document.body.innerHTML
property, either by code or loading content from files.
The minimum size that a gadget can be (both in docked and undocked mode) is 20x57 pixels. Anything smaller will get filled with white. So you cannot create a wide, thin gadget with a label - but on the other hand, this size limitation comes in handy when something go wrong (it makes it so that you don't have lots of almost-invisible gadgets all over the place). In my opinion, 20x20 would do as well - the inability to create 'label' gadgets can be troublesome.
Update: You can, of course, use a transparent background to work around this, but remember that you can place elements on opaque areas only.
Transitions
Ever wondered what the System.Gadget.beginTransition()
and System.Gadget.endTransition(int transitionType, float seconds)
methods are for? They allow you to fluidly change the appearance of your gadget. If you have ever used transition filters in DHTML, you have an idea of how to make it work:
function UIChange()
{
System.Gadget.beginTransition();
System.Gadget.endTransition(System.Gadget.TransitionType.morph, 1);
}
Unfortunately, it does not always work as expected. I couldn't use it in the Garfield gadget, because I'm changing the layout. You can try this if you follow the comments and description in the attached code. Briefly speaking, use the transition only when you want to get a zoom effect. Be careful of the timing - the sidebar is almost unresponsive during the transition and the time unit is in seconds.
At the time of writing this article, the MSDN documentation was still archaic, so it was not possible to find out which TransitionType
s you can use. I asked at the forums, and Jonathan gave me the answer: System.Gadget.TransitionType.morph
and System.Gadget.TransitionType.none
... rich enough, huh? :-)
Draggability of your Gadgets
Normally, you can drag gadgets only by using the sidebar move button (). In order to give your gadget the ability to be dragged by any part of it, set the unselectable
attribute either on any specific element or globally (on the body):
<body unselectable="on">
// you can drag by clicking anywhere inside the gadget
</body>
Note that you cannot drag a gadget when the flyout is being displayed.
Refreshing Data
If your gadget monitors something, you would probably like to refresh data or the information you display. You have two options to do that: window.setInterval
and window.setTimeout
. The first one automatically calls the code you supply repeatedly at the interval you set, and the second one executes it only once, after the specified interval has elapsed:
var cancelID = 0;
function StartRefreshing()
{
cancelID = window.setInterval(RefreshMyGadget, 1000);
}
function StopRefreshing()
{
window.clearInterval(cancelID);
}
var pendingID = 0;
function RefreshOnce()
{
pendingID = window.setTimeout(RefreshMyGadget, 1000);
}
function CancelPendingRefresh()
{
window.clearTimeout(pendingID);
}
function RefreshMyGadget()
{
...
}
You typically use setInterval
function when you are sure you need to refresh your gadget periodically and when you are sure, that the refresh code finishes before the interval elapses (that's more like UI updates rather than downloading files). If this is not your case, you can always call the setTimeout
again at the end of the refresh code as marked in the example above. You can also stop refreshing or cancel the timeout as shown.
Function pointers are used in the example, however, you can use strings as well. This gives you the ability to call functions periodically with different parameters and if you draw this up, you can change the timeout on the fly to get more cool animations:
var aniHandle = 0;
function animateHeight(desiredHeight, delta, timeout)
{
if ((delta > 0 && document.body.style.posHeight < desiredHeight) ||
(delta < 0 && document.body.style.posHeight > desiredHeight))
{
timeout = timeout * 1.3;
document.body.style.posHeight += delta;
aniHandle = window.setTimeout('animateHeight(' + desiredHeight +
',' + delta + ',' + timeout + ')', timeout);
}
else
{
document.body.style.posHeight = desiredHeight;
aniHandle = 0;
}
}
You may like adjusting the delta
value rather than timeout
, that depends. Note that although animating your gadget may look cool, it also may get on user's nerves quite quickly. So please include a code that cancels the timeouts/animation and switches to the state immediately if the user obviously expects it. Similar, if you are changing the appearance according to user's activity, do not disturb only because he moved the cursor through your gadget. See the Garfield gadget for example solutions.
Visibility & Performance
If you refresh data or update UI periodically, you should ensure that there is a reason for that and that you don't waste computer resources. Check the System.Gadget.visible
property for this. It returns false
, when:
- the gadget is docked to the sidebar and has been scrolled offscreen;
- the gadget is docked to the sidebar and the sidebar has been minimized;
- the workstation is locked or the user has used "fast user switching" to switch to another user session;
- the power management timeout for the monitor has elapsed and the monitor is turned off.
As described in the Handling gadget visibility changes post at the Gadget Corner. Of course, you don't have to poll the visible
property, just add a handler to the System.Gadget.visibilityChanged
. There is no reason to duplicate Windows Sidebar team comments and examples, just see the blog for more details.
Downloading and Saving Files
How do I download a file? This is quite a common question and here is the answer. At first, you need to download the file and then you have to save it to the disk. Here is the script:
function DownloadFile(url, savePath)
try
{
var xmlRequest = new XMLHttpRequest();
xmlRequest.open("GET", url, false);
xmlRequest.send(null);
if (xmlRequest.status == 200)
{
var stream = new ActiveXObject("ADODB.Stream");
stream.Type = 1;
stream.Open();
stream.Write(xmlRequest.responseBody);
stream.SaveToFile(savePath, 2);
stream.Close;
stream = null;
}
else
{ ... }
catch(exception) { ... }
For getting the response, you need to instantiate an XMLHttpRequest
object. If you are familiar with AJAX or already did something similar before, note that starting with Internet Explorer 7 (which is what sidebar uses), there is no need to call new ActiveXObject(...)
. In the open
method, you specify which http method should be used, where the data should be sent or come from, and if the request should be synchronous. Normally, you would use GET
, but you can go more advanced with HEAD
, which allows you to download only headers (accessible using the getResponseHeader(string headerName)
method). More documentation on the XMLHttpRequest
can be found on MSDN. The third parameter of the open
method specifies whether the request will be synchronous (false
) or asynchronous (true
). This allows you to process the response asynchronously when it arrives so that the code execution can continue without waiting for the result. If you believe that you really need the asynchronous way...
var xmlRequest;
var savePath = "";
function DownloadFileAsync(url)
{
xmlRequest = new XMLHttpRequest();
xmlRequest.open("GET", url, true);
xmlRequest.onreadystatechange = SaveFile;
xmlRequest.send(null);
}
function CancelDownloading()
{
xmlRequest.abort();
}
function SaveFile()
{
if (xmlRequest.readyState < 4) return;
if (xmlRequest.status == 200)
{
var stream = new ActiveXObject("ADODB.Stream");
stream.Type = 1;
stream.Open();
stream.Write(xmlRequest.responseBody);
stream.SaveToFile(savePath, 2);
stream.Close;
stream = null;
}
}
...you need to supply a pointer to the function that will handle all the XMLHttpRequest
states. A couple of notes when implementing this solution:
- You cannot pass a
string
to onreadystatechange
. So you can't pass any parameters to the function. - You have to store the
XMLHttpRequest
object in a global variable in order to access it in the handler.
Also:
- If downloading binary files, use the
xmlRequest.responseBody
byte array; with text files you can use the xmlRequest.responseText
string, as well as xmlRequest.responseXML
, which gives you the DOM object of the response, so you can perform XPath queries on it. - The size of response the
XMLHttpRequest
can handle is limited. I haven't run into trouble so I don't know what the limit is, but if downloading RSS feeds for example, you will likely hit the limit. - If you try to access a file in another domain, port or protocol method, you get an Access is denied error on the
open
method. So you can't download from HTTP if you open the HTML file from your disk. - If you are getting
HTTP 304 Not Modified
responses or want to avoid Internet Explorer's caching of responses, see the Bloglines Sidebar Gadget article by Jim Rogers.
In order to save data to the disk, you have to create an ADODB.Stream
ActiveX object, as you can see in the example. Some reference documentation can be found at W3Schools. Just to mention: if you work with a text response, you don't need to set the Type
property, and you can use this object to read files on disk, using the LoadFromFile
method.
You may want to ask the user where the file should be saved. You can use System.Shell.saveFileDialog(string path, string filter)
, but:
Accessibility
You should take care of keyboard users. Believe it or not, the sidebar can be accessed using keyboard:
- Windows key + space to display the sidebar and all bring gadgets to front
- Windows key + G to switch between individual gadgets
At that point, you can usually use the Tab key to cycle through elements on your gadget HTML, so it makes sense to make your gadget keyboard accessible. If you heavily use onclick
events, for example, these cannot be fired using keyboard, unless you enclose it with an <a>
tag:
<!--
<a href="javascript:void(0)" onclick="this.blur()"><img onclick="..."/></a>
(Do not include the blur
part if you want to have focus be set to the element after clicking.) This works pretty well, if you don't change the size of gadget. If you do and you hide something, some undesired layout results may occur, because these controls have priority to be shown. The second approach is to handle particular keystrokes yourself, attaching a function to the body's onkeydown
method. For help, some useful key codes are:
function keyboardNavigate()
{
switch (event.keyCode)
{
case 9: break;
case 13: break;
case 27: break;
case 32: break;
case 33: break;
case 34: break;
case 35: break;
case 36: break;
case 37: break;
case 38: break;
case 39: break;
case 40: break;
case 79:
if (event.ctrlKey) ...
break;
case 83:
if (event.ctrlKey) ...
break;
}
}
Key codes are not case-sensitive. Don't forget to set focus to the body during load (document.body.focus()
) so the keystrokes get handled without the necessity of clicking on the gadget. If you have a flyout shown and both pages are handling keystrokes, then the flyout has precedence.
Settings
Opening the Box
You cannot open the settings dialog box from code (unless of course, you make a DLL that will emulate some crazy keystrokes :)). If you need to display the settings page, the best you can do is to load it into a flyout. Remember in this case that you won't have the OK and Cancel buttons, so you will have to create them. How can you find out if the code is displayed in the flyout or in the settings dialog box? You could compare the System.Gadget.settingsUI
and System.Gadget.Flyout.file
strings - or it is more reliable if you store this in a temporary setting:
function ShowSettings()
{
System.Gadget.Settings.write("SettingsInFlyout", true)
System.Gadget.Flyout.file = System.Gadget.settingsUI;
System.Gadget.Flyout.onHide = SettingsClosedFunction;
System.Gadget.Flyout.show = true;
}
function SettingsLoad()
{
...
if (System.Gadget.Settings.read("SettingsInFlyout"))
{
System.Gadget.Settings.write("SettingsInFlyout", false);
divButtons.style.display = 'block';
}
}
You should also handle Enter and Escape keys to turn this into perfection...
Settings Storage
You cannot access elements on the settings page within the main gadget either vice versa. The only way to communicate between these two is to use System.Gadget.Settings
object. You have two options: readString
/writeString
or read
/write
. The name says it quite well - with the first two, you deal only with strings, with the others automatic conversion is performed. That means that the values are stored as strings as well, but the settings component tries to preserve the type. It works pretty well with small integer values and booleans, for example. However, stored dates (and any more complicated objects) will be picked up as strings - for example you can save 1000000 and pick up 1.0 E6, and you may have some localization problems when storing floating numbers (because of different decimal separators in different cultures). If you are familiar with this behavior, you can decide for yourself which methods you will use.
Defaults
From the point of instantiating your gadget up to the first committing of settings dialog box, there are no settings set. If you try to read a setting that does not exist, you get an empty string. It is a good idea to specify a set of default settings:
var defaultSettings = [];
defaultSettings['AutoSave'] = false;
defaultSettings['FavouriteNumber'] = 25;
function readSetting(name)
{
var r = System.Gadget.Settings.read(name);
if (r == '') r = defaultSettings[name];
return r;
}
You can place the array in a localized folder, if you need to specify different defaults for different cultures. See the Localization chapter below.
UI Notes
I ran into three surprises when I was building the user interface:
- No backgrounds allowed
Styles like background-color
or background-image
on the body
tag are simply ignored. However, DirectX filters do work - although the margins of the dialog box are fixed, so you likely won't get a nice effect by setting the background.
-
The dialog box has a maximum width and a minimum size
This was quite tricky to figure out. Like gadgets themselves, the settings body has a minimum allowed size - 146x57 pixels - the same as with the gadget, only expanded because of the OK and Cancel buttons. You can adjust the size by setting the width
and height
styles on the body
. BUT, regardless of what you set in styles, anything above 300px in width is clipped. That means the content is actually there as you have designed, but it is not visible.
- You cannot exit the settings box programmatically - i.e., you can't simulate clicking the OK and Cancel buttons.
Testing Settings
When you try to set settings on a gadget, it works well. Now, when you want to see if the settings are persisted, you might close the gadget and instantiate a new one, but the settings are gone! This is because it is another instance of the gadget and the settings are saved per instance. It makes sense if you realize that you can have multiple instances of the same gadget shown at the same time. And when you have multiple instances, it is unlikely that you want them all to show the same thing, isn't it? ;-)
The solution is easy. No system restarts, no re-logins, just exit (not close) the sidebar. Leave the gadget placed on the screen, right-click on the system tray icon () and choose Exit. Then, run it again from the Start menu (Accessories submenu, if you have searching disabled).
If you need store some settings that are persisted between instances, you can try the Persistent Gadget Settings library from Windows Sidebar team.
Flyouts
Showing Up
Working with flyouts is similar to working with settings, except that you have two-way communication between the flyout and the gadget. The variables are not shared and still the only common object is System.Gadget.Settings
, but, you can access System.Gadget.document
and System.Flyout.document
from each other which gives you access to the DOM of both files. So, as with gadget itself, you have two options how to fill the flyout: either by setting the System.Flyout.file
property to the flyout's file path, or by creating the document using the DOM. Some notes:
Am I a Flyout?
If you use the main gadget file for both the gadget itself and the flyout - as do I in the Garfield gadget - it may come in handy to know whether the page is displayed as a gadget or in the flyout window. My solution looks like this:
function loadGadget()
{
...
if (IsGadget)
{
IsFlyout = System.Gadget.settingsUI !== '';
if (!IsFlyout)
{
System.Gadget.onDock = updateSize;
System.Gadget.onUndock = updateSize;
System.Gadget.settingsUI = "Settings.html";
System.Gadget.onSettingsClosed = settingsClosed;
System.Gadget.Flyout.file = "Garfield.html";
}
else
updateSize();
}
...
}
Localization
Which Culture Is in Use?
You can have quite a lot of culture-specific settings. When you look at the System.Globalization.CultureInfo
class, you will find CurrentCulture
(the Format set in Language and Regional Options (LRO)), CurrentUICulture
(display language of OS you are using) and InstalledCulture
(language of OS you installed, I guess). Moreover, you can set Location and also System Locale in the LRO control panel, and all of these are independent of each other. So... which one is the right one? For what I've tried, I think the CurrentUICulture
is the one taken into account, so that it will work when you install one of the Windows Vista's language packs. This is bad. It would be much easier if user could choose which culture he prefers in the gadgets, and it would be at least more usable if the sidebar looked at some setting that is changeable by user - CurrentCulture
is my preferred, because if you use toLocaleDateString
on the Date
object for example, these settings are used.
The results are:
Any relative URL that you define in any HTML or that you set in script is, regardless of any previous tries or other files, resolved if possible by using the current locale first, and if not found:
<!--
<!--
<!--
<!--
<!--
<!--
<!--
<!--
<!--
<script src="js/localized.js" type="text/javascript"
language="javascript"></script>
So if you have a file in the English locale, you don't need to put it in the root folder as well. This applies to manifest(s) too.
Take It Seriously
Making your gadget localizable is a nice idea, unless you mess it up. If you have decided to localize your gadget, don't forget that there are right-to-left reading locales as well. Usually if you don't think of it during development, the localizers will be unable to create satisfactorily-localized versions. You can handle this case by checking if document.dir == 'rtl'
. This is a very specific problem, so I don't have any general rules. Be aware of your back/forward functions, for example - they should be swapped if pointing to the left/right direction (one more hint: use the text-align: justify
style, which reflects this situation).
It is a good idea to keep as few localizable files as possible. The usual way is to have one localized script file, which defines the culture-dependent variables, and then a global, failure-tolerant function to access it:
var localizedStrings = [];
localizedStrings['SaveCurrent'] = 'Save image to your computer...';
localizedStrings['OpenCurrent'] = 'Open image in web browser';
function getText(key)
{
var r = key;
try
{
r = localizedStrings[key];
if (r === undefined) r = key;
}
catch(e) {}
Maybe in your language, it is acceptable to say There are + pearsCount + pear(s) on the table.. But in my language for example, pear(s) would have to be hruška/šky/šek and moreover, we don't have any there are. So when you are building sentences, you might want to place values on different places in sentences, depending on the culture. If you are familiar with .NET's String.Format
function, you know that you can use {0} to {n} strings as placeholders for values. A very lightweight implementation of this functionality follows:
function getText(key, fills)
{
var r = key;
try
{
r = localizedStrings[key];
if (r === undefined) r = key;
}
catch(e) {}
if (fills != undefined)
{
if (typeof(fills) != Array) fills = new Array(fills.toString());
for (fillIndex in fills)
r = r.replace("{" + fillIndex + "}", fills[fillIndex]);
}
r = r.replace("{{", "{").replace("}}", "}");
return r;
}
Remember that the localized strings can be longer than you expect, so let the UI consume it.
And the last thing: when you work with right-to-left cultures, the localization process will be quite a bit more understandable if you name your strings independent of the culture - for example, use LeftButton
instead of BackButton
. ;-)
Override the Culture
If you don't agree with the fixed-culture behavior, you can fight against it, although it is a pretty advanced challenge. You can either create your own culture-aware system, like storing all the strings in some text or XML file, or you can use the sidebar's system. The pros and cons are clear: With the first, there are no surprises, you can do what you want, and you have it under control, but also it is a lot of work and cultures defined this way cannot be utilized by the sidebar. With the second, you have to be careful when you do things and what you do, and your code has to be written flexibly, but you have compatibility. This is what I've chosen in the Garfield gadget. First, here is the code. Assuming you have the localized strings in the culture/js/Localized.js folder, you can dynamically read it and execute it:
function localize(culture)
{
var path = System.Gadget.path;
if (path.substring(path.length - 1) != '\\') path += '\\';
path += culture;
try
{
var script = "";
var stream = new ActiveXObject("ADODB.Stream");
stream.Open();
stream.CharSet = "UTF-8";
stream.LoadFromFile(path + "\\js\\Localized.js");
script = stream.ReadText();
script = script.replace(/(\s|;|^|\*\/)+(var)(\s+)/gm, "$1;$3");
stream.Close;
stream = null;
eval(script);
}
catch (stringsError) { ... }
}
If you want to use a localized file resource, you need to set the absolute path - otherwise it will go through the culture-match chain described before:
System.Gadget.settingsUI = "/" + culture + "/Settings.html";
...and in this case you also need to ensure that the culture-independent links in such resources are using absolute paths, since you are in another root than you normally would be in:
<!--
<!--
<link href="http://www.codeproject.com/css/settings.css" rel="stylesheet" type="text/css" />
<script src="http://www.codeproject.com/js/settings.js" type="text/javascript"
language="javascript"></script>
For the user's total comfort, you must have the available-cultures-selection box :). For a complete solution for changing the display locale, check the Garfield gadget's code. By the way, you can load any culture you create without waiting for Esperanto or some other release of Windows Vista!
Final Notes
Well, we are almost done. Three final details came to my mind that I wanted to share with you:
Miscellaneous
How to Check if a File Exists
function exists(path)
{
if (!IsGadget) return false;
try { System.Shell.itemFromPath(path); }
catch (notFound) { return false; }
return true;
}
How to Get the System.Shell.Folder Item
try { var folder = System.Shell.itemFromPath(path).SHFolder; }
catch (notFound) { ... }
If an Internet Connection Is Not Available
Please, think carefully when you finalize your gadget. Take into account these two possible situations:
- The internet connection is lost.
- The gadget is started when there is no internet connection.
A red cross instead of an image, never-ending connection timeouts, and cannot display the webpage are things that should not appear on your second gadget.
Deploying
Oh yes, and when we are finished, we still have to deploy the gadget. As I already wrote a while ago, you can install a gadget either by copying it to a gadgets folder (e.g. user's, system's or default user's) or by unpackaging the ZIP archive. You can also pack it into a CAB archive. Why would you want to do that? Because the CAB archive can be signed. Fortunately, the sidebar team knows how costly code signing certificates are, so they did not place any requirements that gadgets must be digitally signed. Follow me as I pack the Garfield gadget for you:
- Start Visual Studio 2005 Command Prompt and navigate to the folder with your gadget.
- Type
cabarc -p -r N MyGadget.gadget *
and press Enter.
You cannot create a CAB Setup project in Visual Studio, since it does not preserve subdirectories.
(-p
preserves directories in archives, -r
includes files from subdirectories, N
creates new archive)
- Type
makecert -sv "MyGadget.pvk" -n "CN=My Company" MyGadget.cer
and press Enter.
This creates a certificate, which you need in order to sign the gadget. Please choose a name other than MyGadget
for your gadget. You will be asked three times for the password. Type anything you want here.
(-sv
creates a private key, -n
sets the Issued To field) - Type
signtool signwizard
and press Enter. The Digital Signature Wizard will be started.
Press Next, Browse and locate the .gadget file we created in step 1. (Note: The open dialog box has an executable files filter by default, so you will need to switch it to All Files (*.*) to see your gadget.
Press Next, choose Custom signing options, and press Next again.
On the Signature Certificate page, click Select from File..., and locate and open the .cer file we created in step 2 (switch filter to X.509 Certificate (*.cer;*.crt). Now you should see details of the certificate we set above and we are ready to continue. Next.
Now the private key comes into play - click Browse and open it. Click Next and re-enter the password we just created.
Choose your favorite hash algorithm (or leave the selected one), press Next and also press Next on the next page.
If you see the Data Description page now, you can fill in a short description of your gadget and web link. Now the Timestamp page. If you want to mark the gadget creation time, check the checkbox and enter http://timestamp.verisign.com/scripts/timstamp.dll (unless you prefer another timestamp provider). Next.
Here it is! Click Finish, re-enter password if asked, and we are done.
If you find it easier or don't want to use the GUI, you can enter
signtool sign /v /a /d "Description of gadget" /du http://your.web.link/ /t http://timestamp.verisign.com/scripts/timstamp.dll MyGadget.gadget
(/v
optional tells you if the command succeeded, /a
tries to find any certificate for signing, /d
, /du
, /t
optional - see above)
If you have a .pfx for your projects generated by Visual Studio, you can use it the following way:
signtool sign /v /f MyFile.pfx /p password /t http://timestamp.verisign.com/scripts/timstamp.dll MyGadget.gadget
(/f
assuming you have the pfx file in the same directory, /p
optional password for your pfx created in Visual Studio)
- Double-click the .gadget file to test whether everything works ok.
Copyrights
For the Garfield gadget, the comics strips are copyrighted by Mr. Jim Davis, Pawn Incorporated. The pictures are freely available on the internet - I haven't needed any special steps to access them, and sidebar is a web browser. Thus I believe I am not doing anything wrong or illegal. The cost of this is that they can change the server, naming, costs, or otherwise change or disable the access as they wish without prior notification, and I cannot and do not give any guarantees that the gadget will work forever.
Should I be wrong, please let me know.
The only supplementary image I use I found using Google (actually there are plenty of this all around) and have downloaded from http://www.lacoctelera.com/myfiles/mariajo/Garfield Sleepy.jpg. I did not find any copyright or legal notice on it anywhere, so if you know who I should ask for permission, I am ready.
If anybody believes I used part of his work in the article and did not mention it sufficiently, contact me and we will correct it.
Feedback
As I have said before, I write what I think might be the right way to do it, and if anyone has a better idea or if anyone sees I am missing something or saying something incorrect, send a message. I hope I showed you something you didn't know, something that will help you, or something useful for you.
Corrections on grammatical/stylistic mistakes are also welcomed - this is my first English article. ;-)
Resources
Have a nice day and lot of fun!
History
- 10th October, 2006 - version 1.0.0.0. Initial 15-minutes release. Navigation by left click / right click.
- 31st January, 2007 - version 2.5. The second gadget. Navigation panel, docking, saving, settings, flyout, and much more.
- 13th February, 2007 - version 3.0. First published. Fixed some bugs and added the ability to change culture.