智能在线表单设计器 Web Form Builder

FreeForm : Silverlight Agile Form Engine
  博客园  :: 首页  :: 新随笔  :: 管理

FreeForm自动计算及数据验证

 

Silverlight中动态数据验证和动态自动计算的Reflection反射实现

 

上周在博客园发了个首页随笔,因为被误认为是广告而被移出首页,这次发首页,特地备足了技术材料,结合FreeForm实际的开发情况,从技术上分析在Silverlight中实现动态数据验证和自动计算的方法。我们知道在.Net 4.0标准类库中,反射的类很全,非常好用,但在Silverlight类库中涉及反射的命名空间虽然也有System.Reflection System.Reflection.Emit ,但类和方法大大的缩减了。以前我们在WinForm或者ASP.Net中实现动态验证和动态计算非常容易,可以通过类似如下代码实现:

 

       

View Code
// compile string code to create class, generate an intance and mothod

public Expression(string expression, string fieldID, AppCom appcom)

{

this.Com = appcom;

if (expression.IndexOf("return") < 0) expression = "return " + expression + ";";

string className = "ExpressionValidate";

string methodName = "Compute";

if (!string.IsNullOrEmpty(fieldID))

{

methodName
= ("Validate_" + fieldID).Replace(":", "_");

methodName
= methodName.Replace("@", "Att_");

}

CompilerParameters cp
= new CompilerParameters();

cp.GenerateInMemory
= true;

cp.ReferencedAssemblies.Add(
"System.dll");

cp.ReferencedAssemblies.Add(
"mscorlib.dll");

cp.ReferencedAssemblies.Add(
"System.Data.dll");

cp.ReferencedAssemblies.Add(
"System.Xml.dll");

// cp.ReferencedAssemblies.Add(Com.ClassDir + "ComFun_cs.dll");

CompilerResults cr
= new CSharpCodeProvider()

.CompileAssemblyFromSource(cp,
string.

Format(
@"using System;

using System.Text.RegularExpressions;

using System.Collections.Generic;

using System.Data.SqlClient;

using System.Xml;

sealed class {0}{{

XmlDocument xdoc;

XmlNamespaceManager nsmgr;

public bool {1}(){{

{2}

}}

{3}

}}
",

className, methodName, expression, appcom.ValidateComFuns)
+ Com.GetComFunInfo());



instance
= cr.CompiledAssembly.CreateInstance(className);

method
= instance.GetType().GetMethod(methodName);



{

MethodInfo SFun
= instance.GetType().GetMethod("Initial");

if (SFun != null)

{

SFun.Invoke(instance,
new object[] { AppCom.XDoc, AppCom.nsmgr });

}

}

CLog.Win_ClassExp.AppendText(appcom.ValidateComFuns);

}



// validate one validation method

public bool Compute()

{

return (bool)method.Invoke(instance, null);

}

 

 

 

其中

sealed class {0}{{

                         XmlDocument xdoc;

                         XmlNamespaceManager nsmgr;

                         public bool {1}(){{

                            {2}

                         }}

                         {3}

                      }}

 

就是我们运行时从XML动态生成的类,从XML动态传入的参数,返回一个True False的验证结果。参数分别对应如下:

{0}className 类名

{1}methodName 方法名

{2}expression 动态验证表达式

{3}:动态加入的调用代码

 

 

很遗憾,这是在标准类库的快捷方便的写法,但是到了Silverlight微缩类库这里,很多类已不存在了。CodeDomProvider类没有了,Microsoft.VisualBasic

命名空间没有了,CompilerParameters 类没有了,CompilerResults类没有了,Microsoft.CSharp.CSharpCodeProvider类没有了,Microsoft.CSharp

下还有2个类:Microsoft.CSharp.RuntimeBinder. RuntimeBinderException类和Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException,很明显这2个类是处理异常的。真的要绝望了。

 

经过捣腾System.Reflection.Assembly类,我们发现有4个方法:

            System.Reflection.Assembly.GetCallingAssembly();

            System.Reflection.Assembly.GetExecutingAssembly();

            System.Reflection.Assembly.Load(assemblyName);

            System.Reflection.Assembly.LoadFrom(assemblyFile);

 

发现Silverlight中的反射与.NET FRAMEWORK中的略有不同。以下分三种情况描述:

 

     1:动态创建当前执行的程序集中的类实例。

     2:动态创建XAP包中其它SILVERLIGHT程序集中的类实例。

     3:动态下载并创建网站上其它SILVERLIGHT程序集中的类实例。

 

我们可以利用后两种方法来动态加载程序集,CSharpCodeProvider那种方便快捷的写法不可能用了,于是只能用System.Reflection.Emit 命名空间,定义动态程序集,关于这方面,微软有详细介绍:

http://msdn.microsoft.com/zh-cn/library/4xtysk39%28v=VS.95%29.aspx

 

首先需要了解生成方法体的 MSIL 指令代码,感觉和大学学的汇编差不多,

http://msdn.microsoft.com/zh-cn/library/system.reflection.emit.opcodes%28v=VS.95%29.aspx

 

贴一个通过Emit动态生成程序集的例子。其实是动态生成了这个类:

public class MyDynamicType

            {

                private int m_number;

 

                public MyDynamicType() : this(42) {}

                public MyDynamicType(int initNumber)

                {

                    m_number = initNumber;

                }

 

                public int Number

                {

                    get { return m_number; }

                    set { m_number = value; }

                }

 

                public int MyMethod(int multiplier)

                {

                    return m_number * multiplier;

                }

            }

 

 

   

View Code
class Example

