Click here to Skip to main content
Click here to Skip to main content

Practices and Hints for Gadgets

, 19 Feb 2007
Rate this:
Please Sign up or sign in to vote.
So you have read the Vista Sidebar gadgets tutorials, played around a bit, and now want to make something more graceful? Do you think it will run like clockwork? It didn't for me, and I've tried to summarize the surprises, gotchas and tips for you.

Sample screenshot of undocked Garfield Comics gadget

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) Wink | ;-)

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 the 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. Smile | :) 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 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 any report 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.

<img src="gadgettips/Tip.gif" />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:

<font style="background-color: #ccff66"><?xml version="1.0" encoding="utf-8" ?></font>
<font style="background-color: #ccff66"><gadget></font>
    <font style="background-color: #ccff66"><name>Garfield Comics</name></font>
    <font color="gray"><namespace>UAM.InformatiX.Windows.Gadgets</namespace></font>
    <font style="background-color: #ccff66"><version>2.5</version></font>
    <author name="Jan Kuèera">
        <info url="http://www.codeproject.com/" text="The Code Project" />
        <logo src="Logo.png"/>
    </author>
    <copyright>&#169; Pawn, Inc. since 1978</copyright>
    <description>Watch Garfield daily comics!</description>
    <icons>
        <icon height="48" width="48" <font style="background-color: #ccff66">src="Garfield128.png"</font> />
    </icons>
    <font style="background-color: #ccff66"><hosts></font>
        <font style="background-color: #ccff66"><host name="sidebar"></font>
            <font style="background-color: #ccff66"><base type="HTML" src="Garfield.html" /></font>
            <font style="background-color: #ccff66"><permissions>full</permissions></font>
            <font style="background-color: #ccff66"><platform minPlatformVersion="1.0" /></font>
            <defaultImage src="Garfield128.png" />
        <font style="background-color: #ccff66"></host></font>
    <font style="background-color: #ccff66"></hosts></font>
<font style="background-color: #ccff66"></gadget></font>

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 green 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 before 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. Smile | :)

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?

<img src="gadgettips/Tip.gif" />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:

// true if the gadget is being hosted as a gadget,
// false otherwise (eg. viewing in web browser)
var IsGadget = (window.System != undefined);

Edit and continue

They 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()
{
  
  // after this, the image of the gadget is frozen
  System.Gadget.beginTransition();
 
  // now do any changes you want - dimensions, content, whatever
  // the changes will not be visible
 
  System.Gadget.endTransition(System.Gadget.TransitionType.morph, 1);
  // now, the frozen image will morph into the new one 
  // over the course of one second
} 

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 TransitionTypes 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? Smile | :)

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()
{
   // calls RefreshMyGadget every second
   cancelID = window.setInterval(RefreshMyGadget, 1000);
}

function StopRefreshing()
{
   // pause continuous refreshing, to resume call StartRefreshing
   window.clearInterval(cancelID);
}

var pendingID = 0;
function RefreshOnce()
{
   // calls RefreshMyGadget after second
   pendingID = window.setTimeout(RefreshMyGadget, 1000);
}
function CancelPendingRefresh()
{
   window.clearTimeout(pendingID);
}

function RefreshMyGadget()
{
   ...
   // uncomment to simulate setInterval by setTimeout
   // pendingID = window.setTimeout(RefreshMyGadget, 1000);
}

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 in 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; //timeout handle for animation
function animateHeight(desiredHeight, delta, timeout)
{
   // desired height not missed?
   if ((delta > 0 && document.body.style.posHeight < desiredHeight) ||
       (delta < 0 && document.body.style.posHeight > desiredHeight))       
   {
     // magic delinealiser
     timeout = timeout * 1.3;                                          
     document.body.style.posHeight += delta;
     aniHandle = window.setTimeout('animateHeight(' + desiredHeight + 
                           ',' + delta + ',' + timeout + ')', timeout);
   }
   else
   {
     // modify the height to exactly
     // match the desired one
     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 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();
  // synchronous open
  xmlRequest.open("GET", url, false);
  xmlRequest.send(null);
  // thus, always xmlRequest.readyState = Loaded here
  if (xmlRequest.status == 200) // HTTP 200 OK (we have data)
  {                                                  
    var stream = new ActiveXObject("ADODB.Stream");
     stream.Type = 1;                            // binary mode
     stream.Open();
     stream.Write(xmlRequest.responseBody);      // write data to the stream
     stream.SaveToFile(savePath, 2);             // overwrite if exists
     stream.Close;
    stream = null;
  }
  else                       // HTTP error, like not found or access denied
  { ... }                    // text available at xmlRequest.statusText}
catch(exception) { ... }     // something else went wrong  

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);          // asynchronous today

  // somebody needs to get notified
  xmlRequest.onreadystatechange = SaveFile;
  // when the response is available
  xmlRequest.send(null);    
}

