Bulletproof Exiting Outlook (and not crashing)

  • Thread starter Thread starter Derek Hart
  • Start date Start date
D

Derek Hart

In building an Outlook integration, I try to exit Outlook this way:



Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing



Before I did this, sometimes this code would not execute, and Outlook would
then get a bit messed up. I would have to load Outlook and wait for it to
repair and/or shut down Outlook in the Task Manager. I don't want this ever
to happen to a user in a commercial application. Is the above code the best
way to handle this scenario. I want to bulletproof an application with
Outlook, and I don't want to corrupt a user's Outlook file.



I am just using standard VB .Net code. Here is a sample. Anything that can
make it safer than my code here?



Try

Dim objOutlook As Outlook.Application

Dim objNameSpace As Outlook.NameSpace

Dim objFolder As Outlook.MAPIFolder

Dim objItem As Outlook.MailItem



objOutlook = New Outlook.Application

objNameSpace = objOutlook.GetNamespace("MAPI")

objNameSpace.Logon()

objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)



' Do email processing



Catch ex As Exception

MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Information)



Finally

Marshal.ReleaseComObject(objOutlook)

objOutlook = Nothing
 
You need to release all of your COM objects (Outlook objects included) and
you should call the garbage collector also in your shutdown code:

GC.Collect()
GC.WaitForPendingFinalizers()

You may need to call ReleaseComObject() in a loop until it returns 0,
depending on your object instantiations and handling.

Any other answers would depend on whether this is addin code or not, and
what context the code would run in.
 
Ken,

This is not an add-in, just a plain old pst file being synchronized with a
database.

Based on the code below, is there something I should do differently to get
the current Outlook instance if it is loaded? I think Outlook can only be
loaded one time when done manually, so when I run the code below, does it
create a whole new instance, or attach to the one that is running?

Derek
 
I would certainly make sure to release all COM objects, including any
Outlook objects such as your objNameSpace, objFolder, etc.

As it's standalone code I'd also handle the Explorer and Inspector Close()
events if you are displaying any UI that can be closed. If there is no UI
then you just need to release everything when you are finished with your
code.

You can only have one instance of Outlook running at any time. I prefer to
do things differently in cases where I'm automating Outlook from a
standalone program. I like to know if I need to start an instance or one was
running already. I use code something like this for that purpose, this
happens to be C# code:

private void InstantiateOutlook()

{

try

{

string olProcess = "Outlook.exe";

SelectQuery query = new SelectQuery("SELECT Name FROM Win32_Process WHERE
name='" + olProcess + "'");

ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);

ManagementObjectCollection objectCollection = searcher.Get();

int collCount = objectCollection.Count;

searcher.Dispose();

objectCollection.Dispose();

searcher = null;

objectCollection = null;

if (collCount != 0)

{

// Outlook already running, hook into the Outlook instance

_outlookApp = Marshal.GetActiveObject("Outlook.Application") as
Outlook.Application;

if (_outlookApp != null) canQuit = false;

}

else

{

// Outlook not already running, start it

Outlook.ApplicationClass _app = new Outlook.ApplicationClass();

_outlookApp = (Outlook.Application)_app;

}

}

catch (System.Exception ex)

{

MessageBox.Show(ex.Message);

}

}
 
Wow... thank you for this code. A few things...

I am wondering if I should simply run this code first to instantiate
Outlook, and then use my code as I did before to go get it. Since you did
not send me a function, it looks like I am not retrieving the instance from
this routine and pulling it into another routine.

A couple variables were not declared in your code, so I am unclear how to
use this. Should I declare this as a global variable. If so, I tried that,
and the code is having problems.
Dim _outlookApp As Outlook.Application

So in my routine I load Outlook this way:
InstantiateOutlook()

Then the global variable _outlookApp is used everywhere. But then at the end
of my routine, I release the com object and set it to nothing. So then I
thought I should put the code as follows in your routine.
objOutlook = New Outlook.Application
If collCount <> 0 Then
' Outlook already running, hook into the Outlook instance
objOutlook =
TryCast(Marshal.GetActiveObject("Outlook.Application"), Outlook.Application)

But that code errors on the TryCast line. Please explain how to use this
code appropriately.

And I am unclear what the canQuit flag is used for:
Dim canQuit As Boolean
 
_outlookApp is declared at a global or class level. I just call the method I
showed to instantiate an Outlook.Application object declared as _outlookApp.
I use the flag value canQuit to know if I should close Outlook when I'm
done, or if it was already running I just leave it alone.

The code I showed is in C#, if you are going to use it in a VB.NET
application you will need to translate it from C#.

That code is pretty much the equivalent of calling GetObject() in VB6 on an
Outlook.Application object and then calling CreateObject() or New if the
GetObject() call fails.
 
