Weather information is popular to display both on the desktop and on the Web. Many applications and widgets are solely devoted to retrieving and displaying this information. Naturally, Ajax is well suited for this type of widget.
The first step in creating this widget is locating a source of weather information. Probably the most popular is the Weather.com XML weather service. It is this information that you will use to create this weather widget.
The use of the Weather.com XML service hinges upon following their guidelines. To use their XML feeds you must first register for a license. To register, go to http://registration.weather.com/registration/xmloap/step1/. After your registration, Weather.com will send you an e-mail with a link to the XML feed SDK and provide you with your license key and partner ID.
For web-based applications, like this widget, you must limit how often you retrieve information from the service. As specified in the SDK documentation, the refresh rate for the Current Conditions information is 30 minutes; therefore, the server application must cache the retrieved weather information and only refresh the information every 30 minutes. There are two ways to accomplish this:
Create a smart thread that runs independently of the web application and pulls the feed every 30 minutes. The application then solely uses the cached feed and never worries about the time span between information pulls.
With every page request, the application can keep track of the last time the feed was retrieved, and allow refreshing of the data only after 30 minutes have passed.
Both architectures will serve this widget's purpose. In fact, the smart thread solution is ideal. Communication between the server and the remote host are the same in both situations; however, the smart thread requires only one file system operation every half hour (writing the cached feed). Unfortunately, ASP.NET applications time out after 20 minutes of inactivity, which can result in old cached data. It is possible to change this behavior; however, it requires access to the machine.config file, and not everyone has access to that configuration file in their environment. Therefore, this widget will use the second solution.
You can use any .NET-enabled language to write the back-end code; however, the examples in this chapter use the C# language. At the heart of the server application lie two classes created within the Wrox.ProfessionalAjax.Weather namespace. You can compile these classes into a class library (a .dll file) or use them natively within the application. The choice is ultimately yours; however, this exercise incorporates the classes directly into the application, so no reference to an external library is needed.
The WeatherSettings class contains all the information required to pull weather information from Weather.com. Only three pieces of information are required to retrieve this data: your license key, your partner ID, and the location ID. These settings are located in an external XML document called config.xml whose structure looks like this:
<weather> <ids> <license>[LICENSEKEY]</license> <partner>[PARTNERID]</partner> <location>[LOCATION CODE]</location> </ids> </weather>
This is a simple XML document, as you can see. The <ids/> element contains the information required to retrieve weather information. The <license/>, <partner/>, and <location/> elements contain the license key, partner ID, and location ID, respectively.
Note |
Note that you need to replace the bracketed items with your own information. |
The WeatherSettings class extracts this information and assigns it to private variables. The class's constructor takes one argument: the path to the application.
public Settings(string path) { XmlDocument xml = new XmlDocument(); xml.Load(path + "/config.xml"); _partnerId = xml.SelectSingleNode("/weather/ids/partner").InnerText; _licenseKey = xml.SelectSingleNode("/weather/ids/license").InnerText; _location = xml.SelectSingleNode("/weather/ids/location").InnerText; }
All data processing of the WeatherSettings class takes place here in the constructor. First, the config.xml file is loaded into an XmlDocument object. This enables you to traverse the DOM tree and extract the needed information. If you use the SelectSingleNode() method, the individual elements contained in the <id/> element are retrieved and their value obtained with the InnerText property. This information is assigned to the private string fields _partnerId, _licenseKey, and _location.
To access these private fields, accessors (which you learned about in Chapter 5) are used to retrieve the value of the corresponding private field:
public string PartnerId { get { return _partnerId; } } public string LicenseKey { get { return _licenseKey; } } public string LocationId { get { return _location; } }
The public fields ParnerId, LicenseKey, and LocationId return the values of _partnerId, _licenseKey, and _location, respectively. These accessors provide read-only access to the private fields.
The WeatherInfo class provides methods to retrieve the information from the Weather.com XML service. Like the WeatherSettings class, the WeatherInfo constructor accepts one argument that contains the path to the application:
public WeatherInfo(string path) { _path = path; _cachedFile = String.Format("{0}/weather_cache.xml",_path); _settings = new Settings(path); }
This class has three private fields, two of which are the strings _path and _cachedFile. The former is assigned the path argument, and the latter contains the path to the cached weather feed. The third private field, _settings, is a WeatherSettings object.
Aside from the aforementioned private members, the WeatherInfo class also contains several private methods. One such method, called _getWebWeather(), gets the weather feed from the remote host and returns a string containing the feed's contents:
According to the SDK, the URL to retrieve the weather feed looks like the following:
The information contained in brackets is the location ID, partner ID, and license key. Using the String.Format() method, you can format the URL to contain your own settings information:
string baseUrl = "http://xoap.weather.com/weather/local/{0}?cc=*&prod=xoap&par={1}&key={2}"; string url = String.Format(baseUrl, _settings.LocationId, _settings.PartnerId, _settings.LicenseKey);
This is primarily where the _settings object is used. The resulting string returned from String.Format() is complete with the required information that the Weather.com guidelines dictate.
The next operation in _getWebWeather() makes a request to the remote host and retrieves the weather feed:
using (WebClient client = new WebClient()) { //Read the results try { using (StreamReader reader = new StreamReader(client.OpenRead(url))) { xmlStr = reader.ReadToEnd(); } } catch (WebException exception) { xmlStr = _writeErrorDoc(exception); } }
At the beginning of this code, a WebClient object is created. If you'll remember from Chapter 5, you used an HttpWebRequest object to retrieve data from the remote host. You could do the same for this widget, but as you can see from the previous code, WebClient uses a much simpler interface.
To read the remote server's response, use the OpenRead() method. This method returns a Stream object that you can read with a StreamReader class. The StreamReader class's ReadToEnd() method reads the stream and returns it as a string to xmlStr. If for some reason this operation fails (most likely as a result of not finding the remote host), an error document is created by the _writeErrorDoc()and used in place of the weather feed.
At this point in the method, you have some XML data to use: the weather feed or the error document. The next few processes in the method perform an XSL transformation on the XML data. Transforming the data at the server is advantageous for several reasons. For one, it greatly simplifies the client-side code. The data sent to the client is already in HTML, so it is easily added to the page. A server-side transformation also makes the client work less. The data is complete when it reaches the client; no other data manipulation, other than placing in the page, is required.
XSL transformations in .NET closely resemble the transformations provided by MSXML covered in Chapter 4. The first step in a transformation is to create the objects involved:
XslTransform xslt = new XslTransform(); XmlDocument xml = new XmlDocument(); using (StringWriter stringWriter = new StringWriter()) {
Transformations in .NET include an XslTransform object, an XmlDocument object, and a StringWriter object, which contains the resulting transformed data. The next step in the transformation process is to load the XML data into the XmlDocument object and the XSL document into the XslTransform object:
xml.LoadXml(xmlStr); xslt.Load(_path + "/weather.xslt");
The LoadXml() method of the XmlDocument class resembles the loadXML() method in the MSXML library. It accepts a string argument representing the XML data. The XSL document is loaded into the XslTransform object via the Load() method. This method takes one argument: a string containing the path to the XSL document.
The final step is to perform the transformation and retrieve the resulting data from the StringWriter:
xslt.Transform(xml, null, stringWriter, null); xmlStr = stringWriter.ToString();
The Transform() method has several overloads. Overloaded methods are methods of the same name that perform the same function, but which accept different arguments. This particular overload accepts the XML document in the first position, an argument list (or parameter list) to pass to the XSLT processor in the second position, the StringWriter to serve as output in the third position, and an XmlResolver to handle any XML namespaces in the last position. Once the transformation is complete, you can use the ToString() method to obtain the transformed data from the StringWriter.
The final step in _getWebWeather() is to cache the data. This is done with a StreamWriter, which writes the data in UTF-8 encoding:
using (StreamWriter writer = new StreamWriter(_cachedFile)) { writer.Write(xmlStr); }
The StreamWriter class constructor accepts a string argument containing the path to the file to write to. The StreamWriter writes the contents of xmlStr to the file and is promptly closed and disposed of when the using block exits.
Note |
In order for the application to update the contents of weather_cache.xml, ASP.NET must have the proper modify permissions for the file. |
The completed code of _getWebWeather() is as follows:
private string _getWebWeather() { //Just to keep things clean, an unformatted URL: string baseUrl = "http://xoap.weather.com/weather/local/{0}?cc=*&prod=xoap&par={1}&key={2}"; //Now format the url with the needed information string url = String.Format(baseUrl, _settings.LocationId, _settings.PartnerId, _settings.LicenseKey); //String that the weather feed will be written to string xmlStr = String.Empty; //Use a web client. It's less coding than an HttpWebRequest. using (WebClient client = new WebClient()) { //Read the results try { using (StreamReader reader = new StreamReader(client.OpenRead(url))) { xmlStr = reader.ReadToEnd(); } } catch (WebException exception) { xmlStr = _writeErrorDoc(exception); } } XslTransform xslt = new XslTransform(); XmlDocument xml = new XmlDocument(); using (StringWriter sringWriter = new StringWriter()) { xml.LoadXml(xmlStr); xslt.Load(_path + "/weather.xslt"); xslt.Transform(xml,null,sringWriter,null); xmlStr = sringWriter.ToString(); } //Write the cached file using (StreamWriter writer = new StreamWriter(_cachedFile)) { writer.Write(xmlStr); } //Finally, return the feed data. return xmlStr; }
Another private method of the WeatherInfo class is the _getCachedWeather() method. As its name suggests, its function is to retrieve the data from the cached feed. Like the previously discussed method, _getCachedWeather() also returns a string:
private string _getCachedWeather() { string str = String.Empty; //Open and read the cached weather feed. using (StreamReader reader = new StreamReader(_cachedFile)) { str = reader.ReadToEnd(); } //Return the contents return str; }
First, the variable str is created and initialized as an empty string; this variable will contain the contents of the cached file when it is read. Next, a StreamReader object is created and opens the cached weather feed, the contents of which are read via the ReadToEnd() method and stored in str. Finally, _getCachedWeather() exits and returns the str value.
The _getWebWeather() and _getCachedWeather() methods are the primary workhorses of the application. However, as you've probably already noted, they're private members. In other words, there is no way to access them externally when you create an instance of this class. A public method, GetWeather(), provides makeshift access to these private methods.
The GetWeather() method is a public method that returns a string retrieved from either _getCachedWeather() or _getWebWeather(). This method determines whether to pull the feed from the Web or from the cache. This decision is based on the time that the cached file was last modified.
The .NET Framework provides a structure called DateTime, which is used extensively throughout the Framework. When a date or time is needed, chances are a DateTime instance is available. Such is the case with getting dates and times associated with a file. To retrieve the "Date Modified" file system property of the cached file, write a public accessor called LastModified as follows:
public DateTime LastModified { get { if ((File.Exists(_cachedFile))) { return File.GetLastWriteTime(_cachedFile); } else { return new DateTime(1,1,1); } } }
This code snippet gets the date and time of when the cached file was last written to. First, you must check the existence of the cached file, or else an error is thrown when you try to retrieve the DateTime information. If it exists, the GetLastWriteTime() method retrieves the date and time of when it was last written to. If the file does not exist, a DateTime instance is created using the earliest possible values by passing the value of 1 for the year, month, and day. This ensures that the application will always pull a new feed if the cached file does not exist.
The GetWeather() method uses this information to decide whether to pull a newer feed:
DateTime timeLimit = LastModified.AddMinutes(30);
Using the AddMinutes() method, 30 minutes is added to the time LastModified returns. This new DateTime instance, timeLimit, will be compared to the current time by using the CompareTo() method:
if (DateTime.Now.CompareTo(timeLimit) > -1) { return _getWebWeather(); } else { return _getCachedWeather(); }
The CompareTo() method returns an integer value. If the current time (specified by DateTime.Now) is greater than timeLimit, the returned integer is greater than zero. If the two times are equal, the method returns zero. If the current time is less than timeLimit, then -1 is returned. In this code, the retrieval of a newer feed occurs only when at least 30 minutes have passed; otherwise, the cached version is retrieved.
One final method of the WeatherInfo class need mentioning: _writeErrorDoc(). As stated before, this method writes a simple error document in XML to be used in the event the remote host cannot be contacted. It accepts one argument, a WebException instance:
private string _writeErrorDoc(WebException exception) { XmlDocument xml = new XmlDocument(); xml.LoadXml("<errorDoc />"); XmlElement alertElement = xml.CreateElement("alert"); alertElement.InnerText = "An Error Occurred!"; XmlElement messageElement = xml.CreateElement("message"); messageElement.InnerText = exception.Message; xml.DocumentElement.AppendChild(alertElement); xml.DocumentElement.AppendChild(messageElement); return xml.OuterXml; }
In the first two lines of code, an XmlDocument object is created and loaded with a simple XML document (only the root element). The error document contains only two elements: <alert/> and <message/>. The DOM methods in .NET resemble the DOM methods in Internet Explorer and Firefox. The first element, <alert/>, is created with the CreateElement() method. Using the InnerText property, it receives its text node. The same procedure is used to create the <message/> element. This element will house the message from the WebException object.
When all elements are created and populated with their needed data, they are appended to the document via the AppendChild() method, and the serialized XML data, output by the OuterXml property, is returned.
To finish off the class, two accessors grant access to the _settings and _cachedFile private members:
public Settings Settings { get { return _settings; } } public string CachedFile { get { return _cachedFile; } }
This will ensure that you can access this data outside of the class, as necessary.
The majority of the work is completed with the server-side application. All that remains is to put the WeatherInfo class to work. The ASP.NET file weather.aspx serves as the proxy between the client and the Weather.com XML service. It is in this page that you will use the WeatherInfo class.
The first step in implementing this class is to create an instance of it:
WeatherInfo weather = new WeatherInfo(Server.MapPath(String.Empty)); string weatherData = weather.GetWeather();
In this code, an instance of the WeatherInfo class is created by passing the path to the application to the constructor with Server.MapPath(Sring.Empty). With this new instance, you can use the GetWeather() method and retrieve the weather information, which is stored in weatherData.
The next step is to set the needed headers:
Response.ContentType = "text/xml"; Response.CacheControl = "no-cache"; Response.AddHeader("Weather-Modified", weather.LastModified.ToFileTime().ToString());
The first two lines should look familiar. They set the MIME content type to text/xml and tell the browser not to cache the feed. The third line, however, shows a custom HTTP header called Weather-Modified. This header provides the client-side code a means to check if a new version of weather data is available.
The value assigned to this header consists of a long integer (returned by the ToFileTime() method) representing the time returned by the LastModified property. Because this value is based on the Modified attribute of the file system (remember the LastModified property of the WeatherInfo class returns that value), it will remain the same until the file is updated. This makes it ideal for checking for new updates on the client-side.
Finally, output the weather data, as follows:
Response.Write(weatherData);
You now need a client to consume the information.
The client code for this widget is surprisingly simple thanks to all the work the server application performs. All that remains for the client is to retrieve the data. The AjaxWeatherWidget class does just that:
function AjaxWeatherWidget(oElement) { this.element = (oElement)?oElement:document.body; this.lastModified = null; this.getWeather(); }
The AjaxWeatherWidget constructor accepts one argument: the HTMLElement to append the data, which is assigned to the element property. In the event that no argument is supplied, element becomes document.body. As in the News Ticker widget example, this class also automatically checks for updates, which is why the lastModified property exists. The constructor calls getWeather(), a method that retrieves the weather data from the server, before it exits.
The getWeather() method contacts the server application and retrieves the weather information with XMLHttp. It also polls the server every minute for updated information:
AjaxWeatherWidget.prototype.getWeather = function () { var oThis = this; var doTimeout = function () { oThis.getWeather(); };
The method starts by creating a pointer to the object by assigning oThis. This pointer is then used inside the nested function doTimeout(), which calls the getWeather() method. The doTimeout() function is used to make continuous updates.
Next, an XMLHttp object makes a request to the server:
AjaxWeatherWidget.prototype.getWeather = function () { var oThis = this; var doTimeout = function () { oThis.getWeather(); }; var oReq = zXmlHttp.createRequest(); oReq.onreadystatechange = function () { if (oReq.readyState == 4) { if (oReq.status == 200) { var lastModified = oReq.getResponseHeader("Weather-Modified"); if (lastModified != oThis.lastModified) { oThis.lastModified = lastModified; oThis.element.innerHTML = oReq.responseText; } } } }; oReq.open("GET", "weather.aspx", true); oReq.send(null);
When the request is successful, the first item retrieved from the server's response is the Weather- Modified header with the getResponseHeader() method. To see if the requested data is newer than the preexisting data, compare the value of the Weather-Modified header to that of the lastModifed property of the AjaxWeatherWidget object. If they match, the data is the same; if not, then updated data exists and is appended to the chosen HTMLElement via the innerHTML property.
Finally, set a timeout for the doTimeout() function, which executes every minute:
Out of the box, this widget fits nicely into a sidebar, providing visitors with the weather information you dictate. The look of the widget relies upon custom images as well as the weather images provided in the SDK (see Figure 8-2).
Giving the widget the look and feel in the example files relies heavily upon CSS positioning; nearly every element is absolutely positioned, so the HTML structure isn't extremely important. All you need is valid (X)HTML:
<div id="weatherContainer"> <div id="weatherIcon"><img src="img/weather/32.png" /></div> <div id="weatherTemp">70</div> <div id="weatherLocation">Dallas, TX (75226)</div> <div id="weatherWind">Wind: <div>13 MPH S</div> </div> <div id="weatherTime">Last Update: <span>7:45 PM</span> </div> </div>