VsxHowTo — 把Windows Forms Designer作為自己的編輯器(2)

  • 2019 年 10 月 5 日
  • 筆記

我們在上一篇文章里利用Windows Forms Designer做了一個簡單的表單設計器,但這個設計器還存在一些問題,比如控制項不能自動命名;文檔窗口不會自動加入dirty標記;不能undo/redo和copy/paste;不能保存和讀取數據等等。這一篇我們來逐一解決這些問題。

控制項自動命名

從toolbox里拖入一個控制項時,如果想讓控制項自動命名,我們需要往DesignerHost里加一個INameCreationService的服務,我沒有研究過為什麼BasicDesignerLoader不默認幫我們加上,不過好在msdn上有一個例子,我稍微簡化了一下它,如下:

using System;using System.ComponentModel;using System.ComponentModel.Design.Serialization;using System.Collections.Generic;using System.Collections; namespace Company.WinFormDesigner{    class NameCreationService : INameCreationService    {        #region INameCreationService 成員         public string CreateName(IContainer container, Type dataType)        {            IList<string> list = new List<string>();            for (int i = 0; i < container.Components.Count; i++)            {                list.Add(container.Components[i].Site.Name);            }                        return CreateNameByList(list, dataType.Name);         }         public bool IsValidName(string name)        {            //name is always valid            return true;        }        public void ValidateName(string name)        {            //do nothing        }         #endregion         /// <summary>        /// 創建一個基於baseName並且在array中不存在的名稱        /// </summary>        public static string CreateNameByList(IList<string> list, string baseName)        {            int uniqueID = 1;            bool unique = false;            while (!unique)            {                unique = true;                foreach (string s in list)                {                    if (s.StartsWith(baseName + uniqueID.ToString()))                    {                        unique = false;                        uniqueID++;                        break;                    }                }            }            return baseName + uniqueID.ToString();        }    }}

我們需要把它的實例加到DesignerHost中。在DesignerLoader中重寫Initialize方法:

protected override void Initialize(){    base.Initialize();              LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());        }

按ctrl + F5運行,啟動VS實驗室之後,打開一個.form文件,在工具箱中拖入一個Button,看,是不是已經自動有了Name了呢?

支援Undo/Redo

這個問題糾結了我好久,因為實在有太多的方法實現Undo和Redo了,最後通過reflector才找到最佳的解決方案。和控制項的自動命名一樣,不能Undo/Redo也是由於少了一個Service:ComponentSerializationService。.net framework提供了一個默認的實現:System.ComponentModel.Design.Serialization.CodeDomComponentSerializationService。我一度以為不能直接使用這個類,因為我們並沒有CodeDom,不過在嘗試了很多方法後,最後在無奈的情況下才去使用這個類,居然成功了,看來絕不能以貌取人。由於已經有實現了,我們只需要把它加到DesignerHost里就行了,依然要修改DesignerLoader的Initialize方法:

protected override void Initialize(){    base.Initialize();        LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));}

大家可以測試一下效果,真的可以undo/redo了,呵呵。

支援Copy/Paste

到目前為止,我們的設計器似乎不支援複製和粘貼。選中一個控制項後,複製的按鈕是可用的,但粘貼卻一直是灰色的,這是怎麼回事呢?沒錯,你猜對了,DesignerHost里少了相應的Service。這個Service叫做System.ComponentModel.Design.Serialization.IDesignerSerializationService。這個介面有兩個方法,分別處理序列化和反序列化的工作,我沒有找到.net framework里公開的實現,所以我們不得不自己實現這個介面了。不過好在我們可以充分利用undo/redo里提到的ComponentSerializationService來幫助我們去序列化和反序列化控制項。我的實現類如下:

using System;using System.ComponentModel.Design.Serialization;using System.ComponentModel.Design;using System.Collections; namespace Company.WinFormDesigner{    class DesignerSerializationService : IDesignerSerializationService    {        private IServiceProvider serviceProvider;        private ComponentSerializationService serializer;         public DesignerSerializationService(IServiceProvider sp)        {            serviceProvider = sp;            serializer = serviceProvider.GetService(typeof(ComponentSerializationService)) as ComponentSerializationService;        }         #region IDesignerSerializationService 成員         public ICollection Deserialize(object serializationData)        {            if (serializer != null && serializationData is SerializationStore)            {                return serializer.Deserialize((SerializationStore)serializationData);            }            return null;         }         public object Serialize(ICollection objects)        {            if (objects == null)            {                objects = new object[0];            }            if (serializer != null)            {                SerializationStore store = serializer.CreateStore();                using (store)                {                    foreach (object obj in objects)                    {                        serializer.Serialize(store, obj);                    }                }                return store;            }            return null;        }         #endregion    }}

接下來,我們需要把它加到DesignerHost里:

protected override void Initialize(){    base.Initialize();    LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));    LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));}

SetDirty

設計器里的控制項改變之後,我們希望在document window那裡能出現dirty的標記。為了實現這個功能,我們需要修改在上一篇中創建的DocumentData類,這個類實現了IVsPersistDocData介面,其中IsDocDataDirty就是用來判斷當前的文檔是否是dirty的。讓我們修改一些這個類:

namespace Company.WinFormDesigner{    class DocumentData : IVsPersistDocData    {              ....       public bool Dirty { get; set; }                    int IVsPersistDocData.IsDocDataDirty(out int pfDirty)        {            pfDirty = Dirty ? 1 : 0;            return VSConstants.S_OK;        }        ....    }}

我們在DocumentData里添加了一個Dirty的bool屬性,並且修改了IVsPersistDocData.IsDocDataDirty,根據Dirty的屬性值來確定pfDirty的值。