When I run this process, it works fine the first time. After the first run,
Outlook is running. Then let's say the user exits Outlook. It is still in
the task manager. Then when your code looks to see if it is there, it is.
But then the code tries to get the Outlook with this line:

objOutlook = TryCast(Marshal.GetActiveObject("Outlook.Application"),
Outlook.Application)

But it is not really there. So the code errors. Should I trap for that and
then get Outlook the other way:
Dim _app As New Outlook.ApplicationClass()
objOutlook = DirectCast(_app, Outlook.Application)

I guess the global variable keeps Outlook in the task manager, but even when
I was not doing this process Outlook still stayed in the task manager. It
never seems to exit, even with this:
Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing

And is there code to exit it properly in the Explorer and Inspector Close()
events? I was unclear on this... I am not displaying Outlook UI, but just
WindowsForms windows that read and write data to/from the pst file. Do I
have to use these events to properly close and get rid of Outlook in the
task manager?
 
If Outlook is not completely exiting as a result of running your code it
means you aren't releasing all your Outlook objects. You need to release
every one of them. It doesn't matter from where your shutdown code is
called, from an Explorer or Inspector Close() event or Application.Quit(),
or anywhere else. You just release everything.
 
Shouldn't this be doing it?

Class Level
Dim objOutlook As Outlook.Application

Subroutine
Dim objNameSpace As Outlook.NameSpace
Dim objFolder As Outlook.MAPIFolder
Dim objItem As Outlook.MailItem
InstantiateOutlook()
objNameSpace = objOutlook.GetNamespace("MAPI")
objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
objNameSpace.Logon()

' Do processing

' Release all objects

Marshal.ReleaseComObject(objOutlook)
Marshal.ReleaseComObject(objNameSpace)
Marshal.ReleaseComObject(objFolder)
Marshal.ReleaseComObject(objItem)
objOutlook = Nothing
objNameSpace = Nothing
objFolder = Nothing
objItem = Nothing

' This is the code for InstantiateOutlook
Private Sub InstantiateOutlook()
Try
Dim canQuit As Boolean
Dim olProcess As String = "Outlook.exe"
Dim query As New SelectQuery("SELECT Name FROM Win32_Process
WHERE name='" & olProcess & "'")
Dim searcher As New ManagementObjectSearcher(query)
Dim objectCollection As ManagementObjectCollection =
searcher.Get()
Dim collCount As Integer = objectCollection.Count
searcher.Dispose()
objectCollection.Dispose()
searcher = Nothing
objectCollection = Nothing
If collCount <> 0 Then
' Outlook already running, hook into the Outlook instance
objOutlook =
TryCast(Marshal.GetActiveObject("Outlook.Application"), Outlook.Application)
If objOutlook IsNot Nothing Then
canQuit = False
End If
Else
' Outlook not already running, start it
Dim _app As New Outlook.ApplicationClass()
objOutlook = DirectCast(_app, Outlook.Application)
End If

SecurityManager1.ConnectTo(objOutlook)
SecurityManager1.DisableOOMWarnings = True

Catch ex As System.Exception
MessageBox.Show(ex.Message)
End Try
End Sub
 
That releases all the objects you are showing, is that release code being
called and are there any other objects not being released?
 
The following is what I have done. At the class level I have the main
variable. Not sure if I should use Dim objOutlook As New
Outlook.Application.
I used GenericOutlookItem for the email just to make sure I only have
emails, and not appointments or other things.

The code has the following problems.

1) If I load Outlook itself and show the interface, all the code works well.
But if I have Outlook not loaded up, this code runs good the first time, but
then gives an error of "Operations Unavailable: (Exception from HResult:
(MK_E_UNAVAILABLE)" when I run the code again. In your InstantiateOutlook
routine, when it checks for collCount, it gets 1 because Outlook is still in
the task manager, but it is a somewhat shutdown copy, so then it hits the
line objOutlook = TryCast(Marshal.GetActiveObject("Outlook.Application"),
Outlook.Application) and errors because
Marshal.GetActiveObject("Outlook.Application") is actually null.

2) I am holding onto the Outlook instance somehow, maybe because the
variable is of class level scope, but Outlook does not go away in the task
manager unless I exit my program.

Can you see here what should be done to simply release Outlook and make #1
and #2 work properly?


Class Level
Dim objOutlook As Outlook.Application

Private Sub btnLoadEmails_Click(ByVal sender As System.Object, ByVal e
As System.EventArgs) Handles btnLoadEmails.Click
Dim objNameSpace As Outlook.NameSpace
Dim objFolder As Outlook.MAPIFolder
Dim objItem As Outlook.MailItem
Dim GenericOutlookItem As Object
Dim Counter As Integer
Dim NumAttachments As Integer

