Profilo di ToddTales of a Gadget Develo...FotoBlogElenchi Strumenti Guida
09 ottobre

Implementing a storage mechanism for Spaces gadgets

Edit:  This is no longer necessary, as Windows Live Spaces now supports get/setProperties on the Module object.  The article is still interesting from a technical perspective, but you no longer need to implement your own preference storage mechanism for Spaces gadgets.

Donavon's LiveSide.net article on building gadgets for Spaces mentions one of the main problems remaining for gadget developers trying to target Spaces, namely how to store preferences for a gadget and reference them in such a way that the gadget can be both configurable for the author and yet show what the author wants to visitors.  Since he didn't go into detail about how to build such a mechanism, I thought I'd do a quick run through of the solution I used.

Prerequisites

To do this, you'll need a couple resources up front that you may or may not have.

  • A web hosting site that allows you to use some sort of server-side language.  I used PHP because that's what my provider made available, but you could do this with ASP, ASP.NET, Perl, Ruby, Python, or even good old C/C++ (as a CGI).
  • A database or other storage mechanism.  I went with MySQL, again because that's what my host provides (I'll not get into my biases against MySQL here ...).  You could do this with file-based storage if you really want to, though I'm going to present a SQL-based solution here.
  • A hosting plan that has adequate bandwidth limits and reliability.  The last thing you want is to have your gadgets stop working because your hosting provider cut you off.  Keep in mind that if your gadget gets popular, what you initially feel is adequate may ultimately be too little to support the load.

Expect to have to pay money for a hosting service that will provide you what you need.  Geocities is not going to cut it.

Storage

While I built my storage service for a single gadget, I looked at what I would need to make it work for multiple gadgets.  I ended up with two tables:

  1. spaces_author, to track the combination of Space + gadget id (where "gadget id" is a string I made up for my gadget, and must be unique for each gadget you use with this service).
  2. spaces_pref, to map name/value pairs to the ID from spaces_author.  This table holds the actual preferences you're going to store.

Here's the schema for spaces_author.  This is from MySQL, so you will need to adjust it for other databases like SQL Server, PostgreSQL, or Oracle.

CREATE TABLE `spaces_author` (
    `id` bigint(20) NOT NULL auto_increment,
    `host` varchar(250) NOT NULL default '',
    `gadget` varchar(250) NOT NULL default '',
    PRIMARY KEY (`id`),
    UNIQUE KEY `host_2` (`host`,`gadget`)
) TYPE=MyISAM COMMENT='Spaces author mapping';

"id" is the unique id of the host + gadget (herein referred to as "author").  "host" is a unique value to identify the Space.  I used the first part of the URL, or "everythingandnothing" from this page's full URL of "everythingandnothing.spaces.live.com".  "gadget", as mentioned before, is a made-up unique key for a gadget.  You can use whatever you like here, change it to an integer, or whatever.

And here's the schema for spaces_pref.

CREATE TABLE `spaces_pref` (
    `authorId` bigint(20) NOT NULL default '0',
    `name` varchar(64) NOT NULL default '',
    `value` varchar(255) NOT NULL default '',
    PRIMARY KEY (`authorId`,`name`)
) TYPE=MyISAM COMMENT='Spaces preference storage';

"authorId" is a link back to the "spaces_author" table (in a better database, I'd actually make this a foreign key; for MySQL, I just didn't bother).  "name" is the name of the preference you're storing, and "value" is the value.  Note the size limitations.  You can adjust those as appropriate for what you want to store and your database's capabilities.

The Web Service

Once you have your storage mechanism in place, you need a way to store and retrieve that data.  This is where the web service comes in.  Since you'll be calling this with the gadget framework's Web.Network.createRequest() method, you're limited to either JSON or REST XML (if you choose to use SOAP, make sure you enable HTTP-GET or you won't be able to call this from your gadget!).  I chose to work with XML as it's simple, I know it, and I've not worked much with JSON.  In the long run JSON is probably a better choice, but I'm not going to cover that here.

I wrote my web service using PHP, and created two different files, one to set preferences and the other retrieve them.  You could do this with a single interface if you like, but it was simpler for me to do it as two separate files.  I'm not going to provide code here, but the idea is simple.  I've omitted some error checking in the workflow here, but you should check for errors at every step and return useful information in an XML or JSON format so your gadget can figure out what went wrong.

  1. Pass in host, gadget, name, and value as GET parameters.
    1. Make sure you validate your parameters!
  2. For Set
    1. Check if the host + gadget key exists in spaces_author
      1. If it does, retrieve the authorId
      2. If it doesn't, insert host + gadget into spaces_author and retrieve the newly created authorId.
    2. Check if authorId + name exists in spaces_pref
      1. If it does, update spaces_pref and set value to the new value.
      2. If it doesn't, insert authorId, name, and value into spaces_pref
    3. Return some indicator that Set succeeded.
  3. For Get, check if the host + gadget key exists in spaces_author
    1. If it does, retrieve authorId
      1. Check if authorId + name exists in spaces_pref
        1. If it does, retrieve the value and return it wrapped in XML
        2. If it doesn't, return an error
    2. If it doesn't, return an error

And a few tips for those who plan to use PHP:

  • Before you echo or print anything, make sure you set your content to application/xml (for XML).  You really should do this first thing in your PHP file, because you may include other files that do some sort of output that you're not aware of.
    • header('Content-Type: application/xml');
  • Make sure you protect yourself against SQL injection.
    • if (get_magic_quotes_gpc() == 1)
      {
          $foo = $_GET["foo"];
      }
      else
      {
          $foo = addslashes($_GET["foo"]);
      }

