65.9K
CodeProject 正在变化。 阅读更多。
Home

xacc.propertygrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (69投票s)

2006年5月15日

CPOL

6分钟阅读

viewsIcon

232357

downloadIcon

2326

一个 ASP.NET PropertyGrid

Screenshot - xaccpg.png

目录

  1. 引言
  2. 特点
  3. 限制
  4. 设计
  5. 实现
  6. 安装
  7. 使用示例:
  8. 关注点
  9. 结论
  10. 参考文献

引言

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 使其不够灵活,但它确实有效。

真正的产品

  1. 规划 - 这个阶段涉及思考很多事情,准确地可视化达到最终结果的路径。
  2. 布局 - 这个阶段涉及分解控件所需的所有元素。作为一个网格,使用简单的 DIV 表/网格相对容易。
  3. 模型 - 这个阶段涉及使用 CSS 和 Javascript 模拟静态 HTML。
  4. ASP.NET 服务器控件 - 这个阶段涉及将静态 HTML 重构为可重用控件,同时 Anthem 在此时被“精简”了。
  5. 将所有内容放在一起 - 最后需要进行一些调整才能使所有内容协同工作。

实现

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

这是一个简单的消除树,用于决定该做什么。

  1. 如果属性或 PropertyGrid 是只读的,则将其渲染为“标签”。
  2. 如果属性支持“SupportedValues”,则将其渲染为带有隐藏的 DropDownList (SELECT) 的“标签”。(注意:这不是很准确,我将尝试为支持值和从字符串转换的属性找到解决方案。)
  3. 如果属性支持从字符串类型转换,则将其渲染为带有隐藏的 TextBox (INPUT) 的“标签”。
  4. 渲染为“标签”。

如果选择了选项 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 原型。这允许我以每个实例的方式处理所有内容,并且允许我动态地将样式注入控件。有两种函数类:响应用户输入的事件处理程序,以及由 BeginEditEndEditCancelEdit 组成的“编辑”函数。

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 具有“实例”感,类名后面会附加 PropertyGridClientID

安装

  1. 将“pg”目录(图像、css 和脚本)复制到您的 webroot 目录。
  2. 添加项目引用,或添加到工具箱。

使用示例:

对象代码

示例属性演示了如何使用属性来控制输出

[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 库
© . All rights reserved.