![]() |
Web Development »
Custom Controls »
General
Intermediate
xacc.propertygridByleppieAn ASP.NET PropertyGrid |
C#, Javascript, CSS, HTML, XHTML, Windows, .NET 1.1, .NET 2.0, ASP.NET, WebForms, VS.NET2003, IE 6.0, Architect, DBA, Dev, QA
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

xacc.propertygrid is an ASP.NET custom control with partial design-time support. It makes use of a host of front- and back-end technologies to provide a dynamic, yet responsive experience to the user. Go to http://xacc.qsh.eu/ (open in new window) right now for a demo! Fiddle, go wild, change the values!
This was a very crude proof of concept, based on Anthem and using its controls. This proved to work well, but the AJAX interaction was too bloated for my liking. Also, the inability to apply CSS in an efficient manner to WebControls made this not very flexible, but it worked.
The control itself is not very exciting, except for the actual binding. Lets look at the code:
object selobj;
false)>
public object SelectedObject
{
get {return selobj;}
set
{
if (selobj != value)
{
selobj = value;
CreateGrid();
}
}
}
ArrayList proplist = new ArrayList();
Hashtable properties = new Hashtable();
ArrayList catlist = new ArrayList();
int catcounter = 0;
int subcounter = 0;
int itemcounter = 0;
void CreateGrid()
{
if (selobj == null)
{
return;
}
Controls.Clear();
properties.Clear();
proplist.Clear();
itemcounter =
catcounter =
subcounter = 0;
Controls.Add( new PropertyGridHeader());
Hashtable cats = new Hashtable();
foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(selobj))
{
if (!pd.IsBrowsable)
{
continue;
}
string cat = pd.Category;
Hashtable mems = cats[cat] as Hashtable;
if (mems == null)
{
cats[cat] = mems = new Hashtable();
}
try
{
PropertyGridItem pgi = new PropertyGridItem(pd);
pgi.controlid = ClientID + "_" + itemcounter++;
properties[pgi.controlid] = pgi;
object o = selobj;
object subo = null;
try
{
subo = pd.GetValue(o);
}
catch
{}
if ( pd.Converter.GetPropertiesSupported())
{
foreach (PropertyDescriptor spd in pd.Converter.GetProperties(subo))
{
if (spd.IsBrowsable)
{
PropertyGridItem pgsi = new PropertyGridSubItem(spd, pgi);
pgsi.controlid = ClientID + "_" + itemcounter++;
pgi.subitems.Add(pgsi);
properties[pgsi.controlid] = pgsi;
}
}
}
mems.Add(pd.Name, pgi);
}
catch (Exception ex)
{
Page.Response.Write(ex);
}
}
this.catlist.Clear();
ArrayList catlist = new ArrayList(cats.Keys);
catlist.Sort();
HtmlContainerControl cc = new HtmlGenericControl("div");
cc.ID = "cats";
Controls.Add(cc);
foreach (string cat in catlist)
{
PropertyGridCategory pgc = new PropertyGridCategory();
pgc.CategoryName = cat;
this.catlist.Add(pgc);
cc.Controls.Add(pgc);
Hashtable i = cats[cat] as Hashtable;
ArrayList il = new ArrayList(i.Keys);
il.Sort();
foreach (string pginame in il)
{
PropertyGridItem pgi = i[pginame] as PropertyGridItem;
proplist.Add(pgi);
pgc.Controls.Add(pgi);
if (pgi.subitems.Count > 0)
{
SubItems si = new SubItems();
pgi.Controls.Add(si);
foreach (PropertyGridItem spgi in pgi.subitems)
{
si.Controls.Add(spgi);
proplist.Add(spgi);
}
}
}
}
Controls.Add( new PropertyGridFooter());
}
Here I simply iterate through the PropertyDescriptorCollection provided from the TypeDescriptor. Using a PropertyDescriptor is much more useful in this scenario, as it takes a lot of boring reflection code out of the game. It also has the added benefit of caching reflection calls, speeding up reuse when the same controls are recreated.
Firstly, all the properties (and sub properties) are added, and their categories tracked. Then categories get sorted alphabetically, category containers are created, and properties are added to them.
Instead of using WebControls, I decided to use custom controls (deriving from Control). This has the advantage that I can practically emit any HTML that I require. As the design was re-factored, I simply have a few to controls to control the emission. They are as follows:
PropertyGridHeader - carries the info at the top of the control.
PropertyGridCategory - created for each category.
PropertyGridItem - 'Top level' properties of the selected object.
SubItems - container for child properties.
PropertyGridSubItem - 'Child level' properties.
PropertyGridFooter - contains help and bottom bar.Lets look how the PropertyGridItem works, specifically its RenderEditor method.
void RenderEditor(HtmlTextWriter writer)
{
if (propdesc.IsReadOnly || ParentGrid.ReadOnly)
{
writer.Write(@"<span title=""{1}""><span" +
@" id=""{0}"" style=""color:gray"">{1}" +
@"</span></span>",
controlid,
PropertyValue);
}
else
{
TypeConverter tc = propdesc.Converter;
if ( tc.GetStandardValuesSupported())
{
string pv = PropertyValue;
writer.Write(@"<a onclick=""{2}.BeginEdit" +
@"(this); return false;"" href=""#""" +
@" title=""Click to edit""><span" +
@" id=""{0}"">{1}</span></a>",
controlid,
pv,
ParentGrid.ClientID);
writer.Write(@"<select style=""display" +
@":none"" onblur=""{0}.CancelEdit(this)"""+
@" onchange=""{0}.EndEdit(this)"">",
ParentGrid.ClientID);
foreach (object si in tc.GetStandardValues())
{
string val = tc.ConvertToString(si);
if (val == pv)
{
writer.Write(@"<option " +
@"selected=""selected"">{0}</option>", val);
}
else
{
writer.Write(@"<option>{0}</option>", val);
}
}
writer.Write("</select>");
}
else
{
if (tc.CanConvertFrom(typeof(string)))
{
writer.Write(@"<a onclick=""{2}.BeginEdit" +
@"(this);return false"" href=""#""" +
@" title=""Click to edit""><span " +
@"id=""{0}"">{1}</span></a>",
controlid,
PropertyValue,
ParentGrid.ClientID);
writer.Write(@"<input onkeydown=""return {0}." +
@"HandleKey(this,event)"" onblur=""{0}.CancelEdit"""+
@"(this) style=""display:none"" type" +
@"=""text"" onchange=""{0}.EndEdit(this)"" />",
ParentGrid.ClientID);
}
else
{
writer.Write(@"<span title=""{1}""><span " +
@"id=""{0}"" style=""color:gray"">{1}</span></span>",
controlid,
PropertyValue);
}
}
}
}
This is a simple elimination tree, deciding what should be done.
PropertyGrid is read-only, render as a 'label'.
If either option 2 or 3 was chosen, when the 'label' gets clicked, the 'label' is hidden, and the 'edit' control shown till the control loses focus or changes its value.
[Skinny.Method]
public string[] GetValues()
{
string[] output = new string[proplist.Count];
for (int i = 0; i < output.Length; i++)
{
output[i] = (proplist[i] as PropertyGridItem).PropertyValue;
}
return output;
}
[Skinny.Method]
public string[] SetValue(string id, string val)
{
if (!ReadOnly)
{
PropertyGridItem pgi = properties[ClientID +
"_" + id] as PropertyGridItem;
pgi.PropertyValue = val;
}
return GetValues();
}
[Skinny.Method]
public string[] GetDescription(string id)
{
PropertyGridItem pgi = properties[ClientID +
"_" + id] as PropertyGridItem;
PropertyDescriptor pd = pgi.Descriptor;
string[] output = new string[] { pd.DisplayName + " : " +
pd.PropertyType.Name, pd.Description };
return output;
}
Initially I used Anthem for AJAX support, but their model seemed too bloated. I really wanted a cut-down version. The result, "Skinny" (I renamed it to prevent conflicts), is a bare bones version of Anthem, and only supports calling methods on a Control, but with the added feature that static methods can be called as well. The PropertyGrid's methods are designed to do as much as possible per 'callback', minimizing traffic needed.
Element =
{
extended: true,
visible: function(vis)
{
if (vis != null) {
if (typeof vis == 'boolean')
vis = vis ? '' : 'none';
this.style.display = vis;
}
return this.style.display != 'none';
},
kids: function(index)
{
if (index == null) {
var c = [];
for (var i = 0; i < this.childNodes.length; i++)
if (this.childNodes[i].nodeType != 3)
c.push($(this.childNodes[i]));
return c;
}
else
{
for (var i = 0, j = 0; i < this.childNodes.length; i++) {
if (this.childNodes[i].nodeType != 3) {
if (j == index)
return $(this.childNodes[i]);
j++;
}
}
return null;
}
},
parent: function()
{
return $((this.parentNode == 'undefined') ?
this.parentElement : this.parentNode);
},
prev: function()
{
var p = this.previousSibling;
while (p.nodeType == 3)
p = p.previousSibling;
return $(p);
},
next: function()
{
var p = this.nextSibling;
while (p.nodeType == 3)
p = p.nextSibling;
return $(p);
}
};
function $(e)
{
function extend(dst,src)
{
if (!dst.extended)
for (var i in src)
dst[i] = src[i];
return dst;
}
return extend( (typeof e == 'string') ?
document.getElementById(e) : e , Element);
}
The $ function was inspired by the Prototype lib, allowing me to use cross browser safe JavaScript. JavaScript has beautiful functional bits. This might not work for everybody, but it has proved handy for me.
The rest of the JavaScript contains three functions for AJAX, and finally the main PropertyGrid prototype. This allows me to handle everything in a per-instance way, and allows me to dynamically inject the styles into the control. There are two classes of functions, the event handlers responding to user input, and the 'editing' functions consisting of BeginEdit, EndEdit and CancelEdit.
The CSS is also not very exciting, except for the CSS injected when the control loads to apply its style. This unfortunately is not supported in Opera, but I have a fix in place for that, which emits the style into the HTML body (but breaking XHTML conformance). Let's look how this is done:
ApplyStyles: function(stylesheet)
{
var self = this;
function rule(sel,val)
{
var sels = sel.split(',');
for (var i = 0; i < sels.length; i++)
{
var s = sels[i];
var re = /\s/;
var res = re.exec(s);
if (res)
s = s.replace(re, '_' + self.id + ' ');
else
s = s + '_' + self.id;
if (stylesheet.addRule) //IE
stylesheet.addRule(s, val);
else if (stylesheet.insertRule) // Moz
stylesheet.insertRule(s + '{' + val + '}',
stylesheet.cssRules.length);
else
return; //opera
}
}
rule('.PG','width:' + this.width + 'px');
rule('.PG *','color:' + this.fgcolor + ';font-family:' +
this.family + ';font-size:' + this.fontsize);
rule('.PGH,.PGF,.PGC,.PGF2','border-color:' +
this.headerfgcolor + ';background-color:' + this.bgcolor);
rule('.PGC *','line-height:' + this.lineheight +
'px;height:' + this.lineheight +'px');
rule('.PGC a,.PGC_OPEN,.PGC_CLOSED',
'width:' + this.padwidth + 'px');
rule('.PGC_HEAD span','color:' + this.headerfgcolor);
rule('.PGI_NONE,.PGI_OPEN,.PGI_CLOSED','width:'+
this.padwidth+'px;height:'+this.LineHeightMargin()+'px');
rule('.PGI_NAME,.PGI_VALUE,.PGI_NAME_SUB','width:'+
this.HalfWidth()+'px;background-color:'+this.itembgcolor);
rule('.PGI_VALUE a,.PGI_VALUE select','width:100%');
rule('.PGI_NAME_SUB span','margin-left:' + this.padwidth + 'px');
rule('.PGI_VALUE a:hover','background-color:' + this.selcolor);
rule('.PGI_VALUE input','width:' + this.HalfWidthLess3() +
'px;line-height:' + this.InputLineHeight() +
'px;height:' + this.InputLineHeight() + 'px');
}
The rules are added to the last style sheet in the browser window. To make CSS 'instance' like, class names are appended with the ClientID of the PropertyGrid.
Sample properties demonstrating attribute usage to control the output:
[Category("Appearance")]
[Description("Change this value, and see the ones below change too." +
"Change a value from below and see how this one changes.")]
public Rectangle Bounds
{
get {return bounds;}
set {bounds = value;}
}
[TypeConverter(typeof(ExpandableObjectConverter))]
public Nested2 NestedStruct
{
get {return n2;}
set {n2 = value;}
}
A more advanced example allowing string[] to be set via a comma delimited string (as there is no access to an Editor dialog currently):
string[] buddies = {"Tom","Dick","Harry"};
[TypeConverter(typeof(StringArrayConverter))]
public string[] Buddies
{
get {return buddies ; }
set {buddies = value; }
}
public class StringArrayConverter :
System.ComponentModel.ArrayConverter
{
public override bool CanConvertTo(ITypeDescriptorContext
context, Type destinationType)
{
if (destinationType == typeof(string))
{
return true;
}
return base.CanConvertTo (context, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext
context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom (context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext
context, CultureInfo culture, object value)
{
if (value is string)
{
return (value as string).Split(',');
}
return base.ConvertFrom (context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext
context, CultureInfo culture,
object value, Type destinationType)
{
if (destinationType == typeof(string))
{
return string.Join(",", value as string[]);
}
return base.ConvertTo (context, culture, value, destinationType);
}
}
NOTE: Your classes must be public, or else you will get SecurityAccess exceptions if you run under a limited ASPNET account (ie. most servers).
In the code-infront:
<%@ Page language="c#"
Codebehind="default.aspx.cs"
AutoEventWireup="false"
Inherits="PropertyGridWeb.WebForm1"
enableViewState="true" %>
<%@ Register TagPrefix="xacc" Namespace="Xacc" Assembly="xacc.propertygrid" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML lang="en">
<HEAD>
<title>ASP.NET PropertyGrid Demo</title>
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<xacc:propertygrid id="pg1" runat="server" ShowHelp="True"></
xacc:propertygrid>
<xacc:propertygrid id="pg2" runat="server" ReadOnly="True" Width="350"
SelectionColor="CadetBlue" BackgroundColor="NavajoWhite"
FontFamily="Tahoma" FontSize="9pt" ForeColor="DimGray"
HeaderForeColor="Brown" ItemBackgroundColor="WhiteSmoke">
</xacc:propertygrid>
</form>
</body>
</HTML>
The design-time support is limited. When you drag and drop the control into a form, the above will be 'generated'. To get the grids looking better, add a designtime link to the style sheet.
The code-behind:
void Page_Load(object sender, System.EventArgs e)
{
pg1.SelectedObject = Global.STATIC;
pg2.SelectedObject = Global.STATIC;
}
Just as you would use the normal PropertyGrid. Keep in mind you are using a 'stateless' environment, hence why I simply used static members in the example.
Possible enhancements:
foo.Save() ).
Thanks to Paul Watson for help on JavaScript and CSS.
Thanks to the authors of Anthem.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 21 Mar 2007 Editor: Sean Ewington |
Copyright 2006 by leppieEverything else Copyright © CodeProject, 1999-2009 Web18 | Advertise on the Code Project |