VSX:如何重新使用现有的XML编辑器来处理转换为XML的二进制文件?

问题描述:

我试图为VS2017(用C#)创建一个扩展VSPackage,它将二进制数据转换为XML,在默认VS XML编辑器和XML语言服务中打开它,然后在保存时将其转换回二进制文件。VSX:如何重新使用现有的XML编辑器来处理转换为XML的二进制文件?

但是,我有麻烦来列出哪些步骤将需要这个。我想到了创建在编辑器厂一个新的编辑器,现在以下为:

  • 创建新的文本缓冲
  • 与转换XML数据
  • 创建核心编辑器给它
  • 与文本喂它缓冲

现在我的尝试是这样的:

private MyPackage _package; // Filled via constructor 
private IServiceProvider _serviceProvider; // Filled via SetSite 

public int CreateEditorInstance(uint grfCreateDoc, string pszMkDocument, string pszPhysicalView, 
    IVsHierarchy pvHier, uint itemid, IntPtr punkDocDataExisting, out IntPtr ppunkDocView, 
    out IntPtr ppunkDocData, out string pbstrEditorCaption, out Guid pguidCmdUI, out int pgrfCDW) 
{ 
    // Initialize and validate parameters. 
    ppunkDocView = IntPtr.Zero; 
    ppunkDocData = IntPtr.Zero; 
    pbstrEditorCaption = String.Empty; 
    pguidCmdUI = Guid.Empty; 
    pgrfCDW = 0; 
    VSConstants.CEF createDocFlags = (VSConstants.CEF)grfCreateDoc; 
    if (!createDocFlags.HasFlag(VSConstants.CEF.OpenFile) && !createDocFlags.HasFlag(VSConstants.CEF.Silent)) 
     return VSConstants.E_INVALIDARG; 
    if (punkDocDataExisting != IntPtr.Zero) 
     return VSConstants.VS_E_INCOMPATIBLEDOCDATA; 

    // Create a sited IVsTextBuffer storing the converted data with the XML data and language service set. 
    IVsTextLines textLines = _package.CreateComInstance<VsTextBufferClass, IVsTextLines>(); 
    SiteObject(textLines); 
    string xmlText = BinaryXmlData.GetXmlString(pszMkDocument); 
    textLines.InitializeContent(xmlText, xmlText.Length); 
    ErrorHandler.ThrowOnFailure(textLines.SetLanguageServiceID(ref Guids.XmlLanguageServiceGuid)); 

    // Instantiate a sited IVsCodeWindow and feed it with the text buffer. 
    IVsCodeWindow codeWindow = _package.CreateComInstance<VsCodeWindowClass, IVsCodeWindow>(); 
    SiteObject(codeWindow); 
    codeWindow.SetBuffer(textLines); 

    // Return the created instances to the caller. 
    ppunkDocView = Marshal.GetIUnknownForObject(codeWindow); 
    ppunkDocData = Marshal.GetIUnknownForObject(textLines); 

    return VSConstants.S_OK; 
} 

private void SiteObject(object obj) 
{ 
    (obj as IObjectWithSite)?.SetSite(_serviceProvider); 
} 

// --- CreateComInstance is a method on my package ---- 
internal TInterface CreateComInstance<TClass, TInterface>() 
{ 
    Guid guidT = typeof(TClass).GUID; 
    Guid guidInterface = typeof(TInterface).GUID; 

    TInterface instance = (TInterface)CreateInstance(ref guidT, ref guidInterface, typeof(TInterface)); 
    if (instance == null) 
     throw new COMException($"Could not instantiate {typeof(TClass).Name}/{typeof(TInterface).Name}."); 

    return instance; 
} 

当我尝试用我的编辑器明确打开文件时,它指出“无法使用所选编辑器打开文件。请选择另一位编辑。“这条消息对我来说没有任何意义,我试图用XML编辑器打开XML数据,但它仍然试图用二进制数据打开文本编辑器。所有我能想到的给它转换后的数据,显然这种方式是不正确的。

  • 我怎么能添加一些步骤,其间获取二进制数据,迅速将其转换为XML,然后将其输送给XML编辑器?
  • 如何在XML编辑器保存文件时将其存储为二进制文件?
  • 是否有可能重用XML为此编辑和语言服务?

对不起,如果这些问题需要冗长的答案;如果我可以指向正确的方向,或者已经做了一些类似的开源扩展(在将文件数据显示在VS代码编辑器中之前进行转换),我会很高兴。

+0

不知道,但我会围绕它通过破解它保存在一个临时目录中的XML文件,在编辑器中打开它,并观察该文件变化。不够优雅,但很快就可以实现。也许有人知道如何让优雅的路线工作... – Will

+0

是的,我想到了VSX的打击感,但后来我可以保留我目前的外部程序来修改文件;加上我不确定这些攻击最终是否容易实现... –

总体思路是让Xml Editor执行通常的操作:打开文档

就你而言,如果我理解正确,你没有一个物理的Xml文档,所以你必须创建一个。文档是在Visual Studio的Running Object Table中注册的东西(它不一定是物理文件)。

一旦你有一个文件,你可以让壳打开它。您可以再次使用ROT来处理BeforeSave and AfterSave events。下面是一些示例代码,应该做的这一切:

public int CreateEditorInstance(uint grfCreateDoc, string pszMkDocument, string pszPhysicalView, IVsHierarchy pvHier, uint itemid, IntPtr punkDocDataExisting, out IntPtr ppunkDocView, out IntPtr ppunkDocData, out string pbstrEditorCaption, out Guid pguidCmdUI, out int pgrfCDW) 
{ 
    ppunkDocView = IntPtr.Zero; 
    ppunkDocData = IntPtr.Zero; 
    pbstrEditorCaption = null; 
    pguidCmdUI = Guid.Empty; 
    pgrfCDW = 0; 

    // create your virtual Xml buffer 
    var data = Package.CreateComInstance<VsTextBufferClass, IVsTextLines>(); 
    SiteObject(data); 

    // this is where you're supposed to build your virtual Xml content from your binary data 
    string myXml = "<root>blah</root>"; 
    data.InitializeContent(myXml, myXml.Length); 
    var dataPtr = Marshal.GetIUnknownForObject(data); 

    // build a document and register it in the Running Object Table 
    // this document has no hierarchy (it will be handled by the 'Miscellaneous Files' fallback project) 
    var rotFlags = _VSRDTFLAGS.RDT_ReadLock | _VSRDTFLAGS.RDT_VirtualDocument; 

    // come up with a moniker (which will be used as the caption also by the Xml editor) 
    // Note I presume the moniker is a file path, wich may not always be ok depending on your context 
    var virtualMk = Path.ChangeExtension(pszMkDocument, ".xml"); 
    var rot = (IVsRunningDocumentTable)_sp.GetService(typeof(SVsRunningDocumentTable)); 
    int hr = rot.RegisterAndLockDocument((uint)rotFlags, virtualMk, null, VSConstants.VSITEMID_NIL, dataPtr, out uint docCookie); 
    if (hr != 0) 
     return hr; 

    try 
    { 
     // ask Visual Studio to open that document 
     var opener = (IVsUIShellOpenDocument)_sp.GetService(typeof(SVsUIShellOpenDocument)); 
     var view = VSConstants.LOGVIEWID_Primary; 
     opener.OpenDocumentViaProject(virtualMk, ref view, 
      out Microsoft.VisualStudio.OLE.Interop.IServiceProvider psp, 
      out IVsUIHierarchy uiHier, 
      out uint id, 
      out IVsWindowFrame frame); 
     if (frame != null) 
     { 
      // Hmm.. the dirty bit (the star after the caption) is not updated by the Xml Editor... 
      // If you close the document (or close VS), it does update it, but it does not react when we type in the editor. 
      // This is unexpected, so, let's do the "dirty" work ourselves 
      // hook on text line events from the buffer 
      var textLineEvents = new TextLineEvents((IConnectionPointContainer)data); 

      // we want to know when to unadvise, to hook frame events too 
      ((IVsWindowFrame2)frame).Advise(textLineEvents, out uint frameCookie); 

      textLineEvents.LineTextChanged += (sender, e) => 
      { 
       // get the dirty bit and override the frame's dirty state 
       ((IVsPersistDocData)data).IsDocDataDirty(out int dirty); 
       frame.SetProperty((int)__VSFPROPID2.VSFPROPID_OverrideDirtyState, dirty != 0 ? true : false); 
      }; 

      // now handle save events using the rot 
      var docEventHandler = new RotDocumentEvents(docCookie); 
      docEventHandler.Saving += (sender, e) => 
      { 
       // this is where you can get the content of the data and save your binary data back 
       // you can use Saved or Saving 

      }; 

      docEventHandler.Saved += (sender, e) => 
      { 
       // manual reset of dirty bit... 
       frame.SetProperty((int)__VSFPROPID2.VSFPROPID_OverrideDirtyState, false); 
      }; 
      rot.AdviseRunningDocTableEvents(docEventHandler, out uint rootCookie); 

      frame.Show(); 
     } 
    } 
    finally 
    { 
     rot.UnlockDocument((uint)_VSRDTFLAGS.RDT_ReadLock, docCookie); 
    } 
    return VSConstants.S_OK; 
} 

private class TextLineEvents : IVsTextLinesEvents, IVsWindowFrameNotify, IVsWindowFrameNotify2 
{ 
    public event EventHandler LineTextChanged; 
    private uint _cookie; 
    private IConnectionPoint _cp; 

    public TextLineEvents(IConnectionPointContainer cpc) 
    { 
     var textLineEventsGuid = typeof(IVsTextLinesEvents).GUID; 
     cpc.FindConnectionPoint(ref textLineEventsGuid, out _cp); 
     _cp.Advise(this, out _cookie); 
    } 

    public void OnChangeLineText(TextLineChange[] pTextLineChange, int fLast) => LineTextChanged?.Invoke(this, EventArgs.Empty); 

    public int OnClose(ref uint pgrfSaveOptions) 
    { 
     _cp.Unadvise(_cookie); 
     return VSConstants.S_OK; 
    } 

    public void OnChangeLineAttributes(int iFirstLine, int iLastLine) { } 
    public int OnShow(int fShow) => VSConstants.S_OK; 
    public int OnMove() => VSConstants.S_OK; 
    public int OnSize() => VSConstants.S_OK; 
    public int OnDockableChange(int fDockable) => VSConstants.S_OK; 
} 

private class RotDocumentEvents : IVsRunningDocTableEvents3 
{ 
    public event EventHandler Saved; 
    public event EventHandler Saving; 

    public RotDocumentEvents(uint docCookie) 
    { 
     DocCookie = docCookie; 
    } 

    public uint DocCookie { get; } 

    public int OnBeforeSave(uint docCookie) 
    { 
     if (docCookie == DocCookie) 
     { 
      Saving?.Invoke(this, EventArgs.Empty); 
     } 
     return VSConstants.S_OK; 
    } 

    public int OnAfterSave(uint docCookie) 
    { 
     if (docCookie == DocCookie) 
     { 
      Saved?.Invoke(this, EventArgs.Empty); 
     } 
     return VSConstants.S_OK; 
    } 

    public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => VSConstants.S_OK; 
    public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => VSConstants.S_OK; 
    public int OnAfterAttributeChange(uint docCookie, uint grfAttribs) => VSConstants.S_OK; 
    public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) => VSConstants.S_OK; 
    public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) => VSConstants.S_OK; 
    public int OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld, string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew) => VSConstants.S_OK; 
} 
+0

