Click here to Skip to main content
Click here to Skip to main content

VB.NET Global Try Catch in the Application Framework

By , 19 Nov 2012
 

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:

.Net Unhandled Exception Screenshot

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:

View Application Events

This will open / generate the ApplicationEvents.vb file.

In the ApplicationEvents.vb code, add the following:
Partial Friend Class MyApplication        
    'One of the global exceptions we are catching is not thread safe, 
    'so we need to make it thread safe first.
    Private Delegate Sub SafeApplicationThreadException(ByVal sender As Object, _
        ByVal e As Threading.ThreadExceptionEventArgs) 

    Private Sub ShowDebugOutput(ByVal ex As Exception)
 
        'Display the output form
        Dim frmD As New frmDebug()
        frmD.rtfError.AppendText(ex.ToString())
        frmD.ShowDialog()
 
        'Perform application cleanup
        'TODO: Add your application cleanup code here.

        'Exit the application - Or try to recover from the exception:
        Environment.Exit(0)
 
    End Sub  
     Private Sub MyApplication_Startup(ByVal sender As Object, _
         ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) Handles Me.Startup
  
         'There are three places to catch all global unhandled exceptions:
         'AppDomain.CurrentDomain.UnhandledException event.
         'System.Windows.Forms.Application.ThreadException event.
         'MyApplication.UnhandledException event.
         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)

        'This is not thread safe, so make it thread safe.
        If MainForm.InvokeRequired Then
            ' Invoke back to the main thread
            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

    ' This call is required by the designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    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

    'Assemblies are listed in the order they are loaded - I prefer them alphabetical.
    'But if the order in which assemblies are being loaded is important, then don't do the sort.
    Dim asmc As New AsmComparer()
    asms.Sort(asmc)

    rtfError.AppendText(vbNewLine)
    For Each asm As Assembly In asms
        'Many of the assemblies are core .Net assemblies. I do not care about them.
        'If you do, comemnt out this next line:
        If IO.Path.GetDirectoryName(asm.Location).ToUpper() <> _
            My.Application.Info.DirectoryPath.ToUpper() Then Continue For

        'Included in this list is the executable path - which is meaningless.
        'Have to cast to Upper (or lower), because one of the paths returns as .EXE, 
        'and the other .exe
        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)

    ''IMPORTANT: This next line is .Net 4.0 only.
    ''       If you need to know if it is a 64 bit OS or not, you will need to use
    ''       a different method for any .Net older than 4.0
    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!)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

B.O.B.
Software Developer (Senior)
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberAlhoot20043 Feb '13 - 13:29 
Thanks , my reason ISNOT nothing
GeneralMy vote of 5memberMihai MOGA14 Dec '12 - 5:15 
This is a great inspiring article. I am pretty much pleased with your good work. You put really very helpful information. Keep it up once again.
GeneralMy vote of 5memberSavalia Manoj M10 Dec '12 - 20:54 
Good Work
GeneralMy vote of 3memberKlaus Luedenscheidt18 Nov '12 - 19:10 
It's never a good idea to edit generated code. You can handle the exceptions by attaching handlers in your sub Main():
 

' Add the event handler for handling UI thread exceptions to the event.
AddHandler Application.ThreadException, AddressOf HandleThreadException
 
' Set the unhandled exception mode to force all Windows Forms errors to go through
' our handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)
 
' Add the event handler for handling non-UI thread exceptions to the event.
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf HandleUnhandledException

AnswerRe: My vote of 3memberB.O.B.18 Nov '12 - 20:29 
Thanks for the feedback, however, if you have 'Enable application framework' checked in a VB.Net project, you cannot have a Sub Main. You can certainly turn of 'Enable application framework', but then you would not be able to use other features such as 'Make single instance application', and the 'StartupNextInstance' event provided in the Application.Designer.vb file.
 
While yes, this file is auto-generated, only part of it is. That part is:
 
        <Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
        Public Sub New()
            MyBase.New(Global.Microsoft.VisualBasic.ApplicationServices.AuthenticationMode.Windows)
            Me.IsSingleInstance = False
            Me.EnableVisualStyles = True
            Me.SaveMySettingsOnExit = True
            Me.ShutdownStyle = Global.Microsoft.VisualBasic.ApplicationServices.ShutdownMode.AfterMainFormCloses
        End Sub
 
        <Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
        Protected Overrides Sub OnCreateMainForm()
            Me.MainForm = Global.GlobalTryCatch.frmMain
        End Sub
 
