Demonstration available
here.
(You’ll need to wait for a moment while it loads the first time).
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.
Enjoy!