function CancelDownloading()
{
  xmlRequest.abort();   // this also removes the onreadystatechange handler
}

function SaveFile()
{
  // You need to check whether the object is ready because
  // this function gets called also on open, send and receive
  if (xmlRequest.readyState < 4) return;    

  if (xmlRequest.status == 200)               // HTTP 200 OK (we have data)
  {
    var stream = new ActiveXObject("ADODB.Stream");
     stream.Type = 1;                   // binary mode, default is 2 - text
     stream.Open();
     stream.Write(xmlRequest.responseBody);   // write data to the stream
     stream.SaveToFile(savePath, 2);          // overwrite if exists
     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:

  • You cannot suggest the filename to the user.However, you can open the dialog in the path you specify. If you don't, user's documents folder will be selected.
  • Use the \0 character to separate desired file types in filter, instead of colon (a bug):
    "All Files (*.*)\0*.*\0Text Files (*.txt)\0*.txt\0\0"
    Update: Jonathan has added this to the list of known bugs - cool. Wink | ;-)
  • The save dialog asks the user whether to replace the file if it already exists, so you can overwrite it without further confirmations.
  • Take into account that the dialog box can be cancelled. An empty string is returned in that case.

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:

<!-- using href="#" causes reload -->
<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; // Tab
     case 13: break; // Enter
     case 27: break; // Escape - good practice to hide flyout here
     case 32: break; // Space
     case 33: break; // Page Up
     case 34: break; // Page Down
     case 35: break; // End
     case 36: break; // Home
     case 37: break; // Left Arrow
     case 38: break; // Up Arrow
     case 39: break; // Right Arrow
     case 40: break; // Down Arrow
     case 79: // O
      if (event.ctrlKey) ... // Open (Ctrl+O)
      break;
     case 83: // S
      if (event.ctrlKey) ... // Save (Ctrl+S)
      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 Smile | :) ). 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:

// The place you show the settings from code
function ShowSettings()
{
  // set it prior the flyout file because
  // the flyout may be already open
  System.Gadget.Settings.write("SettingsInFlyout", true)
                                                        
  System.Gadget.Flyout.file = System.Gadget.settingsUI;
  // remember to simulate closing the dialog box as well
  // if you handle it in the main script
  System.Gadget.Flyout.onHide = SettingsClosedFunction;
  System.Gadget.Flyout.show = true;                    
}

// Located in the settings HTML
function SettingsLoad()
{
  ...
  if (System.Gadget.Settings.read("SettingsInFlyout"))
  {
   // do not forget to clear the state for next call
   System.Gadget.Settings.write("SettingsInFlyout", false);
   divButtons.style.display = 'block'; 
   // <div id="divButtons" style="text-align: right; display: none">
   //   <input type="button" value="OK" onclick="CommitSettings" /> &nbsp;
   //   <input type="button" value="Cancel" onclick="CancelSettings" />
   // </div>
  }
} 

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 = [];                 // store some defaults
defaultSettings['AutoSave'] = false;      // in array
defaultSettings['FavouriteNumber'] = 25;

function readSetting(name)
{
   var r = System.Gadget.Settings.read(name);
   // if none found, return default settings
   if (r == '')  r = defaultSettings[name];
   return r;
} 

