譯者序:本文是園友在看 《(譯)一個通用快速的反射方法調用》 后推薦我看的一片文章,非常感謝,我也從中了解到 .NET數據綁定機制和動態方法特性。
原文:http://www.codeproject.com/Articles/20332/Nested-Property-Binding
源碼下載: Download demo project and source - 907.63 KB
介紹
我目前在開發一個可以與 Microsoft SQL Server 一起工作的對象關系映射項目,在這個項目中,我將面對大量涉及程序操作的數據是對象而不是來源於數據庫的挑戰。如果你曾經嘗試過將一個列表對象綁定到DataGrid ,那么我確信你肯定會遇到一個問題:怎么顯示不是對象類型本身的屬性。
示例:你有一個訂單列表,但是你想同時將訂單類型的客戶名稱和帳單地址兩個屬性一起顯示。
這涉及到嵌套屬性綁定。很多朋友面對上面這種需求會創建指定對象的視圖,但是我通過創建一個ObjectBindingSource
組件實現了一個設計時的解決方案。正如其名,這個組件的數據源是對象而不是DataSet/DataTable。我並不樂意使用DataSet做為綁定方案,因為ObjectBindingSource
組件這已經不會再困擾我了。
下面是這個示例的類結構圖。
從上圖模型得知,Order
類有一個Customer
和DeliveryAddress
屬性,這個簡單的訂單類組成可以實現在訂單上選定一個客戶將顯示使用嵌套屬性綁定的客戶詳細信息;BillingAddress
是Customer類特有的並且不能編輯;DeliveryAddress
是Order類特有的可以編輯。
System.ComponentModel.ITypedList 接口
// 提供發現可綁定列表架構的功能,其中可用於綁定的屬性不同於要綁定到的對象的公共屬性。 public interface ITypedList { // 返回表示用於綁定數據的每項上屬性的 System.ComponentModel.PropertyDescriptorCollection。 PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors); // 返回列表的名稱。 string GetListName(PropertyDescriptor[] listAccessors); }
經過一番搜索,我發現ITypedList是一個簡單的檢查接口。僅僅提供獲取哪些屬性屬於特定類型。我將要討論的不是這個接口是如何實現的,因為在網上已有很多相關文章。因此我開始創建我自己的特殊集合組件,如實現 System.ComponentModel.IBindingList 接口等等。但是很快我便清楚的發現我真正想要的是一個像 System.Windows.Forms.BindingSource 這樣支持嵌套屬性訪問器的組件。
擴展標准System.Windows.Forms.BindingSource組件
為了提供設計時數據綁定功能,我們不得不使用到已有的 BindingSource 組件(或至少實現 IBindingList 接口)。鑒於此,我開始擴展標准 BindingSource 並實現嵌套屬性的支持。事實證明 BindingSource 已經實現了 ITypedList 接口並且是以虛方法的方式進行實現方便進行特定需求的重寫。我為BindingSource 組件擴展了一個屬性 BindableProperties ,它用於給開發者標識 BindingSource 中顯示哪些屬性。然后需要做的是用 BindableProperties 替換掉默認的實現。 ITypedList.GetItemProperties 似乎經常被消費者調用,所以我將CreatePropertyDescriptors方法(創建 propertydescriptors 部分)的代碼移出 GetItemProperties 方法並且通過 _createProperties 字段限制只有DataSource或 DataMember改變時才觸發CreatePropertyDescriptors 方法。
這里還有另一個屬性 AutoCreateObject 稍后會繼續討論。
GetItemProperties 是 ITypedList 接口中非常重要的方法,它先被 BindingSource 實現,然后再被 ObjectBindingSource 組件重寫,以提供獲取自定義屬性描述或嵌套屬性描述。
public override PropertyDescriptorCollection GetItemProperties (PropertyDescriptor[] listAccessors) { //Check to see if the descriptors should be recreated if (_createProperties) CreatePropertyDescriptors(); //Check to see if we have a list of descriptors if (_propertyDescriptors.Count > 0) return new PropertyDescriptorCollection(_propertyDescriptors.ToArray()); else //If not populated for some reason, //we just revert to the default implementation return base.GetItemProperties(listAccessors); }
如果我們看一下CreatePropertyDescriptors的代碼片段,你會發現它是如此易懂:
foreach (BindableProperty bindableProperty in _bindableProperties) { //Get the original propertydescriptor based on the property path in bindableProperty PropertyDescriptor propertyDescriptor = ReflectionHelper .GetPropertyDescriptorFromPath(itemType, bindableProperty.Name); //Create an attribute array and make room for one more attribute Attribute[] attributes = new Attribute[propertyDescriptor.Attributes.Count + 1]; //Copy the original attributes to the custom descriptor propertyDescriptor.Attributes.CopyTo(attributes, 0); //Create a new attribute preserving information about the original property. attributes[attributes.Length - 1] = new CustomPropertyAttribute(itemType, bindableProperty.Name, propertyDescriptor); //Finally add the new custom property descriptor to the list of property descriptors _propertyDescriptors.Add(new CustomPropertyDescriptor (bindableProperty.Name, propertyDescriptor, attributes,_autoCreateObjects)); }
值得注意的是:自定義屬性描述會被創建盡管這個屬性是根對象的成員。這是因為 CustomPropertyDescriptor使用的是已編譯的 get/set 動態訪問器不同於常規的成員反射。我們稍后再看 DynamicAccessor 。
如果你看的仔細,你會注意到這里還有一些特性。當使用嵌套屬性綁定時這里只保留有關的真正基礎對象和屬性的信息。通過查看 CustomPropertyAttribute 你可以輕易找到嵌套屬性的來源。如果嵌套屬性綁定是使用路徑 Order.Customer.BillingAddress.StreetAddress,這個屬性名將被轉化為 Customer_BillingAddress_StreetAddress ,它看起來更像是 Order類型的一部分(比如一個字段命名)。我們繼續探究這個應用程序架構,它在域模型上執行本地化而不是UI組件並且這些信息獲取的非常便捷。
使用 IDynamicAccessor 實現快速動態屬性訪問
DynamicAccessor 的設計思想來源於作者 Herbrandson 。基於 Joel Martinez 發布在 CodeCube.Net 上的類庫,我創建了一個類似的類庫,適合我的動態屬性訪問器需求。總體思想是使用 .Net 2.0 框架提供的 DynamicMethodCompiler 特性。對此特性我不進行詳細討論,只談論動態獲取類型上已編譯好的set/get訪問器以設置和獲取值。這些方案被證實對我非常有用,我將其寫在core library,它還包含其他如文件上傳下載、數據庫操作、特性驗證和日志操作等和這篇文章無關的功能,但卻都是非常易用、通用的功能。
好了,讓我們回到設置和獲取值的討論。基類(TypeDescriptor)已經為我們提供了對基礎對象 set 和 get 值不可缺少方法,但通過查看相關文章得知 .NET 的TypeDescriptor提供的方法實現實際上是使用反射來設置和獲取屬性值的。IDynamicAccessor 通過使用 .NET 2.0 的 DynamicMethodCompiler 特性提供動態編譯版本的 set/ get 訪問器,相比常規反射能獲得更快的速度。
如過你查看 ObjectBindingSource 組件屬性,你會發現這里還有一個新的屬性 AutoCreateObject。這個屬性標識 ObjectBindingSource 組件在設置值時是否自動在 propertypath 中創建不存在的對象。比如:
Order.DeliveryAddress.StreetAddress,如果在 Grid 中為 StreetAddress 輸入一個值,不保證這是一個有效的 DeliverAddress 。如果 AutoCreateObjects = true ,這個 Address 對象在實際設置值時會被創建並分配給 DeliveryAddress 。為此目的,待創建對象(eg:Address)應該有一個無參構造函數。
public override object GetValue(object component) { object instance = GetNestedObjectInstance(component,_propertyPath,false); if (instance != null) return DynamicAccessorFactory.GetDynamicAccessor (instance.GetType()).GetPropertyValue (instance, _originalPropertyDescriptor.Name); else return null; }
首先我們要獲取將要檢索值對象實例的一個引用,通過調用 GetNestedObjectInstance 並提供根對象實例和屬性路徑(example: Customer.Address.StreetAddress)作為參數。第三個參數(bool autoCreate)指示方法是否應該自動創建缺少對象如上文所示,這個參數標識 GetValue 方法中不需要(即直接取 false )。
public override void SetValue(object component, object value) { object instance = GetNestedObjectInstance(component,_propertyPath,_autoCreateObjects); if (instance != null) { DynamicAccessorFactory.GetDynamicAccessor(instance.GetType()). SetPropertyValue(instance, _originalPropertyDescriptor.Name, value); } }
SetValue 使用類似的方式,只是我們會考慮 _autoCreateObjects 選項是否為true。
使用 ObjectBindingSource 組件
使用 ObjectBindingSource 組件的方式和使用 BindingSource 組件大致相同,不同的是你可以編輯 BindableProperties 集合來指定要綁定的屬性(即要顯示的屬性及嵌套屬性)。
如果下載使用 ObjectBindingSource 組件,請注意如果你輸入一個無效的屬性路徑這里不會提示此錯誤;如果輸入的是一個有效的屬性值,那么 Bindable 屬性將忽略 BindingSource 組件對 ITypedList 接口的默認實現。
作者: seesharper
相關鏈接: