I'm writing a VSTO for Outlook 2010 (though it needs to work on 2010-2016, and for the most part does). I'm running into a bizarre problem with, as far as I can tell, event handlers never being removed. This means I can't avoid repeatedly invoking event handlers, which is silly and wasteful.
The code in question happens in the event handler for an Explorer's SelectionChange event. The handler checks whether the selection is a MailItem, and if so, it ensures that the Reply, ReplyAll, and Forward events have a handler. Since a given item may be selected more than once, the SelectionChange handler first removes the Reply/ReplyAll/Forward event handlers, in keeping with the pattern shown here (Prevent event handler being hooked twice, when you don't control the event-bearing class implementation).
The problem is, this isn't preventing the Reply (or other response action) event handler from being called once per instance of the SelectionChange event handler firing. This rapidly reaches a silly number of invocations. I thought it might be a synchronization issue, so I wrapped the event handler removal-and-adding in a lock block, to no avail.
private void SelectionChangeHandler()
{
Outlook.Selection sel = Application.ActiveExplorer().Selection;
// First make sure it's a (single) mail item
if (1 != sel.Count)
{ // Ignore multi-select
return;
}
// Indexed from 1, not 0. Stupid VB-ish thing...
Outlook.MailItem mail = sel[1] as Outlook.MailItem;
if (null != mail)
{
Outlook.ItemEvents_10_Event mailE = mail as Outlook.ItemEvents_10_Event;
lock (this)
{ // For each event, remove the handler then add it again
mailE.Forward -= MailItemResponseHandler;
mailE.Forward += MailItemResponseHandler;
mailE.Reply -= MailItemResponseHandler;
mailE.Reply += MailItemResponseHandler;
mailE.ReplyAll -= MailItemResponseHandler;
mailE.ReplyAll += MailItemResponseHandler;
}
ProcessMailitem(mail);
}
}
And the event handler that is being called way too many times:
private void MailItemResponseHandler (object newItem, ref bool Cancel)
{ // We need to get the responded-to item
// NOTE: There really needs to be a better way to do this
Outlook.MailItem old = GetCurrentMail();
if (null == old)
{ // No mail item selected
return;
}
MessageBox.Show(old.Body);
}
This function will eventually do something much more useful than pop a dialog box, but that was a convenient check for "did I find the correct original message?". I shouldn't be getting the same dialog box over and over again, though, and I am.
Am I doing something wrong? Is this a bug in Outlook, or in VSTO? Anybody know how I can avoid getting the duplicate event handler invocations?
Firstly, mailE variable must be declared on the class level, not local, to prevent it from being garbage collected.
Secondly, before setting the event, the old value (mailE) must be released using Marshal.ReleaseComObject.
The event handler is not actually being attached to the MAPI item that Outlook works with. Instead, it's being attached to a .NET object, called a Runtime Callable Wrapper (RCW), which wraps a COM object. Because of the way RCWs work, getting more than one reference to what seems to be the same object - for example, by getting the activeExplorer.Selection()[1] twice - gives multiple RCWs around different COM objects. This meant that the Outlook.MailItem (or Outlook.ItemEvents_10_Event) that I was attempting to remove events from didn't actually have any events on it; it was new-made each time SelectionChangeHandler fired.
Relatedly, because the only reference to an RCW-wrapped COM object is the RCW itself, letting all variables referencing an RCW go out of scope (or otherwise cease to reference that RCW) will result in the RCW being garbage collected (during which the COM object will be released and deleted). This has two relevant impacts on the code in question:
Because the garbage collection is not immediate, old RCWs (and COM objects) with existing event handlers were lingering on, and Outlook could still trigger events on their COM objects. This is why the event handler would be invoked multiple times.
Because the RCW was going out of scope at the bottom of SelectionChangeHandler, it was only a matter of time until garbage collection swept up all of the RCWs (and event handlers) and released all their COM objects. At that point, no events would be attached to that email.
While in practice, my testing happened over a short enough timeframe that I was more likely to get multiple live RCWs than none, selecting a mail item and not interacting with it (or selecting anything else) for long enough to trigger a garbage collection sweep would, in fact, result in the MailItemResponseHandler not being invoked at all when I clicked Reply.
#DmitryStreblechenko gave me the push in the right direction to work this out, but it took some experimentation to figure out. First of all, the relevant MailItem needed to be globally referenced, so its reference to the evented RCW wouldn't go out of scope and, also importantly, so its RCW could still be directly referenced when the SelectionChangeHandler was invoked again. I renamed the variable selectedMail and referenced it at the class level like such:
Outlook.ItemEvents_10_Event selectedMail;
I then modified SelectionChangeHandler so that whenever it is invoked with a single MailItem currently selected, it first removes all the event handlers from selectedMail, and only then points selectedMail to the newly-selected item. The RCW previously reference by selectedMail becomes eligible for garbage collection, but it has no event handlers on it so we don't really care. SelectionChangeHandler then adds the relevant event handlers to the new RCW now referenced by selectedMail.
private void SelectionChangeHandler()
{
Outlook.Selection sel = activeExplorer.Selection;
// First make sure it's a (single) mail item
if (1 != sel.Count)
{ // Ignore multi-select
return;
}
// Indexed from 1, not 0. Stupid VB-ish thing...
Outlook.MailItem mail = sel[1] as Outlook.MailItem;
if (null != mail)
{
if (null != selectedMail)
{ // Remove the old event handlers, if they were set, so there's no repeated events
selectedMail.Forward -= MailItemResponseHandler;
selectedMail.Reply -= MailItemResponseHandler;
selectedMail.ReplyAll -= MailItemResponseHandler;
}
selectedMail = mail as Outlook.ItemEvents_10_Event;
selectedMail.Forward += MailItemResponseHandler;
selectedMail.Reply += MailItemResponseHandler;
selectedMail.ReplyAll += MailItemResponseHandler;
if (DecryptOnSelect)
{ // We've got a live mail item selected. Process it
ProcessMailitem(mail);
}
}
}
Based on Dmitri's answer and comments, I tried calling Marshal.ReleaseComObject(selectedMail) on the old value of selectedMail before I thought to remove the event handlers instead. This helped a little, but either the COM objects are not released immediately or Outlook can still invoke event handlers through them, because events were still firing multiple times if I selected a given email multiple times in a short period before hitting Reply.
There's still one glitch to work out. If I open an inspector and hit Reply in there without changing my selection in the Explorer, it works fine (the MailItemResponseHandler is invoked). However, if I leave the inspector open, switch back to the explorer and select a different email, and then return to the inspector and hit Reply, it doesn't work. If there's an inspector open for the relevant email when that email gets de-selected, I need to avoid removing the event handlers (and then I do need to remove them when the inspector gets closed, unless the email is still selected in the explorer). Messy, but I'll work it out.
Related
I am using Unity 3D, however, that information should be irrelevant for solving this problem as the core problem is System.Delegate (I wanted to let you know as I'll be linking to some Unity docs for clarification).
I have a custom window that has a custom update function DirectorUpdate. I need this function to run every editor update regardless of what the user/window is doing.
In order for this to be called every editor update, I Combine my method with the Delegate EditorApplication.update:
protected void OnEnable()
{
// If I do the below, base EditorApplication.update won't be called anymore.
// EditorApplication.update = this.DirectorUpdate;
// So I need to do this:
EditorApplication.update = (EditorApplication.CallbackFunction)System.Delegate.Combine(new EditorApplication.CallbackFunction(this.DirectorUpdate), EditorApplication.update);
... // Other stuff
}
Note that this is done inside a window's OnEnable.
The problem is that OnEnable can be called more than once during a single run (for example, when closing the window and then reopening the window during a single editor session) causing
EditorApplication.update = (EditorApplication.CallbackFunction)System.Delegate.Combine(new EditorApplication.CallbackFunction(this.DirectorUpdate), EditorApplication.update);
to be called multiple times, meaning my update method (this.DirectorUpdate) will eventually get called multiple times per update, which is causing some serious bugs.
So, the question is how do I check if EditorApplication.update already has my method "inside" of it. (By inside of it, I of course mean it has already been System.Delegate.Combine(d) to the delegate.)
I am aware that there could be other solutions, for example restoring EditorApplication.update to what it was prior when the window closes, however that won't account for all situations (for example, OnEnable also gets called during window refresh and such) and the bug will persist. (Also, what if another window Concatenates with EditorApplication.update while this window is open?) As such, the best solution would be to check if EditorApplication.update is already callin this method BEFORE Delegate.Combine-ing it.
I think you took the complicated road ;)
Subscribing and unsubscribing events and delegates is as simple as using the operators += and -= like
protected void OnEnable()
{
// You can substract your callback even though it wasn't added so far
// This makes sure it is definitely only added once (for this instance)
EditorApplication.update -= DirectorUpdate;
// This basically internally does such a Combine for you
EditorApplication.update += DirectorUpdate;
... // Other stuff
}
private void OnDisable()
{
// Remove the callback once not needed anymore
EditorApplication.update -= DirectorUpdate;
}
This way you can also have multiple instances of this window open and they all will receive the callback individually.
Btw if this is actually about an EditorWindow then afaik you should not use OnEnabled but you would rather use Awake
Called as the new window is opened.
and OnDestroy.
I am not familiar with what System.Delegate.Combine(d) does, but you can consider instead of enabling/disabling your window, destroying and instantiating it every time, and move your code to the Start or the Awake for it to be called only once per window "activation".
Last but not least, use a mighty boolean in the OnDisable so that you can handle the combine execution if your component was ever disabled. Like so:
bool omgIWasDisabled;
protected void OnEnable()
{
if (!omgIWasDisabled) {
EditorApplication.update = (EditorApplication.CallbackFunction)System.Delegate.Combine(new EditorApplication.CallbackFunction(this.DirectorUpdate), EditorApplication.update);
}
... // Other stuff
}
void OnDisable() {
omgIWasDisabled = true;
}
Hope any of those works out.
i'm tryng to have an handle the event every time I click on a chart, on each worksheet of an opened workbook.
I'm using the SheetActivate event like this:
private static void Xlapp_SheetActivate(Object obj_Ws)
{
myCharts.SetAllCharts((XL.Worksheet)obj_Ws);
}
to call a method every time a worksheet is activated, and then the "SetAllCharts" method in the "MyCharts" class that looks like:
internal void SetAllCharts(object obj_Ws)
{
XL.Worksheet Ws = (XL.Worksheet)obj_Ws;
XL.ChartObjects ChObj = Ws.ChartObjects();
if (ChObj.Count > 0)
{
xlapp.StatusBar = ("Setting " + ChObj.Count + " charts");
foreach (XL.ChartObject obj_Chart in ChObj)
{
XL.Chart myChart = obj_Chart.Chart;
myChart.MouseDown += myChart_MouseDown;
}
}
}
for now, the method handler (simply a xlapp.StatusBar message that reports X and Y of the click) works... only once, the first time I activate a worksheet, on the first click on a chart. After that i'm not getting the message box anymore...
What am I doing wrong??
You have to be careful with the lifetime of the COM objects (or at least the magic .NET wrappers around them) that you set event handlers on. They might be created on the fly as you enumerate a collection and garbage collected soon after, losing your event handler in the process. That's why it works once or a few times.
The test for this, before you start thinking about how to write the code, is to keep the objects in some higher object level or global list.
In your code this would be the obj_Chart object you are enumerating. Just make sure these objects are all kept alive.
(I find this stuff a bit confusing because the opposite happen with WPF - I think the event handlers prevent things from garbage collecting?)
I need to synchronize Outlook contacts to another service. I can subscribe to create, change, and delete events as follows:
Outlook.MAPIFolder folderContacts = this.Application.ActiveExplorer().Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
Outlook.Items contacts = folderContacts.Items;
contacts.ItemAdd += Contacts_ItemAdd;
contact.ItemChange += Contacts_ItemChange;
contacts.ItemRemove += Contacts_ItemRemove;
This works perfectly for create and change as I get the item in the event handler:
private void Contacts_ItemAdd(object Item)
{
Outlook.ContactItem contact = (Outlook.ContactItem)Item;
...
}
However, in case of remove event, I don't get the information of the removed item.
private void Contacts_ItemRemove() {
// how to get deleted item or at least it's EntryID?
}
So how do I get the EntryID of the removed item? I use this ID to identify the item in the other service.
You need to maintain the list of items in each folder. And in the ItemRemove event handler you can compare out the list of existing items with yours. I'd recommend reading the following series of articles which gives an example for the NewMailEx event in Outlook (sometimes it is not fired at all, so developers should looks for possible workarounds like that):
Outlook NewMail event unleashed: the challenge (NewMail, NewMailEx, ItemAdd)
Outlook NewMail event: solution options
Outlook NewMail event and Extended MAPI: C# example
Outlook NewMail unleashed: writing a working solution (C# example)
All MAPI based notifications are raised after the action has already occurred. By the time you get the ItemRemove event, the message is already gone.
On the Extended MAPI level (C++ or Delphi only), when an item is deleted, the store provider raises the following fnevTableModified / TABLE_ROW_DELETED notification (you can see it in OutlookSpy (I am its author) if you click IMAPIFolder button and look at the log at the bottom of the GetContentsTable tab). Only PR_INSTANCE_KEY property is available:
ulEventType: fnevTableModified
tab.ulTableEvent: TABLE_ROW_DELETED
tab.propIndex: (PR_INSTANCE_KEY, cb:4, lpb: 0F 3E D3 A4
tab.propPrior: (PR_NULL, null)
tab.row: (cValues : 0
)
You can only make this work by retrieving PR_INSTANCE_KEY for all items in the folder beforehand so that you can map PR_ENTRYID <-> PR_INSTANCE_KEY.
Outlook Object Model does not expose PR_INSTANCE_KEY in the ItemRemove event. If using Redemption is an option (I am also its author), its RDOItems.ItemRemove event passes the instance key as a parameter. PR_INSTANCE_KEY for all items in a folder can be cached beforehand in a single call using RDOItems.MAPITable.ExecSQL method.
We are running into cases where we change a property on our entity but the PropertyChanged event doesn't fire. We are doing this logic as part of a Save and so the problem seems to be the way DevForce queues events like this during a Save.
Looking at the code for LoadingBlock.Dispose(), I see this:
public void Dispose()
{
this._entityManager.FireQueuedEvents();
this._entityManager.IsLoadingEntity = this._wasLoadingEntity;
}
There is a race condition there that you fire the queued events before changing the IsLoadingEntity property. That means that any new events that get generated during the FireQueuedEvents will get queued (since IsLoadingEntity will still be true) but events that get queued then will never get fired since we already fired the queued events (that we knew about). It seems like DevForce should reset the IsLoadingEntity flag before firing the events. I think that would solve our problems.
Here is some code that might help explain our case. I'll use a Merge call instead of SaveChanges since it's easier to use in a unit test:
//Create the main Entity Manager and a test entity
var em = new EntityManager();
var entity = new MyEntity {SID = 123};
em.AttachEntity(entity);
//Create a second copy of the entity and another Entity Manager - this is just so
// we can trigger a merge and see the bad behavior
var copy = new MyEntity { SID = 123, MergeCount = 20 };
var em2 = new EntityManager();
em2.AttachEntity(copy);
//This code is a bit contrived but it's similar to what we are doing in our actual app
em.EntityChanged += (sender, args) =>
{
//If it is a MyEntity that changed and it was from a Merge, increment the MergeCount property
var e = args.Entity as MyEntity;
if (e != null && args.Action == EntityAction.ChangeCurrentAndOriginal)
{
e.MergeCount++;
}
};
//Set up a PropertyChanged event handler to see what properties got changed (according to INotifyPropertyChanged)
var propertiesChanged = new List<string>();
entity.PropertyChanged += (sender, args) => { propertiesChanged.Add(args.PropertyName); };
//Merge the copy entity
em2.CacheStateManager.GetCacheState().Merge(em, RestoreStrategy.Normal);
//At this point, the MergeCount property will be 21 - as expected
Assert.AreEqual(21, entity.MergeCount);
//We should have seen a PropertyChanged event for MergeCount since we changed the property (it was 20 and we set it to 21)
Assert.IsTrue(propertiesChanged.Contains("MergeCount"));
//In the debugger, if we look at em._queuedEvents, we'll see some items in there. One of the items is the PropertyChanged event
// for MergeCount. It 'fired' but was queued...and it will be queued forever because the LoadingBlock is long gone.
I've found that I can just do another Merge from an empty Entity Manager and that will cause the previously queued events to fire. That is an okay workaround in one case that we ran into this. But I fear there might be other places where we are running into this issue and that workaround won't work for us.
You're right that the IsLoadingEntity flag should be cleared at the beginning of the Dispose logic, and we'll open a bug report for this.
If you're able to use the EntityChanging event instead of EntityChanged that also might be a workaround to try. The changing event isn't queued, so execution of the handler causes the PropertyChanged event to be processed before the LoadingBlock is disposed.
I have this code
List<DaSubscription> lstSubscription=new List<DaSubscription>();
for(int i=0;i<20;i++)//20 is just to simulate the behavior
{
DaSubscription Generic=new DaSubscription();
Generic.DataChanged += new DataChangedEventHandler(Generic_DataChanged);
lstSubscription.add(Generic);
}
//EVENT Handler which is raised from a 3rd party library [COM]
void Generic_DataChanged(DaSubscription aDaSubscription, DaItem[] items, ValueQT[] values, int[] results)
{
UpdateDataChangedDTO(items, values);
}
As the same event handler [m_daSubscription_Generic_DataChanged] is assigned to the multiple instance of same class [m_daSubscription]. Question i have is if at the same time multiple instances invokes this handler how will be handled here. will there will be any instance it shall overwrite the data. or the event handler will be separate for each instance.
The event handlers execute seperately. It sounds like your worried about the parameters being overwritten by another call to the handler. That won't happen (I don't think it is even possible). Since it doesn't look like you are accessing any shared objects in the event handler, you should be perfectly safe.