【问题标题】:Using IVSInvisibleEditor and IVSPersistDocData but how do i release them?使用 IVSInvisibleEditor 和 IVSPersistDocData 但我如何释放它们?
【发布时间】:2014-08-20 14:03:15
【问题描述】:

我在自定义工具窗口中使用 IVsInvisibleEditor 将 t4 文件加载到托管的 vs 编辑器中。我调用传递 t4 文件的 IVsInvisibleEditorManager.RegisterInvisibleEditor 方法,就像 here 一样。然后我使用 GetDocData 方法获取文件内容,然后将它们设置到编辑器的缓冲区。我通过将 getdocdata 的结果转换为 IVsPersistDocData 实例并调用 save 方法来保存编辑器中的更改。在关闭工具窗口时,我尝试通过在 IVsPersistDocData 实例上调用 close 来清理资源。当我尝试再次为同一个文件打开工具窗口时,再次尝试在不可见的编辑器上调用 getdocdata 时出现异常。如果我不调用 IVsPersistDocData 的 close 方法,它会起作用。如何正确关闭所有这些资源(IVsInvisibleEditor、IVsInvisibleEditorManager、IVsPersistDocData),以便在再次尝试使用它们时不会出现异常?

【问题讨论】:

  • 您是遇到实际问题还是只是礼貌 :-) ? IVSpersistDocData.Close 关闭文档。如果你真的想关闭文档,你应该调用它。否则,如果您在 .NET 上运行,则不需要做任何特别的事情(如果您在接口上保留引用,因为如果您有 IntPtr,那么您想释放它们)
  • 好吧,我确实遇到了一个例外,因为我认为 IVsInvisibleEditorManager.RegisterInvisibleEditor 正在将我打开的文件添加到正在运行的文档表中,当我只在 IVSPersistDocData 上使用 close 方法时,我不确定这是否是从正在运行的文档表中删除它,我想我最大的问题是试图了解所有这些部分是如何组合在一起的,我已经阅读了有关该主题的 MSDN 文档,但它没有帮助。你知道有什么更好的资源来尝试学习这些东西吗?
  • LearnVSX (dotneteers.net/blogs/divedeeper/archive/2008/03/17/…) 曾经相当不错,但尚未更新。否则官方文档的某些部分很好:msdn.microsoft.com/en-us/library/bb165336.aspx(例如“编辑器”部分)。还有blogs.msdn.com/b/vsx 不然MSDN上的VSX论坛周围有懂行的人,你可以试试。请注意,您可以使用 IVsRunningDocumentTable 转储 ROT。
  • 在资源方面,几乎所有内容都已过时或未记录。我唯一可行的方法是购买 Reflector 并逐步浏览 Visual Studio 的源代码。
  • @JoshVarty 我过去曾使用过 redgate 的 .reflector 产品,但不知道您可以使用它调试和单步调试 Visual Studio 代码,this 将非常有用!跨度>

标签: editor visual-studio-extensions vspackage


【解决方案1】:

IVsInvisibleEditor 没有 Close 方法,因为它只使用 COM 引用计数:当对象最终调用 IUnknown.Release() 时,它使用它作为关闭底层文件的提示。如果您使用 C++ 编写扩展程序,那么这很容易:只要确保发布它就可以了。但我猜你是用托管代码编写的,这要困难得多。 CLR 使处理这样的对象变得很痛苦。我假设您不是 COM 编组专家,因此对于长时间的讨论,我深表歉意,但了解这一切是如何工作的很重要。

背景: 每当您尝试使用托管代码中的 COM 对象时,CLR 都会创建所谓的“运行时可调用包装器”或 RCW。这是一个小型托管对象,它是本机对象的包装器。在内部,它持有 IUnknown 指针,并“拥有”该对象的 AddRef/Release。这个想法是,当托管代码不再使用 RCW 时,RCW 会被垃圾收集,当这种情况发生时,CLR 然后在底层对象上调用 Release()。

当您调用IVsInvisibleEditorManager.RegisterInvisibleEditor 时,VS 中的本机代码会将指向对象的指针交还给托管代码。然后 CLR 将对象包装在 RCW 中,这意味着除非我们对 Marshal.ReleaseComObject 采取特殊步骤,否则不可见的编辑器将四处浮动,直到 GC 确定 RCW 已消失并且是时候释放()它了。不是你需要的。

