MonoRail(Castle Project 早期 MVC 框架)提供強大的 DataBinder:自動將 `Request.Form`/`QueryString` 依照慣例映射至物件屬性,支援巢狀物件、集合、型別轉換、驗證。ASP.NET WebForms 缺乏同等機制,開發者常手寫大量 `entity.Prop = Request["Prop"]` 程式碼。本文實作輕量級繫結器填補此缺口。
// 型別轉換器介面
public interface IStringConverter {
object ConvertTo(string value, out bool succeeded);
}
// 內建轉換器範例
public class Int32Converter : IStringConverter {
public object ConvertTo(string value, out bool succeeded) {
return int.TryParse(value, out int result) ? (succeeded = true, result) : (succeeded = false, 0);
}
}
public class DateTimeConverter : IStringConverter {
public object ConvertTo(string value, out bool succeeded) {
return DateTime.TryParse(value, out DateTime result) ? (succeeded = true, result) : (succeeded = false, DateTime.MinValue);
}
}
public class ByteArrayConverter : IStringConverter { // HttpPostedFile → byte[]
public object ConvertTo(string value, out bool succeeded) {
// 實際實作需從 HttpContext.Current.Request.Files 取得
succeeded = false; return null;
}
}
public class BindingContext {
public HttpRequest Request { get; }
public IDictionary Converters { get; } = new Dictionary();
public string Prefix { get; set; } // 如 "order."、"user.Address."
public BindingContext(HttpRequest request) => Request = request;
// 命名對應規則:支援 PropName、PrefixPropName、Prefix_PropName、Prefix.PropName
public string[] GetPossibleKeys(string propertyName) {
var keys = new List { propertyName };
if (!string.IsNullOrEmpty(Prefix)) {
keys.Add(Prefix + propertyName);
keys.Add(Prefix + "_" + propertyName);
keys.Add(Prefix + "." + propertyName);
}
return keys.ToArray();
}
}
public static class ObjectBinder {
public static T BindObject(BindingContext ctx) where T : new() {
var obj = new T();
BindObject(ctx, obj);
return obj;
}
public static void BindObject(BindingContext ctx, object target) {
var props = target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanWrite);
foreach (var prop in props) {
var keys = ctx.GetPossibleKeys(prop.Name);
string rawValue = null;
foreach (var key in keys) {
rawValue = ctx.Request[key];
if (rawValue != null) break;
}
if (rawValue == null) continue; // 未提供值,保留預設
// 尋找轉換器
if (ctx.Converters.TryGetValue(prop.PropertyType.FullName, out var converter) ||
ctx.Converters.TryGetValue(prop.Name, out converter)) {
if (converter.ConvertTo(rawValue, out bool ok) is object val && ok) {
prop.SetValue(target, val);
}
} else if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string) || prop.PropertyType == typeof(decimal)) {
// 內建型別直接轉換
var converted = Convert.ChangeType(rawValue, prop.PropertyType);
prop.SetValue(target, converted);
} else if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string)) {
// 巢狀物件遞迴繫結
var nestedCtx = new BindingContext(ctx.Request) { Prefix = prop.Name + "." };
var nestedObj = Activator.CreateInstance(prop.PropertyType);
BindObject(nestedCtx, nestedObj);
prop.SetValue(target, nestedObj);
}
}
}
}
// 註冊全域轉換器 (Application_Start)
BindingContext.GlobalConverters[typeof(DateTime).FullName] = new DateTimeConverter();
BindingContext.GlobalConverters["BirthDate"] = new DateTimeConverter(); // 指定屬性專用
// 頁面中使用
protected void btnSave_Click(object sender, EventArgs e) {
var ctx = new BindingContext(Request);
var order = ObjectBinder.BindObject(ctx);
var customer = ObjectBinder.BindObject(ctx, "cust"); // Prefix="cust."
OrderService.Save(order, customer);
}
| 特性 | 自訂繫結器 | ASP.NET Core Model Binding |
|---|---|---|
| 架構整合 | 獨立庫,需手動呼叫 | 內建管道,自動執行 |
| 型別轉換 | IStringConverter 介面 | IModelBinder / TypeConverter |
| 驗證 | 手動整合 DataAnnotations | 內建 ModelState + DataAnnotations |
| 巢狀/集合 | 遞迴實作,支援基本語法 | 完整支援複雜巢狀、字典、陣列 |
| 效能 | 反射為主,可快取 PropertyInfo | 編譯運算式樹,極高效能 |