You can place the array in 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? Wink | ;-)

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:

  • If the flyout is already shown, all your changes are reflected immediately. If not, you can show or hide the flyout by setting System.Gadget.Flyout.show = true, or false, respectively.

  • Every time the System.Gadget.Flyout.show property is set to true, the flyout page will reload.

  • If you have HTML template for the flyout and you want to just add some contents to it, you have to wait until the flyout file actually is done loading, which is typically not immediately after showing it. Instead, attach a handler to System.Gadget.Flyout.onShow (also thanks Jonathan to answering this thread):
    function ShowFlyout()
    {
      // not necessary if you use only one file for the flyout
      System.Gadget.Flyout.file = 'myflyout.html';
     
      System.Gadget.Flyout.onShowing = FlyoutLoaded;
      System.Gadget.Flyout.show = true;
    }
    
    function FlyoutLoaded()
    {
      //you can call System.Gadget.Flyout.document.getElementById(...) here
    }
  • Toggle the flyout's visibility instead of just setting it on. Pressing the 'show button' again does not automatically hide the flyout; you have to click outside the gadget.

  • As I have already mentioned, you cannot move gadget when the flyout is shown, regardless of whether the gadget is docked or undocked.

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)
    {
        // flyout and the actual gadget share the same System.Gadget object
        // so this block will already be executed and settingsUI set if we
        // are opening flyout from the gadget
        IsFlyout = System.Gadget.settingsUI !== '';
        if (!IsFlyout)                         
        {          
          // oh yes - and we don't want to replace handlers already attached
           System.Gadget.onDock   = updateSize;
           System.Gadget.onUndock = updateSize;
           System.Gadget.settingsUI = "Settings.html";
           System.Gadget.onSettingsClosed = settingsClosed;
           // we do not change flyout contents so we can set it during setup
           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:

  • You cannot control the locale of your gadget - the sidebar will choose.

  • The only way how to find out the locale of your gadget is to declare a helper variable in one of the localized files, like:
    var GadgetLocale = "en-us"; 
    ...which you will have to manually rewrite with each localized version.

  • You cannot test localization! If you have an English installation of Windows Vista and do not have any language packages, you cannot test your localized code.

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:

    <!-- every relative url is attempted to be localized first -->
    <!-- if your UI culture is cs-cz, the following  -->
    <!-- locations are tried in written order until  -->
    <!-- match is found:                             -->
    <!-- 1: cs-cz/js/localized.js                    -->
    <!-- 2: cs/js/localized.js                       -->
    <!-- 3: en-us/js/localized.js                    -->
    <!-- 4: en/js/localized.js                       -->
    <!-- 5: js/localized.js                          -->
    <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:

// localized script file:
var localizedStrings = [];   // creates empty array

localizedStrings['SaveCurrent']   = 'Save image to your computer...';
localizedStrings['OpenCurrent']   = 'Open image in web browser';

// global function in culture independent script file:
function getText(key)
{
  var r = key;               // if something goes wrong, return key itself
  try
  {
    r = localizedStrings[key];     // try to look for localized version
    if (r === undefined) r = key;  // if not found use key itself
  }
  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:

// ============================================================ getText = //
//                                                                        //
//  Returns localized text from Localized.js by key if found; otherwise   //
//  the key itself. The text can contain "{0}", "{1}", etc. place holders //
//  which will be filled from the fills parameter.                        //
//                                                                        //
//  Syntax: getText(string key, object fills)                             //
//                                                                        //
//  Parameters: key   - a string containg the key to look for             //
//              fills - if non-array type then it goes instead of {0};    //
//                      if non-indexed array then the first element will  //
//                      be placed instead of {0} (after conversion to     //
//                      string), the second replaces {1} and so on;       //
//                      if indexed array then use indexes in brackets,    //
//                      like {firstindex}, {secondindex}, ...             //
//                                                                        //
// ---------------------------------------------------------------------- //
function getText(key, fills)
{
 var r = key;
 try                                     // you already know this part from
 {                                       // example above
   r = localizedStrings[key];
   if (r === undefined) r = key;
 }
 catch(e) {}

 if (fills != undefined)
  {
   // place single value into array, beware of numbers, which are treated
   // as expected array length
   if (typeof(fills) != Array) fills = new Array(fills.toString());
   for (fillIndex in fills)                // iterate over indexes of array
    r = r.replace("{" + fillIndex + "}", fills[fillIndex]);
  }

 // for some consistency with .NET
 // however {{0}} will be treated as index
 r = r.replace("{{", "{").replace("}}", "}");
 return r;                                       
}

// and sample usage:
// en-us culture ... localizedStrings['PearsStuff'] = 
//                                     'There are {0} pears on the table.';
// cs-cz culture ... localizedStrings['PearsStuff'] = 
//                                                 'Hrušek na stole: {0}.';
// call getText('PearsStuff', 5)

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. Wink | ;-)

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;                                    // GadgetPath\culture

  try
  {
    var script = "";
    
    // using XMLHttpRequest to access /culture/js/Localized.js
    // throws Access denied error
    var stream = new ActiveXObject("ADODB.Stream");          
     stream.Open();
     // eval does not like Chinese...
     stream.CharSet = "UTF-8";
     stream.LoadFromFile(path + "\\js\\Localized.js");
     // read all the text of Localized.js
     script = stream.ReadText();                           
     // filter out all declarations since they would be treated as local
     script = script.replace(/(\s|;|^|\*\/)+(var)(\s+)/gm, "$1;$3");
     // and would hide the global ones we are trying to replace
     stream.Close;                                                  
    stream = null;
    // run the script
    // this will replace localizedStrings array
    eval(script);                                           
  }
  // you may want to revert the operation if it fails - 
  //see the Garfield gadget
  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:
  <!-- we can use absolute paths if we don't 
                             expect/want these files to be localized    -->
  <!-- we have to use absolute paths if we want 
                                to override automatic culture selection -->
  <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 Smile | :) . 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

// System.Shell.Folder.parse does not work
try { var folder = System.Shell.itemFromPath(path).SHFolder; }
catch (notFound) { ... }                          // folder does not exist

If an internet connection is not available

Please, think carefully when you finalize your gadget. Take into account these 2 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:
  1. Start Visual Studio 2005 Command Prompt and navigate to the folder with your gadget.

  2. 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)

  3. 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 another name 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)

  4. 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)

  5. 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. Wink | ;-)

