零.引言
PropertyGrid用來顯示某一對象的屬性,但是並不是所有的屬性都能編輯,基本數據類型(int, double等)和.Net一些封裝的類型(Size,Color等)可以編輯,但是對於自己定義的類型屬性,是不能編輯的,本文主要講述如何為自定義類型作為屬性時,在PropertyGrid中進行編輯,以及進行設計時序列化,本文主要參考MSDN,錯誤和不足之處還望指正。
一.自定義類屬性
在PropertyGrid中能夠編輯的都是.Net中自己封裝的類,如果在一個類中有一個屬性是我們自己定義的類型,在PropertyGrid中會是怎樣的呢?看下面這個例子:
假如現在有一個類Line:
public class Line { Point P1; Point P2; public Point Point1 { get{return P1;} set{P1 = value;} } public Point Point2 { get{return P2;} set{P2 = value;} } public Line(Point point1, Point point2) { P1 = point1; P2 = point2; } }
有一個控件類包含一個Line類型的屬性:
public class MyControl : System.Windows.Forms.UserControl { Line _line; public MyControl() { _line = new Line(new Point(0, 0),new Point(100, 100)); } public Line MyLine { get{return _line;} set{_line = value;} } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { e.Graphics.DrawLine(Pens.Red, this._line.Point1, this._line.Point2); base.OnPaint(e); } }
重新生成,從工具箱中將該控件拖入Form中,查看他的屬性,在PropertyGrid中顯示如下:
可見MyLine屬性的值不顯示,且是不能編輯的。這是因為PropertyGrid根本不知道Line是個什么類型,不知道要怎么顯示,如果要其能在PropertyGrid中顯示,必須給他提供轉換器。
二.轉換器概念
PropertyGrid中屬性的值都是以字符串的形式呈現給我們看的,顯示一個對象的屬性時,要將對象的屬性值轉換為字符串顯示出來,而設置屬性時,要將字符串轉換為對象的屬性值。這就需要一個轉換器。在.Net中定義了一個TypeConverter 類,用來作為這些轉換器的基類。.Net為一些類設計了專門的轉換類,如:System.Drawing.ColorConverter ,System.Drawing.FontConverter等等,(具體參見MSDN中TypeConverter的繼承關系)因此在PropertyGrid中能直接編輯這些屬性。我們自己定義的類沒有這樣的類型轉換器,因此在PropertyGrid中無法編輯,需要設計自己的轉換器。
先來看一下MSDN中對TypeConverter的描述:TypeConverter類提供一種將值的類型轉換為其他類型以及訪問標准值和子屬性的統一方法。
繼承者說明:
從 TypeConverter 繼承,以實現您自己的轉換要求。當從類繼承時,可以重寫以下方法:
- 若要支持自定義類型轉換,請重寫 CanConvertFrom、CanConvertTo、ConvertFrom 和 ConvertTo 方法。
- 若要轉換必須重新創建對象才能更改其值的類型,請重寫 CreateInstance 和 GetCreateInstanceSupported 方法。
- 若要轉換支持屬性 (Property) 的類型,請重寫 GetProperties 和 GetPropertiesSupported 方法。如果轉換的類沒有屬性 (Property),而您需要實現屬性 (Property),則可以將 TypeConverter.SimplePropertyDescriptor 類用作實現屬性 (Property) 說明符的基。當從 TypeConverter.SimplePropertyDescriptor 繼承時,必須重寫 GetValue 和 SetValue 方法。
- 若要轉換支持標准值的類型,請重寫 GetStandardValues、GetStandardValuesExclusive、GetStandardValuesSupported 和 IsValid 方法。
三.添加轉換器
好了,了解了基本原理后,我們來為Line添加轉換器。這里新建一個LineConverter類,並繼承於TypeConverter。

public class LineConverter : TypeConverter { //該方法判斷此類型可以轉換為哪些類型 public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) { return true; } if (destinationType == typeof(InstanceDescriptor)) { return true; } //調用基類方法處理其他情況 return base.CanConvertTo(context, destinationType); } //該方法判斷哪些類型可以轉換為此類型 public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) { return true; } return base.CanConvertFrom(context, sourceType); } // 將該類型轉換為字符串和InstanceDescriptor public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string) && value != null) { Line t = (Line)value; string str = t.Point1.X + "," + t.Point1.Y + "," + t.Point2.X + "," + t.Point2.Y; return str; } if (destinationType == typeof(InstanceDescriptor) && value != null) { ConstructorInfo ci = typeof(TestTypeConverter.Line).GetConstructor(new Type[] { typeof(Point), typeof(Point) }); Line t = (Line)value; return new InstanceDescriptor(ci, new object[] { t.Point1, t.Point2 }); } return base.ConvertTo(context, culture, value, destinationType); } //字符串和InstanceDescriptor轉換為該類 public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { string str = (string)value; str = str.Trim(); string[] v = str.Split(','); if (v.Length != 4) { throw new NotSupportedException("Invalid parameter format"); } int x1 = 0; int y1 = 0; int x2 = 0; int y2 = 0; bool res = int.TryParse(v[0], out x1); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[1], out y1); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[2], out x2); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[3], out y2); if (res == false) throw new NotSupportedException("Invalid parameter format"); Line line = new Line(new Point(x1, y1), new Point(x2, y2)); return line; } return base.ConvertFrom(context, culture, value); } }
在轉換器類中,我們重寫了四個函數,也就是在繼承者說明中第一條的四個函數:
① CanConvertTo:用來說明此類可以轉換為哪些類型,能轉換就返回true,這里我們讓他能轉換為string和InstanceDescriptor,InstanceDescriptor是存儲描述對象實例的信息,這些信息可用於創建對象的實例。一般轉換都要實現這個轉換,后面進行說明。
② CanConvertFrom:說明該類能有什么類型轉換過來,能轉換返回true,這里只對string類型進行轉換。
③ ConvertTo:具體的轉換實現,也就是提供方法將該類轉換為在CanConvertTo中確定可以轉換的類型。這里實現string和InstanceDescriptor的轉換。
④ ConvertFrom:具體的轉換實現,也就是提供方法將該類轉換為在CanConvertFrom中確定可以轉換的類型。這里實現string的轉換。
注意,每個方法處理完自己的轉換后,依然要調用基類的函數來處理其他的情況。
重寫這四個方法后,給Line類型加上TypeConverter特性,在Line類定義的上面加上[TypeConverter(typeof(LineConverter))]。這樣在需要轉換的地方,就會調用我們所寫的方法進行轉換,Line屬性在PropertyGrid中顯示時,會調用ConvertTo,轉換為字符串輸出;設置屬性時,會調用ConvertFrom將字符串轉換為屬性值,當然,字符串必須要有一定的格式,這就需要在轉換的過程中進行判斷,見ConvertFrom方法。
重新生成,現在看MyControl中MyLine屬性在PropertyGrid中的顯示:
可以看到MyLine屬性顯示出來了(以x1,y1,x2,y2的格式,我們在ConverTo方法中指定的),並可以編輯了,但必須是x1,y1,x2,y2的格式,否則會出現如下錯誤提示:
這是我們在ConvertFrom方法中進行判斷的。
四.編輯復雜屬性的子屬性
現在可以編輯屬性了,但是必須使用x1,y1,x2,y2固定的格式,不方便,而且容易出錯,可不可以分別設置Line的兩個點呢,可以,這里就需要編輯子屬性了。Line中有Point1和Point2兩個屬性,我們讓他們也顯示出來。
這里就是繼承者說明中的第三條了。重寫 GetProperties 和 GetPropertiesSupported 方法。在LineConverter中重寫這兩個方法:
public override bool GetPropertiesSupported(ITypeDescriptorContext context) { return true; //return base.GetPropertiesSupported(context); } public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) { return TypeDescriptor.GetProperties(value, attributes); //return base.GetProperties(context, value, attributes); }
重新生成,現在看MyControl中MyLine屬性在PropertyGrid中的顯示:
可以看到現在我們可以編輯MyLine的子屬性了。
五.屬性的設計時串行化
最后說一說ConvertTo方法中為什么要實現InstanceDescriptor的轉換。Visual Studio在我們編輯控件時,會在Form1.Designer.cs文件中自動的生成代碼,設置控件的屬性,這叫屬性對設計時序列化,如下:
我們定義的MyLine屬性,在這里並沒有,如何使它出現在這里呢,只需給MyLine屬性加上DesignerSerializationVisibility特性。如下
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] public Line MyLine { get { return _line; } set { _line = value; } }
這里DesignerSerializationVisibility.Visible表明代碼生成器生成對象的代碼。DesignerSerializationVisibility.Content說明該屬性在編輯時要代碼生成器產生對象內容的代碼,而不是對象本身的代碼。DesignerSerializationVisibility.Hide代碼生成器不生成對象的代碼。
重新生成,並改變控件的位置,打開Form1.Designer.cs,會發現自動生成了MyLine屬性的設置值。
這是如何生成的呢,關鍵就在ConvertTo方法中實現InstanceDescriptor的轉換,該方法告訴代碼生成器,如何去構造一個Line類型,生成時,調用Line的構造函數構造新對象初始化MyLine屬性值。
六.總體的代碼
下面是完整的代碼:

using System; using System.ComponentModel; using System.ComponentModel.Design.Serialization; using System.Drawing; using System.Globalization; using System.Reflection; namespace TestTypeConverter { //線條類 [TypeConverter(typeof(LineConverter))] public class Line { // Line members. Point P1; Point P2; public Point Point1 { get { return P1; } set { P1 = value; } } public Point Point2 { get { return P2; } set { P2 = value; } } public Line(Point point1, Point point2) { P1 = point1; P2 = point2; } } //轉換器類 public class LineConverter : TypeConverter { public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) { return true; } if (destinationType == typeof(InstanceDescriptor)) { return true; } return base.CanConvertTo(context, destinationType); } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) { return true; } return base.CanConvertFrom(context, sourceType); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string) && value != null) { Line t = (Line)value; string str = t.Point1.X + "," + t.Point1.Y + "," + t.Point2.X + "," + t.Point2.Y; return str; } if (destinationType == typeof(InstanceDescriptor)) { ConstructorInfo ci = typeof(TestTypeConverter.Line).GetConstructor(new Type[] { typeof(Point), typeof(Point) }); Line t = (Line)value; return new InstanceDescriptor(ci, new object[] { t.Point1, t.Point2 }); } return base.ConvertTo(context, culture, value, destinationType); } public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { string str = (string)value; str = str.Trim(); string[] v = str.Split(','); if (v.Length != 4) { throw new NotSupportedException("Invalid parameter format"); } int x1 = 0; int y1 = 0; int x2 = 0; int y2 = 0; bool res = int.TryParse(v[0], out x1); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[1], out y1); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[2], out x2); if (res == false) throw new NotSupportedException("Invalid parameter format"); res = int.TryParse(v[3], out y2); if (res == false) throw new NotSupportedException("Invalid parameter format"); Line line = new Line(new Point(x1, y1), new Point(x2, y2)); return line; } return base.ConvertFrom(context, culture, value); } public override bool GetPropertiesSupported(ITypeDescriptorContext context) { return true; //return base.GetPropertiesSupported(context); } public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) { return TypeDescriptor.GetProperties(value, attributes); //return base.GetProperties(context, value, attributes); } } //控件類 public class MyControl : System.Windows.Forms.UserControl { Line _line; public MyControl() { _line = new TestTypeConverter.Line( new Point(0, 0), new Point(100, 100) ); } [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] public Line MyLine { get { return _line; } set { _line = value; } } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { e.Graphics.DrawLine(Pens.Red, this._line.Point1, this._line.Point2); base.OnPaint(e); } } }
新建一個Windows工程,添加該文件,在工具箱中找到我們的MyControl控件,拖入Form中,在屬性框中查看控件的屬性。
(原文)