{

public static void Demo(System.Windows.Controls.TextBlock outputBlock)

{



AssemblyName aName
= new AssemblyName("DynamicAssemblyExample");

AssemblyBuilder ab
=

AppDomain.CurrentDomain.DefineDynamicAssembly(

aName,

AssemblyBuilderAccess.Run);



// Create the module.

ModuleBuilder mb
= ab.DefineDynamicModule(aName.Name);



TypeBuilder tb
= mb.DefineType(

"MyDynamicType",

TypeAttributes.Public);



// Add a private field of type int (Int32).

FieldBuilder fbNumber
= tb.DefineField(

"m_number",

typeof(int),

FieldAttributes.Private);



// Define a constructor that takes an integer argument and

// stores it in the private field.

Type[] parameterTypes
= { typeof(int) };

ConstructorBuilder ctor1
= tb.DefineConstructor(

MethodAttributes.Public,

CallingConventions.Standard,

parameterTypes);



ILGenerator ctor1IL
= ctor1.GetILGenerator();

// For a constructor, argument zero is a reference to the new

// instance. Push it on the stack before calling the base

// class constructor. Specify the default constructor of the

// base class (System.Object) by passing an empty array of

// types (Type.EmptyTypes) to GetConstructor.

ctor1IL.Emit(OpCodes.Ldarg_0);

ctor1IL.Emit(OpCodes.Call,

typeof(object).GetConstructor(Type.EmptyTypes));

// Push the instance on the stack before pushing the argument

// that is to be assigned to the private field m_number.

ctor1IL.Emit(OpCodes.Ldarg_0);

ctor1IL.Emit(OpCodes.Ldarg_1);

ctor1IL.Emit(OpCodes.Stfld, fbNumber);

ctor1IL.Emit(OpCodes.Ret);



// Define a default constructor that supplies a default value

// for the private field. For parameter types, pass the empty

// array of types or pass null.

ConstructorBuilder ctor0
= tb.DefineConstructor(

MethodAttributes.Public,

CallingConventions.Standard,

Type.EmptyTypes);



ILGenerator ctor0IL
= ctor0.GetILGenerator();

// For a constructor, argument zero is a reference to the new

// instance. Push it on the stack before pushing the default

// value on the stack, then call constructor ctor1.

ctor0IL.Emit(OpCodes.Ldarg_0);

ctor0IL.Emit(OpCodes.Ldc_I4_S,
42);

ctor0IL.Emit(OpCodes.Call, ctor1);

ctor0IL.Emit(OpCodes.Ret);



// Define a property named Number that gets and sets the private

// field.

//

// The last argument of DefineProperty is null, because the

// property has no parameters. (If you don't specify null, you must

// specify an array of Type objects. For a parameterless property,

// use the built-in array with no elements: Type.EmptyTypes)

PropertyBuilder pbNumber
= tb.DefineProperty(

"Number",

PropertyAttributes.HasDefault,

typeof(int),

null);



// The property "set" and property "get" methods require a special

// set of attributes.

MethodAttributes getSetAttr
= MethodAttributes.Public |

MethodAttributes.SpecialName
| MethodAttributes.HideBySig;



// Define the "get" accessor method for Number. The method returns

// an integer and has no arguments. (Note that null could be

// used instead of Types.EmptyTypes)

MethodBuilder mbNumberGetAccessor
= tb.DefineMethod(

"get_Number",

getSetAttr,

typeof(int),

Type.EmptyTypes);



ILGenerator numberGetIL
= mbNumberGetAccessor.GetILGenerator();

// For an instance property, argument zero is the instance. Load the

// instance, then load the private field and return, leaving the

// field value on the stack.

numberGetIL.Emit(OpCodes.Ldarg_0);

numberGetIL.Emit(OpCodes.Ldfld, fbNumber);

numberGetIL.Emit(OpCodes.Ret);



// Define the "set" accessor method for Number, which has no return

// type and takes one argument of type int (Int32).

MethodBuilder mbNumberSetAccessor
= tb.DefineMethod(

"set_Number",

getSetAttr,

null,

new Type[] { typeof(int) });



ILGenerator numberSetIL
= mbNumberSetAccessor.GetILGenerator();

// Load the instance and then the numeric argument, then store the

// argument in the field.

numberSetIL.Emit(OpCodes.Ldarg_0);

numberSetIL.Emit(OpCodes.Ldarg_1);

numberSetIL.Emit(OpCodes.Stfld, fbNumber);

numberSetIL.Emit(OpCodes.Ret);



// Last, map the "get" and "set" accessor methods to the

// PropertyBuilder. The property is now complete.

pbNumber.SetGetMethod(mbNumberGetAccessor);

pbNumber.SetSetMethod(mbNumberSetAccessor);



// Define a method that accepts an integer argument and returns

// the product of that integer and the private field m_number. This

// time, the array of parameter types is created on the fly.

MethodBuilder meth
= tb.DefineMethod(

"MyMethod",

MethodAttributes.Public,

typeof(int),

new Type[] { typeof(int) });



ILGenerator methIL
= meth.GetILGenerator();

// To retrieve the private instance field, load the instance it

// belongs to (argument zero). After loading the field, load the

// argument one and then multiply. Return from the method with

// the return value (the product of the two numbers) on the

// execution stack.

methIL.Emit(OpCodes.Ldarg_0);

methIL.Emit(OpCodes.Ldfld, fbNumber);

methIL.Emit(OpCodes.Ldarg_1);

methIL.Emit(OpCodes.Mul);

methIL.Emit(OpCodes.Ret);



// Finish the type.

Type t
= tb.CreateType();



// The code can be executed immediately. Start by getting reflection

// objects for the method and the property.

MethodInfo mi
= t.GetMethod("MyMethod");

PropertyInfo pi
= t.GetProperty("Number");



// Create an instance of MyDynamicType using the default

// constructor.

object o1 = Activator.CreateInstance(t);



// Display the value of the property, then change it to 127 and

// display it again. Use null to indicate that the property

// has no index.

outputBlock.Text
+= String.Format("o1.Number: {0}\n", pi.GetValue(o1, null));

pi.SetValue(o1,
127, null);

outputBlock.Text
+= String.Format("o1.Number: {0}\n", pi.GetValue(o1, null));



// Call MyMethod, passing 22, and display the return value, 22

// times 127. Arguments must be passed as an array, even when

// there is only one.

object[] arguments = { 22 };

outputBlock.Text
+= String.Format("o1.MyMethod(22): {0}\n",

mi.Invoke(o1, arguments));



// Create an instance of MyDynamicType using the constructor

// that specifies m_Number. The constructor is identified by

// matching the types in the argument array. In this case,

// the argument array is created on the fly. Display the

// property value.

object o2 = Activator.CreateInstance(t,

new object[] { 5280 });

outputBlock.Text
+= String.Format("o2.Number: {0}\n", pi.GetValue(o2, null));

}

}

 

