9

我試圖爲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的打擊感,但後來我可以保留我目前的外部程序來修改文件;加上我不確定這些攻擊最終是否容易實現... –

回答

5

總體思路是讓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很快就會頒發遠! –