哇,这个作品完美!还剩下一个小问题。是否有可能使“未保存的更改”星号在编辑标题中工作?而且:你是怎么知道这一切的,我应该阅读的任何文学作品? :D –

+0

你说得对。这是意想不到的,也许是因为它不是一个真正的文件?缓冲区确实设置了脏位,但是当我们改变文本时XmlEditor不使用它(当我们关闭文件/项目/解决方案时,我们得到一个警告并且设置了*),XmlEditor就会使用它。我添加了一个解决方法...关于VS开发,除了官方SDK(这是池)之外,没有特定的文献。只需经验(以及使用诸如Reflector,ILSpy等工具的Visual Studio程序集中的大量探索:-)注意,这是“旧”接口。较新的WPF等人。更好。 –

+0

这绝对是奇怪的行为,我每分钟都会尝试一下您的解决方案。与此同时,我试图寻找一个错误,出于某种神秘的原因,保存时希望将带有XML内容的Temp.txt写入当前工作目录中 - 弹出一个消息框,它不能存储某些东西在C:\ Windows \ system32 \ Temp.txt或VS devenv.exe路径中。我绝对没有添加这样的代码,我在内存中进行了转换,似乎在保存和保存之间发生......也许我只是尝试将当前目录更改为实际的临时路径...:O Bounty很快就会颁发远! –