Silverlight Weather Demonstration

Demonstration available here. (You’ll need to wait for a moment while it loads the first time).

Stale Weather data for Madison Wisconsin as this isn't live ...

I’ve created a reasonably simple, yet multi-technology (and discipline) demonstration using Silverlight for the user interface and ASP.NET as the back-end. The demonstration uses:

  • Silverlight 2.0
  • Data binding
  • Delayed downloading of images
  • “Web services”
  • ASP.NET
  • LINQ
  • Extension Methods
  • Value Converters
  • XAML
  • IHttpAsyncHandler
  • HttpWebRequest
  • Google’s Weather Web Service (more of a weather XML end point)
  • and more!!! :)

 

The Silverlight Weather control is a UserControl with a few bound TextBlocks, an Image, and a few rectangles, and the data input controls.

<TextBlock Margin="8,10,0,10" 
           Grid.ColumnSpan="2" 
           Grid.Row="5" 
           Text="{Binding CurrentWeather.Location}" 
           TextWrapping="Wrap" 
           x:Name="txtLocation1" 
           VerticalAlignment="Center" 
           Foreground="#FFF1F1F1" 
           FontSize="12" 
           FontWeight="Normal"/>

 

Standard Silverlight data binding … grabbing the CurrentWeather’s Location property (which in the example in the screen shot is Madison, WI). What I continue to find is that Silverlight does not allow data binding to the UserControl itself — which is a common trick I use in WPF all the time. I don’t know if it’s a bug or a feature though. So, instead of simple code like this:

public Page()
{
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
}

void Page_Loaded(object sender, RoutedEventArgs e)
{
    this.DataContext = this;
}

I’m forced to rely on a secondary layer for binding:

public Page()
{
    InitializeComponent();
    this.Loaded += newRoutedEventHandler(Page_Loaded);
    _binding = newPageDataBinding();
}

void Page_Loaded(objectsender, RoutedEventArgs e)
{
    LayoutRoot.DataContext = _binding;
}

This technique adds what isn’t a terribly useful layer for a project like this, but it shouldn’t be necessary. To work around the issue, I created a new class, called PageDataBinding which has the few properties I wanted to expose to the user interface:

public class PageDataBinding: INotifyPropertyChanged
{
    private WeatherCondition _currentWeather;

    public WeatherCondition CurrentWeather
    {
        get { return _currentWeather; }
        set
        {
            if (_currentWeather != value)
            {
                _currentWeather = value;
                RaisePropertyChanged("CurrentWeather");
            }
        }
    }

    #region INotifyPropertyChanged Members

    protected void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion
    
}

Above you should see the CurrentWeather property which I’ve used in the Silverlight UI. In addition to the typical button for activating the control, I’ve made the Enter key also trigger the weather fetching.

private void txtLocation_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
    {
        FetchWeather();
        e.Handled = true;
    }
}

Above, after (starting the process of) fetching the weather, I’ve set the Handled property to true, which indicates no further processing of the key is needed.

I wrapped the basic request to Google into a class called Weather. It’s the job of the Weather class to package the request and submit it to my exposed web service. Google’s web servers do not have any of the cross domain permission files needed by Silverlight for it to directly access them; so, I created a proxy web service just for the purpose.

Using the HttpWebRequest object is quite simple:

public void Download()
{
    try
    {
        HttpWebRequest request = WebRequest.Create(
            new Uri(Application.Current.Host.Source,
                string.Format(
                "../GetWeather.ashx?location={0}", _location))) as HttpWebRequest;
        request.Method = "GET";
        this._responseResult = 
            request.BeginGetResponse(new AsyncCallback(RequestCallback), request);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
    }
}

I’ve just made the request to my web server and passed the location parameter directly. If it’s gibberish, Google just returns an error, so I don’t bother with any special processing (and in the end, it’s just a demonstration).

When the response is received, the RequestCallback method is called:

private void RequestCallback(IAsyncResult result)
{
    HttpWebRequest request = result.AsyncState as HttpWebRequest;

    HttpWebResponse response = (HttpWebResponse) request.EndGetResponse(result);

    Stream s = response.GetResponseStream();

    StreamReader reader = new StreamReader(s);
    string webResult = reader.ReadToEnd();
    Debug.WriteLine(webResult);
    reader.Close();
    response.Close();

    if (WeatherDataDownloaded != null)
    {
        WeatherDataDownloaded(this, new WeatherDataDownloadedEventArgs(webResult));
    }
}

Not necessarily production ready code, but it works for this demo. The result is returned, and gathered into a string. Once complete, the Weather class raises an event with the result to the host user interface.

I won’t replicate all the code here, but once the user interface event code is called, it begins to parse the XML data:

