Introduction
This example shows how to setup a global try-catch block using the .NET Application Framework. The example code is in VB.NET but can easily be converted to C# as well.
Background
As I am not a perfect programmer, and there are times when an exception slips through my fingers, and I get an unhandled exception which usually results in the application crashing very ungracefully. This example shows how to not only setup a global try-catch block so that you can handle the crash more gracefully, but also how to style a form that matches your main application that provides additional information, and an easy 'Copy Text' button that places the text in the clipboard so that the user can e-mail it to tech support. You could take this a step further, and change the button to something 'Send Report' where it e-mails the text automatically or something similar.
Often times, when an unhandled exception occurs, the user takes a screenshot and forwards it on to tech support who then forward it to me such as the one shown below. While this information could be invaluable to track down where the exception is occurring, since it is only a screenshot, I cannot read all the text. So solve this, I have created a simple form which I can style to match my main application and display not only the information from .Net unhandled exception but some additional information as well:
There are similar ways of doing a global try-catch without using the .NET Application Framework, but then you would not be able to use the extra features of the .NET Application Framework such as 'Make single instance application'.
Using the code
Make sure to check the 'Enable application framework' in the Project Properties -> Application Settings.
On that same page, click the 'View Application Events' button:
This will open / generate the ApplicationEvents.vb file.
In the ApplicationEvents.vb code, add the following:
Partial Friend Class MyApplication
Private Delegate Sub SafeApplicationThreadException(ByVal sender As Object, _
ByVal e As Threading.ThreadExceptionEventArgs)
Private Sub ShowDebugOutput(ByVal ex As Exception)
Dim frmD As New frmDebug()
frmD.rtfError.AppendText(ex.ToString())
frmD.ShowDialog()
Environment.Exit(0)
End Sub
Private Sub MyApplication_Startup(ByVal sender As Object, _
ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) Handles Me.Startup
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf AppDomain_UnhandledException
AddHandler System.Windows.Forms.Application.ThreadException, AddressOf app_ThreadException
End Sub
Private Sub app_ThreadException(ByVal sender As Object, _
ByVal e As Threading.ThreadExceptionEventArgs)
If MainForm.InvokeRequired Then
MainForm.Invoke(New SafeApplicationThreadException(AddressOf app_ThreadException), _
New Object() {sender, e})
Else
ShowDebugOutput(e.Exception)
End If
End Sub
Private Sub AppDomain_UnhandledException(ByVal sender As Object, _
ByVal e As UnhandledExceptionEventArgs)
ShowDebugOutput(DirectCast(e.ExceptionObject, Exception))
End Sub
Private Sub MyApplication_UnhandledException(sender As Object, _
e As Microsoft.VisualBasic.ApplicationServices.UnhandledExceptionEventArgs) _
Handles Me.UnhandledException
ShowDebugOutput(e.Exception)
End Sub
End Class
Optional
If you want output similar to the screenshot above, the code for it:
Public Sub New()
On Error Resume Next
InitializeComponent()
rtfError.AppendText("Product Name: " & My.Application.Info.ProductName & vbNewLine)
rtfError.AppendText("Product Version: " & My.Application.Info.Version.ToString() & vbNewLine)
Dim asms As New List(Of Assembly)
For Each asm As Assembly In My.Application.Info.LoadedAssemblies
asms.Add(asm)
Next asm
Dim asmc As New AsmComparer()
asms.Sort(asmc)
rtfError.AppendText(vbNewLine)
For Each asm As Assembly In asms
If IO.Path.GetDirectoryName(asm.Location).ToUpper() <> _
My.Application.Info.DirectoryPath.ToUpper() Then Continue For
If asm.Location.ToUpper() = Application.ExecutablePath.ToUpper() Then Continue For
rtfError.AppendText("Loaded Assembly: " & asm.ToString() & vbNewLine)
Next asm
rtfError.AppendText(vbNewLine)
rtfError.AppendText("OS Name: " & My.Computer.Info.OSFullName & vbNewLine)
rtfError.AppendText("OS Version: " & My.Computer.Info.OSVersion & vbNewLine)
rtfError.AppendText("OS Platform: " & IIf(Environment.Is64BitOperatingSystem, _
"x64", "x86") & vbNewLine)
rtfError.AppendText("Physical Memory: " & _
FormatBytes(My.Computer.Info.AvailablePhysicalMemory) & " / " & _
FormatBytes(My.Computer.Info.TotalPhysicalMemory) & _
" (Free / Total)" & vbNewLine)
rtfError.AppendText("Virtual Memory: " & _
FormatBytes(My.Computer.Info.AvailableVirtualMemory) & " / " & _
FormatBytes(My.Computer.Info.TotalVirtualMemory) & _
" (Free / Total)" & vbNewLine)
rtfError.AppendText(vbNewLine)
rtfError.AppendText("Error Output:" & vbNewLine)
End Sub
Private Function FormatBytes(ByVal bytes As Long) As String
If bytes < 1000 Then
Return CStr(bytes) & "B"
ElseIf bytes < 1000000 Then
Return FormatNumber(bytes / 1024, 2) & "KB"
ElseIf bytes < 1000000000 Then
Return FormatNumber(bytes / 1048576, 2) & "MB"
Else
Return FormatNumber(bytes / 1073741824, 2) & "GB"
End If
End FunctionPrivate Sub btnCopy_Click(sender As System.Object, e As System.EventArgs) Handles btnCopy.Click
My.Computer.Clipboard.Clear()
My.Computer.Clipboard.SetText(rtfError.Text, TextDataFormat.Text)
My.Computer.Clipboard.SetText(rtfError.Rtf, TextDataFormat.Rtf)
End Sub
Public Class AsmComparer
Implements IComparer(Of Assembly)
Public Function Compare(x As System.Reflection.Assembly, y As System.Reflection.Assembly) _
As Integer _
Implements System.Collections.Generic.IComparer(Of System.Reflection.Assembly).Compare
Return String.Compare(x.ToString(), y.ToString())
End Function
End Class
History
11/19/2012 - Updated to use the proper ApplicationEvents.vb file instead of the Application.Designer.vb file. (Thanks to Zac Greve to pointing this out!)
7/27/2018 - Update source download to include a C# project that has similar code.