使用IVSInvisibleEditor和IVSPersistDocData,但我如何发布它们?

时间:2014-08-20 14:03:15

标签: editor visual-studio-extensions vspackage

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

1 个答案:

答案 0 :(得分:8)

IVsInvisibleEditor没有关闭它的方法,因为它只使用COM引用计数:当对象获得它对IUnknown.Release()的最终调用时,它使用它作为& #39;提示关闭底层文件。如果您正在使用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的人之间共享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时,该对象已经为我们添加了AddRef。当我们创建RCW时,本机对象获得另一个AddRef()。如果我们不在原生指针上调用Release,那么我们也会泄漏它。但是在上面的代码之后,this.invisibleEditor拥有我们以后可以使用的唯一RCW。一旦你准备关闭这整件事,你所要做的就是打电话:

Marshal.ReleaseComObject(this.invisibleEditor)

基础COM对象将立即被销毁。

相关问题