DataGridView绑定复杂实体(属性本身又是实体)

 

     今天我们来讨论下一个实体中某个属性又是实体的类型的集合绑定到DataGridView上的问题。

   先来写一个Student类

   

 1 public class Student
 2 
 3     {
 4 
 5         public int StudentNo  { get; set; }
 6 
 7         public string StudentName { get; set; }
 8 
 9         public string Address { get; set; }
10 
11         public int GradeId { get; set; }
12 
13        
14 
15         public string Gender { get; set; }
16 
17         public string LoginPwd { get; set; }
18 
19         public DateTime Birthday { get; set; }
20 
21         public string Email { get; set; }
22 
23         public string  Phone { get; set; }
24 
25            
26 
27     }

 

然后有一个Result

  

 public  class Result

    {

        public Student Student { get; set; }

        public Subject Subject { get; set; }

        public DateTime ExamDate { get; set; }

        public int StudentResult { get; set; }

}

 

     我们可以看到Result中有一个属性Student的类型是另外一个实体类Student。

     当我们通过List<Result>拿到符合条件的数据后,就可以将List<Result>中的数据绑定到DataGridView上了。但是随之又一个问题产生就,就是DataPropertyName属性应该赋什么值??写Student.StudentName,结果打印出来为空。

   今天本人讲两种解决方案:

 方案一:通过重写ToString()方法的方案解决

  01.如果能保证在实体Result中的Student属性到时候在界面上行只显示Student中的一个属性,例如StudentName,那么我们重写Student的ToString()方法来达到目的。

  重写代码如下:

  

 public override string ToString()

        {

            return StudentName;

        }

 

   02.这时候只需要设置DataPropertyName的值为Student,即可,因为设置了Student后,会自动调用它的ToString()方法将StudentName返回。

   

  方案二:在DataGridView控件中,通过在CellFormatting事件中通过反射获取。

   01.当数据无法显示在DataGirdView中的时候,其实通过断点调试法,我们发现数据已经在集合中已经有了。只是无法显示在DataGridView控件中而已。那么我们在DataGridView控件的CellFormatting事件中编码实现。故名思意,就是在列加载的时候出发的时间,又称为重写该事件。

代码如下:

        

  //找到绑定字段中带下划线的列

            if ((dataGridView1.Rows[e.RowIndex].DataBoundItem != null) &&

                (dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Contains(".")))

            {

                //用split方法将其分割成一个数组

                string[] names = dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Split('.');

                object obj = dataGridView1.Rows[e.RowIndex].DataBoundItem;//获取到当前记录绑定的类型

                for (int i = 0; i < names.Count(); ++i)

                {

                    try

                    {

                        //通过反射的方式获取当前列的属性值,如StudentName

                        //第一次循环到Student,第二次拿到的是StudentName

                        var result = obj.GetType().GetProperty(names[i]).GetValue(obj, null);

                        obj = result;

                        e.Value = result.ToString();//拿到对应的值

                    }

                    catch (Exception)

                    {

                        return;

                        throw;

                    }

                }

            }

 

  02.这时候要给DataPropertyName属性赋值为Student.StudentName

  解析:这种方式可以处理Student中多列出现在DataGridView中的情况,是比较好的解决方案。而且这样解决方案可以处理Result中引入多个外部实体的属性的情况。

方案三:同方案二一样:msdn提供的一种方式,但是会报错,暂时收录:

 

 if ((dataGridView1.Rows[e.RowIndex].DataBoundItem != null) &&

 

             (dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Contains("_")))

            {

 

                string[]nameAndProp= dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Split(new char[] { '_' });

                //该行代码会报错,无法将当前行的类型读取转汉城DataRowView

            ObjectunboundValue= ((DataRowView)dataGridView1.Rows[e.RowIndex].DataBoundItem)[nameAndProp[0]];

        PropertyInfo objectProperty=unboundValue.GetType().GetProperty(nameAndProp[1]);

        e.Value=objectProperty.GetValue(unboundValue,null).ToString();

 

            }

 

 