下面我們需要在控制項修改的時候給這個Dirty屬性賦值。怎樣捕獲控制項修改的事件呢?可以通過IComponentChangeService服務的ComponentChanged事件來捕獲。在DesignerLoader的初始化方法里,取得這個服務,並添加ComponentChanged的事件處理:

protected override void Initialize(){    base.Initialize();     LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));    LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));    IComponentChangeService service = LoaderHost.GetService(typeof(IComponentChangeService)) as IComponentChangeService;    service.ComponentChanged += new ComponentChangedEventHandler(service_ComponentChanged); }

然後就可以在service_ComponentChanged方法里給DocumentData的Dirty屬性賦值了,當然,在這之前,我們需要修改一下DesignerLoader的構造函數,以便我們可以取得DocumentData的引用:

class DesignerLoader : BasicDesignerLoader{    private DocumentData _data;    public DesignerLoader(DocumentData data)    {        _data = data;    }     protected override void Initialize()    {        base.Initialize();         LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());        LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));        LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));        IComponentChangeService service = LoaderHost.GetService(typeof(IComponentChangeService)) as IComponentChangeService;        service.ComponentChanged += new ComponentChangedEventHandler(service_ComponentChanged);     }     void service_ComponentChanged(object sender, ComponentChangedEventArgs e)    {        _data.Dirty = true;    }     protected override void PerformFlush(IDesignerSerializationManager serializationManager)    {    }     protected override void PerformLoad(IDesignerSerializationManager serializationManager)    {        LoaderHost.Container.Add(new UserControl());    }}

最後,記得要修改EditorFactory里構造DesignerLoader時的程式碼:

...var data = new DocumentData();var designerLoader = new DesignerLoader(data);...

編譯並運行項目,可以然後拖一個控制項到設計器,Dirty的標記出來了。

保存/載入文檔

要實現文檔的保存與載入,需要讓DocumentData實現IVsPersistDocData和IPersistFileFormat介面。在上次我們已經簡單實現了IVsPersistDocData了,現在我們需要再讓他實現IPersistFileFormat介面。用於保存的方法是IVsPersistDocData.SaveDocData和IPersistFileFormat.Save。

保存文檔,無非就是把DesignerHost中正在設計的UserControl以及它的子控制項用某種方式序列化到文件里,而載入文檔則相反:讀取文件,並反序列化成控制項,並把控制項加到DesignerHost里。

我們定義一個類用於處理控制項的序列化和反序列化:

using System.Windows.Forms; namespace Company.WinFormDesigner{    class ControlSerializer    {        public string DocumentMoniker { get; set; }         public ControlSerializer(string documentMoniker)        {            DocumentMoniker = documentMoniker;        }         public Control Deserialize()        {            /*             * 讀取文件DocumentMoniker的內容,並把它反序列化成Control。             * 下面的程式碼只是模擬這個過程,並沒有真正讀取文件並反序列化             * 注意控制項有可能是複合控制項,這種控制項的子控制項是不需要加到DesignerHost里的,             * 所以我給控制項的Tag屬性設了一個Designable的字元串,目的是在後面             * 區分出哪些控制項是需要設計的,哪些控制項是屬於不需要設計的             * */             const string designable = "Designable";            UserControl uc = new UserControl();            uc.Controls.Add(new Label { Text = "Label1", Tag = designable });            uc.Controls.Add(new TextBox { Tag = designable, Location = new System.Drawing.Point(200, 500) });            return uc;        }         public void Serialize(Control control)        {            /*             * 序列化control,並保存到文件DocumentMoniker中。有些屬性比如Site之類的,最好不要序列化             * 下面的程式碼只是模擬這個過程,沒有真正的序列化控制項             * */            MessageBox.Show("序列化。。。");        }    }}

注意到我並沒有實現序列化和反序列化的真正邏輯,因為這和vsx無關,大家可以自己去實現。不過要注意的是Control的部分屬性是沒有必要序列化到文件里的,所以在序列化的時候要過濾些屬性,例如根據BrowsableAttribute來決定哪些屬性可以被序列化。

我們需要把文件的路徑傳給DocumentData,並且在DocumentData里定義一個Control類型的屬性:

class DocumentData : IVsPersistDocData, IPersistFileFormat{       ...    public Control Control { get; set; }    public string DocumentMoniker { get; set; }    public DocumentData(string docPath)    {        DocumentMoniker = docPath;        _fileName = docPath;    }    ...}

在DesignerLoader的PerformLoad方法里,調用反序列化方法,並把反序列化出來的控制項加到DesignerHost里:

class DesignerLoader : BasicDesignerLoader{    ...    protected override void PerformLoad(IDesignerSerializationManager serializationManager)    {        ControlSerializer serializer = new ControlSerializer(_data.DocumentMoniker);        Control control = serializer.Deserialize();        //把控制項的引用傳給DocumentData,這樣它保存的時候就可以序列化這個控制項了        _data.Control = control;        AddControl(control);    }     private void AddControl(Control parent)    {        LoaderHost.Container.Add(parent);        foreach (Control child in parent.Controls)        {            if (child.Tag == "Designable")            {                AddControl(child);            }        }    }}

在DocumentData的Save方法里,調用如下的程式碼:

class DocumentData : IVsPersistDocData, IPersistFileFormat{    ....    private void SaveFile(string fileName)    {        ControlSerializer serializer = new ControlSerializer(fileName);        serializer.Serialize(Control);        Dirty = false;    }    ....}

這樣就可以保存和載入文檔了。