“第二选择”
上篇中,关于Editor说了那么多,完了吗?没有,上篇仅仅介绍了操作属性的UITypeEditor而已。还记得DataGrid的属性窗口的下方的“属性生成器...”吗?
当我们点击“属生生成器...”后,IDE弹出一个窗体,提供我们全方位的操作DataGrid属性的交互界面,这个界面比PropertyGrid提供更方便易用的,更符合DataGrid“国情”。所以,用户有了属性窗格之外的第二个选择。
那么这个“属性生成器...”是什么呢?它也是一个Editor,只是它不是一个UITypeEditor,而是一个ComponentEditor,对,它是一个组件编辑器,它不是用来编辑某个属性的,而是用来操作整个控件的。
下面我们就以实例来看看要实现组件编辑器,要做哪些工作?
控件主体类
[
Designer(typeof(MyDesigner)),
Editor(typeof(MyComponentEditor), typeof(ComponentEditor))
]
public class MyControl :WebControl {
}
在这里我们用到了两个Attribute来描述我们的控件主体类:Designer和Editor,第一个为控件主体类关联一个设计器,这里之所以要用到设计器,因为要方便的调用组件编辑器要借助Designer类。第二个Attribute为控件主体类关联了一个编辑器,大家可以看到它的第二个参数变成了ComponentEditor而不是UITypeEditor。
编辑器窗体类
public class MyComponentEditorForm : System.Windows.Forms.Form {
private MyControl _myControl;
public myControlComponentEditorForm(myControl component) {
InitializeComponent();
_myControl = component;
//用_myControl的属性初始化本窗体上的操作控件(如一些TextBox,还以我以前讲到的PropertyGrid的值)。
}
//以下是用户点击确定完成编辑的逻辑纲要
private void okButton_Click(object sender, System.EventArgs e) {
这里使用PropertyDescriptor来为Component赋值与直接用 _myControl.Property_1 = textBox1.Text 这样的逻辑来赋值有一个好处,就是支持操作的Undo功能#region 这里使用PropertyDescriptor来为Component赋值与直接用 _myControl.Property_1 = textBox1.Text 这样的逻辑来赋值有一个好处,就是支持操作的Undo功能
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(_myControl);
try {
PropertyDescriptor property_1 = props["Property_1"];
if (textProperty != null) {
textProperty.SetValue(_myControl, textBox1.Text);
}
}
catch {
}
DialogResult = DialogResult.OK;
Close();
#endregion
}
}
编辑器类
public class MyComponentEditor : WindowsFormsComponentEditor {
//操作控件主逻辑
public override bool EditComponent(ITypeDescriptorContext context, object component, IWin32Window owner) {
MyControl control = component as MyControl;
if (Control == null) {
throw new ArgumentException("操作对象检查,只能是特定的类", "component");
}
IServiceProvider site = control.Site;//每个控件都有一个Site属性,使用它的GetService方法使得在设计环境下,控件能得到各种设计期服务。如IComponentChangeService、IDesignerEventService、IDesignerHost、IDesignerOptionService等等
IComponentChangeService changeService = null;//IComponentChangeService规定当组件被增删改时设计界面的修改,并提供方法来引发 ComponentChanged 或 ComponentChanging 事件。不由.net fw实现,由VS.net实现。
DesignerTransaction transaction = null;//DesignerTransaction提供一种方法来对一系列的设计时操作进行分组,从而提高性能并使得大多数类型的更改都能撤消。
bool changed = false;
try {
if (site != null) {
IDesignerHost designerHost = (IDesignerHost)site.GetService(typeof(IDesignerHost));//规定了支持设计器事务、管理组件和设计器、得到设计器信息。由VS.net实现。
transaction = designerHost.CreateTransaction("事务分组名");//DesignerTransaction由IDesignerHost得到
changeService = (IComponentChangeService)site.GetService(typeof(IComponentChangeService));
if (changeService != null) {
try {
changeService.OnComponentChanging(control, null);//第二个参数为MemberDescriptor它是EventDescriptor和PropertyDescriptor类的基类表示正在更改的成员。如果此更改与单个成员无关,则它将为空引用。
}
catch (CheckoutException ex) {//此处的CheckoutException是签入VSS失败
if (ex == CheckoutException.Canceled)
return false;
throw ex;
}
}
}
try {
//以下代码实现调用一个编写好的编辑器窗口
MyComponentEditorForm form = new MyComponentEditorForm(control);
if (form.ShowDialog(owner) == DialogResult.OK) {//from.ShowDialog(owner)指定from为owner的一个模态窗口。
changed = true;
}
}
finally {
if (changed && changeService != null) {//如果点击了编辑器窗口的确定,引发已经更改事件
changeService.OnComponentChanged(Control, null, null, null);//由于更改不与单个属性有关,所以后面的三个参数都为null
}
}
}
finally {
if (transaction != null) {
if (changed) {//一切正常的话,提交设计器事件
transaction.Commit();
}
else {
transaction.Cancel();
}
}
}
return changed;
}
}
当我们做了这些事后,已经可以使用属性编辑器了,我们可以看到在属性窗格的最后面的属性页面按钮已经可用,我们可以点击这个按钮打开属性编辑器
Designer
“WYSWYG”
也许你会说:记得DataGrid的属性编辑器可以用属性窗格下方的超链接和右键菜单打开啊,为什么这里还不行?
要回答这个问题,我们就得用到Designer
设计器是用来管理设计时控件呈现行为的类。WebForm和WinForm的核心设计器都来自System.ComponentModel.Design.ComponentDesigner,所以两者都有相同的架设,不过两者的引擎却是完全不同的,WebForm使用IE做为引擎,WinForm使用GDI+做为引擎。由于我对WinForm接触不多,所以以下的论述以WebForm控件的Designer开发为内容。
书接上文,我们还是还是先来看看怎么完成上面的问题。要使用右键打开组件编辑器等功能可以通过定制设设计器动词来实现。
设计器动词
设计器动词是设计界面中的命命,设计器都提供了一个设计器动词集合DesignerVerbCollection Verbs{get;}
下面来看我们的设计器类如何定制Verbs:
public class MyControlDesigner : ControlDesigner {
private DesignerVerbCollection designerVerbs;
public override DesignerVerbCollection Verbs {
get {
if (designerVerbs == null) {
designerVerbs = new DesignerVerbCollection();
designerVerbs.Add(new DesignerVerb("属生编辑", new EventHandler(this.OnControlPropertyBuilder)));//增加一个动词,关联一个方法
}
return designerVerbs;
}
}
private void OnControlPropertyBuilder(object sender, EventArgs e) {
MyComponentEditor compEditor = new MyComponentEditor();
compEditor.EditComponent(Component);
}
}
现在你可以看到,我们在属性窗格的下方和上下文菜单中看到打开编辑器的命令了。
Designer的主要功能其实是实现控件在设计期能“所见即所得”,所以我们对设计期还要有更多的了解,让我们来看以下代码:
public override void Initialize(IComponent component) {
if (!(component is MyControl)) {
throw new ArgumentException("Component must be a MyControl control.", "component");
}
base.Initialize(component);
}
public override string GetDesignTimeHtml() {
MyControl control = (MyControl)Component;
string designTimeHtml = String.Empty;
try {
designTimeHtml = base.GetDesignTimeHtml();
}
catch (Exception e) {
designTimeHtml = GetErrorDesignTimeHtml(e);
}
return designTimeHtml;
}
protected override string GetEmptyDesignTimeHtml() {
return CreatePlaceHolderDesignTimeHtml("右键点击设置控件的属性. ");
}
protected override string GetErrorDesignTimeHtml(Exception e) {
return CreatePlaceHolderDesignTimeHtml("生成错误.");
}
对于这些覆写方法,我们都可以望文生义得知它的意义。
不过有几点是要注意的:
1、如果控件是复合控件(就是多个基本控件合成的控件。),你最好让设它的设计器在GetDesignerTimeHtml方法中先调用一下它的含有确保子控件不为null的方法。
2、ControlDesigner基类的GetDesignTimeHtml方法会调用控件的RenderControl方法返回HTML字符串,所以默认情况下,控件在设计期的样子和运行期的样子会差不多。那么我们有没有办法让设计期和运行期不同呢?当然可以,最简单的办法是overrideGetDesignTimeHtml方法实现自己的呈现逻辑,不过你也可以使用另外一个技巧,那就是运行期是会调用OnPerRender方法再调用Render方法,而默认实现下设计期是没有调用OnPerRender方法的,所以,你可以利用这种差别,方便的使两者同中有异。
另外模板控件的设计类我们留待模板控件专门的文章中再讲述