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; } ....}
這樣就可以保存和載入文檔了。