![]() |
Desktop Development »
Desktop Gadgets »
General
Intermediate
Practices and Hints for GadgetsBy Jan KuceraSo 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. |
Javascript, HTMLVista, Visual Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

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...
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.
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.
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.
"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.
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.
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:
<?xml version="1.0" encoding="utf-8" ?>
<gadget>
<name>Garfield Comics</name>
<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 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.
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.
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?
"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.
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.NetworkInterfaceSystem.Net.RecycleBin.percentFullSystem.Net.SoundIf 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.
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);
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 ( |
The settings page is loaded every time you display the
settings dialog box. So clicking on the gadget settings icon ( |
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. |
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.
Before I give you a few general hints I discovered, I'll say a few things about the attached source code.
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.
This paragraph is here only to note that some security precautions may impact the functionality. Gadgets:
I made this list from the Sidebar Security post at Gadget Corner, where you can find more details on this topic.
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.
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? :-)
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.
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.
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:
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.
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:
onreadystatechange. So you can't
pass any parameters to the function.XMLHttpRequest object in a global variable
in order to access it in the handler.Also:
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.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.open method.
So you can't download from HTTP if you open the HTML file from your disk.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:
"All Files (*.*)\0*.*\0Text Files (*.txt)\0*.txt\0\0"You should take care of keyboard users. Believe it or not, the sidebar can be accessed using keyboard:
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.
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:
// 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" />
// <input type="button" value="Cancel" onclick="CancelSettings" />
// </div>
}
}
You should also handle Enter and Escape keys to turn this into perfection...
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.
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.
I ran into three surprises when I was building the user interface:
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.
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.
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:
System.Gadget.Flyout.show
= true, or false, respectively.System.Gadget.Flyout.show property
is set to true, the flyout page will reload.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
}
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();
}
...
}
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:
var GadgetLocale = "en-us";...which you will have to manually rewrite with each localized version.
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.
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. ;-)
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 :). 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!
Well, we are almost done. Three final details came to my mind that I wanted to share with you:
function exists(path)
{
if (!IsGadget) return false;
try { System.Shell.itemFromPath(path); }
catch (notFound) { return false; }
return true;
}
// System.Shell.Folder.parse does not work
try { var folder = System.Shell.itemFromPath(path).SHFolder; }
catch (notFound) { ... } // folder does not exist
cabarc -p -r N MyGadget.gadget * and press Enter.-p preserves directories in archives, -r includes
files from subdirectories, N creates new archive)makecert -sv "MyGadget.pvk" -n "CN=My Company" MyGadget.cer
and press Enter.-sv creates a private key, -n sets the Issued
To field)signtool signwizard and press Enter. The Digital Signature
Wizard will be started..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..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.http://timestamp.verisign.com/scripts/timstamp.dll
(unless you prefer another timestamp provider). Next.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)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).gadget file to test whether everything works
ok.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 19 Feb 2007 Editor: J. Dunlap |
Copyright 2007 by Jan Kucera Everything else Copyright © CodeProject, 1999-2009 Web10 | Advertise on the Code Project |