深入ASP.NET数据绑定(下)——多样的绑定方式
Author: 黄及峰
Date: 2008-05-05
在这个系列的上篇 中介绍了数据绑定语法的原理以及.NET 中如何实现单向绑定,中篇 我们简单的介绍了ASP.NET 2.0 中新增的Bind 语法配合DataSourceControl 来实现数据的自动双向绑定。这两部分的内容相对动态抽象并且不常接触,没有很好的源代码支持很难解释清楚,要想真正弄清它们的内部原理,还需要大家亲自动手去反编译分析动态编译的程序集。
在了解了数据绑定语法的原理后,我还想来谈谈我中实践过程中遇到的一些问题以及其它实用的绑定技巧。首先我们就来说说,特殊字段名的问题。我们知道在数据库当中,如果表名或字段名中包含有一些特殊的不能是合法的字符时,都会使用[] 将它们引起来,以便他们能够正常使用。但是在<%# Eval("")%> 的绑定语句当中,同时可以使用[] ,但是对于字段名中包含"(",")","[","]" 这4 个字符却始终运行出错。假设像我下面这样来绑定" 电压(V)" :
<%# Eval(" 电压(V)")%>
那么就会得到一个运行时错误:
DataBinding:“System.Data.DataRowView” 不包含名为“ 电压” 的属性。
表明括号是被认为是一个特殊字符,那我们如果给字段名加上[] ,如下:
<%# Eval("[ 电压(V)]")%>
此时,我们会得到另一个运行时错误:
电压(V 既不是表DataTable1 的DataColumn 也不是DataRelation 。
表明,即使加上[] 也无法解决这个特殊字段名的问题。同时字段名中如果也存在中括号,也是会出现这样的问题的。但是这样的字段名却在GridView 的自动生成列中能被正常绑定呢?问题会出现在哪里呢?分析和对比GridView 的自动生成列与Eval 这样的绑定语法在最终执行绑定代码上的不同,我们可以发现,GridView 的自动生成列取值并不是使用DataBinder.Eval 这个方法,它内部有自己的取值方式,但是在实现上却是大同小异的。那究竟是在哪里出现了问题呢?我们找出DataBinder 类的定义:
- 1: [AspNetHostingPermission(SecurityAction.LinkDemand, Level=200)]
- 2: public sealed class DataBinder
- 3: {
- 4: // Fields
- 5: private static readonly char[] expressionPartSeparator = new char[] { '.' };
- 6: private static readonly char[] indexExprEndChars = new char[] { ']', ')' };
- 7: private static readonly char[] indexExprStartChars = new char[] { '[', '(' };
- 8:
- 9: // Methods
- 10: public static object Eval(object container, string expression)
- 11: {
- 12: if (expression == null)
- 13: {
- 14: throw new ArgumentNullException("expression");
- 15: }
- 16: expression = expression.Trim();
- 17: if (expression.Length == 0)
- 18: {
- 19: throw new ArgumentNullException("expression");
- 20: }
- 21: if (container == null)
- 22: {
- 23: return null;
- 24: }
- 25: string[] expressionParts = expression.Split(expressionPartSeparator);
- 26: return Eval(container, expressionParts);
- 27: }
- 28:
- 29: private static object Eval(object container, string[] expressionParts)
- 30: {
- 31: object propertyValue = container;
- 32: for (int i = 0; (i < expressionParts.Length) && (propertyValue != null); i++)
- 33: {
- 34: string propName = expressionParts[i];
- 35: if (propName.IndexOfAny(indexExprStartChars) < 0)
- 36: {
- 37: propertyValue = GetPropertyValue(propertyValue, propName);
- 38: }
- 39: else
- 40: {
- 41: propertyValue = GetIndexedPropertyValue(propertyValue, propName);
- 42: }
- 43: }
- 44: return propertyValue;
- 45: }
- 46:
- 47: public static string Eval(object container, string expression, string format)
- 48: {
- 49: object obj2 = Eval(container, expression);
- 50: if ((obj2 == null) || (obj2 == DBNull.Value))
- 51: {
- 52: return string.Empty;
- 53: }
- 54: if (string.IsNullOrEmpty(format))
- 55: {
- 56: return obj2.ToString();
- 57: }
- 58: return string.Format(format, obj2);
- 59: }
- 60:
- 61: public static object GetDataItem(object container)
- 62: {
- 63: bool flag;
- 64: return GetDataItem(container, out flag);
- 65: }
- 66:
- 67: public static object GetDataItem(object container, out bool foundDataItem)
- 68: {
- 69: if (container == null)
- 70: {
- 71: foundDataItem = false;
- 72: return null;
- 73: }
- 74: IDataItemContainer container2 = container as IDataItemContainer;
- 75: if (container2 != null)
- 76: {
- 77: foundDataItem = true;
- 78: return container2.DataItem;
- 79: }
- 80: string name = "DataItem";
- 81: PropertyInfo property = container.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
- 82: if (property == null)
- 83: {
- 84: foundDataItem = false;
- 85: return null;
- 86: }
- 87: foundDataItem = true;
- 88: return property.GetValue(container, null);
- 89: }
- 90:
- 91: public static object GetIndexedPropertyValue(object container, string expr)
- 92: {
- 93: if (container == null)
- 94: {
- 95: throw new ArgumentNullException("container");
- 96: }
- 97: if (string.IsNullOrEmpty(expr))
- 98: {
- 99: throw new ArgumentNullException("expr");
- 100: }
- 101: object obj2 = null;
- 102: bool flag = false;
- 103: int length = expr.IndexOfAny(indexExprStartChars);
- 104: int num2 = expr.IndexOfAny(indexExprEndChars, length + 1);
- 105: if (((length < 0) || (num2 < 0)) || (num2 == (length + 1)))
- 106: {
- 107: throw new ArgumentException(SR.GetString("DataBinder_Invalid_Indexed_Expr", new object[] { expr }));
- 108: }
- 109: string propName = null;
- 110: object obj3 = null;
- 111: string s = expr.Substring(length + 1, (num2 - length) - 1).Trim();
- 112: if (length != 0)
- 113: {
- 114: propName = expr.Substring(0, length);
- 115: }
- 116: if (s.Length != 0)
- 117: {
- 118: if (((s[0] == '"') && (s[s.Length - 1] == '"')) || ((s[0] == '\'') && (s[s.Length - 1] == '\'')))
- 119: {
- 120: obj3 = s.Substring(1, s.Length - 2);
- 121: }
- 122: else if (char.IsDigit(s[0]))
- 123: {
- 124: int num3;
- 125: flag = int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out num3);
- 126: if (flag)
- 127: {
- 128: obj3 = num3;
- 129: }
- 130: else
- 131: {
- 132: obj3 = s;
- 133: }
- 134: }
- 135: else
- 136: {
- 137: obj3 = s;
- 138: }
- 139: }
- 140: if (obj3 == null)
- 141: {
- 142: throw new ArgumentException(SR.GetString("DataBinder_Invalid_Indexed_Expr", new object[] { expr }));
- 143: }
- 144: object propertyValue = null;
- 145: if ((propName != null) && (propName.Length != 0))
- 146: {
- 147: propertyValue = GetPropertyValue(container, propName);
- 148: }
- 149: else
- 150: {
- 151: propertyValue = container;
- 152: }
- 153: if (propertyValue == null)
- 154: {
- 155: return obj2;
- 156: }
- 157: Array array = propertyValue as Array;
- 158: if ((array != null) && flag)
- 159: {
- 160: return array.GetValue((int) obj3);
- 161: }
- 162: if ((propertyValue is IList) && flag)
- 163: {
- 164: return ((IList) propertyValue)[(int) obj3];
- 165: }
- 166: PropertyInfo info = propertyValue.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance, null, null, new Type[] { obj3.GetType() }, null);
- 167: if (info == null)
- 168: {
- 169: throw new ArgumentException(SR.GetString("DataBinder_No_Indexed_Accessor", new object[] { propertyValue.GetType().FullName }));
- 170: }
- 171: return info.GetValue(propertyValue, new object[] { obj3 });
- 172: }
- 173:
- 174: public static string GetIndexedPropertyValue(object container, string propName, string format)
- 175: {
- 176: object indexedPropertyValue = GetIndexedPropertyValue(container, propName);
- 177: if ((indexedPropertyValue == null) || (indexedPropertyValue == DBNull.Value))
- 178: {
- 179: return string.Empty;
- 180: }
- 181: if (string.IsNullOrEmpty(format))
- 182: {
- 183: return indexedPropertyValue.ToString();
- 184: }
- 185: return string.Format(format, indexedPropertyValue);
- 186: }
- 187:
- 188: public static object GetPropertyValue(object container, string propName)
- 189: {
- 190: if (container == null)
- 191: {
- 192: throw new ArgumentNullException("container");
- 193: }
- 194: if (string.IsNullOrEmpty(propName))
- 195: {
- 196: throw new ArgumentNullException("propName");
- 197: }
- 198: PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propName, true);
- 199: if (descriptor == null)
- 200: {
- 201: throw new HttpException(SR.GetString("DataBinder_Prop_Not_Found", new object[] { container.GetType().FullName, propName }));
- 202: }
- 203: return descriptor.GetValue(container);
- 204: }
- 205:
- 206: public static string GetPropertyValue(object container, string propName, string format)
- 207: {
- 208: object propertyValue = GetPropertyValue(container, propName);
- 209: if ((propertyValue == null) || (propertyValue == DBNull.Value))
- 210: {
- 211: return string.Empty;
- 212: }
- 213: if (string.IsNullOrEmpty(format))
- 214: {
- 215: return propertyValue.ToString();
- 216: }
- 217: return string.Format(format, propertyValue);
- 218: }
- 219:
- 220: internal static bool IsNull(object value)
- 221: {
- 222: if ((value != null) && !Convert.IsDBNull(value))
- 223: {
- 224: return false;
- 225: }
- 226: return true;
- 227: }
- 228: }
其中我们可以发现有三个静态只读变量:
- private static readonly char[] expressionPartSeparator = new char[] { '.' };
- private static readonly char[] indexExprEndChars = new char[] { ']', ')' };
- private static readonly char[] indexExprStartChars = new char[] { '[', '(' };
OK ,我们先不看代码,就应该知道问题就出在这个地方。当我们分析哪里用到indexExprEndChars 时分找到这个方法:
public static object GetIndexedPropertyValue(object container, string expr)
我们不需要阅读里面的代码,通过下面的expr 参数注释我们就可以很快得到答案:
expr 从 container 对象到要放置在绑定控件属性中的公共属性值的导航路径。此路径必须是以点分隔的属性或字段名称字符串,如C# 中的 Tables[0].DefaultView.[0].Price 或 Visual Basic 中的 Tables(0).DefaultView.(0).Price 。
它告诉我们,我们不仅可以使用字段名的方式,同时还可以使用索引下标的方式来绑定字段值(C# 和VB ,分别使用[] 和() 来取索引值),正因为如此,我们才不可以在字段名中使用括号和中括号。如上我们假设" 电压(V)" 字段的索引下标是2 ,那么我们可以像下面这样绑定,来解决特别字段名带来的问题:<td><%# Eval("[2])")%></td>
上面的注释同时还告诉,我们是可以通过一个对象的导航路径如 对象. 属性. 子属性 的方式来绑定一个数据项的间接属性,这个我们可以通过对expressionPartSeparator 静态字段的使用,得以验证:
- 1: public static object Eval(object container, string expression)
- 2: {
- 3: if (expression == null)
- 4: {
- 5: throw new ArgumentNullException("expression");
- 6: }
- 7: expression = expression.Trim();
- 8: if (expression.Length == 0)
- 9: {
- 10: throw new ArgumentNullException("expression");
- 11: }
- 12: if (container == null)
- 13: {
- 14: return null;
- 15: }
- 16: string[] expressionParts = expression.Split(expressionPartSeparator);
- 17: return Eval(container, expressionParts);
- 18: }
- 19: private static object Eval(object container, string[] expressionParts)
- 20: {
- 21: object propertyValue = container;
- 22: for (int i = 0; (i < expressionParts.Length) && (propertyValue != null); i++)
- 23: {
- 24: string propName = expressionParts[i];
- 25: if (propName.IndexOfAny(indexExprStartChars) < 0)
- 26: {
- 27: propertyValue = GetPropertyValue(propertyValue, propName);
- 28: }
- 29: else
- 30: {
- 31: propertyValue = GetIndexedPropertyValue(propertyValue, propName);
- 32: }
- 33: }
- 34: return propertyValue;
- 35: }
前面的那个Eval 重载,把expression 表达式用expressionPartSeparator 字符分隔开,然后调用内部的Eval(object,string[]) 重载,在这个重载中,按顺序去一级一级递归遍历属性值,最终找到最后的那个绑定字段值。所以我们是可以绑定跨级的间接属性和关联DataRowRelation 行的值。
还想在再来说说其它的绑定方式,李涛在它的博客浅谈.NET 中的数据绑定表达式( 二) 中提到了绑定数据的七种方式,分别为:
<%#Container.DataItem%>
<%#GetDataItem()%>
<%#Eval(" 字段名")%>
<%#DataBinder.Eval(Container.DataItem," 字段名")%>
<%#((DataRowView)Container.DataItem)[" 字段名"] %>
<%#((Type)Container.DataItem). 成员 %>
<%#((Type)GetDataItem()). 成员 %>
如果按要我来分的话,我只会分成两类:强类型绑定和反射绑定。不论是Container.DataItem 还是GetDataItem() ,都是得到当前的正在绑定的上下文数据对象,然后转换成他们的原始类型,使用索引或强类型的方式来绑定字段值。而Eval 就是使用反射的方式来进行通用化的绑定,这样我们就完全没有必要关心被绑定的数据源是什么类型,在很多场合下这是非常有益的。
从性能方式来考虑,强类型绑定肯定要比反射绑定性能来得好。这其中的原因就不多作解释了,但是对于强类型来说是使用Container.DataItem 还是GetDataItem 的方式来取得上下文数据对象,性能应该差别不大的。我们在前面已经提到到,在Page 的作用域内,会把所有的被绑定(遍历的数据项或整个集合)保存在一个堆栈,方面我们来读取,我们只需要读取堆栈的顶部元素就可以方便的得到当前正在被绑定数据行项;而Container 而更像是一个动态的,关键字作用的变量,因为你在绑定不同对象时Container 的类型是不一样的,假设你当前正在绑定Repeater 那么它的类型是RepeaterItem ,只是为了方便我们强类型取得当前Repeater 行对象而产生的动态属性,其实它并不是Page 的一个公有或私有属性。所以我认为两种取得DataItem 的方式在性能上实际是没有多大区别的。
当然我们在选择是使用强类型绑定还是反射绑定时,主要还是取决你的需要。我个人认为,为了使用解决方案通用化,而不必在关心绑定的数据类型是什么类型,应尽量使用Eval 的方式来绑定字段。在实践当中,绑定字段的消费上还不是非常多的,为了灵活和通用这点性能损失我认为是值得的。另外就是如上的特殊字段的情况,我当然也可以使用强类型绑定的方式来解决:
<%#((System.Data.DataRowView)Container.DataItem)[" 电压(a)"]%>
特殊字段的解决之道有很多,比如我们还可以重写Page 的Eval 方法达到我们的目的。选择哪种方案,就是取决于我们实际需要了。
上面我们从特殊字段名出发,分析了DataBinder 在反射取得字段值时所做的一些特殊处理,进而引出我们平常可能会被忽略的一些非常有用的绑定方式,如:索引下标绑定和间接字段绑定,而这些对于我们解决一些疑难问题会有很大的帮助,特别跨级的字段绑定,如果我们没有了解的话,可能就需要在服务器代码中做很多的类型转换和处理。最后我们还讨论了其它的几种绑定方式,以及它们各种的性能和应用场合。
三天,用篇文章来分析了ASP.NET 在数据绑定的一个原理,其中很多内容并不是我们平常数据绑定时所需要掌握的知识。但是掌握了它们却对我们在数据绑定时,有更多的把握。正因为内容的动态性,和过于抽象,而本人又无法找到一种最为合适的语言来组织和解释这些知识,代码太多,全部贴出来又感觉找不到重点;贴重要的部分,又感觉跨度太大。所以三篇下来很多要领解释的不是很清楚,大家权当它是一个引子,更多的原理还需要大家自己亲自去分析和阅读代码。