所以,只需调用 Marshal.ReleaseComObject,就可以了,对吧?错误的!一般来说,Marshal.ReleaseComObject should be considered dangerous 因为 CLR 在这里有另一个棘手的行为。想象一下,您要打开一个文件的不可见编辑器,当您打开它时,Visual Studio 中的另一个组件也打开了同一个文件,并且管理器返回了 IVsInvisibleEditor 的同一个本机实例。您已经拥有本地对象实例的 RCW。对于这个其他组件,CLR 将“啊哈!其他人已经拥有该对象”并将与您拥有的相同的 RCW 交给他们。如果他们调用Marshal.ReleaseComObject 并破坏了 COM 对象,那将意味着您手头的对象刚刚被僵尸化。这就是ReleaseComObject 危险的原因:只有在您知道自己是唯一持有该 RCW 的人时才能调用它,但默认情况下,CLR 在任何需要的人之间共享 RCW。

这就是为什么您不能从托管代码中正确使用IVsInvisibleEditorManager 的原因:当您调用RegisterInvisibleEditor 时,您将得到一个共享的RCW。你不能在它上面调用 `ReleaseComObject 而不会破坏其他人。选择获得独特的 RCW 并非易事。

正确解决此问题的第一步是为IVsInivisibleEditorManager 定义我们自己的接口。这就是我们在托管 VS 代码的某些部分中定义它的方式:

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("14439CDE-B6CF-4DD6-9615-67E8B3DF380D")]
internal interface IIntPtrReturningVsInvisibleEditorManager
{
    int RegisterInvisibleEditor(
        [MarshalAs(UnmanagedType.LPWStr)] string pszMkDocument,
        IVsProject pProject,
        uint dwFlags,
        IVsSimpleDocFactory pFactory,
        out IntPtr ppEditor);
}

这你可以坚持在你的程序集中,没有问题。这是它在 Microsoft.VisualStudio.Shell 互操作程序集中的定义方式,但有一个关键区别:ppEditor 参数不是一个 COM 对象(这将为我们提供一个共享的 RCW),我们只为该对象返回一个 IntPtr。 CLR 将保持不变,这是关键:我们需要控制如何将其转换为 RCW。然后你要做的是首先获取IVsInvisibleEditorManager 接口,然后将其转换为你自己的接口。这是有效的,因为将 RCW 转换为 COM 接口很神奇:只要底层对象说它支持接口(通过指定的 GUID 查找),CLR 就会伪造它并说 RCW 可以转换为接口——甚至是一个你定义了自己。然后,您可以调用RegisterInvisibleEditor 并返回一个 IntPtr,然后为其创建一个唯一的 RCW。这是获取文档数据的代码:

var invisibleEditorManager = (IIntPtrReturningVsInvisibleEditorManager)serviceProvider.GetService(typeof(SVsInvisibleEditorManager));
var invisibleEditorPtr = IntPtr.Zero;
Marshal.ThrowExceptionForHR(invisibleEditorManager.RegisterInvisibleEditor(filePath, null, 0, null, out invisibleEditorPtr));

try
{
    this.invisibleEditor = (IVsInvisibleEditor)Marshal.GetUniqueObjectForIUnknown(invisibleEditorPtr);

    var docDataPtr = IntPtr.Zero;
    Marshal.ThrowExceptionForHR(invisibleEditor.GetDocData(fEnsureWritable: 0, riid: typeof(IVsTextLines).GUID, ppDocData: out docDataPtr));

    try
    {
        var docData = Marshal.GetObjectForIUnknown(docDataPtr);

        // use docData how you want, probably by getting the text of it               
    }
    finally
    {
        Marshal.Release(docDataPtr);
    }
}
finally
{
    // Since we have a unique RCW holding onto the object, we must release our direct pointer as well
    Marshal.Release(invisibleEditorPtr);
}

这些 finally 块很关键:当我们返回一个表示 COM 对象的 IntPtr 时,该对象已经为我们添加了引用。当我们创建 RCW 时,本机对象会获得另一个 AddRef()。如果我们不在本地指针上调用 Release,那么我们也会泄漏它。但是在上面的代码之后,this.invisibleEditor 保存了我们以后可以使用的唯一 RCW。一旦你准备好关闭这整个事情,你所要做的就是打电话:

Marshal.ReleaseComObject(this.invisibleEditor)

并且底层的COM对象会被立即销毁。

【讨论】:

  • 你是对的,我不是 COM 专家,我正在使用托管代码,但非常感谢长时间的讨论!这完美地解决了我的问题并解释了很多。多次重新打开文件时不再出现异常。
  • 我早就想写这个了,你的问题变成了强制函数。 :-)
  • 接下来的问题当然是:在使用托管代码时,VS SDK 提供的数百个接口中有多少应该以这种方式重新定义?这些是什么?为什么这从未记录在案?没有冒犯和很好的答案,但很遗憾在这里发现这些东西:-)
  • 这是我唯一想到的。关于诊断的好点,我会在内部发送一些电子邮件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 2020-05-23
相关资源
最近更新 更多