Search This Blog

Loading...

Tuesday, January 13, 2009

How to use CustomTypeDescriptor to show Custom Object's Properties in FarPoint

The binding and the displaying of data is a task that is best handled separately from your normal logic code. Microsoft understands this and allows the use of attributes and other decorative thing to make the code more readable. One of the example is the PropertyGrid example in .Net, where one can set the display name of a property not by writing imperative code, but by putting on a declarative attribute ( DisplayName ("My Name")) in order to dictate the run time appearance of the property grid.

This is one step forward from the good old days when the logic code is cluttered up with trivial appearance information, and there was no delineation between the logic and the markup.

But I am not here to talk about how this attribute thing  is making my code clearer in terms of separation of concern. I am here to show you how to use attributes to use CustomTypeDescriptor to show Custom Object's Properties in FarPoint .

FarPoint is a spreadsheet-like component. It is a vast improvement over its .Net controls. For example, it can bind to an object with a list as its child property, but we can't do the same thing for .Net's DataGridView . The only downside is, you gonna pay for it. :(

Assume that we need to display the students' result in data grid. We have the following class definition:


public class Score
{
[DisplayName("Subject")]
public string SubjectName{get; set;}
[DisplayName("Result")]
public string Grade{get; set;}}

public class StudentInformation
{
public string Name{get;set;}
public string Id{get;set;}
}

[TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))]
public class Student
{
public StudentInformation StudentInfo{ get; set; }
public List<Score> Score{get; set;}
public Student()
{
StudentInfo=new StudentInformation();
Score=new List<Score>();
}
}


When the information is displayed, we want to show only the "Name" property in StudentInformation; we are not interested in the ID property field however. To accomplish this, we then can define a new type of TypeDescriptionProvider, called MyTypeDescriptionProvider that manipulates how the hierarchical data should be displayed.

This MyTypeDescriptionProvider needs to be applied on top of the student class in order to take effect. Here's the code:


public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
private ICustomTypeDescriptor td;
public MyTypeDescriptionProvider(): this(TypeDescriptor.GetProvider(typeof(Student)))
{
}

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;
}
}

public class MyCustomTypeDescriptor : CustomTypeDescriptor
{
public MyCustomTypeDescriptor(ICustomTypeDescriptor parent): base(parent)
{
}
public override PropertyDescriptorCollection GetProperties()
{
var cols = base.GetProperties();
var studentPD = cols["StudentInfo"];
var studentPDChildProperties = studentPD.GetChildProperties();
PropertyDescriptor[] array = new PropertyDescriptor[2];
array[0] = new SubPropertyDescriptor(studentPD, studentPDChildProperties["Name"], "Student Name");
array[1] = cols["Score"];
var newcols = new PropertyDescriptorCollection(array);
return newcols;
}
}

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);
}
}

To understand the above code I suggest you to step through it in debugger and see how the values changed. In a nutshell, this code, upon execution, will take the a StudentInfo object, rip it apart and get the "Name" property of it and rebind it to the FarPoint Spreadsheet.

And now you can just bind the Student object to the FarPoint object!


public static List<student> StudentList()
{
List<student> list = new List<student>();
Student s = new Student();
s.StudentInfo.Name = "John Smith";
s.StudentInfo.Id = "100001";
Score sc = new Score();
sc.SubjectName = "math";
sc.Grade = "A";
s.Score.Add(sc);
sc = new Score();
sc.SubjectName = "English";
sc.Grade = "A";
s.Score.Add(sc);
list.Add(s);
s = new Student();
s.StudentInfo.Name = "David Black";
s.StudentInfo.Id = "100002";
sc = new Score();
sc.SubjectName = "math";
sc.Grade = "B";
s.Score.Add(sc);
sc = new Score();
sc.SubjectName = "English";
sc.Grade = "A";
s.Score.Add(sc);
list.Add(s);
return list;
}

private void Form1_Load(object sender, EventArgs e)
{
fpSpread1.DataSource = StudentList();
}

Here's the end result:

Note also that by using the DisplayName attribute, we can set whatever name we like in the column header. For example, I bind "Subject" to the Score.SubjectName.

You may ask whether you can do the same for DataGridView control? The answer is part of it. You can slice and dice the sub properties and display only the data you want, but you can't display list inside an object.

Isn't this databinding thing an improvement over imperactive code, whereby you get to loop over the collection ( and the sub collection), set the column header manually and other boring coding in order to display data?

No comments: