Linking to functionality within a Silverlight application

When the ASP.NET 3.5 Extensions are released, it will be easier to manage the history of your Silverlight application, and provide working links to areas of functionality. Raj demonstrates a technique using the beta extensions from Silverlight. The technique uses the new addHistoryPoint method of Sys.Application. Yahoo has some code to support history control as well, but you’d need to wrap the functionality in a similar manner (and if you’re already using ASP.NET AJAX, you’re users may need to download even more Javascript just to use your Silverlight page … :) ).

Silverlight (and Flex), not for LOB applications?

Shawn suggests that line of business applications should not be written using Silverlight (and hence Flex), instead XBAPs (the run in browser Windows-only WPF solution).

IT shops, with trimmed budgets and staff, have little time to maintain workstations and troubleshoot interactions between various installed applications. Web applications, although they may be more challenging to write (regardless of adding complex RIA components), offer long term benefits for maintenance, updates, deployment and access control that are extremely difficult to emulate using any modern non-browser based alternative, from terminal servers to WPF XBAPs.

And no, I am not a firm believer that a user’s needs should be weighed less than the needs of the IT shop. By no means. But, I do believe that the experience and requirements of many LOB applications can be readily constructed within a web application. Moreover, many LOB applications should be web applications for portability and access reasons.

Not all LOB applications will benefit from the added functionality that would be provided by a Silverlight or Flash/Flex component. In fact, many users may needlessly suffer from a nearly pointless developer adventure into new technology explorations rather than being able to perform the same business task more efficiently with what might amount to a fully functional, yet boring LOB application.

Many applications would benefit however from added pizzazz. There’s plenty of power brewing within the likes of Silverlight and Flex. It needs to be measured and watched carefully, so that it doesn’t explode into the user’s face though.

I do take issue with suggesting that an XBAP is typically a better solution. WPF, even in its most reduced form, is heavy weight and complex. Being browser embedded, it also won’t behave like either a browser application or a fully installed application. So, neither experience is fully replicated for the end user. Eventually, it may be that every desktop has the latest and greatest version of the .NET runtime to allow for the latest XBAPs to work — but there in lies more of the problem — the desktop is making a key decision into whether an application may run, rather than the deploying machine. That stinks — and fewer companies and users want that.

Yes, a WPF application using XBAPs may, at some levels, be easier to write. It depends a lot on the developer and on the application needed. Many WPF applications are as hard to write as a web application (if not harder). It’s give and take. Some features are easier in one and vice versa. RIA code using Silverlight is easier to create than their WPF counterparts in some ways, only because the functionality is limited (and a few things simplified). But, throw in the web mix for interaction, and I agree that the complexities increase.

So much depends on the expectations and needs of the user. If you want a web experience with pages, etc., I wouldn’t consider an XBAP to be an ideal choice. If it’s mostly a single page where much of the interaction occurs, I could see XBAPs being a reasonable choice for Windows-only organizations. But, those must consider deployment, hardware requirements, .NET versioning, and more.

(Don’t get me wrong, I really like WPF — just installed as a traditional application. :) ).

Oh — and more importantly — don’t switch to either technology if there’s no business reason (sorry developers).

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!

Oh Silverlight 2.0 Console (Example and Source code)

Try it here.

See a picture below: :)

image

I ran into one odd issue when using Silverlight 2.0 Beta 2….

 

void Page_Loaded(object sender, RoutedEventArgs e)
{
    // if you don't invoke this asynchronously, you can't add at load time
    // (seems to be a silverlight 2.0 issue)
    Dispatcher.BeginInvoke(delegate()
    {
        consoleCanvas.AnimateNewChild(
                string.Format("Welcome!\nThe time right now is {0}.", 
                DateTime.Now.ToLocalTime()));
        consoleCanvas.AnimateNewChild("\n\nThanks for stopping by WiredPrairie.us. "
            + "This code may be used for only for good, not evil.");
    });
}

The code above for example — if the ConsoleCanvas control (the simple base control for the console output-like functionality) performs any UpdateLayout calls within the context of the Load event, the entire Silverlight control fails to load, silently. The only work-around I could discover was to delay what I wanted to occur by using the asynchronous invoke method on the Dispatcher object.

The easiest way to force a UIElement to measure itself is to use a little trick:

_holder.Width = this.ActualWidth;
child.Width = this.ActualWidth;
_holder.Children.Add(child);
_holder.UpdateLayout();

child.Width = child.DesiredSize.Width;
child.Height = child.DesiredSize.Height;

The console control maintains a visible (yet opacity is set to 0.0) StackPanel. Each time a new line of output is added via the AnimateNewChild method, it’s placed in the StackPanel. The StackPanel (_holder) is resized to fit the current width of the entire ConsoleCavnas control. Next, the UpdateLayout method of the StackPanel is called — which forces an immediate measure of all of the children (and thus forces any children of the TextPanel to potentially re-layout). The width and height are grabbed and permanently stored as the new size for the child.

Instead of using Storyboards and various animations, I found it more convenient to simply use a DispatchTimer and do the animations manually.

List<FrameworkElement> remove = new List<FrameworkElement>();

for(int childIndex = 0; childIndex < this.Children.Count; childIndex++)
{
    FrameworkElement child = this.Children[childIndex] as FrameworkElement;

    if (!child.Equals(_holder))
    {                        
        child.Arrange(new Rect(0, Canvas.GetTop(child), 0, 0));
        double top = Canvas.GetTop(child);
        top -= 2.0;
        Canvas.SetTop(child, top);                       
        if (top + child.Height < 0.0)
        {
            remove.Add(child);
        }
    }    
}
// remove them all 
remove.ForEach(fe => this.Children.Remove(fe as UIElement));                

If an element is no longer visible on the screen, it is removed using a Lambda expression and the ForEach method on the List object instance named remove (after the repositioning loop has completed).

Download source, etc. here.