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对象会被立即销毁。