Sunday, May 3, 2009

Solving Silverlight's Annoying Image Control Error Code: 4001 Category: ImageError Message: AG_E_NETWORK_ERROR

If you've done anything with Silverlight's Image Control, you've certainly seen an error similar to this at some point:

The dreaded AG_E_NETWORK_ERROR from Silverlight's Image control (a.k.a. Error Code 4001, ImageError).

Basically, it means the image file you referenced in the Source attribute has some type of invalid Uri. The typical solution that most blog entries and forum posts offer is simply to fix your Uri reference. For more information on typical scenarios for setting the Image control's Source property, see Pete Brown's post on Path and File Resolution (it is still relevant even though it referenced Silverlight 2 Beta 1).

Who Wrote This Control, Anyway?

Sure, simply fixing the Uri may work for some, but what if you are accessing 3rd party feeds that are passing you invalid Url's or those images simply no longer exist? What if your application just can't gurantee that all the images you try to reference actually exist? It seems pretty poor user experience to popup a scripting error dialog--for each image that is missing! On top of that, the rest of the Silverlight page seems to stop processing!

The further I dug when trying to see what options there were for eliminating this problem, the worse it got:

  • There are no events on the Image control that get thrown when the download error occurs every time; you would think that not being able to find the image referenced would call the ImageError event, but when working with a data source of more than several images, it does NOT trap ALL of the errors! That exception seems to be only work reliably for problems creating a Bitmap from the ImageSource that is a valid, existing Uri...
  • You still see these errors when you are debugging, even though the Application_UnhandledException event handler explicitely checks and is only supposed to report the error to the DOM when a dubugger is not attached...
  • In fact, there is no place in the stack for you to handle these exceptions prior to it being thrown out to the JavaScript function onSilverlightException (it seems to come from the Bitmap class's downloading function and directly calls out to the hosting object tag's "onerror" param)--really!?!?!?
  • The Image control is a sealed class! I always get so frustrated when classes are sealed. Most of the time, it just prevents you from being able to tweak things in a way that works for you. Why do that, really? Given that the Image control can call cross-domain to get images without cross-domain security restrictions, I actually see a possible, valid reason...

The first bullet just ticks me off...really? No reliable events? ImageFailed seemed to work at first, but after a tiny bit of testing, it certainly was not good enough. The second one actually got me quite curious. Looking at the Application_UnhandledException code, we shouldn't be seeing any unhandled Silverlight Exceptions being thrown when a debugger is attached:

 public partial class App : Application
{

 public App()
 {
  this.Startup += this.Application_Startup;
  this.Exit += this.Application_Exit;
  this.UnhandledException += this.Application_UnhandledException;

  InitializeComponent();
 }

 private void Application_Startup(object sender, StartupEventArgs e)
 {
  this.RootVisual = new Page();
 }

 private void Application_Exit(object sender, EventArgs e)
 {

 }
 private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
 {
  // If the app is running outside of the debugger then report the exception using
  // the browser's exception mechanism. On IE this will display it a yellow alert
  // icon in the status bar and Firefox will display a script error.
  if (!System.Diagnostics.Debugger.IsAttached)
  {

   // NOTE: This will allow the application to continue running after an exception has been thrown
   // but not handled.
   // For production applications this error handling should be replaced with something that will
   // report the error to the website and stop the application.
   e.Handled = true;
   Deployment.Current.Dispatcher.BeginInvoke(delegate { ReportErrorToDOM(e); });
  }
 }
 private void ReportErrorToDOM(ApplicationUnhandledExceptionEventArgs e)
 {
  try
  {
   string errorMsg = e.ExceptionObject.Message + e.ExceptionObject.StackTrace;
   errorMsg = errorMsg.Replace('"', '\'').Replace("\r\n", @"\n");

   System.Windows.Browser.HtmlPage.Window.Eval("throw new Error(\"Unhandled Error in Silverlight 2 Application " + errorMsg + "\");");
  }
  catch (Exception)
  {
  }
 }
}
 

That made me realize that the Image (or Bitmap) control was somehow handling the exception and explicitly calling the onerror function that is set on the Silverlight control's param list (or via a similar property on the Silverlight ASP.NET control).

Ugh. So the Image control throws an uncatchable exception directly out to JavaScript, does not publish any events you can subscribe to that can help you gracefully handle this very typical situation, and the class is sealed so there is no way for you to override this horrible user experience in a user experience-driven technology! WTF? Who wrote this control, anyway? Maybe I shouldn't know, considering how this all makes me feel ;-).

First, Get The JavaScript Errors to Stop

In the spirit of having a minimal shippable solution, we simply need to find a way to get the JavaScript errors to stop so that our users don't see them when they happen. This isn't necessarily the intended final solution, but will get something working that is acceptable.

