我正在为Outlook 2010编写一个VSTO(尽管它需要在2010-2016上工作,而且大部分都是如此)。我遇到了一个奇怪的问题,据我所知,事件处理程序永远不会被删除。这意味着我无法避免反复调用事件处理程序,这是愚蠢和浪费。
有问题的代码发生在Explorer的SelectionChange
事件的事件处理程序中。处理程序检查选择是否为MailItem
,如果是,则确保Reply
,ReplyAll
和Forward
事件具有处理程序。由于可以多次选择给定项目,SelectionChange
处理程序首先删除Reply
/ ReplyAll
/ Forward
事件处理程序,与显示的模式保持一致{{3 (当您不控制事件承载类实现时,阻止事件处理程序被挂钩两次。)
问题是,这不会阻止Reply
(或其他响应操作)事件处理程序在SelectionChange
事件处理程序触发的每个实例中被调用一次。这很快就达到了愚蠢的调用次数。我认为这可能是一个同步问题,所以我将事件处理程序移除并添加到lock
块中,但无济于事。
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);
}
}
被调用的事件处理程序太多次了:
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);
}
这个函数最终会比弹出一个对话框更有用,但这是一个方便的检查“我找到了正确的原始消息吗?”。我不应该一遍又一遍地使用相同的对话框,而且我是。
我做错了吗?这是Outlook或VSTO中的错误吗?有人知道如何避免重复的事件处理程序调用吗?
答案 0 :(得分:1)
首先,必须在类级别而不是本地级别声明mailE变量,以防止它被垃圾回收。
其次,在设置事件之前,必须使用Marshal.ReleaseComObject释放旧值(mailE)。
答案 1 :(得分:1)
事件处理程序实际上并未附加到Outlook使用的MAPI项目。相反,它被附加到一个名为Runtime Callable Wrapper(RCW)的.NET对象,它包装了一个COM对象。由于RCW的工作方式,获得多个似乎是同一个对象的引用 - 例如,通过获取activeExplorer.Selection()[1]
两次 - 给出了围绕不同COM对象的多个RCW。这意味着我试图删除事件的Outlook.MailItem
(或Outlook.ItemEvents_10_Event
)实际上没有任何事件;每次SelectionChangeHandler
解雇时都是新制作的。
相关地,因为对RCW包装的COM对象的唯一引用是RCW本身,所以让引用RCW的所有变量超出范围(或者不再引用RCW)将导致RCW被垃圾收集(在哪个COM对象将被释放和删除)。这对相关代码有两个相关影响:
SelectionChangeHandler
底部的范围,所以垃圾收集扫描所有RCW(和事件处理程序)并释放所有COM对象只是时间问题。此时,该电子邮件不会附加任何事件。
MailItemResponseHandler
未被调用。MailItem
需要全局引用,因此它对事件RCW的引用不会超出范围,而且重要的是,当SelectionChangeHandler
时它的RCW仍然可以直接引用}再次被调用。我重命名了变量selectedMail
并在类级别引用它,如:
Outlook.ItemEvents_10_Event selectedMail;
然后,我修改了SelectionChangeHandler
,以便无论何时使用当前选中的MailItem
调用它,它首先会从selectedMail
中删除所有事件处理程序,然后才会点selectedMail
到新选择的项目。先前由selectedMail
引用的RCW有资格进行垃圾收集,但它没有事件处理程序,因此我们并不在意。 SelectionChangeHandler
然后将相关的事件处理程序添加到selectedMail
引用的新RCW。
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);
}
}
}
根据Dmitri的回答和评论,在尝试删除事件处理程序之前,我尝试在Marshal.ReleaseComObject(selectedMail)
的旧值上调用selectedMail
。这有点帮助,但是COM对象不会立即释放,或者Outlook仍然可以通过它们调用事件处理程序,因为如果我在点击Reply之前在短时间内多次选择给定的电子邮件,事件仍然会多次触发。
还有一个问题需要解决。如果我打开一个检查器并在那里点击“回复”而不更改我在Explorer中的选择,它就可以正常工作(调用MailItemResponseHandler
)。但是,如果我打开检查器,请切换回资源管理器并选择其他电子邮件,然后返回检查器并单击“答复”,它不起作用。如果在取消选择该电子邮件时检查员打开相关电子邮件,我需要避免删除事件处理程序(然后我需要在检查员关闭时将其删除,除非在资源管理器中仍然选择了电子邮件) 。凌乱,但我会解决它。