Putting it all together

Now you have a storage backend and a web service to access it.  How do you use this in a gadget?  To reduce load, you should only use this storage mechanism for gadgets running on Spaces, and continue to use module.[set|get]Preference() for Live.com gadgets.  I'm going to make a couple assumptions here:

  • You've defined a variable m_isSpace that is true if the gadget is running on a Spaces page.
  • You've defined a variable m_isAuthor that is true if the gadget is running in author mode.
  • If m_isSpace is true, you've populated a variable m_host that contains a unique key to identify this Space (I suggest you parse window.location like Donavon suggests).

Saving is easy.  Here's a sample function:

function SetData()
{
    if (!m_isSpaces)
    {
        // Not on a Space.  Use the normal preference mechanism
        m_module.setPreference("tag", m_tag);
    }
    else
    {
        if (!m_isAuthor)
        {
            // On a Space, but as a visitor.  No saving for you!
            return;
        }
        else
        {
            // On a Space as an author.  We need to kick off a web request to do the save
            var url = // TODO:  Build your URL here, with your host key, gadget key, etc
                + "&f=" + Math.random(); // Break the cache, since we want to make sure we do save

            var req = Web.Network.createRequest(
                Web.Network.Type.XML
                , url
                , null
                , OnSetData);

            req.execute();
        }
    }
}

OnSetData doesn't actually do anything, because saving is best-effort

function OnSetData(response)
{
    // Don't much care what happens here!
}

Retrieving data is a bit more involved.

function GetData()
{
    if (!m_isSpaces)
    {
        // Not on a Space.  Use the proper getPreference call
        m_tag = m_module.getPreference("tag");
    }
    else
    {
        // On a Space, so kick off a web request
        var url = // TODO: Build your URL here, wit your host key, gadget key, etc 

        var req = Web.Network.createRequest(
            Web.Network.Type.XML
            , url
            , null
            , OnGetData);

        m_retrievingData = true;
        req.execute();
    }
}

Note "m_retrievingData = true;".  I'll get to that later.

OnGetData does the actual work of pulling out the retrieved value.

function OnGetData(response)
{
    if (response.readyState == 4 && response.status == 200 && response.responseXML != null)
    {
        var error = response.responseXML.selectSingleNode("//error");

        if (!error)
        {
            // Whee!
            m_yourValue = response.responseXML.selectSingleNode("//get_pref").text;
        }
        else
        {
            // uh ...
            m_yourValue = null;
        }

        m_retrievingData = false;
    }
}

In this example, I'm assuming that "<error />" (with some information inside it) is the root of the returned document if something went wrong, and "<get_pref />" (with a value inside) is the root of the returned document if everything went right.

Now, about that m_retrievingData variable.  These calls run asynchronously, but you may need the returned data to finish initialization of your gadget.  The m_retrievingData variable indicates that you're still waiting on this data to return, so you need to wait for that.  The simplest thing to do is to kick off a timer and check the state of m_retrievingData.

In this.initialize, add "setTimeout(OnLoadingWait, 100);" after you've completed all initialization that can be done without the retrieved data.  Here's OnLoadingWait:

function OnLoadingWait()
{
    if (m_retrievingData)
    {
        // Still waiting on data
        setTimeout(OnLoadingWait, 100);
    }
    else
    {
        if (m_yourValue != null)
        {
            DoSomethingWithData();
        }
        else
        {
            if (m_isSpaces && !m_isAuthor)
            {
                // No tag, but we can't let the viewer do anything about it
                // A failure message would be good
            }
            else
            {
                // No tag, but we're in author mode.  Give the author a way to set preferences
            }
        }
    }
}

And you're done!

Caveats

This approach works for me, but it may not work for you.  The Spaces team will surely heed the cries from the gadget developer community and provide a standard storage mechanism soon enough.  The code is not guaranteed to be bug free.  The code was written with the assumption that you're only storing or retrieving a single value at a time.  You could certainly modify your web service to handle multiple name/value pairs to minimize the number of web calls you have to make.  Code and ideas are provided as-is, and I'm in no way responsible for any bandwidth overage charges you may incur by using this method with a popular gadget.

Commenti

Attendere...
Il commento immesso è troppo lungo. Immetti un commento più breve.
Immissione non effettuata. Riprova.
Impossibile aggiungere il commento al momento. Riprova più tardi.
Per aggiungere un commento è necessaria l'autorizzazione di un genitore. Chiedi autorizzazione
I tuoi genitori hanno disattivato i commenti.
Impossibile eliminare il commento al momento. Riprova più tardi.
Hai raggiunto il numero massimo di commenti pubblicabili giornalmente. Riprova tra 24 ore.
Impossibile lasciare commenti. La funzionalità è stata disattivata perché i sistemi hanno rilevato una possibile attività di spamming dal tuo account. Se ritieni che il tuo account è stato disattivato per errore, contatta il supporto tecnico di Windows Live.
Esegui il seguente controllo di protezione per completare la pubblicazione del commento.
I caratteri digitati nel controllo di protezione devono corrispondere ai caratteri dell'immagine o della riproduzione audio.

Per aggiungere un commento, accedi con il tuo Windows Live ID (se utilizzi Hotmail, Messenger o Xbox LIVE possiedi già un Windows Live ID). Accedi


Non hai ancora un Windows Live ID? Registrati

Riferimenti

L'URL di riferimento per questo intervento è:
http://everythingandnothing.spaces.live.com/blog/cns!759F0B3484575700!142.trak
Blog che fanno riferimento a questo intervento
  • Nessuno