YbSoftwareFactory 各种插件的基础类库中又新增了两个方便易用的功能:动态属性扩展与键值生成器,本章将分别介绍这两个非常方便的组件。
一、动态属性扩展
在实际的开发过程中,你肯定会遇到数据库字段不够用的情况,临时增加一个字段有时是很麻烦的一件事。例如需要修改 SQL 语句、视图、存储过程等等,即使你使用的是 ORM 组件,也需要增加和配置映射,每次修改完成后还需反复进行测试,非常的不方便,如果软件已经为客户部署好了的话,嘿嘿,不用说,肯定更让你头疼;而客户临时要添加新的字段的情况却是非常普遍的。另外,有些对象其实不适合放到一张主表中,即使这是 1:1 的关系,因为直接添加到一张表可能会存在一定的性能问题,例如图片、文件等信息,某些时候查询 N 多记录返回大量信息通常不是合理和明智的做法,在字段数量很多的情况下,对于某些不重要的字段信息保存到其他表中通常是可以提升查询性能的。
本章介绍的动态属性扩展功能主要就是解决此类问题,可以灵活、方便的扩展属性。
注:动态属性扩展组件主要面向正在开发中的审批流组件而设计的,其目的是为终端用户提供灵活、方便、易用的属性自定义的功能。动态属性扩展组件已集成到数据字典组件、组织机构管理组件中。
本组件具有如下显著特点:
- 自动完成动态属性值的加载和保存,通过键/值对的方式实现动态扩展属性的数据库保存和加载,非常的方便。如果你想玩得更高级点,可以直接从界面绑定一个动态属性,然后保存到数据库并能重新加载并绑定到界面上,这一过程无需你像某些软件类似的对所谓的元数据进行管理和配置,非常灵活。
- 能自动完成属性类型的转换,因为字段的属性值是通过键值对的方式保存到指定的数据库表中,因此需要把数据库中保存的文本型的属性值自动转换成指定的类型(如日期、整数、二进制信息)等。本文介绍的动态属性扩展功能可完成此类型的转换。
- 支持对象的序列化,这对于使用 WCF、Web Service、Web API 等类似的技术进行远程数据交互是很有必要的。
至于具体的实现原理,毫无疑问是利用了 .NET 4.0 的 Dynamic 特性,如下是核心基类的实现代码:
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Dynamic;
5 using System.Reflection;
6
7 namespace Yb.Data.Provider
8 {
9 [Serializable]
10 public class ExtensionObject: DynamicObject, IDynamicMetaObjectProvider
11 {
12 object _instance;
13
14 Type _instanceType;
15 PropertyInfo[] _cacheInstancePropertyInfos;
16 IEnumerable<PropertyInfo> _instancePropertyInfos
17 {
18 get
19 {
20 if (_cacheInstancePropertyInfos == null && _instance != null)
21 _cacheInstancePropertyInfos = _instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
22 return _cacheInstancePropertyInfos;
23 }
24 }
25
26 public ExtensionObject()
27 {
28 Initialize(this);
29 }
30
31 /// <remarks>
32 /// You can pass in null here if you don't want to
33 /// check native properties and only check the Dictionary!
34 /// </remarks>
35 /// <param name="instance"></param>
36 public ExtensionObject(object instance)
37 {
38 Initialize(instance);
39 }
40
41
42 protected virtual void Initialize(object instance)
43 {
44 _instance = instance;
45 if (instance != null)
46 _instanceType = instance.GetType();
47 }
48
49 /// <param name="binder"></param>
50 /// <param name="result"></param>
51 /// <returns></returns>
52 public override bool TryGetMember(GetMemberBinder binder, out object result)
53 {
54 result = null;
55
56 // first check the Properties collection for member
57 if (Properties.Keys.Contains(binder.Name))
58 {
59 result = Properties[binder.Name];
60 return true;
61 }
62
63
64 // Next check for Public properties via Reflection
65 if (_instance != null)
66 {
67 try
68 {
69 return GetProperty(_instance, binder.Name, out result);
70 }
71 catch (Exception)
72 { }
73 }
74
75 // failed to retrieve a property
76 return false;
77 }
78
79 /// <param name="binder"></param>
80 /// <param name="value"></param>
81 /// <returns></returns>
82 public override bool TrySetMember(SetMemberBinder binder, object value)
83 {
84
85 // first check to see if there's a native property to set
86 if (_instance != null)
87 {
88 try
89 {
90 bool result = SetProperty(_instance, binder.Name, value);
91 if (result)
92 return true;
93 }
94 catch { }
95 }
96
97 // no match - set or add to dictionary
98 Properties[binder.Name] = value;
99 return true;
}
/// <param name="binder"></param>
/// <param name="args"></param>
/// <param name="result"></param>
/// <returns></returns>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if (_instance != null)
{
try
{
// check instance passed in for methods to invoke
if (InvokeMethod(_instance, binder.Name, args, out result))
return true;
}
catch (Exception)
{ }
}
result = null;
return false;
}
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="result"></param>
/// <returns></returns>
protected bool GetProperty(object instance, string name, out object result)
{
if (instance == null)
instance = this;
var miArray = _instanceType.GetMember(name, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
)
{
];
if (mi.MemberType == MemberTypes.Property)
{
result = ((PropertyInfo)mi).GetValue(instance,null);
return true;
}
}
result = null;
return false;
}
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
protected bool SetProperty(object instance, string name, object value)
{
if (instance == null)
instance = this;
var miArray = _instanceType.GetMember(name, BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance);
)
{
];
if (mi.MemberType == MemberTypes.Property)
{
((PropertyInfo)mi).SetValue(_instance, value, null);
return true;
}
}
return false;
}
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="args"></param>
/// <param name="result"></param>
/// <returns></returns>
protected bool InvokeMethod(object instance, string name, object[] args, out object result)
{
if (instance == null)
instance = this;
// Look at the instanceType
var miArray = _instanceType.GetMember(name,
BindingFlags.InvokeMethod |
BindingFlags.Public | BindingFlags.Instance);
)
{
] as MethodInfo;
result = mi.Invoke(_instance, args);
return true;
}
result = null;
return false;
}
public object this[string key]
{
get
{
try
{
// try to get from properties collection first
return Properties[key];
}
catch (KeyNotFoundException ex)
{
// try reflection on instanceType
object result = null;
if (GetProperty(_instance, key, out result))
return result;
// nope doesn't exist
throw;
}
}
set
{
if (Properties.ContainsKey(key))
{
Properties[key] = value;
return;
}
// check instance for existance of type first
var miArray = _instanceType.GetMember(key, BindingFlags.Public | BindingFlags.GetProperty);
)
SetProperty(_instance, key, value);
else
Properties[key] = value;
}
}
/// <param name="includeInstanceProperties"></param>
/// <returns></returns>
public IEnumerable<KeyValuePair<string,object>> GetProperties(bool includeInstanceProperties = false)
{
if (includeInstanceProperties && _instance != null)
{
foreach (var prop in this._instancePropertyInfos)
yield return new KeyValuePair<string, object>(prop.Name, prop.GetValue(_instance, null));
}
foreach (var key in this.Properties.Keys)
yield return new KeyValuePair<string, object>(key, this.Properties[key]);
}
/// <param name="item"></param>
/// <param name="includeInstanceProperties"></param>
/// <returns></returns>
public bool Contains(KeyValuePair<string, object> item, bool includeInstanceProperties = false)
{
bool res = Properties.ContainsKey(item.Key);
if (res)
return true;
if (includeInstanceProperties && _instance != null)
{
foreach (var prop in this._instancePropertyInfos)
{
if (prop.Name == item.Key)
return true;
}
}
return false;
}
/// <param name="key"></param>
/// <returns></returns>
public bool Contains(string key, bool includeInstanceProperties = false)
{
bool res = Properties.ContainsKey(key);
if (res)
return true;
if (includeInstanceProperties && _instance != null)
{
foreach (var prop in this._instancePropertyInfos)
{
if (prop.Name == key)
return true;
}
}
return false;
}
}
}
ExtensionObject
具体的使用,仅需继承该对象即可。为了更好的说明具体用法,请查看如下已测试通过的单元测试代码:
2 public class User : ExtensionObject
3 {
4 public Guid UserId { get; set; }
5 public string Email { get; set; }
6 public string Password { get; set; }
7 public string Name { get; set; }
8 public bool Active { get; set; }
9 public DateTime? ExpiresOn { get; set; }
10
11 public User()
12 : base()
13 { }
14
15 // only required if you want to mix in seperate instance
16 public User(object instance)
17 : base(instance)
18 {
19 }
20 }
21
22 /// <summary>
23 /// ExtensionData 的测试
24 ///</summary>
25 [TestMethod()]
26 public void ExtensionObjectTest()
27 {
28 //清空数据库存储的属性值,方便进行测试
29 ExtensionDataApi.ClearExtensionDataOfApplication();
30
31 var user = new User();
32 // 设置已有属性
33 dynamic duser = user;
34 user.UserId = Guid.NewGuid();
35 duser.Email = "19892257@qq.com";
36 user.Password = "YbSofteareFactory";
37
38 // 设置动态属性
39 duser.FriendUserName = "YB";
40 duser.CreatedDate = DateTime.Now;
;
42 duser.Age = 27.5;
43 duser.LastUpdateId = (Guid?)null;
44 duser.LastUpdatedDate=null;
45
46 // 动态属性值保存
47 ExtensionDataApi.SaveExtensionObject(user.UserId,user);
48
49 // 从数据库中加载属性值
50 var obj = user.LoadExtensionData(user.UserId);
51
52 // 测试是否加载正确
53 Assert.AreEqual(obj.FriendUserName, "YB");
54 Assert.IsNotNull(obj.CreatedDate);
);
56 Assert.AreEqual(obj.Age, 27.5);
57 Assert.IsNull(obj.LastUpdateId);
58 Assert.IsNull(obj.LastUpdatedDate);
59
60 var items = ExtensionDataApi.FindExtensionDataBy(user.UserId.ToString(), user);
61 //测试保存的动态属性数
);
63
64 // 修改动态属性值
;
66 // 新增动态属性
67 duser.Tag = null;
;
69 //使用扩展方法进行保存动态属性值至数据库
70 user.SaveExtensionData(user.UserId);
71
72 items = ExtensionDataApi.FindExtensionDataBy(user.UserId.ToString(), user);
73 //判断保存的属性数量是否正确
);
75
76 //使用扩展方法动态从数据库中加载属性
77 obj = user.LoadExtensionData(user.UserId);
78
79 Assert.AreEqual(obj.Tag, null);
);
81
82 duser.ComplexObject = user;
83
84 //设置新值
85 duser.Tag = true;
86 ExtensionDataApi.SaveExtensionObject(user.UserId, user);
87 obj = ExtensionDataApi.LoadExtensionObject(user.UserId, user);
88 // 验证加载的属性新值是否正确
89 Assert.IsTrue(obj.Tag);
90
91 //返回对象数组的属性字典方法测试
92 var dic = ExtensionDataApi.FindExtensionDataDictionaryBy(new string[]{user.UserId.ToString()}, user.GetType().FullName);
);
94
95 //byte[] 测试,对可方便存储文件、图片等内容
, , , , , , , , };
97 ExtensionDataApi.SaveExtensionObject(user.UserId, user);
98 obj = ExtensionDataApi.LoadExtensionObject(user.UserId, user);
);
Assert.AreEqual(obj.Image[], );
//Json 序列化测试,对 Web Api 等非常重要
string json = JsonConvert.SerializeObject(duser, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
TypeNameAssemblyFormat = FormatterAssemblyStyle.Full
});
Assert.IsNotNull(json);
json = JsonConvert.SerializeObject(user);
Assert.IsNotNull(json);
}
ExtensionObjectTest
二、键值生成器
键值的生成看似简单,其实实现起来却并不容易,因为这里面有并发性、生成效率等等方面的考虑。同时,对键值的管理也是非常重要的,试想想,不同位置的两个客户端同时生成了相同的键值是什么后果吧。
本章要介绍的键值生成器组件非常灵活和高效,它具有如下非常实用的功能:
- 支持绝大多数情况下指定格式的键值生成,例如可指定前缀、后缀、客户端应用程序编号(多客户端下非常有用)、日期(例如yyyy、yyyyMM、yyyyMMdd、yyyyMMddHH等)以及流水号长度。
- 支持批量生成键值,一次可以生成指定数量的键值组。
- 在满足特定性能的前提下,可有效解决常见的并发情况,有效防止键值冲突。
对于具体的使用方式,同样还是来看看已通过测试的部分单元测试代码:
2 ///GetNextID 的测试
3 ///</summary>
4 [TestMethod()]
5 public void GetNextIDTest()
6 {
7 IdGeneratorApi.ClearAllIdGenerator();
8
9 var user = new User();
10
11 //生成类似 U-01-201308-001格式的ID,%A表示输出客户端编号,%D表示输出日期时间
12 var idGen = new IdGenerator()
13 {
14 Type = typeof (User).FullName,
15 DateFormat = "yyyyMM",
16 GenFormat = "U-%A-%D-",
17 Id = Guid.NewGuid(),
,
,
21 };
22 //API基本方法测试
23 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
24 var item = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
25 Assert.IsNotNull(item);
26 item = IdGeneratorApi.GetIdGeneratorBy(user);
27 Assert.IsNotNull(item);
28 item = IdGeneratorApi.GetIdGeneratorBy("not exist's record");
29 Assert.IsNull(item);
30 //API基本方法测试
31 Assert.IsTrue(IdGeneratorApi.IdGeneratorExists(user));
32 Assert.IsFalse(IdGeneratorApi.IdGeneratorExists("dkakd_test_a"));
33
34 //生成ID号
35 var str = IdGeneratorApi.GetNextID(user);
36 Assert.AreEqual("U-02-201308-001", str);
37 str = IdGeneratorApi.GetNextID(user);
38 Assert.AreEqual("U-02-201308-002", str);
39
40 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
41 //无需生成日期,当前生成的ID号类似于U-02--003
42 idGen.DateFormat = string.Empty;
43
44 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
45 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
46
47 //生成下一ID号
48 str = IdGeneratorApi.GetNextID(user);
49 Assert.AreEqual("U-02--003", str);
50 str = IdGeneratorApi.GetNextID(user);
51 Assert.AreEqual("U-02--004", str);
52
53 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
54 // 如下代码修改生成的ID号类似于U-0005-
55 idGen.DateFormat = "yyyyMM";
56 //未设置%D,将不再输出日期
57 idGen.GenFormat = "U-%v-";
58 //修改生成编号的长度为4
;
60 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
61
62 str = IdGeneratorApi.GetNextID(user);
63 Assert.AreEqual("U-0005-", str);
64 str = IdGeneratorApi.GetNextID(user);
65 Assert.AreEqual("U-0006-", str);
66
67 //API基本方法测试
68 IdGeneratorApi.DeleteIdGenerator(idGen);
69 item = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
70 Assert.IsNull(item);
71 item = IdGeneratorApi.GetIdGeneratorBy(user);
72 Assert.IsNull(item);
73
74 IdGeneratorApi.ClearAllIdGeneratorOfApplication();
75 }
76
77 /// <summary>
78 ///GetNextGroupID 的测试,批量生产ID号
79 ///</summary>
80 [TestMethod()]
81 public void GetNextGroupIDTest()
82 {
83 IdGeneratorApi.ClearAllIdGeneratorOfApplication();
84
85 var user = new User();
86
87 var idGen = new IdGenerator()
88 {
89 Type = typeof(User).FullName,
90 DateFormat = "yyyyMM",
91 GenFormat = "U-%a-%D-%v",
92 Id = Guid.NewGuid(),
,
,
96 };
97
98 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
99
//批量生成3个ID号
);
Assert.IsTrue(str.Length==);
Assert.IsTrue(str[]=="U-02-201308-001");
Assert.IsTrue(str[]=="U-02-201308-002");
Assert.IsTrue(str[]=="U-02-201308-003");
idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
// 如下修改将生成类似于T0004的ID,将忽略日期和客户端编号
idGen.GenFormat = "T%v";
idGen.ValueLength = ;
IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
str = IdGeneratorApi.GetNextGroupID(user,);
Assert.IsTrue(str.Length==);
Assert.IsTrue(str[]=="T0004");
Assert.IsTrue(str[]=="T0005");
idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
//修改生成的ID格式
idGen.DateFormat = "yyyy";
//生成类似于01-0010/2013的ID号,%a为客户端编号,%v为流水号,%d将输出日期时间,此处为年份
idGen.GenFormat = "%a-%v/%d";
//指明流水号长度为4,类似于0001
;
IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
str = IdGeneratorApi.GetNextGroupID(user,);
Assert.IsTrue(str.Length==);
Assert.IsTrue(str[]=="02-0001/2013");
Assert.IsTrue(str[]=="02-0002/2013");
IdGeneratorApi.ClearAllIdGenerator();
}
public class User
{
public string Id { get; set; }
public string UserName { get; set; }
}
IdGeneratorTest
目前的开发重心将逐渐向审批流的开发过渡,未来的审批流组件将由表单设计器、流程设计器和审批流底层组件三大部分组成,具有灵活、简单、易用的特点,如下是流程设计器的预览界面: