Visual Studio 屬性視窗能根據屬性型別自動提供對應編輯器:Color 顯示調色盤、Font 顯示字體對話框、列舉顯示下拉選單、日期顯示日期選擇器。這背後是 TypeConverter 機制。自訂複雜屬性時,實作專屬 TypeConverter 可獲得同樣的設計時體驗。
| 屬性型別 | 預設轉換器 | 設計時編輯器 |
|---|---|---|
| string | StringConverter | 文字方塊 |
| int, double, bool | NumberConverter / BooleanConverter | 文字方塊 / 核取方塊 |
| enum | EnumConverter | 下拉選單 |
| Color | ColorConverter | 調色盤 |
| Font | FontConverter | 字體對話框 |
| DateTime | DateTimeConverter | 日期選擇器 |
| Unit | UnitConverter | 尺寸編輯器 |
| Url | UrlConverter | URL 編輯器 |
public class GameSettings
{
public string GameName { get; set; }
public int MaxPlayers { get; set; }
public bool AllowSpectators { get; set; }
public DifficultyLevel Difficulty { get; set; }
}
public enum DifficultyLevel { Easy, Normal, Hard, Expert }
public class GameSettingsConverter : TypeConverter
{
// 1. 能否從字串轉換 (支援宣告式標記)
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
// 2. 字串轉換為物件 (格式: "GameName|MaxPlayers|AllowSpectators|Difficulty")
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
string[] parts = str.Split('|');
if (parts.Length == 4)
{
return new GameSettings
{
GameName = parts[0],
MaxPlayers = int.Parse(parts[1]),
AllowSpectators = bool.Parse(parts[2]),
Difficulty = (DifficultyLevel)Enum.Parse(typeof(DifficultyLevel), parts[3])
};
}
}
return base.ConvertFrom(context, culture, value);
}
// 3. 物件轉換為字串 (序列化至標記)
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is GameSettings settings)
{
return $"{settings.GameName}|{settings.MaxPlayers}|{settings.AllowSpectators}|{settings.Difficulty}";
}
return base.ConvertTo(context, culture, value, destinationType);
}
// 4. 支援屬性網格展開 (子屬性編輯)
public override bool GetPropertiesSupported(ITypeDescriptorContext context) => true;
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(GameSettings));
// 可自訂屬性順序、屬性描述、編輯器
return props.Sort(new[] { "GameName", "MaxPlayers", "Difficulty", "AllowSpectators" });
}
// 5. 支援標準值清單 (下拉選單)
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => true;
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
// 針對 Difficulty 屬性提供標準值
return new StandardValuesCollection(Enum.GetValues(typeof(DifficultyLevel)));
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => true; // 僅允許清單值
}
public class GameControl : WebControl
{
private GameSettings _gameSettings = new GameSettings();
[Category("Game Settings")]
[Description("遊戲設定")]
[TypeConverter(typeof(GameSettingsConverter))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public GameSettings GameSettings
{
get => _gameSettings;
set => _gameSettings = value ?? new GameSettings();
}
}
public class PlayerCollection : CollectionBase
{
public Player this[int index] => (Player)List[index];
public void Add(Player player) => List.Add(player);
public void Remove(Player player) => List.Remove(player);
}
public class Player
{
public string Name { get; set; }
public int Score { get; set; }
public TeamColor Team { get; set; }
}
public enum TeamColor { Red, Blue, Green, Yellow }
public class PlayerCollectionConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string str)
{
// 格式: "Name1,Score1,Team1;Name2,Score2,Team2"
PlayerCollection collection = new PlayerCollection();
string[] players = str.Split(';');
foreach (string p in players)
{
string[] parts = p.Split(',');
if (parts.Length == 3)
{
collection.Add(new Player
{
Name = parts[0],
Score = int.Parse(parts[1]),
Team = (TeamColor)Enum.Parse(typeof(TeamColor), parts[2])
});
}
}
return collection;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is PlayerCollection collection)
{
return string.Join(";", collection.Cast().Select(p => $"{p.Name},{p.Score},{p.Team}"));
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class GameSettingsEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal; // 模態對話框
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (editorService != null)
{
GameSettings settings = (GameSettings)value ?? new GameSettings();
using (GameSettingsDialog dialog = new GameSettingsDialog(settings))
{
if (editorService.ShowDialog(dialog) == DialogResult.OK)
{
return dialog.Settings;
}
}
}
return value;
}
}
// 套用編輯器
[Editor(typeof(GameSettingsEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(GameSettingsConverter))]
public GameSettings GameSettings { get; set; }
public class ColorPickerEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown; // 下拉面板
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (editorService != null)
{
ColorPickerControl picker = new ColorPickerControl((Color)value);
editorService.DropDownControl(picker);
return picker.SelectedColor;
}
return value;
}
}
| 屬性 | 用途 | 典型值 |
|---|---|---|
DesignerSerializationVisibility | 控制屬性如何序列化至 InitializeComponent | Visible (預設), Hidden, Content (複雜物件/集合) |
PersistenceMode | 控制屬性如何持久化至 .aspx 標記 | Attribute (簡單型別), InnerProperty (複雜物件), EncodedInnerDefaultProperty |
DefaultValue | 指定預設值,設計器不產生程式碼/標記 | 型別預設值或自訂值 |
Browsable | 是否在屬性視窗顯示 | true (預設), false |
TypeConverter 是屬性型別與設計時編輯器的橋樑:
ConvertFrom (字串→物件)、ConvertTo (物件→字串/其他型別)GetPropertiesSupported/GetProperties 啟用子屬性網格編輯GetStandardValues* 提供下拉選單選項DesignerSerializationVisibility/PersistenceMode/DefaultValue 精確控制程式碼/標記生成實作完整的 TypeConverter,讓自訂複雜屬性擁有與內建型別同等的設計時體驗,是專業控件開發的重要指標。