关于这篇文章的标题我斟酌了很久,总觉得不管用哪个标题,都很难用不多的文字准确的描述其内容,最后还是觉得用这样一个标题吧。
先谈谈最近我们项目团队中遇到的两个需求:
1、有很多地方都需要在表格或者列表中实现多选,比如有一个人员列表,对应的数据源假设是List<EmployeeInfo>,要想实现多选,第一感觉就是在EmployeeInfo中增加Boolean Selected这样一个属性,这样固然可以解决问题,但在其他地方根本就用不到这个属性,而且如果后面还是其他类似的问题的话,EmployeeInfo这个类就会变得越来越大,越来越臃肿,于是乎就增加了下面的类:
public class SelectableInfo<T> { public Boolean Selected { get; set; } public T Value { get; set; } }
似乎这样可以解决问题,但在与UI控件绑定的时候出现了问题,将类似"Value.Name"这样的值作为PropertyName时根本不被识别。
2、在项目中某个地方需要有一个日程表,比如纵轴是人员,横轴是日期,但横轴是不固定的,日期范围是可以由用户自由选择的,于是乎设计了类似下面的数据结构:
public class ScheduleItem<TMain, TItem> { public ScheduleItem() { this.Items = new List<TItem>(); } public TMain Main { get; set; } public List<TItem> Items { get; private set; } }
似乎这样的话就可以直接用一个List<ScheduleItem<EmployeeInfo, EmployeeScheduleItemInfo>>作为UI控件的数据源,动态的将"Main.Name", "Items[0].Name", "Items[1].Name", ..., "Items[n].Name"作为绑定列就能解决问题了,但跟上面的问题一样,这些PropertyName在运行时根本不被识别。
当然,对上面的两个问题,如果直接将DataTable作为数据源,当然这些问题也就都不存在了,但DataTable本身只能算是是一个弱类型对象的List,至于弱类型对象与强类型对象之间的优缺点,就不在此篇文章的讨论范围内了,也无意就此说明或者解释些什么,就事论事,只是想解决类似的问题。
我也在网上找了一下相关的解决方案,似乎只有这一篇文章http://blogs.msdn.com/b/msdnts/archive/2007/01/19/how-to-bind-a-datagridview-column-to-a-second-level-property-of-a-data-source.aspx谈到了类似的问题,并且给出了解决方案,我也仔细拜读了这篇文章,并按他的方法尝试了一番,确实能解决部分问题,但总觉得其实现方式不慎完美,其一,他是通过TypeDescriptionProviderAttribute来实现的,而在.NET FrameWork中,对Attribute的使用是有一些限制的,比如Attribute不支持泛型声明,也不支持变量,灵活性总共不高,其二,总觉得这种方式其实某种程度上可以理解为“篡改”了元数据,很难说会不会对系统其他地方的运行带来问题,比如在反射该类型时等等诸如此类的地方还是有可能会出问题的。
后来在csdn上看到有人也问到类似的问题,有一位高人提到可以用ITypedList实现,但大概一般高人都习惯性的点到为止,并没有给出完整的解决方案。只好仔细的研究了一下ITypedList的相关内容,最终总算是折腾出了一个自己还比较满意的解决方案,代码不是很复杂,也就懒得去一一说明了。
下面是我的解决方案:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text.RegularExpressions; namespace DynamicBinding { public class PropertyBindingList<T> : BindingList<T>, ITypedList { private List<String> bindProperties; private Dictionary<String, PropertyDescriptor> propertyDescriptorDictionary; public PropertyBindingList() { this.bindProperties = new List<String>(); this.innerPropertyDescriptorCollection = TypeDescriptor.GetProperties(typeof(T)); this.propertyDescriptorDictionary = new Dictionary<String, PropertyDescriptor>(); } public void AddBindProperty(String propertyName) { if (this.bindProperties.Contains(propertyName)) { throw new ArgumentException(String.Format(@"The property ""{0}"" is already exists.", propertyName), "propertyName"); } this.bindProperties.Add(propertyName); } public void RemoveBindProperty(String propertyName) { this.bindProperties.Remove(propertyName); } private PropertyDescriptorCollection innerPropertyDescriptorCollection; public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { var array = new PropertyDescriptor[this.innerPropertyDescriptorCollection.Count + this.bindProperties.Count]; this.innerPropertyDescriptorCollection.CopyTo(array, 0); for (var i = 0; i < this.bindProperties.Count; i++) { array[this.innerPropertyDescriptorCollection.Count + i] = this.GetPropertyDescriptor(this.bindProperties[i]); } return new PropertyDescriptorCollection(array); } private PropertyDescriptor GetPropertyDescriptor(String propertyPath) { if (String.IsNullOrEmpty(propertyPath)) { throw new ArgumentNullException("propertyPath"); } var array = propertyPath.Split(‘.‘); var first = array.First(); var propertyDescriptor = this.GetPropertyDescriptor(this.innerPropertyDescriptorCollection, first); for (var i = 1; i < array.Length; i++) { propertyDescriptor = this.CreatePropertyDescriptor(propertyDescriptor, array[i]); } return propertyDescriptor; } private PropertyDescriptor GetPropertyDescriptor(PropertyDescriptorCollection propertyDescriptorCollection, String name) { var regex = new Regex(@"(?<name>\w+)\[(?<index>\d+)\]"); var match = regex.Match(name); if (match.Success) { var propertyName = match.Groups["name"].Value; var indexText = match.Groups["index"].Value; var index = Int32.Parse(indexText); var arrayPropertyDescriptor = propertyDescriptorCollection[propertyName]; if (arrayPropertyDescriptor == null) { throw new ArgumentOutOfRangeException(String.Format(@"Can not find property descriptor ""{0}"" in propertyDescriptorCollection.", propertyName)); } var itemPropertyDescriptorName = String.Format("{0}[{1}]", arrayPropertyDescriptor.Name, index); PropertyDescriptor itemPropertyDescriptor; if (!this.propertyDescriptorDictionary.TryGetValue(itemPropertyDescriptorName, out itemPropertyDescriptor)) { itemPropertyDescriptor = new InnerItemPropertyDescriptor( itemPropertyDescriptorName, arrayPropertyDescriptor, index); this.propertyDescriptorDictionary.Add(itemPropertyDescriptorName, itemPropertyDescriptor); } return itemPropertyDescriptor; } else { var result = propertyDescriptorCollection[name]; if (result == null) { throw new ArgumentOutOfRangeException(String.Format(@"Can not find property descriptor ""{0}"" in propertyDescriptorCollection.", name)); } return result; } } private PropertyDescriptor CreatePropertyDescriptor(PropertyDescriptor parentPropertyDescriptor, String name) { var regex = new Regex(@"(?<name>\w+)\[(?<index>\d+)\]"); var match = regex.Match(name); if (match.Success) { var propertyName = match.Groups["name"].Value; var indexText = match.Groups["index"].Value; var index = Int32.Parse(indexText); var propertyDescriptorName = parentPropertyDescriptor.Name + "." + propertyName; PropertyDescriptor arrayPropertyDescriptor; if (!this.propertyDescriptorDictionary.TryGetValue(propertyDescriptorName, out arrayPropertyDescriptor)) { var properties = TypeDescriptor.GetProperties(parentPropertyDescriptor.PropertyType); var valuePropertyDescriptor = properties[propertyName]; if (valuePropertyDescriptor == null) { throw new ArgumentOutOfRangeException(String.Format(@"Can not find property descriptor ""{0}"" in type ""{1}"".", propertyName, parentPropertyDescriptor.PropertyType)); } arrayPropertyDescriptor = new InnerPropertyDescriptor(propertyDescriptorName, parentPropertyDescriptor, valuePropertyDescriptor); this.propertyDescriptorDictionary.Add(propertyDescriptorName, arrayPropertyDescriptor); } var itemPropertyDescriptorName = String.Format("{0}[{1}]", arrayPropertyDescriptor.Name, index); PropertyDescriptor itemPropertyDescriptor; if (!this.propertyDescriptorDictionary.TryGetValue(itemPropertyDescriptorName, out itemPropertyDescriptor)) { itemPropertyDescriptor = new InnerItemPropertyDescriptor( itemPropertyDescriptorName, arrayPropertyDescriptor, index); this.propertyDescriptorDictionary.Add(itemPropertyDescriptorName, itemPropertyDescriptor); } return itemPropertyDescriptor; } else { var propertyDescriptorName = parentPropertyDescriptor.Name + "." + name; PropertyDescriptor propertyDescriptor; if (!this.propertyDescriptorDictionary.TryGetValue(propertyDescriptorName, out propertyDescriptor)) { var properties = TypeDescriptor.GetProperties(parentPropertyDescriptor.PropertyType); var valuePropertyDescriptor = properties[name]; if (valuePropertyDescriptor == null) { throw new ArgumentOutOfRangeException(String.Format(@"Can not find property descriptor ""{0}"" in type ""{1}"".", name, parentPropertyDescriptor.PropertyType)); } propertyDescriptor = new InnerPropertyDescriptor( propertyDescriptorName, parentPropertyDescriptor, valuePropertyDescriptor); this.propertyDescriptorDictionary.Add(propertyDescriptorName, propertyDescriptor); } return propertyDescriptor; } } public String GetListName(PropertyDescriptor[] listAccessors) { return typeof(T).Name; } private abstract class BasePropertyDescriptor : PropertyDescriptor { public BasePropertyDescriptor(String name) : base(name, null) { } public override bool IsReadOnly { get { return false; } } public override void ResetValue(object component) { } public override bool CanResetValue(object component) { return false; } public override bool ShouldSerializeValue(object component) { return true; } } private class InnerPropertyDescriptor : BasePropertyDescriptor { public InnerPropertyDescriptor(String name, PropertyDescriptor parentPropertyDescriptor, PropertyDescriptor valuePropertyDescriptor) : base(name) { this.ParentPropertyDescriptor = parentPropertyDescriptor; this.ValuePropertyDescriptor = valuePropertyDescriptor; } public PropertyDescriptor ParentPropertyDescriptor { get; private set; } public PropertyDescriptor ValuePropertyDescriptor { get; private set; } public override Type ComponentType { get { return this.ParentPropertyDescriptor.PropertyType; } } public override Type PropertyType { get { return this.ValuePropertyDescriptor.PropertyType; } } public override object GetValue(object component) { var parentPropertyValue = this.ParentPropertyDescriptor.GetValue(component); if (parentPropertyValue != null) { return this.ValuePropertyDescriptor.GetValue(parentPropertyValue); } return null; } public override void SetValue(object component, object value) { var parentPropertyValue = this.ParentPropertyDescriptor.GetValue(component); if (parentPropertyValue != null) { this.ValuePropertyDescriptor.SetValue(parentPropertyValue, value); this.OnValueChanged(component, EventArgs.Empty); } } } private class InnerItemPropertyDescriptor : BasePropertyDescriptor { public InnerItemPropertyDescriptor(String name, PropertyDescriptor parentPropertyDescriptor, Int32 index) : base(name) { this.ParentPropertyDescriptor = parentPropertyDescriptor; this.Index = index; } public PropertyDescriptor ParentPropertyDescriptor { get; private set; } public Int32 Index { get; private set; } public override Type ComponentType { get { return this.ParentPropertyDescriptor.PropertyType; } } public override Type PropertyType { get { return this.ParentPropertyDescriptor.PropertyType.GetElementType(); } } public override object GetValue(object component) { var parentPropertyValue = this.ParentPropertyDescriptor.GetValue(component) as IList; if (parentPropertyValue != null && parentPropertyValue.Count > this.Index) { return parentPropertyValue[this.Index]; } return null; } public override void SetValue(object component, object value) { var parentPropertyValue = this.ParentPropertyDescriptor.GetValue(component) as IList; if (parentPropertyValue != null && parentPropertyValue.Count > this.Index) { parentPropertyValue[this.Index] = value; this.OnValueChanged(component, EventArgs.Empty); } } } } }
以下是测试示例:
using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace DynamicBinding { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new TestForm()); } } partial class TestForm { private System.ComponentModel.IContainer components = null; protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows 窗体设计器生成的代码 private void InitializeComponent() { this.dataGridView1 = new System.Windows.Forms.DataGridView(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); this.SuspendLayout(); // // dataGridView1 // this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill; this.dataGridView1.Location = new System.Drawing.Point(0, 0); this.dataGridView1.Name = "dataGridView1"; this.dataGridView1.RowTemplate.Height = 23; this.dataGridView1.Size = new System.Drawing.Size(784, 562); this.dataGridView1.TabIndex = 0; // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(784, 562); this.Controls.Add(this.dataGridView1); this.Name = "TestForm"; this.Text = "TestForm"; ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); this.ResumeLayout(false); } #endregion private System.Windows.Forms.DataGridView dataGridView1; } public partial class TestForm : Form { public TestForm() { InitializeComponent(); } protected override void OnLoad(EventArgs e) { this.dataGridView1.AutoGenerateColumns = true; var list = new PropertyBindingList<TestA>(); list.AddBindProperty("List[0].BID"); list.AddBindProperty("List[0].List[0].CID"); list.AddBindProperty("List[0].List[0].CName"); list.AddBindProperty("List[0].List[1].CID"); list.AddBindProperty("List[0].List[1].CName"); list.AddBindProperty("List[1].BName"); list.AddBindProperty("List[1].List[0].CID"); list.AddBindProperty("List[1].List[0].CName"); list.AddBindProperty("List[1].List[1].CID"); list.AddBindProperty("List[1].List[1].CName"); list.Add(new TestA { AID = 1, AName = "A001", List = new TestB[] { new TestB { BID = 11, BName = "B11", List = new TestC[] { new TestC { CID = 111, CName = "C111" }, new TestC { CID = 112, CName = "C112" } } }, new TestB { BID = 12, BName = "B12", List = new TestC[] { new TestC { CID = 113, CName = "C113" }, new TestC { CID = 114, CName = "C114" } } }, new TestB { BID = 13, BName = "B13" } } }); list.Add(new TestA { AID = 1, AName = "A001" }); this.dataGridView1.DataSource = list; base.OnLoad(e); } } public class TestA { public Int32 AID { get; set; } public String AName { get; set; } public TestB[] List { get; set; } } public class TestB { public Int32 BID { get; set; } public String BName { get; set; } public TestC[] List { get; set; } } public class TestC { public Int32 CID { get; set; } public String CName { get; set; } } }
以下是测试结果: