一直都在说反射很有用 谈谈大型.NET ERP系统有哪些地方用到了反射
反射Reflection,MFC时代叫RTTI(Runtime Type Identification) 运行时类型识别,提供一种动态创建对象的能力。
这里不谈反射的概念和基本用法,仅仅就我遇到的ERP系统中,有哪些地方用到了反射,是如何用的。
1 操作对象的属性或方法 Get/Set property and invoke method
通过反射调用,代码中很容易形成抽象化的公共代码,比如,系统中很多地方,直接用反射对对象赋值,参考:
ReflectionHelper.SetPropertyValue(salesOrder, "OrderAmount", 3123.54);
这样直接给销售订单的订单金额赋值3123.54,也可以调用方法:
ReflectionHelper.InvokeMethod(license, "VerifyExpiredDate");
2 调用功能 Execute function
ERP系统中的每个功能对应一个类型定义,用一个特性FunctionCodeAttribute标识出来,参考下面的代码定义。
[FunctionCode("ICIMSR")] public partial class InventoryReceipt :Foundation.Forms.EntryForm { private IInventoryMovementManager _inventoryMovementManager; private InventoryMovementEntity _inventoryMovement;
运行时通过遍历程序集中类型有加FunctionCodeAttribute特性的,即可找到这个功能对应的类型,实例化类型即可。
public void OpenFunctionForm(string functionCode) { functionCode = functionCode.ToUpper().Trim(); Type formBaseType = null; if (!_formBaseType.TryGetValue(functionCode, out formBaseType)) { Assembly assembly = Assembly.GetExecutingAssembly(); foreach (Type type in assembly.GetTypes()) { try { object[] attributes = type.GetCustomAttributes(typeof(FunctionCode), true); foreach (object obj in attributes) { FunctionCode attribute = (FunctionCode)obj; if (!string.IsNullOrEmpty(attribute.Value)) { if (!_formBaseType.ContainsKey(attribute.Value)) _formBaseType.Add(attribute.Value, type); if (formBaseType == null && attribute.Value.Equals(functionCode,StringComparison.InvariantCultureIgnoreCase)) formBaseType = type; } if (formBaseType != null) { goto Found; } } } catch { } } } Found: if (formBaseType != null) { object entryForm = Activator.CreateInstance(formBaseType) as Form; Form functionForm = (Form)entryForm; OpenFunctionForm(functionForm); } }
这个用法就是.NET反射实现插件化应用程序的例子。
3 简化重复代码 Avoid duplicate code
比如一个考勤系统中,定义8个时间点,字段名称依次是T1Begin, T2End,T2Begin,T2End,T3Begin,T3End,T4Begin,T4End。在使用这8个变量的时候,可以在系统中直接调用逐个上述字段,也可以用一个循环语句完成调用,参考下面的代码:
ShiftEntity shift.... for (int shiftIndex = 1; shiftIndex <= 4; shiftIndex++) { object begin= ReflectionHelper.GetPropertyValue(shift, string.Format("T{0}Begin", shiftIndex)); object end= ReflectionHelper.GetPropertyValue(shift, string.Format("T{0}End", shiftIndex));
如代码中所示,使用反射方法获取对象的值,节省了一部分代码,代码的可维护性也好一点。
再参看下面的ERP自定义字段的例子,供应商表(Vendor)表增加20个自定义字段,SQL语句看起来是这样的:
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_1] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_2] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_3] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_4] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_5] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_6] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_7] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_8] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_9] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_10] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_11] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_12] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_13] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_14] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_15] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_16] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_17] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_18] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_19] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_20] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
我用反射调用这些属性,参考下面的代码,简洁容易理解:
for (int i = 1; i <= 20; i++) { string fieldName = string.Format("UserDefinedField{0}", i); decimal fieldValue = (decimal) ReflectionHelper.GetPropertyValue(vendor, fieldName); amount += fieldValue; }
再举一个数据读取的例子,先参考下面的硬编码的例子程序。
System.DateTime start = DateTime.Now; System.Data.IDataReader dr = GetData(recordCount); while (dr.Read()) { CustomTypes.Employees newEmployee = new CustomTypes.Employees(); newEmployee.Address = dr["Address"].ToString(); newEmployee.BirthDate = DateTime.Parse(dr["BirthDate"].ToString()); newEmployee.City = dr["City"].ToString(); newEmployee.Country = dr["Country"].ToString(); newEmployee.EmployeeID = Int32.Parse(dr["EmployeeID"].ToString()); newEmployee.Extension = dr["Extension"].ToString(); newEmployee.FirstName = dr["FirstName"].ToString(); newEmployee.HireDate = DateTime.Parse(dr["HireDate"].ToString()); newEmployee.LastName = dr["LastName"].ToString(); newEmployee.PostalCode = dr["PostalCode"].ToString(); newEmployee.Region = dr["Region"].ToString(); newEmployee.ReportsTo = Int32.Parse(dr["ReportsTo"].ToString()); newEmployee.Title = dr["Title"].ToString(); newEmployee.TitleOfCourtesy = dr["TitleOfCourtesy"].ToString(); pgbHardCodedConversion.Increment(1); } dr.Close();
如果用反射,参考一下代码例子:
List<Employees> employee= MapDataToBusinessEntityCollection<Employees>(sqlDataReader);
public static List<T> MapDataToBusinessEntityCollection<T> (IDataReader dr) where T : new() { Type businessEntityType = typeof (T); List<T> entitys = new List<T>(); Hashtable hashtable = new Hashtable(); PropertyInfo[] properties = businessEntityType.GetProperties(); foreach (PropertyInfo info in properties) { hashtable[info.Name.ToUpper()] = info; } while (dr.Read()) { T newObject = new T(); for (int index = 0; index < dr.FieldCount; index++) { PropertyInfo info = (PropertyInfo) hashtable[dr.GetName(index).ToUpper()]; if ((info != null) && info.CanWrite) { info.SetValue(newObject, dr.GetValue(index), null); } } entitys.Add(newObject); } dr.Close(); return entitys; }
一对一比较没有看出优势,但是如果有很多个数据读取器读取数据转化为实体对象集合,则只需要像上面那样一句话调用,代码可复用,开发效率相当高。
4 运行时绑定 Runtime bind
需要设计一个水晶报表查看器,它不依赖于具体的水晶报表运行库,编译时完全不知道是在引用水晶报表,运行时检测环境安装的水晶报表版本,调用相应类型的水晶报表运行库。
object subreports = ReflectionHelper.GetPropertyValue(reportDocument, "Subreports"); IEnumerator subreportEnumerator = (IEnumerator)ReflectionHelper.InvokeMethod(subreports, "GetEnumerator"); while (subreportEnumerator.MoveNext()) { object subreport = subreportEnumerator.Current; string reprotName = (string)ReflectionHelper.GetPropertyValue(subreport, "Name"); if (string.Compare(reprotName, subreportName, true) == 0) { return subreport; } }
反射中类型都是object,所以foreach操作需要变成方法调用。
当需要延迟绑定类型时,代码可以考虑用反射编程。虽然书写代码和调试困难,但是运行时灵活,可适应性好。
5 从资源文件或内存中创建类型实例 Invoke method from embeded resouces
当.NET程序集不是以编译时添加引用的方式引用,则可以用反射调用这些程序集中的方法。比如我将程序集编译完成后,以嵌入式资源文件的形式附加到主程序中,主程序要能调用嵌入的资源文件程序集,必须用反射调用。
Assembly library = Assembly.Load("License"); Type type = library.GetType("License.ErpLicense"); object empLicense= ReflectionHelper.CreateObjectInstance(type); ReflectionHelper.InvokeMethod(empLicense, "VerifyLicense");
6 检测环境 Environment detection
这个和第4步的用意一样,凡是不能在编译时确定引用到的类型,运行时只有用反射尝试调用引用它的类型。以水晶报表环境检测的例子,需要从高版本到低版本逐个遍历,直到找到合适的版本。
以下是水晶报表运行库检测代码片段。
CrystalReportVersion? nullable = null; ArrayList list = null; if (supportedVersions == null) { list = new ArrayList(Enum.GetValues(typeof(CrystalReportVersion))); } else { list = new ArrayList(supportedVersions); } list.Sort(); for (int i = list.Count - 1; i >= 0; i--) { CrystalReportVersion crystalReportVersion = (CrystalReportVersion) list[i]; try { if (IsCrystalReportInstalled(crystalReportVersion, false)) { nullable = new CrystalReportVersion?(crystalReportVersion); break; } } catch { } } if (!nullable.HasValue) { throw new ApplicationException("Crystal Reports runtime is not installed"); }
我们要检测系统是否安装特定系统的运行库,先尝试以反射的方式创建这个类型的实例,如果返回空对象,则有可能表示未安装系统运行库。
7 克隆对象 Clone object
.NET中克隆/复制一个对象,能够以序列化的方式做深拷贝(Deep copy)实现,参考下面的代码例子。
EntityCollection collection = ... byte[] bytes = Serialize(collection); EntityCollection receiptCollection = (EntityCollection)Deserialize(bytes); public static object Deserialize(byte[] buffer) { BinaryFormatter binaryF = new BinaryFormatter(); MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length, false); object obj = binaryF.Deserialize(ms); ms.Close(); return obj; } public static byte[] Serialize(object obj) { BinaryFormatter binaryF = new BinaryFormatter(); MemoryStream ms = new MemoryStream(); binaryF.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); byte[] buffer = new byte[(int)ms.Length]; ms.Read(buffer, 0, buffer.Length); ms.Close(); return buffer; }
代码中的receiptCollection和collection是指向两块不同的内存地址,也就是改变一个对象的值不会影响另一个对象。
这种复制对象的方法要求原对象的类型与新的对象的类型完全一样,否则会抛出类型异常。
有时候我们需要另外一种复制,将一个对象中的属性值,复制到另一个对象中去,举例子说明:
销售订单SalesOrder有OrderNo, OrderDate, CustomerNo三个属性,销售订单修改SalesOrderAmendment也有
OrderNo, OrderDate, CustomerNo三个属性,当我们想做销售订单的修改业务,需要根据销售订单创建销售订单修改单,可以用下面的方法:
SalesOrder salesOrder...
SalesOrderAmendment orderAmendment=new SalesOrderAmendment();
orderAmendment.OrderNo=salesOrder.OrderNo;
orderAmendment.OrderDate=salesOrder.OrderDate;
orderAmendment.CustomerNo=salesOrder.CustomerNo;
标准的销售订单实体可能有50个以上的属性,则上面的赋值代码需要重复很多次。借助于反射,用一句话完成相同属性的赋值,参考下面的代码:
SalesOrder salesOrder... SalesOrderAmendment orderAmendment=new SalesOrderAmendment(); CopyObjectFieldValue(salesOrder,orderAmendment); public static void CopyObjectFieldValue(IEntity2 sourceEntity, IEntity2 targetEntity) { BindingFlags flags = (BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); CopyObjectFieldValue(sourceEntity, targetEntity, flags); }
也就是说反射可以将两个对象的公共属性的值,从一个对象复制到另一个对象,这在ERP的单据复制,单据下推等功能中有重要的作用,节省了大量的重复性代码。
在对象赋值的过程中,要考虑对象为可空类型或泛型的情况。比如整数类型与可空的整数类型,是可以相互赋值的,但是在反射中,我们需要取它的原生类型,参考下面的例子代码。
Type dataType = column.DataType;
if (ReflectionHelper.IsNullable(dataType))
dataType = ReflectionHelper.GetUnderlyingTypeOf(dataType);
对象之间的赋值操作也不复杂,参考下面的代码,无非是获取属性,设置属性值。
public void CopyTo(object sourceObject, object targetObject) { object[] value = new object[1]; Type sourceType = sourceObject.GetType(); Type targetType = targetObject.GetType(); LoadProperties(sourceObject, sourceType); LoadProperties(targetObject, targetType); List<PropertyInfo> sourcePoperties = Properties[sourceType.FullName] as List<PropertyInfo>; List<PropertyInfo> targetProperties = Properties[targetType.FullName] as List<PropertyInfo>; foreach (PropertyInfo sourceProperty in sourcePoperties) { PropertyInfo targetPropertyInfo = targetProperties. Find(delegate(PropertyInfo prop) { return prop.Name == sourceProperty.Name ; }); if (sourceProperty.CanRead) { if (targetPropertyInfo.CanWrite) { value[0] = sourceProperty.GetValue(sourceObject, BindingFlags.Public, null, null, null); targetPropertyInfo.SetValue(targetObject, value, null); } } } }
8 预加载类型 Preload type
通过在系统启动时预先创建对象的实例(通常是一些控件),如果稍后执行的界面功能中用到这些控件时,界面的响应速度会快一些,参考下面的代码例子加深印象。
public void PreloadAssemblyType() { LoadAssembly("Foundation.Component", "Foundation.WinUI.Image"); LoadAssembly("Foundation.Component", "Foundation.WinUI.GroupBox"); LoadAssembly("Foundation.Component", "Foundation.WinUI.Label"); LoadAssembly("Foundation.Component", "Foundation.WinUI.Button");
LoadAssembly的程序代码如下,仅仅是创建对象的实例:
private static void LoadAssembly(string assemblyName, string className) { object obj2 = null; try { obj2 = ReflectionHelper.CreateObjectInstance(assemblyName, className); } catch { } finally { obj2 = null; GC.Collect(); }
}
9 类型转换与赋值 Convert type and its value
反射应用于类型转化的例子,比较合理的一个例子是将List<T>转化为DataTable,或是将DataTable转化为List<T>。
public static DataTable ToDataTable<T>(this IEnumerable<T> varlist) { DataTable dtReturn = new DataTable(); PropertyInfo[] oProps = null; if (varlist == null) return dtReturn; foreach (T rec in varlist) { if (oProps == null) { oProps = ((Type) rec.GetType()).GetProperties(); foreach (PropertyInfo pi in oProps) { Type colType = pi.PropertyType; if ((colType.IsGenericType) && (colType.GetGenericTypeDefinition() == typeof (Nullable<>))) { colType = colType.GetGenericArguments()[0]; } dtReturn.Columns.Add(new DataColumn(pi.Name, colType)); } } DataRow dr = dtReturn.NewRow(); foreach (PropertyInfo pi in oProps) { dr[pi.Name] = pi.GetValue(rec, null) == null ? DBNull.Value : pi.GetValue(rec, null); } dtReturn.Rows.Add(dr); } return dtReturn; }
仅仅获取类型的公共属性,再复制到另一个对象中,抽象的代码通用性也高。
10 测试 Test
实际环境中,有些类型所依赖的环境是不容易测试的,可以考虑用反射创建一些fake值,注入到要测试的类型中。
这样可以让难以模拟测试的类型,通过测试。
技术水平有限,写的不对的地方请多指正。