【问题标题】:Get Hashcode for Excel Workbook in VSTO to enable buttons based on state在 VSTO 中获取 Excel 工作簿的哈希码以启用基于状态的按钮
【发布时间】:2013-04-03 03:37:14
【问题描述】:

我正在为 excel 创建一个 VSTO 功能区插件,并且我在我的应用程序中存储了一些工作簿状态信息,用于更新已启用的可视按钮。考虑到可能有多个工作簿,我将此状态对象存储在 ThisAddIn 类的字典中。我的问题是我不知道如何为工作簿获取唯一的哈希/密钥/Guid,因为我得到的只是一个不断更改哈希的 COM 包装器。很公平,我完全理解这一点。

我使用了很长时间的一个解决方案是创建一个 guid 并将其存储在工作簿的 CustomDocumentProperties 中,并根据它作为键映射状态。这至少有效,但是如果我创建工作簿的副本并在同一个应用程序实例中打开它并且现在有多个具有相同 guid 的工作簿,它将失败..

我现在有了一个想法,我想我可以在 Workbook_Open 事件上刷​​新这个 Guid。但这似乎仍然是一个狡猾的解决方案。

我在这里找到的第二个解决方案: http://social.msdn.microsoft.com/Forums/en-US/vsto/thread/04efa74d-83bd-434d-ab07-36742fd8410e/

所以我使用了那些家伙的代码并创建了这个:

public static class WorkbookExtensions
{
    public static IntPtr GetHashery(this msExcel.Workbook workbook)
    {
        IntPtr punk = IntPtr.Zero;
        try
        {
            punk = Marshal.GetIUnknownForObject(workbook);
            return punk;
        }
        finally
        {
            //Release to decrease ref count
            Marshal.Release(punk);
        }
    }
}

它运行得很好几分钟,直到它在访问 Application.ActiveWorkbook 时开始给我一个臭名昭著的错误“无法使用已与其底层 RCW 分离的 COM 对象”。

这是引用 Workbook COM 对象的安全方式吗? 如果我有两个功能区应用程序都使用此方法来获取单个工作簿 GUID,该怎么办? 如果其中一个应用程序在我的状态对象上运行垃圾收集器,它调用终结器来调用 Marshal.FinalReleaseComObject(workbook),该怎么办? 有什么方法可以获取工作簿的引用计数,这样我就不会在其他功能区应用程序完成之前调用 FinalRelease? 清理 VSTO 中的 Workbook COM 对象以保持与这些其他应用公平竞争的最佳做法是什么?

当然,我不是第一个希望根据工作簿状态启用按钮的人,其他人是如何做到这一点的?我在 Stack Overflow 上查看了其他几篇文章,但没有一篇对 Workbook Guid 解决方案有帮助。

我正在使用功能区设计器,并连接到工作簿加载和停用事件。

提前致谢,希望我提供了所有详细信息。

【问题讨论】:

  • 无法使用与底层 RCW 分离的 COM 对象 - 是的,我讨厌这个错误,它是一个 PITA,请阅读:jake.ginnivan.net/vsto-com-interop
  • 为什么在同一个函数中释放IUnknown指针?尝试保留它并仅在工作簿关闭或类似情况时释放它。
  • 太棒了,谢谢@JeremyThompson!从那篇文章中可以学到很多东西。 VSTOContrib 看起来很适合我的下一个项目,但它并不能解决我眼前的问题。 @SimonMourier - 我对本机代码不是很了解,但我想如果我不在这里释放指针,那么我会忘记它以便稍后发布?

标签: c# .net excel com vsto


【解决方案1】:

我最终通过简单地将 IntPtr 转换为 long 来解决这个问题,然后处理 IntPtr 不会影响我。我不需要保留 IntPtr,因为我真正需要的只是工作簿的独特之处。

以下代码允许我存储特定于工作簿的状态信息,因此我可以根据自定义对象工作簿状态更新功能区中按钮的视觉状态。您可以在自定义 WorkbookState 类中存储您想要的任何信息,但通常它是您不希望保留在电子表格本身中的特定于会话的信息。

单独的工作簿扩展:

public static class WorkbookExtensions
{
    public static long GetHashery(this msExcel.Workbook workbook)
    {
        if (workbook == null)
        {
            throw new ArgumentNullException("workbook");
        }

        IntPtr pUnknown = IntPtr.Zero;
        try
        {
            pUnknown = Marshal.GetIUnknownForObject(workbook);
            return pUnknown.ToInt64();
        }
        finally
        {
            // GetIUnknownForObject causes AddRef.
            if (pUnknown != IntPtr.Zero)
            {
                Marshal.Release(pUnknown);
            }
        }
    }
}

然后在我的 VSTO/ExcelDna ThisAddIn 类中,我使用上述方法存储所有工作簿状态的映射,以找到唯一的工作簿哈希键:

private Dictionary<long, WorkbookState> _workbookStates = new Dictionary<long, WorkbookState>();
public WorkbookState WorkbookState
{
    get
    {
        long hash = Application.ActiveWorkbook.GetHashery();
        WorkbookState state;
        if (!_workbookStates.TryGetValue(hash, out state))
        {
            state = _workbookStates[hash] = new WorkbookState();
        }
        return state;
    }
}

当然,现在我可以从功能区应用程序的任何位置访问我的 WorkbookState,只需调用 ThisAddIn.WorkbookState

【讨论】:

  • 这会在从 Task.Run 调用时为 ActiveWorkbook 提供另一个哈希值。
  • 我从工作表和功能区调用哈希方法得到了两个不同的 long
  • 嗯,你提出了很好的观点@ChrisHayes 和 user3009578。从那以后,我了解到 ActiveWorkbook 只能从 UI 线程调用。由于 ActiveWorkbook 可以更改,因此从任何线程而不是 UI 线程引用它是一个不好的策略。因此,我发现最好的策略是将 ActiveWorkbook 缓存在用户操作事件处理程序中,并将该 ActiveWorkbook 用于所有线程。如果后台线程需要在没有用户操作的情况下访问 ActiveWorkbook,则通过侦听 Application.ActiveWorkbookChanged 事件来缓存您自己的副本。无需投票^_^
【解决方案2】:

您也可以使用 workbook.Application.Hwnd 但这只会在您使用 Excel 2013/2016 时对您有所帮助,它会为每个工作簿打开新窗口。

Excel 2010 在同一窗口下打开所有工作簿。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多