Resources

Have a nice day and lot of fun!

History

  • October 2006 the 10th, version 1.0.0.0. Initial 15-minutes release. Navigation by left click / right click.
  • January 2007 the 31st, version 2.5. The second gadget. Navigation panel, docking, saving, settings, flyout, and much more.
  • February 2007 the 13th, version 3.0. First published. Fixed some bugs and added the ability to change culture.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

Share

About the Author

Jan Kučera
Web Developer
Czech Republic Czech Republic
No Biography provided

Comments and Discussions

 
BugTypo PinmemberMadhur Ahuja16-Sep-13 22:38 
Generalgreat article. PinmembernK0de10-Apr-12 10:06 
QuestionQuestion on how to make a chat into a gadget. PinmemberDoggirl327-Jun-11 6:23 
Generalcool stuff PinmemberMoped1-Apr-10 3:12 
GeneralGreat work! Pinmemberwind_2328-Apr-08 5:05 
QuestionInternet Explorer? PinmemberDoncp17-Feb-08 12:41 
AnswerRe: Internet Explorer? PinmemberJan Kucera17-Feb-08 19:50 
GeneralRe: Internet Explorer? PinmemberDoncp18-Feb-08 6:41 
GeneralRe: Internet Explorer? PinmemberJan Kucera19-Feb-08 1:39 
GeneralDisplay Alerts PinmemberLastwebpage29-Mar-07 12:20 
GeneralRe: Display Alerts PinmemberJan Kucera29-Mar-07 20:06 
QuestionHave a tip you want to share? PinmemberJan Kucera5-Mar-07 20:02 
AnswerRe: Have a tip you want to share? PinmemberDeepWaters6-Mar-08 15:37 
GeneralRe: Have a tip you want to share? PinmemberJan Kucera7-Mar-08 5:33 
GeneralRe: Have a tip you want to share? PinmemberDeepWaters9-Mar-08 15:14 
GeneralComments and one question... :-) PinmemberJesus Jimenez1-Mar-07 11:36 
GeneralRe: Comments and one question... :-) PinmemberJan Kucera5-Mar-07 10:12 
GeneralRe: Comments and one question... :-) PinmemberJesus Jimenez5-Mar-07 11:33 
GeneralRe: Comments and one question... :-) PinmemberJan Kucera5-Mar-07 19:39 
GeneralRe: Comments and one question... :-) PinmemberJan Kucera29-Mar-07 9:54 
GeneralVery Nice PinmvpRama Krishna Vavilala1-Mar-07 4:32 
GeneralRe: Very Nice PinmemberJan Kucera5-Mar-07 9:52 
NewsGarfield's bugs and suggestions PinmemberJan Kucera21-Feb-07 2:46 
GeneralRe: Garfield's bugs and suggestions PinmemberJaroslav Klima21-Feb-07 11:55 
GeneralRe: Garfield's bugs and suggestions PinmemberJan Kucera24-Feb-07 0:28 
GeneralRe: Garfield's bugs and suggestions PinmemberN3croman12-Apr-07 13:42 
GeneralRe: Garfield's bugs and suggestions PinmemberJan Kucera12-Apr-07 20:26 
GeneralRe: Garfield's bugs and suggestions PinmemberN3croman13-Apr-07 2:18 
GeneralRe: Garfield's bugs and suggestions PinmemberJan Kucera13-Apr-07 19:42 
GeneralBrilliantly written PinmemberAndrew Tweddle20-Feb-07 20:45 
GeneralRe: Brilliantly written PinmemberJan Kucera21-Feb-07 3:03 
GeneralRe: Brilliantly written PinmemberJames Ashley24-Feb-07 7:23 
GeneralRe: Brilliantly written PinmemberJan Kucera5-Mar-07 9:50 
GeneralGreat article ... Pinmember_oti19-Feb-07 13:40 
GeneralRe: Great article ... PinmemberJan Kucera21-Feb-07 2:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140826.1 | Last Updated 19 Feb 2007
Article Copyright 2007 by Jan Kučera
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid