最近在做一个公共库资源网站的项目,之前在公共控件展示这一块的设计时,打算将所有的控件信息写到数据库中,然后通过对数据库的操作来维护公共控件演示模块。但是,这样设计存在一个很大的问题,一是公共库中的控件十分多,按照那样设计的话,需要为每一个控件单独设置一个页面,而且,一旦控件有更新,就需要操作数据库,无法实现同步更新。所以,后来考虑使用反射技术。然而,当采用反射来动态演示公共控件时,遇到了许多难题,不过,经过自己两个星期的努力,总算是磨出来了,真的发现许多问题都是被逼出来的。以下是演示效果,下面我来一一分析其实现过程。
左侧栏,是用一个TreeView控件来列出所有的自定义控件,可是,公共库Dtsc.Common.Controls中还存在一些过时了的控件,我不想让其展示出来,怎么办呢?
我们可以在公共库中新建一个EnitityMappingAttribute类,代码如下:
namespace Dtsc.Common.Controls { /// <summary> /// 自定义特性 属性或者类可用 支持继承 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Method, Inherited = true)] public class EnitityMappingAttribute : Attribute { private string tagName; /// <summary> /// 标记名 /// </summary> public string TagName { get { return tagName; } set { tagName = value; } } } }
这个类的作用是为Dtsc.Common.Controls库中需要展示的控件添加一个特殊属性作为标记,以标识其将要展示,没有标记的控件类将不会展示,这样我们就可以帅选出哪些控件类需要展示了。
新建了一个EnitityMappingAttribute类后,我们还要在需要展示的控件类中添加这个特殊属性标记,下面以一控件为例,如下:
namespace Dtsc.Common.Controls { /// <summary> /// 自定义文本域控件 /// </summary> [Description("自定义文本域控件")] [EnitityMapping(TagName = "演示")] public sealed class TextAreaDs : TextBox
在此,我们添加了一个[EnitityMapping(TagName = "演示")]特殊的属性标记,切记,这是在添加了EnitityMappingAttribute类之后才会有的自定义属性。好了,接下来,我们就可以实现右侧控件列表了,代码如下:
/// <summary> /// 利用反射加载标记控件名称 /// </summary> private void BindControlNames() { string namespaceName = "Dtsc.Common.Controls"; Assembly ab = Assembly.Load(namespaceName); //获取程序集类型 Type[] T = ab.GetTypes(); bool isEnable = true; //是否已过时 bool isReflector = false; //是否利用反射动态生成 foreach (Type t in T) { isReflector = false; //是否利用反射来展示 foreach (object obj in t.GetCustomAttributes(false)) { if (obj is EnitityMappingAttribute) { isReflector = true; break; } } if (isReflector && t.IsClass && t.IsPublic && t.Namespace == namespaceName) //加载出所有没过时且需要展示的自定义控件 { // if (t.BaseType.Name == "Control" || t.BaseType.Name == // "CompositeControl" || t.BaseType.Name == "WebControl" || t.BaseType.Name == "TreeView" //|| t.BaseType.Name == "TextBox" || t.BaseType.Name == "Button" || t.BaseType.Name == "TextBox" || // t.BaseType.Name == "DropDownList" || t.BaseType.Name == "GridView"|| t.BaseType.Name=="Label"|| // t.BaseType.Name == "HtmlControl") //帅选控件类 TreeNode tn = new TreeNode(); tn.Text = t.Name; tn.Value = t.FullName; tn.Target = "mainContent"; tn.NavigateUrl = "~/Views/PublicControlModule/MainContent.aspx?name=" + tn.Text + "&&fullname=" + tn.Value; tvControlsName.Nodes.Add(tn); } } }
这样,在左侧中,即可动态加载所有加了自定义属性EnitityMapping的控件名称。(需要在每一个需要展示的控件类中加上EnitityMapping标记)
接下来,我们看右侧的功能,最上面是展示了该控件的说明信息,而控件的说明信息,我们就是从其Description属性中获取的,以OrganDeptPartyPersonTree控件为例
/// <summary> /// 此控件构筑机构组织党员树 /// </summary> [ControlValueProperty("SelectedValue")] [SupportsEventValidation] [DefaultEvent("SelectedNodeChanged")] [Description("此控件构筑机构组织党员树")] [EnitityMapping(TagName = "演示")] public class OrganDeptPartyPersonTree : TreeView
下面,我们来看如何获取OrganDeptPartyPersonTree中Description属性的内容,如下注释处“获取控件说明信息”所示,那么属性设置模块呢,如下代码所示,这里也使用到了我们之前创建的自定义属性标记EnitityMapping,我们知道一个继承了TreeView控件的类,是包括许许多多的属性的,而我们只需要展示一部分属性,那么怎么做呢?还是和之前一样,在每一个需要展示的属性前面加上自定义标记[EnitityMapping(TagName = "演示")],这样则可以获取加了次标记的所有属性集合。然后根据这些属性的类型来动态生成属性设置模块,bool类型的属性,就给起动态生成一个单选列表,enum类型的属性,就给其动态生成一个下拉列表框,其它的默认给其生成一个文本框。
/// <summary>
/// 加载属性配置窗体
/// </summary>
private void LoadPropertyView()
{
Assembly ab = Assembly.Load(nameSpaceName);
//获取程序集类型
Type[] T = ab.GetTypes();
if (!string.IsNullOrEmpty(controlName))
{
foreach (Type t in T)
{
if (t.Name == controlName)
{
currentType = t;
//获取控件说明信息
foreach (object obj in t.GetCustomAttributes(false))
{
if (obj is DescriptionAttribute)
{
str = "<span class='littleTitle'>控件说明:</span>" + (obj as DescriptionAttribute).Description;
controlInfo.InnerHtml = str;
}
}
#region 动态加载属性设置列表
var result = from p in t.GetProperties()
let attr = p.GetCustomAttributes(typeof(EnitityMappingAttribute), false)
where attr.Length > 0
select p;
foreach (var item in result)
{
TableRow tr = new TableRow();
TableCell tc = new TableCell();
tc.Text = item.Name;
//获取属性的信息
foreach (object obj in item.GetCustomAttributes(false))
{
if (obj is DescriptionAttribute)
{
tc.ToolTip = (obj as DescriptionAttribute).Description;
}
}
tc.Width = 120;
tr.Cells.Add(tc);
TableCell tc1 = new TableCell();
if (item.PropertyType == typeof(String) || item.PropertyType == typeof(Int32) ||
item.PropertyType == typeof(Unit) || item.PropertyType == typeof(DateTime))
//字符串、整型、日期以文本框显示
{
TextBox txt = new TextBox();
if (item.Name == "OrganId")
{
txt.Text = SetDefaultOrganId(item.Name);
}
else if (item.Name == "DeptId")
{
txt.Text = SetDefaultDepartId(item.Name);
}
else if (item.Name == "FormID")
{
txt.Text = "form1";
}
else if (item.Name == "DataTextField")
{
txt.Text = "OrganId";
}
tc1.Controls.Add(txt);
}
else if (item.PropertyType == typeof(bool)) //bool类型以单选框显示
{
RadioButtonList rbl = new RadioButtonList();
rbl.Items.Add(new ListItem("是", "ture"));
rbl.Items.Add(new ListItem("否", "false"));
rbl.Items[0].Selected = true;
rbl.RepeatDirection = RepeatDirection.Horizontal;
tc1.Controls.Add(rbl);
}
else if (item.PropertyType.BaseType == typeof(Enum)) //枚举则以下拉列表框显示
{
Array array = Enum.GetValues(item.PropertyType);
DropDownList ddl = new DropDownList();
foreach (Enum ae in array)
{
ddl.Items.Add(new ListItem(ae.ToString(), (Convert.ToInt32(ae)).ToString()));
tc1.Controls.Add(ddl);
}
}
else //默认为以文本框的形式显示
{
TextBox txt = new TextBox();
tc1.Controls.Add(txt);
}
tr.Cells.Add(tc1);
this.propertyTb.Rows.Add(tr);
}
#endregion
}
}
}
}
接下来,我们动态生成了这个控件的属性设置模块,然后对属性进行了一系列的设置之后,我们要让其在效果演示中显示出来,如下:
/// <summary>
/// 设置属性并展示
/// </summary>
void SetProperty()
{
ViewState["PropertyName"] = null;
//查询要展示的属性集合
var result = from p in currentType.GetProperties()
let attr = p.GetCustomAttributes(typeof(EnitityMappingAttribute), false)
where attr.Length > 0
select p;
object ect = Assembly.Load(nameSpaceName).CreateInstance(Request.QueryString["fullname"]);
string partStr = string.Empty;
foreach (var item in result)
{
foreach (TableRow tr in propertyTb.Rows)
{
if (tr.Cells[0].Text == item.Name)
{
if (tr.Cells[1].Controls[0] is TextBox) //文本框
{
string txt=((TextBox)tr.Cells[1].Controls[0]).Text;
if(!string.IsNullOrEmpty(txt))
{
try
{
switch (item.PropertyType.FullName)
{
case "System.Int32":
item.SetValue(ect,Convert.ToInt32(txt), null);
break;
case "System.String":
if (item.Name == "MaximumValue" || item.Name == "MinimumValue") //这里太特殊了,控件中居然给这个属性设置为string
{
item.SetValue(ect, txt, null);
break;
}
item.SetValue(ect, txt, null);
break;
case "System.Web.UI.WebControls.Unit":
item.SetValue(ect, (Unit)int.Parse(txt), null);
break;
case "System.DateTime":
item.SetValue(ect, Convert.ToDateTime(txt), null);
break;
default:
break;
}
partStr += item.Name + "=\"" + txt + "\" ";
}
catch(FormatException e)
{
errorMsg = "输入格式有误!"+e.Message;
return;
}
ViewState["PropertyName"] += item.Name+":"+txt + ",";
}
}
else if (tr.Cells[1].Controls[0] is RadioButtonList) //单选按钮
{
bool isOrNo = ((RadioButtonList)tr.Cells[1].Controls[0]).Items[0].Selected == true ? true : false;
item.SetValue(ect, isOrNo, null);
partStr += item.Name + "=\"" + isOrNo + "\" ";
ViewState["PropertyName"] += item.Name+":"+isOrNo + ",";
}
else if (tr.Cells[1].Controls[0] is DropDownList) //下拉列表
{
string content = ((DropDownList)tr.Cells[1].Controls[0]).SelectedItem.Text;
switch (item.PropertyType.BaseType.ToString())
{
case "Enum":
item.SetValue(ect, content, null);
break;
default:
break;
}
partStr += item.Name + "=\"" + content + "\" ";
ViewState["PropertyName"] += item.Name + ":" + content + ",";
}
}
}
}
this.pnlView.Controls.Clear();
if (controlName == "EmptyData") //EmptyData控件和ExportToExcel控件必须和GridView控件一起使用
{
gv = new GridView();
gv.ID = "gv1";
strControlCode += "<asp:GridView ID=\"" + gv.ID + "\" runat=\"server\"> </asp:GridView>";
if (controlName == "EmptyData")
{
(ect as EmptyData).GridId = "gv1";
partStr += " GridId=\"" + gv.ID + "\"";
}
this.pnlView.Controls.Add(gv);
}
else if (controlName == "PickList")
{
(ect as PickList).Width = 150;
}
else if (controlName == "SmartGridView")
{
CodeAreaCondition condition = new CodeAreaCondition();
IList<CodeArea> list = BizCodeArea.GetList(condition);
(ect as SmartGridView).DataSource = BizComOrganInfo.GetList();
(ect as SmartGridView).DataBind();
}
else if (controlName == "AutoCompleteTextBox")
{
(ect as AutoCompleteTextBox).DataSouce = BizComOrganInfo.GetList();
(ect as AutoCompleteTextBox).DataBind();
}
c = ect as Control;
c.ID = controlName + "1";
c.EnableViewState = true;
this.pnlView.Controls.Add(c);
strControlCode += " <Dtsc:" + controlName + " ID=\"" + c.ID + "\" runat=\"server\" " + partStr + "/>";
txtControlCode.Text = strControlCode;
}
关于“属性设置源码”模块,如何实现呢,在对属性进行了配置之后,我要现实其.NET属性代码,哈哈,我是自己用字符串拼接出来的,因为我想不到其它的办法,不知道微软在VS2010中一拖控件就自动生成的那一堆代码是否也是通过这字符串拼接实现的,呵呵~如上蓝色代码所示。
好了,到现在为止,看上去是没什么问题了吧!NO!还有一个十分头痛的问题,那就是我们动态生成的控件,它一回发,之前给它设置的属性,就全部消失了,那么,我们如何来保存其状态呢?我们知道,我们拖上去的控件,是交由.NET自动给我们对其进行状态保存的,至于它具体是如何实现的,我们都不要去管,但是现在动态生成控件时,就需要我们自己对其状态进行保存了,这里,我采用了ViewState+Hastable的方式,将控件的属性以特殊字符串的形式存到ViewState中如“控件名称:控件属性,”+ “控件名称:控件属性,”+...,然后拿出这种特殊字符串,回发加载控件时,再重新给控件属性赋值。,但是存到Hastable中的键值对都是以object形式存的,而给控件属性赋值的时候,你不知道该属性是什么类型的,你给其赋值,类型不对就会报错,那么,我们怎么判断呢?有时候,连我自己都觉得自己是个天才!哈哈!
看如下代码:
/// <summary> /// 页面回发后,再次加载控件,不生成属性设置代码 /// </summary> void CreateControl() { //查询要展示的属性集合 var result = from p in currentType.GetProperties() let attr = p.GetCustomAttributes(typeof(EnitityMappingAttribute), false) where attr.Length > 0 select p; object ect = Assembly.Load("Dtsc.Common.Controls").CreateInstance(Request.QueryString["fullname"]); string partStr = string.Empty; c = ect as Control; foreach (var item in result) { foreach (TableRow tr in propertyTb.Rows) { if (tr.Cells[0].Text == item.Name) { if (tr.Cells[1].Controls[0] is TextBox) //文本框 { string txt = ht[item.Name]==null?string.Empty: ht[item.Name].ToString(); if(!string.IsNullOrEmpty(txt)) { try { switch (item.PropertyType.FullName) { case "System.Int32": item.SetValue(ect,Convert.ToInt32(txt), null); break; case "System.String": //这里太特殊了,控件中居然给这个属性设置为string if (item.Name == "MaximumValue" || item.Name == "MinimumValue") { item.SetValue(ect, txt.ToString(), null); break; } item.SetValue(ect, txt, null); break; case "System.Web.UI.WebControls.Unit": item.SetValue(ect, (Unit)int.Parse(txt), null); break; case "System.DateTime": item.SetValue(ect, Convert.ToDateTime(txt), null); break; default: break; } } catch(FormatException) { return; } } } else if (tr.Cells[1].Controls[0] is RadioButtonList) //单选按钮 { bool isOrNo = Convert.ToBoolean(ht[item.Name]); item.SetValue(ect, isOrNo, null); } else if (tr.Cells[1].Controls[0] is DropDownList) //下拉列表 { string content =ht[item.Name].ToString(); switch (item.PropertyType.BaseType.ToString()) { case "Enum": item.SetValue(ect, content, null); break; default: break; } } } } } this.pnlView.Controls.Clear(); c.ID = controlName + "1"; this.pnlView.Controls.Add(c); }
当然是再次利用反射,遍历动态生成的属性设置列表,然后从hastable中拿值来和属性名称进行匹配,而我们是可以判断属性的类型的,这样就可以根据其类型而对其属性进行赋值,这样就不会出错啦!
最后,只要在Page_Load()中添加如下代码即可:
if (!Page.IsPostBack) { LoadControlView(); titleDiv.InnerText = controlName; } else { if (ViewState["PropertyName"] != null) { CreateControl(); } }