void WeatherDataDownloaded(object sender, WeatherDataDownloadedEventArgs e)
{
    XElement xd = XElement.Parse( e.Data, LoadOptions.None);

    var conditions = from current_condition in xd.Descendants("current_conditions")
                     select new WeatherCondition()
                     {
                         Condition = current_condition.GetChildElementAttributeValue("condition", "data"),
                         TemperatureF = current_condition.GetChildElementAttributeValue("temp_f", "data"),
                         TemperatureC = current_condition.GetChildElementAttributeValue("temp_c", "data"),
                         Humidity = current_condition.GetChildElementAttributeValue("humidity", "data"),
                         IconPath = current_condition.GetChildElementAttributeValue("icon", "data"),
                         Wind = current_condition.GetChildElementAttributeValue("wind_condition", "data")
                     };

As you can see, I use LINQ and an extension method I frequently use to obtain a named attribute from an element. The extension method, GetChildElementAttributeValue doesn’t do much really:

public static string GetChildElementAttributeValue(
    this XElement element, XName elementName, XName attributeName)
{
    XElement subElement = element.Element(elementName);
    if (subElement != null)
    {
        XAttribute attr = subElement.Attribute(attributeName);
        if (attr != null)
        {
            return attr.Value;
        }
    }

    return null;
}

The primary function (of this function!) is to protect against null elements when trying to read an attribute value.

The attribute values are stored in a WeatherCondition object. Once the object has been created, and a little more work is done, the code must notify the UI’s bound objects.

WAIT! If the notification happens now (on the currently executing thread), the Silverlight runtime will absolutely throw an exception, just like WPF would. The current executing thread is not the user interface thread, so I’ve used the Dispatcher to execute the update on the user interface thread:

Dispatcher.BeginInvoke(delegate()
{
    _binding.CurrentWeather = conditions.First<WeatherCondition>();
});

The remainder of the Silverlight code is mostly basic plumbing and user interface manipulation. There should be more error trapping than there is … but, I’m leaving that for another day (or for you!).

Rather than introducing a true web service into the project, I instead created an IHttpAsyncHandler in ASP.NET. This way, the handler could efficiently make requests of the Google Weather API and return them, without blocking further requests. In an ideal world, I’d add caching and more error handling ….

Here’s nearly the entire handler — as I think it’s useful to demonstrate a live, working usage of an ASP.NET async HttpHandler. If you’re not familiar with IHttpAsyncHandler, and you’re writing HttpHandlers, … get familiar with it if you care about scalability/performance.

public IAsyncResult BeginProcessRequest(HttpContext context,
AsyncCallback cb, object extraData) { _context = context; HttpWebRequest request = WebRequest.Create( new Uri( string.Format( http://www.google.com/ig/api?weather={0},
context.Request.QueryString["location"])) ) as HttpWebRequest; _completedCallback = cb; return request.BeginGetResponse(
new AsyncCallback(GetResponseCallback), request); } private void GetResponseCallback(IAsyncResult result) { Debug.WriteLine("GetResponseCallback"); HttpWebRequest request = result.AsyncState as HttpWebRequest; HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse; Stream s = response.GetResponseStream(); StreamReader reader = new StreamReader(s); string responseOutput = reader.ReadToEnd(); reader.Close(); response.Close(); _context.Response.ContentType = "text/xml"; _context.Response.Write(responseOutput); _completedCallback.Invoke(result); } public void EndProcessRequest(IAsyncResult result) { _context.Response.End(); }

I’ve also created an IValueConverter to take the path to an image and convert it to a bitmap image.

BitmapImage bi = new BitmapImage();
bi.UriSource = new Uri("/Images/" + path, UriKind.Relative);  

The images are stored on the web server rather than being embedded directly into the XAP file. Note that for this to work, the images must be stored in the ClientBin/Images folder, … not the /Images folder of the web server (as the path is relative to the location of the Silverlight download, not to the web application).

One issue that I ran into that had me puzzled for a while. AG_E_NETWORK_ERROR. This error occurred when trying to directly download the images Google uses for weather icons. That error had me baffled, until I realized that Silverlight cannot download images from other domains, unless they have the cross domain XML files properly configured for remote access. Google, sadly, does not have them, so I created my own images using Expression Design. The original file can be found in the download if you’re interested (as a series of layers).

Download source code for entire project (including graphics) here.

image image image

Enjoy!

2 Comments

  1. {System.Security.SecurityException: Security error.
    at System.Net.Browser.BrowserHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
    at System.Net.Browser.BrowserHttpWebRequest.c__DisplayClass5.b__4(Object sendState)
    at System.Net.Browser.AsyncHelper.c__DisplayClass2.b__0(Object sendState)}

    getting an error. pls suggest me how to resolve

    1. How are you hosting this?

      You’ll either need to host it and have the GetWeather.ashx on the same domain or use a cross domain file to access the data.

Comments are closed.