防止Outlook VSTO中的重复事件处理程序调用

时间:2016-07-21 03:31:18

标签: c# events outlook outlook-addin

我正在为Outlook 2010编写一个VSTO(尽管它需要在2010-2016上工作,而且大部分都是如此)。我遇到了一个奇怪的问题,据我所知,事件处理程序永远不会被删除。这意味着我无法避免反复调用事件处理程序,这是愚蠢和浪费。

有问题的代码发生在Explorer的SelectionChange事件的事件处理程序中。处理程序检查选择是否为MailItem,如果是,则确保ReplyReplyAllForward事件具有处理程序。由于可以多次选择给定项目,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中的错误吗?有人知道如何避免重复的事件处理程序调用吗?

2 个答案:

答案 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对象将被释放和删除)。这对相关代码有两个相关影响:

  1. 由于垃圾收集不是即时的,因此现有事件处理程序的旧RCW(和COM对象)会延迟,Outlook仍然可以触发其COM对象上的事件。这就是多次调用事件处理程序的原因。
  2. 因为RCW超出了SelectionChangeHandler底部的范围,所以垃圾收集扫描所有RCW(和事件处理程序)并释放所有COM对象只是时间问题。此时,该电子邮件不会附加任何事件。
    • 在实践中,我的测试发生在足够短的时间内,我更有可能获得多个实时RCW而不是没有,选择一个邮件项目而不与它交互(或选择其他任何东西)足够长的时间来触发垃圾事实上,当我点击回复时,集合扫描会导致MailItemResponseHandler 未被调用
  3. @DmitryStreblechenko让我朝着正确的方向努力解决这个问题,但需要进行一些实验才能弄明白。首先,相关的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)。但是,如果我打开检查器,请切换回资源管理器并选择其他电子邮件,然后返回检查器并单击“答复”,它不起作用。如果在取消选择该电子邮件时检查员打开相关电子邮件,我需要避免删除事件处理程序(然后我需要在检查员关闭时将其删除,除非在资源管理器中仍然选择了电子邮件) 。凌乱,但我会解决它。