方案四:msdn解决方案,通过特性实现,How to bind a DataGridView column to a second-level property of a data source,但是本人实验后,无法解决。

暂且收录如下:

    This is a frequently asked question. Suppose that we have a class Person which has a property of type Address(another class). The class Address has its own properties. Now we create a collection of Person objects, e.g. List<Person> and would like to bind a DataGridView to the collection.

 

The following is the code for class Person and Address.

 

class Person

{

         private string id;

         private string name;

         private Address homeAddr;

         public string ID

         {

                   get { return id;}

                   set { id = value;}

         }

         public string Name

         {

                   get { return name;}

                   set { name = value;}

         }

         public Address HomeAddr

         {

                   get { return homeAddr;}

                   set { homeAddr = value;}

         }       

}

 

class Address

{

         private string cityname;

         private string postcode;

         public string CityName

         {

                   get { return cityname;}

                   set { cityname = value;}

         }

         public string PostCode

         {

                   get { return postcode;}

                   set { postcode = value;}

         }

}

 

 

As we all know, DataGridView columns could only be bound to the first-level properties of class Person, e.g. the ID, Name or HomeAddr properties. The following is the code to bind a DataGridView to a collection of Person objects.

 

List<Person> persons = new List<Person>();

// add some Person objects to the collection

...

dataGridView1.DataSource = persons;

dataGridView1.Columns[0].DataPropertyName = "ID";

dataGridView1.Columns[1].DataPropertyName = "Name";

dataGridView1.Columns[2].DataPropertyName = "HomeAddr";

 

When a DataGridView column is bound to the HomeAddr property, the name of the class Address will be displayed under the column, i.e. projectname.Address.

 

If we set the DataPropertyName property of a DataGridView column to "HomeAddr.CityName", an empty text will be displayed under the column, because DataGridView could not find a property called "HomeAddr.CityName" in the class Person.

 

Is there a way to bind a DataGridView column to the CityName or PostCode property of the HomeAddr property? The answer is YES!

 

.NET Framework 1.x can implement ICustomTypeDescriptor interface for the class Person. When a Person object in the collection is going to be displayed in a control, data binding calls ICustomTypeDescriptor.GetProperties method to get the object's properties. When implementing ICustomTypeDescriptor.GetProperties method, we could create PropertyDescriptor instances for the second-level properties and return them with the original PropertyDescriptor instances of class Person.

 

There're two problems here. One problem is that if there's no object in the collection, the ICustomTypeDescriptor.GetProperties method won't be called so that DataGridView couldn't 'see' secondary-level properties of class Person. The other problem is that it requires modifying the class Person because the class Person needs to implement the ICustomTypeDescriptor interface.

 

To work around the first problem, we could resort to ITypedList interface, i.e. implement the ITypedList interface for the collection. If a data source has implemented the ITypedList interface, data binding calls ITypedList.GetItemProperties to get the properties available for binding. In the ITypedList.GetItemProperties method, we could create an instance of class Person and then call TypeDescriptor.GetProperties(component) method passing the Person instance as the parameter, which in turn calls ICustomTypeDescriptor.GetProperties of the class Person implementation.

 

.NET Framework 2.0 way is to make use of TypeDescriptionProvider and CustomTypeDescriptor classes, which expand support for ICustomTypeDescriptor. It allows you to write a separate class that implements ICustomTypeDescriptor (for convenience, we could derive the class directly from CustomTypeDescriptor which provides a simple default implementation of the ICustomTypeDescriptor interface) and then to register this class as the provider of description for other types.

 

Even if a type is added a TypeDesciptionProvider, data binding won't call the custom type descriptor's GetProperties method to get the object's properties. So we still need to implement ITypedList interface for the collection and in the ITypedList.GetItemProperties method, call TypeDescriptor.GetProperties(type) which in turn calls the custom type descriptor's GetProperties method.

 

To take advantage of this new functionality, we first need to create a TypeDescriptionProvider, which simply derives from TypeDescriptionProvider and overrides its GetTypeDescriptor method. GetTypeDescriptor returns the ICustomTypeDescriptor implementation or the derived CustomTypeDescriptor that TypeDescriptor should use when querying for property descriptors.

 

When creating providers that are only intended to augment or modify the existing metadata for a type, rather than completely replace it, a recommended approach is to call 'TypeDescriptor.GetProvider' method in the new providers' constructor to get the current provider for the specified type. This base provider can then be used whenever you need to access the underlying type description.

 

As for PropertyDescriptor, it is an abstract class that derives from another abstract class, MemberDescriptor. MemberDescriptor provides the basic information for each property, and PropertyDescriptor adds functionality related to changing a property's value and determining when that value has changed. To create a PropertyDescriptor on the class Person for each second-level property, we first need to create a custom class that derives from PropertyDescriptor. Pass the original PropertyDescriptor of the second-level property in the new PropertyDescriptor class's constructor, and then use the original PropertyDescriptor whenever we need to query the information of the second-level property.

 

The following is the code of a custom class that derives from PropertyDescriptor.

 

public class SubPropertyDescriptor : PropertyDescriptor

{

        private PropertyDescriptor _subPD;

        private PropertyDescriptor _parentPD;

 

        public SubPropertyDescriptor(PropertyDescriptor parentPD,PropertyDescriptor subPD,string pdname)

            : base(pdname,null)

        {           

            _subPD = subPD;

            _parentPD = parentPD;

        }

 

        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;

        }

 

        public override Type ComponentType

        {

            get { return _parentPD.ComponentType; }

        }

        public override Type PropertyType { get { return _subPD.PropertyType; } }

 

        public override object GetValue(object component)

        {

           return _subPD.GetValue(_parentPD.GetValue(component));

        }

 

        public override void SetValue(object component, object value)

        {           

            _subPD.SetValue(_parentPD.GetValue(component), value);

            OnValueChanged(component, EventArgs.Empty);

        }

}

 

 

The following is the code of a derived CustomTypeDescriptor class.

 

public class MyCustomTypeDescriptor : CustomTypeDescriptor

{      

        public MyCustomTypeDescriptor(ICustomTypeDescriptor parent)

            : base(parent)

        {

        }

 

        public override PropertyDescriptorCollection GetProperties()

        {

            PropertyDescriptorCollection cols = base.GetProperties();

 

            PropertyDescriptor addressPD = cols["HomeAddr"];

            PropertyDescriptorCollection homeAddr_child = addressPD.GetChildProperties();

          

            PropertyDescriptor[] array = new PropertyDescriptor[cols.Count + 2];

            cols.CopyTo(array, 0);

            array[cols.Count] = new SubPropertyDescriptor(addressPD,homeAddr_child["CityName"],"HomeAddr_CityName");

            array[cols.Count + 1] = new SubPropertyDescriptor(addressPD, homeAddr_child["PostCode"], "HomeAddr_PostCode");

 

            PropertyDescriptorCollection newcols = new PropertyDescriptorCollection(array);

            return newcols;           

        }    

  }

 

 

The following is the code of the custom TypeDescriptorProvider.

 

public class MyTypeDescriptionProvider : TypeDescriptionProvider

{

        private ICustomTypeDescriptor td;

 

        public MyTypeDescriptionProvider()

           : this(TypeDescriptor.GetProvider(typeof(Person)))

        {           

        }

        public MyTypeDescriptionProvider(TypeDescriptionProvider parent)

            : base(parent)

        {

        }

        public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)

        {

            if (td == null)

            {

                td = base.GetTypeDescriptor(objectType, instance);

                td = new MyCustomTypeDescriptor(td);

            }

            return td;

        }       

}

 

 

At the end, we adorn TypeDescriptionProviderAttribute to the class Person.

 

[TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))]

class Person

{...}

 

Then we could bind a DataGridView column to the second-level properties in the class Person as follows:

 

dataGridView1.Columns[2].DataPropertyName = "HomeAddr_CityName";

dataGridView1.Columns[3].DataPropertyName = "HomeAddr_PostCode";

  我们今天的探讨就此结束!前两种方案大家可以尽情使用!

 

你可能感兴趣的:(datagridview)