xacc.propertygrid






4.94/5 (69投票s)
一个 ASP.NET PropertyGrid
目录
- 引言
- 特点
- 限制
- 设计
- 实现
- 安装
- 使用示例:
- 关注点
- 结论
- 参考文献
引言
xacc.propertygrid 是一个具有部分设计时支持的 ASP.NET 自定义控件。它利用了许多前端和后端技术,为用户提供动态且响应迅速的体验。立即访问 http://xacc.qsh.eu/(在新窗口中打开)进行演示!尽情修改这些值!
特点
- ASP.NET 服务器控件
- 易于使用,如同 WinForms 版本
- 包含 WinForms PropertyGrid 的所有功能,但不包含编辑器和设计器
- 自动数据绑定
- 自动错误检查
- 可定制
- 可折叠
- '实时模式'
- 非常轻量级(15kb Javascript 和 CSS)
- 响应迅速
- 兼容 ASP.NET 1.1 和 2.0
- 兼容所有现代浏览器(IE 6、Firefox 1.5+、Opera 8+)
- W3C XHTML 1.0 兼容
- W3C CSS 兼容
限制
- 无法在不刷新页面的情况下添加子属性(在这种情况下,控件会自动重新加载页面)
- 目前不支持编辑器
- 有限的设计时支持
- IE 7 似乎有问题,我将在他们发布“稳定”版本时支持此浏览器
设计
第一个实现
这是一个非常粗糙的概念验证,基于 Anthem 并使用了其控件。事实证明它工作得很好,但 AJAX 交互对我来说太臃肿了。此外,无法有效地将 CSS 应用于 WebControls 使其不够灵活,但它确实有效。
真正的产品
- 规划 - 这个阶段涉及思考很多事情,准确地可视化达到最终结果的路径。
- 布局 - 这个阶段涉及分解控件所需的所有元素。作为一个网格,使用简单的 DIV 表/网格相对容易。
- 模型 - 这个阶段涉及使用 CSS 和 Javascript 模拟静态 HTML。
- ASP.NET 服务器控件 - 这个阶段涉及将静态 HTML 重构为可重用控件,同时 Anthem 在此时被“精简”了。
- 将所有内容放在一起 - 最后需要进行一些调整才能使所有内容协同工作。
实现
ASP.NET 服务器控件
控件本身并不令人兴奋,除了实际的绑定。让我们看看代码
object selobj;
[Browsable(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());
}
在这里,我只是遍历 TypeDescriptor
提供的 PropertyDescriptorCollection
。在这种情况下,使用 PropertyDescriptor
更有用,因为它省去了大量乏味的反射代码。它还具有缓存反射调用的附加好处,从而在重用相同的控件时加快速度。
首先,添加所有属性(和子属性),并跟踪它们的类别。然后按字母顺序对类别进行排序,创建类别容器,并将属性添加到其中。
我决定使用自定义控件(继承自 Control)而不是 WebControls。这的好处是我可以发出我需要的任何 HTML。随着设计的重构,我只需要几个控件来控制输出。它们如下:
PropertyGridHeader
- 包含控件顶部的信息。PropertyGridCategory
- 为每个类别创建。PropertyGridItem
- 所选对象的“顶层”属性。SubItems
- 子属性的容器。PropertyGridSubItem
- “子级别”属性。PropertyGridFooter
- 包含帮助和底部栏。
让我们看看 PropertyGridItem
是如何工作的,特别是它的 RenderEditor
方法。
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);
}
}
}
}
这是一个简单的消除树,用于决定该做什么。
- 如果属性或
PropertyGrid
是只读的,则将其渲染为“标签”。 - 如果属性支持“SupportedValues”,则将其渲染为带有隐藏的 DropDownList (SELECT) 的“标签”。(注意:这不是很准确,我将尝试为支持值和从字符串转换的属性找到解决方案。)
- 如果属性支持从字符串类型转换,则将其渲染为带有隐藏的 TextBox (INPUT) 的“标签”。
- 渲染为“标签”。
如果选择了选项 2 或 3,当“标签”被单击时,“标签”将被隐藏,并且“编辑”控件会显示,直到控件失去焦点或更改其值。
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;
}
最初我使用 Anthem 进行 AJAX 支持,但它们的模型似乎太臃肿了。我真的很想要一个简化版本。“Skinny”(我将其重命名以防止冲突)是 Anthem 的一个基本版本,并且只支持调用 Control 上的方法,但增加了可以调用静态方法的功能。PropertyGrid
的方法设计为尽可能多地进行每次“回调”,从而最大限度地减少了所需的流量。
Javascript
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);
}
$
函数受到 Prototype lib 的启发,允许我使用跨浏览器安全的 Javascript。Javascript 具有优美的函数式特性。这可能对每个人都不适用,但对我来说已经证明很方便。
其余的 JavaScript 包含三个用于 AJAX 的函数,最后是主要的 PropertyGrid
原型。这允许我以每个实例的方式处理所有内容,并且允许我动态地将样式注入控件。有两种函数类:响应用户输入的事件处理程序,以及由 BeginEdit
、EndEdit
和 CancelEdit
组成的“编辑”函数。
CSS
CSS 也不那么令人兴奋,除了控件加载时注入的用于应用其样式的 CSS。这在 Opera 中不幸不支持,但我有一个修复方法,它会将样式输出到 HTML body(但这会破坏 XHTML 合规性)。让我们看看这是如何完成的
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');
}
规则被添加到浏览器窗口中的最后一个样式表中。为了使 CSS 具有“实例”感,类名后面会附加 PropertyGrid
的 ClientID
。
安装
- 将“pg”目录(图像、css 和脚本)复制到您的 webroot 目录。
- 添加项目引用,或添加到工具箱。
使用示例:
对象代码
示例属性演示了如何使用属性来控制输出
[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;}
}
一个更高级的示例,允许通过逗号分隔的字符串设置 string[]
(因为目前无法访问 Editor 对话框)
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);
}
}
注意: 您的类必须是公共的,否则在受限的 ASPNET 帐户(例如,大多数服务器)下运行时会收到 SecurityAccess 异常。
ASP.NET
在代码隐藏中
<%@ 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>
设计时支持是有限的。当您将控件拖放到表单上时,上面将是“生成”的。为了让网格看起来更好,请向样式表添加一个 designtime 链接。
代码隐藏部分
void Page_Load(object sender, System.EventArgs e)
{
pg1.SelectedObject = Global.STATIC;
pg2.SelectedObject = Global.STATIC;
}
就像您使用普通的 PropertyGrid
一样。请记住,您使用的是“无状态”环境,因此我在示例中只使用了静态成员。
关注点
- Javascript 太棒了!
- DevBoi (离线) - 所有标准都在浏览器中
- FireFox WebDeveloper - 非常方便,但在添加的样式表上会失败
- FireBug - 非常方便的 DOM 浏览器
- IE Web Toolbar - 非常方便,在 Firefox 失败的地方也能工作
- VS.NET JavaScript 调试 - 要在 VS.NET 中调试 JavaScript,只需在 IE 中启用它,然后在 .js 文件中设置断点(.aspx 文件不起作用),然后就可以了!
结论
可能的增强功能
- 编辑器支持。
- 折叠/展开动画。
- 公开所选对象的“动词”方法(例如,
foo.Save()
)。 - 更好的设计时支持,而不会使控件臃肿。
- 更多的 Javascript 客户端利用。
感谢 Paul Watson 在 Javascript 和 CSS 方面的帮助。
感谢 Anthem 的作者。
参考文献
- EMCA 262
- W3C CSS
- W3C XHTML
- Prototype 库