|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
Introductionxacc.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! Features
Limitations
DesignThe 1st implementationThis 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 real thing
ImplementationASP.NET server controlThe control itself is not very exciting, except for the actual binding. Lets look at the code: object selobj;
Here I simply iterate through the 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:
Lets look how the 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.
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. AJAX[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 JavascriptElement =
{
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 The rest of the JavaScript contains three functions for AJAX, and finally the main CSSThe 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 Installation
Usage exampleObject codeSample 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[] 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). ASP.NETIn 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 Points of interest
ConclusionPossible enhancements:
Thanks to Paul Watson for help on JavaScript and CSS. Thanks to the authors of Anthem. References
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||