Try
InstantiateOutlook()

objNameSpace = objOutlook.GetNamespace("MAPI")
objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
objNameSpace.Logon()

For Each GenericOutlookItem In objFolder.Items
If TypeOf GenericOutlookItem Is Outlook.MailItem Then
objItem = CType(GenericOutlookItem, Outlook.MailItem)

''' Do Email processing on objItem

Marshal.ReleaseComObject(objItem)
End If
Next

Marshal.ReleaseComObject(objOutlook)
Marshal.ReleaseComObject(objNameSpace)
Marshal.ReleaseComObject(objFolder)

objOutlook = Nothing
objNameSpace = Nothing
objFolder = Nothing
objItem = Nothing
GenericOutlookItem = Nothing
Catch ex As Exception
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Information)
End Try
End Sub


Private Sub InstantiateOutlook()
Try
Dim canQuit As Boolean
Dim olProcess As String = "Outlook.exe"
Dim query As New SelectQuery("SELECT Name FROM Win32_Process
WHERE name='" & olProcess & "'")
Dim searcher As New ManagementObjectSearcher(query)
Dim objectCollection As ManagementObjectCollection =
searcher.Get()
Dim collCount As Integer = objectCollection.Count
searcher.Dispose()
objectCollection.Dispose()
searcher = Nothing
objectCollection = Nothing
If collCount <> 0 Then
' Outlook already running, hook into the Outlook instance
objOutlook =
TryCast(Marshal.GetActiveObject("Outlook.Application"), Outlook.Application)
If objOutlook IsNot Nothing Then
canQuit = False
End If
Else
' Outlook not already running, start it
Dim _app As New Outlook.ApplicationClass()
objOutlook = DirectCast(_app, Outlook.Application)
End If
Catch ex As System.Exception
MessageBox.Show(ex.Message)
End Try
End Sub
 
Also I ordered your book to see your ideas on this, but it won't come for a
week. Would appreciate if you can look at my other message.
 
If you found an existing Outlook session that you hooked into you want to
leave Outlook running when you are finished with your code. If you started
Outlook yourself and did not find a running instance of Outlook you will
want to shut Outlook down when you are finished with your code. In that case
you will want to use the Quit() method to tell Outlook to close.

Move your release of Outlook to after you release all other Outlook objects
(namespace, etc.). Then:

objOutlook.Quit()
Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing

The canQuit variable should be declared at global or class scope so it
retains its value and can be checked when you are ready to end your code.
Then if canQuit = True you call the Outlook.Application.Quit() method, if
it's False you don't so the Outlook session can continue.

See if that fixes the problem.
 
If it is not loaded to begin with I still get an error the SECOND time I try
to run this process. Is this routine correct? I declared canQuit at the
global level, but it is not used anywhere else.Why can't I exit Outlook
unless I exit my application?

Private Sub InstantiateOutlook()
Try
Dim olProcess As String = "Outlook.exe"
Dim query As New SelectQuery("SELECT Name FROM Win32_Process
WHERE name='" & olProcess & "'")
Dim searcher As New ManagementObjectSearcher(query)
Dim objectCollection As ManagementObjectCollection =
searcher.Get()
Dim collCount As Integer = objectCollection.Count
searcher.Dispose()
objectCollection.Dispose()
searcher = Nothing
objectCollection = Nothing
If collCount <> 0 Then
' Outlook already running, hook into the Outlook instance
objOutlook =
TryCast(Marshal.GetActiveObject("Outlook.Application"), Outlook.Application)
If objOutlook IsNot Nothing Then
canQuit = False
End If
Else
' Outlook not already running, start it
Dim _app As New Outlook.ApplicationClass()
objOutlook = DirectCast(_app, Outlook.Application)
End If

Catch ex As System.Exception
MessageBox.Show(ex.Message)
End Try
End Sub


And then I have this routine:
Dim objOutlook As Outlook.Application
Dim canQuit As Boolean


Private Sub btnLoadEmails_Click(ByVal sender As System.Object, ByVal e
As System.EventArgs) Handles btnLoadEmails.Click

Dim objNameSpace As Outlook.NameSpace
Dim objFolder As Outlook.MAPIFolder
Dim objItem As Outlook.MailItem
Dim GenericOutlookItem As Object
Dim Counter As Integer
Dim Iterations As Integer
Dim NumAttachments As Integer

Try
InstantiateOutlook()

objNameSpace = objOutlook.GetNamespace("MAPI")
objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
objNameSpace.Logon()

For Each GenericOutlookItem In objFolder.Items
If TypeOf GenericOutlookItem Is Outlook.MailItem Then
' See if this email's EntryID is already in the
datatable.
' A faster, more efficient way it to use an array, or
hashtable.
objItem = CType(GenericOutlookItem, Outlook.MailItem)
Marshal.ReleaseComObject(objItem)
End If
Next

GenericOutlookItem = Nothing
objOutlook.Quit()
Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing
Catch ex As Exception
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Information)
End Try
End Sub
 
OK, narrowing it down - this routine does properly exit Outlook every time,
UNTIL the For Next loop is put in. I added code to try to release objItem. I
took a guess that releasing objItem and setting it to nothing, would release
Outlook, but it does not. So how do I properly release the item? Without
being able to do that, Outlook cannot exit properly. At that point, all
should be well.

Dim objNameSpace As Outlook.NameSpace
Dim objFolder As Outlook.MAPIFolder
Dim iterations As Integer
Try
InstantiateOutlook()
objNameSpace = objOutlook.GetNamespace("MAPI")
objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
objNameSpace.Logon()

For Each objItem As Outlook.MailItem In objFolder.Items
iterations += 1
Marshal.ReleaseComObject(objItem)
objItem = Nothing
If iterations = 50 Then Exit For
Next

Marshal.ReleaseComObject(objFolder)
Marshal.ReleaseComObject(objNameSpace)
objOutlook.Quit()
Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing
 
Why on earth are you setting up a loop to iterate an Items collection
assigning items to an object and then doing nothing but releasing them?

All you should need is to test canQuit and if it's false Outlook was already
started and you just release your objects. If canQuit is true you call the
Quit() method.

Why make things a lot more complicated and non-functional than you need to?
 
I only showed this routine and removed a hundred lines that process the
email. I was showing you that I cannot release Outlook this way. If I run
this code and Outlook is not started, it starts Outlook with
InstantiateOutlook, but then I cannot release Outlook because of the For
Next loop. If I take this loop out, Outlook fully exits. If I add this loop
in, Outlook can never exit until I exit the application.
Marshal.ReleaseComObject(objItem) and objItem = Nothing are not enough to
release Outlook. Is that clear?

For Each objItem As Outlook.MailItem In objFolder.Items
iterations += 1

' Do email processing...

Marshal.ReleaseComObject(objItem)
objItem = Nothing
If iterations = 50 Then Exit For
Next
 
If adding in that loop prevents Outlook from being released it would be
logical to assume that you are doing something in the loop that's preventing
Outlook from exiting. Obviously since you aren't showing the loop code no
one can say what's causing the problem.

At a guess you are creating explicit or implicit objects that aren't being
released. I would first use a For loop with a counter instead of a For Each
loop and declare the objItem object outside the loop, releasing it inside
the loop as you show. Then I'd look for compound dot operators, which create
invisible object variables that cannot be released. For example,
objItem.Recipients.Item(1).Address creates an invisible Recipients
collection variable plus a Recipient variable. Neither can be released. You
want to avoid that sort of thing and explicitly declare each object so it
can be explicitly released.
 
I would have gone searching through my code, however, this code below,
exactly as shown, causes Outlook to hang. This code is without doing
anything with objItem. That's why I thought this might be easy for you to
recognize. There must be some code to possibly close objItem somehow. It is
not any other code that manipulates objItem at all. This code below, with
the for loop, makes Outlook hang in memory until the program is exited. If I
just instantiate Outlook and remove this loop, it exits perfectly. This code
below does not allow Outlook to be released. objItem is declared inside this
sub, do that is not an issue. Any ideas?

For Each objItem As Outlook.MailItem In objFolder.Items
iterations += 1

Marshal.ReleaseComObject(objItem)
objItem = Nothing
If iterations = 50 Then Exit For
Next
 
To reiterate, here is the exact code. You mentioned something about Explorer
and Inspector Close() events. Could these be used to release Outlook? Why
can't I release Outlook, simply looping the emails?

' Global level
Dim objOutlook As Outlook.Application
Dim canQuit As Boolean

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
Dim objNameSpace As Outlook.NameSpace
Dim objFolder As Outlook.MAPIFolder
Dim iterations As Integer

Try
InstantiateOutlook()
objNameSpace = objOutlook.GetNamespace("MAPI")
objFolder =
objNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
objNameSpace.Logon()

For Each objItem In objFolder.Items
iterations += 1
Marshal.ReleaseComObject(objItem)
objItem = Nothing
If iterations = 50 Then Exit For
Next

Marshal.ReleaseComObject(objFolder)
Marshal.ReleaseComObject(objNameSpace)
objOutlook.Quit()
Marshal.ReleaseComObject(objOutlook)
objOutlook = Nothing
GC.Collect()
GC.WaitForPendingFinalizers()
Catch ex As Exception
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Information)
End Try
End Sub
 
Back
Top