I happen to use the object tag that the Silverlight test page gives you, rather than the ASP.NET Silverlight control. There are several reaons for this, including the fact that there is a problem with ViewState and the "ToolboxBitmapImage" for that control that will cause your Silverlight application to stop working until you reset IIS. It was introduced in ASP.NET 3.5 SP1 and Microsoft does not have a solution as of this posting. But I digress...

This first solution is the most basic that works in all circumstances, but it is a bit of a heavy-handed one: simply swallow ALL JavaScript errors. You can do this by simply adding this bit of JavaScript anywhere on your hosting page in your web project:


    

The First Small Refinement - May Be Harmful if Swallowed

You can certainly ship your Silverlight application at this point, so that is great. If you are working in Scrum or some other agile environment, now is a good time to check in to Continuous Integration to have a "worst-case scenario" point that contains potentially shippable software!

Now that you have something you can ship, you can (and should) spend the time to refine and refactor. "Why?" you ask? There are side-effects...

While this solution stops the annoying popups from the Image control, it also swallows JavaScript errors on that page! So certainly, this isn't the best solution, especially during development since you won't see any of your JavaScript errors or any unhandled Silverlight errors at all.

As a result, I recommend showing the errors during development, maybe even in your continuous integration environment, but suppressing them once in a pre-production or production environment. This can be accomplished using either a web.config setting that is read by your code-behind to decide whether your page should add this script block or not, or through conditional attributes on methods that decide whether to swallow the errors or not.

To accomplish the web.config setting per-environment, see my the reference to Scott Hanselman's article in my earlier post on Environment-Specific ServiceReferences.ClientConfig files. Simply create a control that outputs the JavaScript to define the window.onerror function (a Literal control that has its Visible property set based on a web.config setting would do fine. The literal itself would look something like this on the page markup, somewhere inside the head of the page:


    

If you'd like to keep that logic to the page without the overhead of a web.config, you can also use Conditionals. Again, you can create different configurations per environment, or simply differentiate between Debug and Release. First, set a compilation constant in the project Properties of the web project:

In the above example, I used SHOW_SILVERLIGHT_ERRORS as the compilation constant (although, strictly speaking, it's really a flag dealing with all JavaScript errors, not just Silverlight; more on that later). When set, we hide the Literal that outputs the script that swallows the errors. I specifically set Visible to true on the control in the markup so that we swallow the errors unless otherwise specified (would be bad to accidentally go to production without swallowing the errors).

Then, I updated the code-behind of the page to add a conditional onto a method so that the Literal control's visibility is set to false only when we explicitely say we want to show these errors. The logic may seem a little backwards at first, but the goal is to default to swallowing the errors and to only show the errors when we explicitely say that we want to see them:

using System;
using System.Diagnostics;
 
namespace ImageControlFun.Web
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            //Show JavaScript errors (if we are supposed to)
            ShowJavaScriptErrors();
        }
 
        [Conditional("SHOW_SILVERLIGHT_ERRORS")]
        private void ShowJavaScriptErrors()
        {
            SwallowJSErrors.Visible = false;
        }
    }
}
    

With the code above, the ShowJavaScriptErrors method call is only inserted into the MSIL when the compilation constant is defined. Otherwise, there is no function call to turn off the visibility of the Literal.

Good Enough For Now

With either change described above, this is again a good place to check-in your code as a release candidate. You can now control the swallowing of all JavaScript errors depending on the environment using Project Configurations or configuration settings. This definitely can be harmful as it also hides all JavaScript errors on your host page, so we definitely want to continue iterating on this problem. But for now, this will allow you to ship something that can add value.

Some other options for refinement of swallowing JavaScript errors in the future include:

  • Instead of throwing the JavaScript error at the end of onSilverlightException, simply do nothing
  • Instead of throwing the JavaScript error, call an HTTP Handler or Web Service that is hosted in the same site (or on a site that you have a cross-domain exception with) to log all JavaScript errors; great for getting real metrics about what scripting issues users are seeing (Silverlight induced or otherwise)!
  • If you like some combination of options, you could change the onerror param on the object tag (using web.config or conditional compilation) to call the default onSilverlightException locally and in your integration environment and call a different function that does logging in testing, pre-production and production.

Now That the Errors Are Gone...

...you are still left with an experience that leaves something to be desired. At least the user experience is not in-yo-face horrible anymore, but this is Silverlight. It's supposed to be clean, smooth, and high-quality. You get no image when there is an error loading from an invalid source url. There is no "loading" or "default" image that is shown prior to the real image being loaded over the network or in an error condition. Even worse, there doesn't appear to be any events, hooks, or override points to be able to respond to these things! I'm sure these issues all came as ways to ensure there weren't any security holes, but sheesh!

Stay tuned for more iterative refinements on the user experience of the Image control (Behaviors and more)! For now, we've got something working.

In the meantime, I'd love to hear any other solutions folks have come up with!