好了,原理清晰了,一起看看在Siverlight上实现的例子吧(一定要看最后一节的“动态自定义验证

”),感谢大家。

 

到这里还是请管理员不要把我的帖子当做广告,我想这么实实在在的Siverlight广告实在是没有的。

FreeForm自动计算

创建2个控件用于演示自动计算:

 

进入Runtime Design 添加自动计算

字符型公式

 

选择ToUpper

 

然后将鼠标放在括号()的中间

 

请按键盘 Ctrl 键,将自动出现当前的控件列表,选择FieldA

点击 Add ,并点击 OK

 

FieldA输入abcd,然后进入Runtime Design 测试自动计算

点击“Test Calculation

可以看到效果:

 

其他操作的方法也是类似。

 

时间公式

 

FieldA添加公式ShortDateString

 

效果:

 

选择 Now

 

点击“Test Calculation

可以看到效果:

数字公式

 

还记得圆面积的计算公式吗?是πr²(圆周率*半径的平方)

 

添加公式:

我们现在要试验一下,半径输入5

 

点击“Test Calculation

可以看到效果:

 

 

FreeForm提供了2种数据验证方式:一般验证和自定义验证,前者是为较简单的验证提供支持,后者可以编写复杂的组合,可以用于较复杂的业务系统。

 

FreeForm一般验证

进入Runtime Design 添加验证:

 

一些例子

比如我们要规定Min Age最小年龄必须等于1,最大年龄必须小于等于150,那么可以这么定义:

 

如果我们输入了错误的值,点击“Test Validations”或者“Design Check”中的“Check”。

会自动出现错误提示。

支持的验证方法还有很多,不一一介绍,其中就包括几乎万能的正则表达式。

 

FreeForm动态自定义验证

一般验证有局限性,那就是只能一个语句一个语句的录入,一些复杂的验证难以实现,于是我们就开发了动态的客户自定义验证方式.

 

进入Runtime Design 添加自定义验证:

 

一些例子

比如我们规定在性别选择了Male或者Female的时候,real Age必须大于Min Age,同时,必须小于Max Age,在不选MaleFemale的时候,不予验证。

 

我们可以这样定义(选择简练的3目运算符):

( [Male] == 'True' ||  [Female] == 'True') ?

( [Male] == 'True' ||  [Female] == 'True') && ( [RealAge]  >= [MinAge]) && ( [RealAge] <= [MaxAge] ) : true

 

或者用更直观的If-Else写法:

if ([Male] == 'True' ||[Female] == 'True')

{( [RealAge]  >= [MinAge]) && ( [RealAge] <= [MaxAge] ) ;}

else {true;}

声明变量也是可以的:

var MaleorFemale=([Male] == 'True' ||[Female] == 'True');

if (MaleorFemale == true)

{( [RealAge]  >= [MinAge]) && ( [RealAge] <= [MaxAge] ) ;}

else {true;}

 

 

千万不要忘了键盘 Ctrl 键,它将自动出现当前的控件列表,非常方便。效果如下:

 

测试一下效果

当有一项性别选中时,年龄大于150

提示错误:

并且显示红框:

 

当性别不选的时候,即使大于150也不会抱错。

 

这个例子我们放在Demo菜单里,您可以测试。

验证可以通过点击“Test Validations”或者“Design Check”中的“Check”。

 

 

 

 

我们的网站(昕友软件):http://crmwin.com