However, there are 5 events in this file that you are allowed to tap into in this file: (Have a look at http://msdn.microsoft.com/en-us/library/ms172938%28v=vs.80%29.aspx[^])
* NetworkAvailabilityChanged - Occurs when the network availability changes.
* Shutdown - Occurs when the application shuts down.
* Startup - Occurs when the application starts.
* StartupNextInstance - Occurs when attempting to start a single-instance application and the application is already active.
* UnhandledException - Occurs when the application encounters an unhandled exception.
 
The last event (UnhandledException) unfortunately does not properly trap all unhandled exceptions, which is why in my example I wire in extra event handlers during the Startup event.
 
StartupNextInstance is particularly useful in 'Single Instance' applications (where if the application is already running, a 2nd occurrence is actively prevented by .Net). This even fires whenever the application is launched and it is already running. In this event, in the StartupNextInstanceEventArgs are two important properties: BringToForeground and CommandLine.
 
BringToForeground allows you specify if the already running application is brought to the foreground or not.
 
The CommandLine is the command line arguments that were used to launch the 2nd (and subsequent) instances of your application. This is important because you can have your application running, and allow it to receive command line arguments from another program, parse the command line, and do actions based on it. For instance, one application project I work on, we have a command line 'bridge' that we supply to 3rd party applications. These other applications are in the same market, but they don't do the same thing that we do - they are sort of sibling products of ours. So we provide them with a command line bridge, which they then provide their user's with a 'button' that automatically launches our application and passes over any relevant information for what the user needs to do in our application.
 
This allows the user to keep our application running without having to close it every time they want to bridge from the sibling application.
 
I hope this clarifies why I did my example and why it was needed.
 
One final note is that I am aware that if you turn off the Application Framework in the project settings and instead use a Sub Main, that is possible to create code for a single instance application - and I have so those solutions - I just feel those solutions are messier and more complex than just using the Application Framework.
 
Whether a developer uses the Application Framework or not is entirely up to them - I just prefer it.
GeneralRe: My vote of 3memberKlaus Luedenscheidt18 Nov '12 - 21:24 
Thanks for the clarification. I never attended the "Use Application Framework" feature and wired the exceptions by myself. There are also ways to keep sure that only one instance of the application is running which i use when needed. But after your explanation i think about to use the framework in the future.
 
Regards
Klaus
 
P.S.: I changed my rating to 4 Smile | :)
SuggestionRe: My vote of 3memberZac Greve18 Nov '12 - 22:52 
On the 'application' properties page (where all of the general options are), there is a button that says 'View Application Events'. You can then use the 'Startup' event to wire up event handlers, and use the 'UnhandledException' event to handle otherwise unhandled exceptions.
 
There are also 'NetworkAvailabilityChanged', 'StartupNextInstance', and 'Shutdown' events.
I think computer viruses should count as life. I think it says something about human nature that the only form of life we have created so far is purely destructive. We've created life in our own image.
Stephen Hawking

GeneralRe: My vote of 3memberB.O.B.18 Nov '12 - 23:03 
Ah! I forgot about that! I have seen it before, but forgot it was there.
 
I will get that fixed in the demo project and revise the article tomorrow. Thanks so much for pointing that out! Thumbs Up | :thumbsup:
GeneralRe: My vote of 3memberZac Greve18 Nov '12 - 23:34 
No problem! I use that feature quite a bit for initializing external libraries like GeckoFX. (Might get link later. Typing on iPad)
I think computer viruses should count as life. I think it says something about human nature that the only form of life we have created so far is purely destructive. We've created life in our own image.
Stephen Hawking

AnswerRe: My vote of 3memberB.O.B.19 Nov '12 - 5:52 
Fixed and updated! Thanks again for pointing that. I have used that button in the past in previous projects, I just totally forgot it existed. Dead | X|
42! Because it is the answer to everything! Life, the universe, everything!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 19 Nov 2012
Article Copyright 2012 by B.O.B.
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid