ThisOutlookSession Public WithEvents variables stopped being public - outlook

I use event handlers to automatically sort email in Outlook 2016. Those event handlers have been defined in ThisOutlookSession as Public WithEvents variables, they are set using the Application_Startup event, and they call different procedures located in a different code module which directly reference these event handler objects during the procedures.
This morning I get "object is required" error whenever these procedures are run. I turned on Option Explicit and it revealed that when I try to run the procedure manually I get a compile error indicating these variables (the public variable event handlers in ThisOutlookSession) are not defined. The error(s) stop when I either move the procedure to ThisOutlookSession or reference the event handler as "ThisOutlookSession.EventHandler1" instead of just "EventHandler1".
I could use these workarounds, but it would be a lot of work. Any idea what happened or how this could be fixed (without resorting to workarounds)?
Example code (in ThisOutlookSession):
Public WithEvents InboxItems As Outlook.Items
Public WithEvents InboxFolder As Outlook.Folder
Private Sub Application_Startup()
Initialize_handler
End Sub
Public Sub Initialize_handler()
Set InboxItems = Application.GetNamespace("MAPI").GetDefaultFolder(olFolderInbox).Items
Set InboxFolder = Application.GetNamespace("MAPI").GetDefaultFolder(olFolderInbox)
End Sub
Private Sub InboxItems_ItemAdd(ByVal oMail As Object)
Call Inbox_Sort
End Sub
Example code (in separate code module "Custom")
Sub Inbox_Sort()
Dim ML As Outlook.mailItem
Dim oObj As Object
For Each oObj In InboxItems
If TypeOf oObj Is mailItem Then
Call Sort(oObj)
Else
oObj.Move Application.GetNamespace("MAPI").GetDefaultFolder(olFolderInbox)
End If
Next
End Sub

Related

VSTO Outlook Visual Basic - Handle Mail Item Closure

I'm creating an Outlook Add-In, and am struggling to detect and handle the event when a mail item is closed. The inspectors_NewInspector function is triggered sucessfully (as I can obtain the subject and sender of the email), but I cannot get the mailItem_close() function to be called at all.
Public Class ThisAddIn
Public WithEvents inspectors As Outlook.Inspectors
Public WithEvents mailItem As Outlook.MailItem
Private Sub ThisAddIn_Startup() Handles Me.Startup
inspectors = Me.Application.Inspectors
End Sub
Private Sub ThisAddIn_Shutdown() Handles Me.Shutdown
End Sub
Private Sub inspectors_NewInspector(ByVal Inspector As Microsoft.Office.Interop.Outlook.Inspector) Handles inspectors.NewInspector
If Inspector.CurrentItem.size > 0 And Inspector.CurrentItem.class = 43 Then
mailItem = Inspector.CurrentItem
Dim mSubject As String = mailItem.Subject
Dim mFrom As String = mailItem.SenderEmailAddress
Dim mTime As String = mailItem.ReceivedTime
'MsgBox(mSubject)
End If
End Sub
Private Sub mailItem_Close()
MsgBox("closing")
End Sub
It seems you have copied the code from VBA to a VB.NET add-in. In add-ins you need to use the AddHandler method to attach an event handler programmatically, see How do I create an event handler for a programmatically created object in VB.NET? for more information.
Note, each time the NewInspector event is fired the mailItem is overwritten, so the previous item instance is lost and can't fire events:
Private Sub inspectors_NewInspector(ByVal Inspector As Microsoft.Office.Interop.Outlook.Inspector) Handles inspectors.NewInspector
If Inspector.CurrentItem.size > 0 And Inspector.CurrentItem.class = 43 Then
mailItem = Inspector.CurrentItem
Dim mSubject As String = mailItem.Subject
Dim mFrom As String = mailItem.SenderEmailAddress
Dim mTime As String = mailItem.ReceivedTime
'MsgBox(mSubject)
End If
End Sub
Private Sub mailItem_Close()
MsgBox("closing")
End Sub
Instead, you need to maintain a list or array of items, so each time the NewInspector event is fired you will add an item to the list to keep the reference alive, so the events will be fired.
You may find the Implement a wrapper for inspectors and track item-level events in each inspector article helpful.
With huge thanks to Eugene, the solution was indeed to use AddHandler.
Private Sub inspectors_NewInspector(ByVal Inspector As Microsoft.Office.Interop.Outlook.Inspector) Handles inspectors.NewInspector
If Inspector.CurrentItem.size > 0 And Inspector.CurrentItem.class = 43 Then
inspectorEvents = TryCast(Inspector, Outlook.InspectorEvents_Event)
AddHandler inspectorEvents.Close, AddressOf inspectorEvents_Close
End If
End Sub
Public Sub inspectorEvents_Close()
Dim currentInspector As Outlook.Inspector
currentInspector = Application.ActiveInspector
mailItem = currentInspector.CurrentItem
Dim mSubject As String = mailItem.Subject
Dim mFrom As String = mailItem.SenderEmailAddress
Dim mTime As String = mailItem.ReceivedTime
MsgBox("closing" & mSubject)
End Sub
I'm occasionally getting multiple close handlers firing when a mail message is closed, but I should be able to get around that by maintaining an array / list of inspector ids and ensuring only one is created per mail item.

How do I listen to "ItemAdd" for all my "Sent items" folders?

My Outlook consists of 3 user mailboxes (cached) and 10 shared mailboxes (online).
I need to catch when a mail in either of these mailboxes is sent, so I googled that I should listen to "ItemAdd" event.
Problem is, that ItemAdd event is not fired.
Here's my test code:
Imports System.Runtime.InteropServices
Public Class ThisAddIn
Private sentFolders As New List(Of Outlook.Folder)
Private Sub ThisAddIn_Startup() Handles Me.Startup
Call InitSentFolders()
End Sub
Private Sub ThisAddIn_Shutdown() Handles Me.Shutdown
End Sub
Private Sub InitSentFolders()
Dim ns As Outlook.NameSpace = Application.GetNamespace("MAPI")
Dim stores As Outlook.Stores = ns.Stores
For i As Integer = 1 To stores.Count
Try
Dim store As Outlook.Store = stores(i)
Try
Dim sentFolder As Outlook.Folder = TryCast(store.GetDefaultFolder(Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderSentMail), Outlook.Folder)
AddHandler DirectCast(sentFolder.Items, Outlook.Items).ItemAdd, AddressOf ItemAdd
sentFolders.Add(sentFolder)
Catch ex As Exception
End Try
Marshal.ReleaseComObject(store)
Catch ex As Exception
End Try
Next
Marshal.ReleaseComObject(stores)
Marshal.ReleaseComObject(ns)
End Sub
Private Sub ItemAdd(ByVal ItemObject As Object)
If TypeOf (ItemObject) Is Outlook.MailItem Then
Dim item As Outlook.MailItem = CType(ItemObject, Outlook.MailItem)
MsgBox(item.Sender.ToString)
Marshal.ReleaseComObject(item)
End If
End Sub
End Class
Any idea why it is not fired?
Thanks
That is a widely spread mistake for beginners...
You need declare the source object at the global scope (for example, at the add-in class) and keep it alive to get the event. Or the garbage collector swipes the heap and source object will be destroyed.
In your case define a list of Outlook folders where you can keep all references.

ExecuteComplete ADODB Connection event not fired with adAsyncExecute parameter

I have a problem trying to catch the completion of a stored proc execute asynchronously.
Below my code VBA (in a class module named clsAsync):
Option Explicit
Private WithEvents cnn As ADODB.Connection
Private Sub cnn_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
MsgBox "Execution completed"
End Sub
Sub execSPAsync()
Set cnn = New ADODB.Connection
Set rst = New ADODB.Recordset
cnn.ConnectionString = "connection to my database SQLSEREVER"
cnn.Open
cnn.Execute "kp.sp_WaitFor", adExecuteNoRecords, adAsyncExecute
End Sub
This class is PublicNotCreatable.
To call the sub execSPAsync from a module I use the following code:
Sub testASYNC()
Dim a As New clsAsync
Call a.execSPAsync
End Sub
The stored procedure is very simple:
alter PROC kp.sp_WaitFor
AS
WAITFOR DELAY '00:00:05'
My problem is that the event ExecuteComplete is not fired at all, while if I comment the adAsynExecute parameter all is working fine.
Any idea on how to solve my question?
I solved my problem replacing the calling code:
Sub testASYNC()
Dim a As New clsAsync
Call a.execSPAsync
End Sub
with this new code:
Private a As clsAsync
Sub testASYNC()
Set a = New clsAsync
Call a.execSPAsync
End Sub
In the async mode, the object "a" is no longer available at the end of the procedure (scope visibility issue).

VBA Custom Event Not Found when Raised in UserForm

I was following this MSDN guide on creating custom events. I feel like I understand the process now, but I cannot figure out why I am getting a Compile Error: Event Not Found for RaiseEvent ItemAdded. The weird thing is, the ItemAdded event is recognized by the IDE (I can type it in all lowercase and it is then automatically formatted properly), so I know that it is recognized by VB.
DataComboBox Class Module Code:
Public Event ItemAdded(sItem As String, fCancel As Boolean)
Private pComboBox As Control
Public Property Set oComboBox(cControl As Control)
Set pComboBox = cControl
End Property
Public Property Get oComboBox() As Control
oComboBox = pComboBox
End Property
Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
End Sub
The UserForm contains two controls - a CommandButton named btnAdd and a ComboBox named cboData.
UserForm Code:
Private WithEvents mdcbCombo As DataComboBox
Private Sub UserForm_Initialize()
Set mdcbCombo = New DataComboBox
Set mdcbCombo.oComboBox = Me.cboData
End Sub
Private Sub mdcbCombo_ItemAdded(sItem As String, fCancel As Boolean)
Dim iItem As Long
If LenB(sItem) = 0 Then
fCancel = True
Exit Sub
End If
For iItem = 1 To Me.cboData.ListCount
If Me.cboData.List(iItem) = sItem Then
fCancel = True
Exit Sub
End If
Next iItem
End Sub
Private Sub btnAdd_Click()
Dim sItem As String
sItem = Me.cboData.Text
AddDataItem sItem
End Sub
Private Sub AddDataItem(sItem As String)
Dim fCancel As Boolean
fCancel = False
RaiseEvent ItemAdded(sItem, fCancel)
If Not fCancel Then Me.cboData.AddItem (sItem)
End Sub
You cannot raise an event outside the classes file level.
Add a routine like this inside "DataComboBox1" to allow you to raise the event externally.
Public Sub OnItemAdded(sItem As String, fCancel As Boolean)
RaiseEvent ItemAdded(sItem, fCancel)
End Sub
Then call the OnItemAdded with the current object.
Example...
Private WithEvents mdcbCombo As DataComboBox
...
mdcbCombo.OnItemAdded(sItem, fCancel)

VBA: WithEvents puzzle

I have a UserForm, xForm, that is being instantiated in a class module (let's say TestClass) as:
'TestClass
Dim Form as New xForm
Private WithEvents EvForm as MSForms.UserForm
Set EvForm = Form
At the class module of the xForm itself I have some code that must be executed on Form Closing, ONLY if the form actually closes:
'xForm class module
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'Do some cleanup, otherwise the app would hang
'If not closing, don't cleanup anything, otherwise the app would hang
End Sub
The QueryClose event is also treated in TestClass, and could avoid the form from closing:
'TestClass
Private Sub EvForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'Verify if closing is allowed based on User Control values
Cancel = Not ClosingIsAllowed '<-- Pseudocode on the right side of "="
End Sub
How can I test for Cancel = True, set in TestClass, in the xForm class module?
Let's rephrase it: If Cancel is set to True in TestClass, I must not do the cleanup code in the xForm class module. How can I accomplish that?
Until now, I have thought off of implementing another event in the xForm class (My_QueryClose?) and raise it on the QueryClose event. Outside the Code Behind Form I would deal only with the My_QueryClose event, so taking full control over what is happening. Is this a viable/better approach?
Can't make heads or tails of your custom event idea, but the way to get one class to talk to another (form or anything else, doesn't matter) is to link them up; here's a clean example:
Basic TestClass holds form object (no events needed here, let the form handle that)
'TestClass code
Private MyForm As UserForm
Private mbleCanClose As Boolean
Public Property Get CanClose() As Boolean
CanClose = mbleCanClose
End Property
Public Property Let CanClose(pbleCanClose As Boolean)
mbleCanClose = pbleCanClose
End Property
Public Property Get MyFormProp() As UserForm1
Set MyFormProp = MyForm
End Property
Add a custom object and property to the form itself
'UserForm1 code
Private mParent As TestClass
Public Property Get Parent() As TestClass
Set Parent = mParent
End Property
Public Property Set Parent(pParent As TestClass)
Set mParent = pParent
End Property
Invoking the form on TestClass creation looks like this:
'TestClass code
Private Sub Class_Initialize()
Set MyForm = New UserForm1
Load MyForm
Set MyForm.Parent = Me
End Sub
And then when it's time to close the form, you check whether you can:
'UserForm1 code
Public Function WillMyParentLetMeClose() As Boolean
If Not (mParent Is Nothing) Then
WillMyParentLetMeClose = mParent.CanClose
End If
End Function
Private Sub CommandButton1_Click()
If WillMyParentLetMeClose = True Then
Unload Me
End If
End Sub
Here's what it would like to invoke
'standard module code
Public Sub Test_TestClass()
Dim myclass As TestClass
Set myclass = New TestClass
myclass.MyFormProp.Show
End Sub
A work around declaring another event
The code bellow do what I was expecting, although it is not as neat as I wish it could be.
In the UserForm1 code:
'***** UserForm1
Public Event MyQueryClose(ByRef Cancel As Integer, ByRef CloseMode As Integer, ByRef Status As String)
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Dim Status As String
Cancel = True
Status = "QueryClose"
Debug.Print "Entered QueryClose"
Debug.Print "Cancel = " & Cancel
Debug.Print "Status = " & Status
Debug.Print "Just before raising MyQueryClose"
RaiseEvent MyQueryClose(Cancel, CloseMode, Status)
Debug.Print "Just got back from MyQueryClose"
Debug.Print "Cancel = " & Cancel
Debug.Print "Status = " & Status
End Sub
In the Class1 code:
'***** Class1
Dim UserForm As New UserForm1
Private WithEvents UF As UserForm1
Sub DoIt()
Set UF = UserForm
UserForm.Show
End Sub
Private Sub UF_MyQueryClose(Cancel As Integer, CloseMode As Integer, Status As String)
Debug.Print "Just entered MyQueryClose"
Cancel = False
Status = "MY QueryClose"
End Sub
In a basic module, to test the Class:
'***** Basic module
Sub TestClass()
Dim C As New Class1
C.DoIt
End Sub
And here's the end result (debug window):
TestClass
Entered QueryClose
Cancel = -1
Status = QueryClose
Just before raising MyQueryClose
Just entered MyQueryClose
Just got back from MyQueryClose
Cancel = 0
Status = MY QueryClose

Resources