I’ve struggle for a while now trying to come up with a good way to handle app crashes in my Windows Phone applications.
The problem I have is that in the Application_Unhandled
event handler, there’s really no good way to pop up a task (i.e.: an email compose task) to send off debugging information.
There are a number of ways that I’ve seen various people get around this, and I’ve tried most of these. One that I have in one of my current apps is the method of Navigating to an ErrorPage, populating the screen, and providing the user a way to send that data. It’s basically a faux-messagebox scenario. It’s not bad, but it just seems clumsy to me. After they report, what do you do? auto-navigate back? exit the app (by force-crashing?) hmm.
Today I decided to do some more spelunking, starting out at windowsphonegeek.com for any posts they’ve done related to it. This led me to a gem of a post on MSDN by Andy Pennell. He provided a snippet of code dubbed LittleWatson (homage to Windows proper’s Dr. Watson) that works like this:
- On app crash, call LittleWatson.ReportException()
- Little Watson takes the exception detail, and writes it to a .txt file in isolated storage
- Let your app continue crashing and exit out
- On the load of the main page for your app (or any page you deep link to with a live tile), make a call to LittleWatson.CheckForPreviousException() in the constructor of the page
- LittleWatson hits isolated storage, checks for the file. If found, reads the contents, then pops a MessageBox for the user. If user says “OK”, an EmailComposeTask is fired off
Pretty slick if you ask me. The one thing I don’t like is that in order to get the exception detail from the user, they’ve got to re-launch your app after a crash. Now granted, any time an app’s crashed on me I usually try at least one more time, but what if you get a particularly impatient user who just throws it away upon the first crash? About the only thing that helps you there is Microsoft’s own automatic crash collection data which users have to opt in to, and I’ve personally found less than helpful to be honest.
So anyway, I took Andy’s code and added a few pieces of flair:
public class LittleWatson { const string filename = "LittleWatson.txt"; public static void ReportException(Exception ex, string extra = null) { try { using (var store = IsolatedStorageFile.GetUserStoreForApplication()) using (var output = new StreamWriter(store.OpenFile(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))) { if (!string.IsNullOrWhiteSpace(extra)) { output.WriteLine(extra); output.WriteLine(); } output.WriteLine(ex.ToString()); output.WriteLine(); output.WriteLine(Reporting.GetDebugInfo()); } } catch (Exception) { } } public static void CheckForPreviousException() { try { string contents = null; using (var store = IsolatedStorageFile.GetUserStoreForApplication()) { if (store.FileExists(filename)) { using (TextReader reader = new StreamReader(store.OpenFile(filename, FileMode.Open, FileAccess.Read, FileShare.None))) { contents = reader.ReadToEnd(); } SafeDeleteFile(store); } } if (contents != null) { var appName = Application.Current.GetAttribute<AssemblyTitleAttribute>(a => a.Title); if (MessageBoxResult.OK == MessageBox.Show(string.Format(ResourceStrings.BugReport_PromptMessage, appName), ResourceStrings.BugReport_PromptTitle, MessageBoxButton.OKCancel)) { var email = new EmailComposeTask { To = Application.Current.GetAttribute<AssemblySupportEmailAddressAttribute>(a => a.Address), Subject = string.Concat(appName, " error report"), Body = contents, }; SafeDeleteFile(IsolatedStorageFile.GetUserStoreForApplication()); // line added 1/15/2011 email.Show(); } } } catch (Exception) { } finally { SafeDeleteFile(IsolatedStorageFile.GetUserStoreForApplication()); } } private static void SafeDeleteFile(IsolatedStorageFile store) { try { store.DeleteFile(filename); } catch (Exception ex) { } } }
Most notably, I used some application assembly attributes to send the e-mail where it needs to go, and provide the application name. This allows me to put this library in my Common collection, and re-use it across any of my apps. w00t.
While the Title attribute is present by default, the E-mail address one isn’t. To use this, you’ve got to add the following attribute class (somewhere).
[AttributeUsage(AttributeTargets.Assembly, Inherited = false)] [ComVisible(true)] public class AssemblySupportEmailAddressAttribute : Attribute { public string Address { get; private set; } public AssemblySupportEmailAddressAttribute(string emailAddress) { Address = emailAddress; } }
and then add the attribute to your app’s AssemblyInfo.cs file:
[assembly: AssemblySupportEmailAddress("support@mydomain.org")]
Andy’s post specifies it, but I figure I’ll give you the code to do the other two pieces in your app as well.
In App.cs:
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e) { LittleWatson.ReportException(e.ExceptionObject); }
and in MainPage.xaml.cs:
public MainPage() { LittleWatson.CheckForPreviousException(); }
So what do you think? Do you like this approach, see some issues, have something different you’re doing to capture & report crashes in your apps? Let me know in the comments.