TypeConverter
在本系列的上篇文章中,和大家控讨了控件开发与propertyGrid的关系,不知现在大家现在对propertygrid有没有一个较全面的了解,也不知大家有没有做个工程,把propertyGrid拉进去鼓捣鼓捣?
“另起炉灶”
现在我们来思考一个问题:假于,propertygrid没有把属性和事件分成两个分页来显示,会产生什么效果?
那还用说,太乱了。
那如果你设计的控件有很多的属性,而一些关联性很强,或都是操作一个方面的,那么我们可以把它们分门别类,摆到一起,怎么做呢?
我们可以给这个控件类指定以下Attribute:
[PropertyTab(
typeof
(YourPropertyTab), PropertyTabScope.Component)]
public
class
YourControlClass
{
}
其中,前面一个参数指定处理PropertyTab的类,后一个参数说明要应用在什么时候,Component为当前组件专用,Document当前文档专用,Global只能由父给件显式去除,Static不能去除。
internal
class
YourPropertyTab : PropertyTab
{
internal YourControlType target;
public override string TabName
{
get
{
return "选项卡的名字";
}
}
public override Bitmap Bitmap
{
get
{
return new Bitmap(base.Bitmap, new Size(16,16));//这里是使用保存为嵌入资源的和YourPropertyTab类同名的.bmp文件
}
}
public override bool CanExtend(object o)
{
return o is YourControlType;//什么时候用这个Tab
}
public override PropertyDescriptorCollection GetProperties(object component, Attribute[] attrs) {
return GetProperties(null, component, attrs);
}
/**//// 主要的逻辑. 在这里定义如何实现分Tab显示
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object component, Attribute[] attrs)
{
YourControlType uc = component as YourControlType;
if (uc == null)
{
//以下代码实现不是YourControlType时,使用本身类型的逻辑。
TypeConverter tc = TypeDescriptor.GetConverter(component);
if (tc != null)
{
return tc.GetProperties(context, component, attrs);
}
else
{
return TypeDescriptor.GetProperties(component, attrs);
}
}
target = uc;
ArrayList propList = new ArrayList();
//..建立一个属性List
propList.Add(new YourPropertyDescriptor(this);
PropertyDescriptor[] props = (PropertyDescriptor[])propList.ToArray(typeof(PropertyDescriptor));
return new PropertyDescriptorCollection(props);
}
//我们还要建立自定义的PropertyDescriptor供GetProperties方法使用。
private class YourPropertyDescriptor : PropertyDescriptor
{
YourPropertyTab owner;
public NumPointsPropertyDescriptor(YourPropertyTab owner) ://注意这里的参数
base("PropertyName", new Attribute[]{CategoryAttribute.Data, RefreshPropertiesAttribute.All})//第二个参数是指定属性改变时,与 //其它属性的联动,整个属性页是否刷新,All-刷新,Default-不,Repaint-重画属性窗口
{
this.owner = owner;
}
public override Type PropertyType//属性的类型
{
get
{
return typeof(int);
}
}
属性关联对象是什么类型
public override Type ComponentType
{
get
{
return typeof(YourControlType);
}
}
public override bool IsReadOnly {get{return false;}}
public override object GetValue(object o) //和关联对象的什么属性相关
{
return ((YourControlType)o).Proterty_1;
}
public override void SetValue(object o, object value) //和关联对象的什么属性相关
{
YourControlType uc = o as YourControlType;
uc.Property_1 = (int)value;
}
public override void ResetValue(object o){}//望文生义
public override bool CanResetValue(object o) //望文生义
{
return false;
}
/**////Does this property participate in code generation?
public override bool ShouldSerializeValue(object o)
{
return false;
}
}
}
类型转换器
public
class
YourConverter : ExpandableObjectConverter
{
do something,example: override CovertTo function
}
public
class
YourControl
{
[TypeConverter(typeof(YourConverter))]
public YourPropertyClass ExpandableProperty
{
.
}
}
6、为属性提供一个设计期值的下拉列表,正如我上篇文章所述。
7、 System.Web.UI.WebControls namespace & System.ComponentModel namespace下已经有了这些转换器:UnitConverter,BooleanConverter,CharConverter, EnumConverter,CollectionConverter,ArrayConverter,BaseNumberConverter,ByteConverter, ReferenceConverter,CultureInfoCOnverter,DateTimeConverter,DecimalConverter, DoubleConverter,ExpandableObjectConverter,GuidConverter,Int16(32/64)Converter, sByteConverter,SingleConverter,StringConverter,TimeSpanConverter,TypeListConverter, UInt16(32/64)Converter,ObjectConverter,PropertyConverter,DataBindingCollectionConverter, DataFieldConverter,DataMemberConverter,DataSourceConverter,FontNamesConverter, FontUnitConverter,TargetConverter,ValidatedControlConverter,CursorConverter...... (WinForm的我就不列出来了)
下面,我们就重点来看看如何实现不同类型和String怎么样来转换。
下面这个实例应该就得达到这个目的。
public
class
YourTypeConverter : TypeConverter
{//类型转换器需直接或间接继承自TypeConverter类,
//上面例子继承自ExpandableObjectConverter也是间接继承TypeConverter类
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {//源类型能不能转换成转换器关联类型
if (sourceType == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, sourceType);//我们实现了和String的转换,所以为String的话为True,然后,简单调用基类的方法就行。
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {//string能不能转换成目标类
if ((destinationType == typeof(string)) ||
(destinationType == typeof(InstanceDescriptor))) {//System.ComponentModel.Design.Serialization.InstanceDescriptor
//提供创建对象的实例所需的信息,此处返回转换器关联类型的实例
return true;
}
return base.CanConvertTo(context, destinationType);//只有实现了ConverTo目标类型才能为TRUE(我们在后面实现了string),如果String可以转换成目标类型, //那么也会为TRUE,因为这里我们可以将之转成String,再调用String的转换。
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {//从一种类型转换成我们的类型
if (value == null) {
return new YourType();//空值来,默认实现
}
if (value is string) {//如果是string,定制转化逻辑
string s = (string)value;
if (s.Length == 0) {
return new YourType();
}
//以下的例子实现"20,30"转换成new YourType(20,30),分两步,分隔,变Int
string[] parts = s.Split(culture.TextInfo.ListSeparator[0]);
if (parts.Length != 2) {
throw new ArgumentException("Invalid YourType", "value");
}
TypeConverter intConverter = TypeDescriptor.GetConverter(typeof(Int32));
return new YourType((int)intConverter.ConvertFromString(context, culture, parts[0]),
(int)intConverter.ConvertFromString(context, culture, parts[1]));
}
return base.ConvertFrom(context, culture, value);//还是要调用一下基类的方法。
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {//我们的类型转换成其它类型
if (value != null) {//先确定源类型是不我们的要转的类型。
if (!(value is YourType)) {
throw new ArgumentException("Invalid YourType", "value");
}
}
if (destinationType == typeof(string)) {//转成string的逻辑
if (value == null) {
return String.Empty;
}
YourType yourType = (YourType)value;
//以下的例子将yourType的两个int属性变成"属性值1,属性值2"的字符串。
TypeConverter intConverter = TypeDescriptor.GetConverter(typeof(Int32));
return String.Join(culture.TextInfo.ListSeparator,
new string[] {
intConverter.ConvertToString(context, culture, yourType.Value_1),
intConverter.ConvertToString(context, culture, yourType.Value_2)
});
}
else if (destinationType == typeof(InstanceDescriptor)) {
//以下的例子实现如果源类型本身就是这个类型时,我们的处理逻辑,
//这里就用了System.Reflection namespace下的方法来分别调用YourType的无参构造函数和有两个参数的构造函数。
if (value == null) {
return null;
}
MemberInfo mi = null;//通过对YourType MetaData的访问得到它的构造函数
object[] args = null;
YourType yourType = (YourType)value;
if (yourType.IsEmpty) {
mi = typeof(YourType).GetConstructor(new Type[0]);
}
else {
Type intType = typeof(int);
mi = typeof(YourType).GetConstructor(new Type[] { intType, intType });
args = new object[] { yourType.Value_1, yourType.Value_2 };
}
if (mi != null) {
return new InstanceDescriptor(mi, args);//根据选择的构造子和参数建立实例
}
else {
return null;
}
}
return base.ConvertTo(context, culture, value, destinationType);//还是调用基类的方法。
}
}
大家看到在上面的代码中我们老是在override方法中最后调用基类的方法,这么做是省去实现转换失败时的返回等逻辑的实现,避免出现莫名其妙的错误。
现 在,我们回过头来看看上面的那张图,我们使用了继承自ExpandableObjectConverter的转换器,而且,我们可以看到,我们既可以分开 来为属性的子属性赋值,也可以总的为属性赋值,如图中所示,"100,50,50"这个字符串值发映的就是被TypeConverter转换了的值,在需 要的时候,TypeConverter也会将它转换为YourType.Property_1= 100,YouType.Property2=50......这样的实际类型的实例及赋予它正确的值。
UITypeEdit
“我要红桃”
假如,你现在在做一个“扑克”控件,扑克牌有个属性--花色,你想在用户选择花色这个属性后,属性窗口呈现的不仅仅是文字,还有一个小小的花色图标来表示 花色,“红桃”就有个小“红桃”图标在前面显示,“黑桃”就有个“黑桃”图标在前面显示,就像你选择其它控件的BackColor时,颜色前还有个小方色 块来表示选定的颜色,多体贴人的设计啊。
现在,我们就来做这件事:
public
class
Squeezer
{
.
public CardTypes CardType
{
}
}
[Editor(
typeof
(CardTypesEditor),
typeof
(System.Drawing.Design.UITypeEditor))]
public
class
CardTypes
{
..
}
public
class
CardTypesEditor : UITypeEditor
{
public override bool GetPaintValueSupported(ITypeDescriptorContext context)
{
return true;//支持画小图
}
public override void PaintValue(PaintValueEventArgs pe) //定义根据值画小图的逻辑
{
string bmpName = null;
CardTypes C = (CardTyes)pe.Value;
switch(C.Value)
{
case CarderTypes.HongTao:
bmpName = "红桃.bmp";//图片必须是嵌入的资源,大小为16*16,类型为BMP
break;
}
Bitmap b = new Bitmap(typeof(GradeEditor), bmpName);
pe.Graphics.DrawImage(b, pe.Bounds);
b.Dispose();
}
}
在上面的代码中,我们通过EditorAttribute来使花色类和一个Editor关联,再通过这个Editor来实现画示意小图的功能。
好了,现在你的创造力可能又在鼓动你思考一个新问题了,我不想让让用户仅仅通过一个简单的只呈现值的下拉列表(通过EnumConverter实现的)来选择属性的值,我想实现像BackColor、Dock这样的非常友善的交互给用户使用,好吧,我们来进入下一步。
首 先我们要制做一个合适的小窗口(CardTypesEditorControl)来定制交互时的界面,这个窗体继承自 System.Window.Forms.UserControl或System.Window.Forms.From,总之它就是一个WinFrom窗 体,这个窗体怎么做,我这里就不展开论述了,只是你要在这个窗体类中聚合(不是组合,这里是引用,由Editor传过来)一个 IWindowsFormsEditorService,已便更好的交互,并且,能控件何时关闭打开的这个下列式的窗口,比如说在鼠标按钮的Up事件中 edSvc.CloseDropDown(),这样用户点击鼠标进行了选择之后就能关闭窗体,返回值。
好,我们来看我们的Editor怎么扩充:
CardTypes target;
CardTypesEditorControl ui;
public
CardTypesUIEditor(CardTypes target)
{
this.target = target;
}
//
通过Editor能到用户操作的值的逻辑实现
public
override
object
EditValue(ITypeDescriptorContext context, IServiceProvider sp,
object
value)
{
// 得到IDE的交互服务
IWindowsFormsEditorService edSvc = (IWindowsFormsEditorService)sp.GetService(typeof(IWindowsFormsEditorService));
if (edSvc == null)
{
return value;
}
if (ui == null)
{
ui = new CardTypesEditorControl();//建立属性操作窗体实例
}
ui.SelectedCardTypes = (CardTypes)value;//原始值,SelectedCardTypes属性只是一个例子,你可以用任意实现自己的逻辑。
ui.EditorService = edSvc;//传服务过去,见代码上面的说明
ui.Target = (CardTypes)context.Instance;//得到连接对象的实例
edSvc.DropDownControl(ui);//把窗体显示为一个下接式窗体,可选的值还有edSvc.ShowDialog(ui),
//这样的话以一个弹出窗体的形式显示窗体,也许你更喜欢这种方式。
return ui.SelectedCardTypes;//我们在窗体逻辑中更改SelectedCardTypes值,在操作窗体关闭时得到这个值。
}
//
窗口出现样式,可选值还有Modal,在属性后出出现一个省略号,点击弹出模式窗体或有窗口的对话框,就像CollectionEditor,
//
None,不出现任何操UI,老老实实填值
public
override
System.Drawing.Design.UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return System.Drawing.Design.UITypeEditorEditStyle.DropDown;
}
呵呵,是不是现在有得花样给你玩了?
在EditValue 方法中用一句return UrlBuilder.BuildUrl( (IComponent) context.Instance, null, (string) value, Caption, Filter,Options)你就可以做出一个URL编辑器;用ColorEditor.ColorUI colorUI = new ColorEditor.ColorUI(this);colorUI.Start(edSvc, value);edSvc.DropDownControl(colorUI);这样的几句话就能实现一个颜色选择交互界面。(注意代码没有处理null 等异常情况)
ComponentEditor
“第二选择”
上篇中,关于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方法的,所以,你可以利用这种差别,方便的使两 者同中有异。
另外模板控件的设计类我们留待模板控件专门的文章中再讲述。
在上一篇文章中,我们已经接触了TypeConverter,现在我们全面的来看看这个Interpreter:
1、TypeConverter从System.ComponentModel.TypeConverter类派生。
2、用使用[TypeConverter(typeof(YourTypeConverter))]绑定到属性。
3、在设计期和运行期都要用到TypeConverter,因为,两个时期都有特定类型-String之间的转换。
4、功能一:把属性转换成字符串显示在属性浏览器,把属性浏览器的设的值转换成属性需要的类型。
5、功能二:为子属性提供一个展开/折叠的UI。如下图:
要实现这个功能非常之简单,你先让属性通过TypeConverterAttribute关联到转换器类System.ComponentModel.ExpandableObjectConverter,或者继承自这个类的转换器。