(一). 概述
业余时间做了一个非常有用的控件, 介绍一下.
一般当我们要实现这样一个计算功能页面:
TextBox1(单价) * TextBox2(数量) = TextBox3(总和);
并且当在TextBox1或TextBox2中输入数据, 鼠标离开时, TextBox3控件能够即时重新计算新值(乘积).
一般我们的做法步骤是:
1. 拖三个控件到页面上, 默认三个TextBox控件ID分别为: TextBox1, TextBox1,
TextBox3.
2. 写个JavaScript 函数, 能够计算 TextBox1和TextBox2的乘积, 并赋值给TextBox3
即时最新值.
<script language='javascript'>
function compute()
{
var _num = parseFloat(document.getElementById('TextBox1').value);
var _price = parseFloat(document.getElementById('TextBox2').value);
document.getElementById('TextBox3').value=_num*_price;
}
</script>
3. 注册TextBox1和TextBox2的onblur事件(TextBox控件失去焦点时触发).
this.TextBox1.Attributes.Add("onblur","compute()");
this.TextBox2.Attributes.Add("onblur","compute()");
OK, 这样固然能够完成. 也存在以下缺点:
1. 不够通用, 如果好多页面都需要这么一个控件, 那得写上面这些代码到所有
页面中. 开发效率不高. 且容易出错.
2. 页面中代码比较乱. 嵌入好多JavaScript代码. 难以维护.
3. 假如运算表达式非常复杂[如: A*B+(B*C)+Math.E ] 每次设置JS也比较麻烦.
基于以上缺陷,
下面是一个通用的自定义控件AutoComputeControl(自动计算), 能够弥补以上缺点,
且具有通用性,
特点如下:
1. 使用简单, 只需设置一个表达式属性Expression, 控件能够自动完成所有JS脚本.
其中表达式由: 运算符和变量组成(控件的ID), 等一下会详细介绍使用方法.
2. 不仅支持简单运算, 更大的优势是支持复杂表达式, 如:
price*(num+2*(3+6))*Math.E = sum , 即
TextBox1*(TextBox2+2*(3+6)) * Math.E =TextBox3
仅通过将控件的: ID和运算符 任意组合成计算表达式赋值给控件Expression属性,
其它的工作由本控件来完成, 本控件能够自动生成所有JS脚本. 并最终在页面客
户端呈现.
3. 另外, 支持Math对象下面的属性和方法:
Math属性: Math.E Math.LN10 Math.LN2 Math.LOG10E Math.LOG2E
Math.PI Math.SQRT1_2 Math.SQRT2
Math方法: Math.abs(x) Math.acos(x) Math.asin(x) Math.atan(x)
Math.atan2(x,y) Math.ceil(x) Math.floor(x) Math.cos(x)
Math.exp(x) Math.log(x) Math.max(x,y) Math.min(x,y)
Math.pow(x,y) Math.random() Math.round(20.49) Math.sin(x)
Math.sqrt(x) Math.tan(x)
例如: Math.sin(Math.sqrt(price1*price2))+Math.E*num=sum
即 Math.sin(Math.sqrt(TextBox1*TextBox2))+Math.E*TextBox3=TextBox4
4. 最终用户还可以在控件中输入表达式:
例如, 用户在TextBox1框中输入: 6*(5+2)
再在TextBox2中输入: 3+Math.E*Math.PI
再把表达式: TextBox1*(TextBox2+2*(3+6)) * Math.E =TextBox3
赋值给本控件属性Expression, 仍然能够正确计算出结果.
5. 在一个页面中放多个本控件.
比如: 拖个本控件到页面跟 TextBox1/TextBox2/TextBox3 组合,
再拖另一个控件跟另一组 TextBox4/TextBox5/TextBox5 组合.
注意: 不要两组表达式产生冲突, 比如把TextBox1即在第一组又在
第二组, 这样脚本生成是正确的, 但这样自动生成的客户端脚本, 会
为TextBox1注册两个onblur事件, 那么默认第二个onblur起效, 不能
同时起效. 这是JavaScript 语法规定的.
实现原理: 本自定义控件的Expression属性, 如: TextBox1*(TextBox2+TexBox3)*0.90=TextBox4
中包括:
1. 控件的ID. 2. 表达式之间的关系.
然后自定义控件内核代码会:
1. 用编译算法扫描表达式属性(Expression)值, 分析运算关系.
2. 根据运算关系动态生成要呈现到客户端的JavaScript, 再最终由自定义控件呈现.
(二). 使用步骤
1. 拖三个TextBox控件到页面上, 如图:
2. 设置TextBox1的ID属性为: price; TextBox2的ID属性为: num; TextBox3的ID属性为: sum.
3. 再添加一个AutoCompute控件(本文章主讲控件), 并设置其: Expression 属性值为:
price * num = sum , 意思是: [单价] * [数量] = [总额]
4. 运行即可. 运行后当输入[单价]和[数量]时, 程式能够自动计算[总额]的值.
测试: 当鼠标从[单价]或[数量]控件失去焦点时, 总额值能够重新被计算显示.
5. 扩展:
a. 您也可以输入更复杂的表达式, 比如从上面DropDownList(测试用的控件)里面随便选几个.
b. 不仅能够计算三个TextBox的表达式(如上), 您还可以添加N个TextBox表达式.
功能比较强大吧 :) :)
(三). 表达式规则:
支持JavaScript运算规则, 并支持Math对象的所有属性和方法.
(四). 核心代码
1. Node类文件Node.cs, 用于编译算法中存储数据结点和字符结点
1
/**/
/// <summary>
2 /// Author: [ ChengKing(ZhengJian) ]
3 /// Blog: Http://blog.csdn.net/ChengKing
4 /// </summary>
5 /// <summary>
6 /// Node 的摘要说明
7 /// </summary>
8 /// <summary>
9 /// 结点类[操作符结点]
10 /// </summary>
11
public
class
Node
12
{
13 public string str; //存储本节点字串
14 public int startIndex; //用于存储一个结点所在[运算表达式]的开始索引位置
15 public int endIndex; //用于存储一个结点所在[运算表达式]的结束索引位置
16
17
18 public Node(int startIndex, int endIndex, string str)
19 {
20 this.str = str;
21 this.startIndex = startIndex;
22 this.endIndex = endIndex;
23 }
24 }
2. 主要控件类 AutoCompute.cs 代码
1
///
<summary>
2
///
Author: [ ChengKing(ZhengJian) ]
3
///
Blog: Http://blog.csdn.net/ChengKing
4
///
</summary>
5
[DefaultProperty(
"
Text
"
)]
6
[ToolboxData(
"
<{0}:AutoCompute runat=server></{0}:AutoCompute>
"
)]
7
public
class
AutoCompute : Control
8
{
9
[Bindable(
true
)]
10
//
[Category("外观")]
11
[DefaultValue(
"
[AutoCompute \
"
AutoCompute1\
"
]
"
)]
12
[Localizable(
true
)]
13
public
string
Text
14
{
15
get
16
{
17
String s
=
(String)ViewState[
"
Text
"
];
18
return
((s
==
null
)
?
String.Empty : s);
19
}
20
21
set
22
{
23
ViewState[
"
Text
"
]
=
value;
24
}
25
}
26
27
[Bindable(
true
)]
28
[DefaultValue(
""
)]
29
[Localizable(
true
)]
30
public
string
Expression
31
{
32
get
33
{
34
string
s
=
(
string
)
this
.ViewState[
"
Expression
"
];
35
return
((s
==
null
)
?
String.Empty : s);
36
}
37
set
38
{
39
this
.ViewState[
"
Expression
"
]
=
value;
40
}
41
}
42
43
protected
override
void
Render(HtmlTextWriter writer)
44
{
45
if
(DesignMode)
46
{
47
this
.Controls.Clear();
48
LiteralControl lc
=
new
LiteralControl();
49
lc.Text
=
this
.Text;
50
this
.Controls.Add(lc);
51
}
52
base
.Render(writer);
53
}
54
55
protected
override
void
OnPreRender(EventArgs e)
56
{
57
base
.OnPreRender(e);
58
59
ConvertHelper _ConvertHelper
=
new
ConvertHelper();
60
string
strClientScript;
61
try
62
{
63
if
(
this
.Expression.Trim().Length
!=
0
)
64
{
65
_ConvertHelper.Main(Page,
this
.Expression);
66
_ConvertHelper.RegisterClientScript(
this
.Page);
67
}
68
else
69
{
70
strClientScript
=
"
alert('No Set [Expression] Property!');
"
;
71
if
(
!
Page.ClientScript.IsStartupScriptRegistered(
"
Default_Property
"
))
72
{
73
Page.ClientScript.RegisterStartupScript(
this
.GetType(),
"
Default_Property
"
, strClientScript,
true
);
74
}
75
}
76
}
77
catch
78
{
79
strClientScript
=
"
alert('The [Expression] format is not correct!');
"
;
80
if
(
!
Page.ClientScript.IsStartupScriptRegistered(
"
Default_Property
"
))
81
{
82
Page.ClientScript.RegisterStartupScript(
this
.GetType(),
"
Default_Property
"
, strClientScript,
true
);
83
}
84
}
85
86
}
87
88
}
3. ConvertHelper.cs类文件, 主要实现编译算法以及JavaScript脚本生成注册功能.
1
///
<summary>
2
///
Author: [ ChengKing(ZhengJian) ]
3
///
Blog: Http://blog.csdn.net/ChengKing
4
///
</summary>
5
///
<summary>
6
///
ConvertHelper 的摘要说明
7
///
</summary>
8
///
<summary>
9
///
算法概述:
10
///
引用概念: [数据变量结点]: 用户命名的字串, 如: total = price*num, 则 "price" 字串就为数据变量结点
11
///
1. 抽取出操作运算符. 并记住所有运算符的索引
12
///
2. 两个操作符之间的字符串为[数据变量结点(用户命名的字串)], 但下面几种情况要排除:
13
///
a. 提取的相邻字符中, 右边字符为"("操作符时, 中间不是数据变量结点.
14
///
b. 两个操作符相邻时, 其中间没有字符串, 显然也就没有数据变量结点.
15
///
c. 数据变量结点必须是字符串变量, 不能为数值.
16
///
d. 排除Math.E等常量情况(Math.E/Math.LN10/Math.LN2/Math.LOG10E/Math.LOG2E/ Math.PI/Math.SQRT1_2/Math.SQRT2).
17
///
</summary>
18
public
class
ConvertHelper
19
{
20
///
<summary>
21
///
存放JavaScript运算符的各种结点
22
///
</summary>
23
private
string
[] OP_Chars
=
new
string
[
7
] {
"
+
"
,
"
-
"
,
"
*
"
,
"
/
"
,
"
(
"
,
"
)
"
,
"
,
"
};
24
25
///
<summary>
26
///
自定义变量前缀符号
27
///
</summary>
28
private
string
VarPreSymbol
=
"
_
"
;
29
30
///
<summary>
31
///
存储要读取控件的属性(如: t.text/t.Value etc)
32
///
</summary>
33
private
string
ValueSymbol
=
"
.value
"
;
34
35
///
<summary>
36
///
存储compute方法脚本变量
37
///
</summary>
38
private
string
ComputeScript
=
""
;
39
40
///
<summary>
41
///
存储onblur方法脚本变量
42
///
</summary>
43
private
string
OnblurScript
=
""
;
44
45
///
<summary>
46
///
区别于方法名的序列号[依次递增, 如: compute1, compute2, compute3 ]
47
///
</summary>
48
private
int
SequenceNum
=
1
;
49
50
///
<summary>
51
///
抽取出运算符结点[其中包括运算符结点的位置信息]
52
///
</summary>
53
///
<param name="strObject"></param>
54
///
<returns></returns>
55
private
List
<
Node
>
BuildOPNode(
string
strObject)
56
{
57
int
beginIndex
=
0
;
//
记录当前处理结点的起始索引
58
List
<
Node
>
nodes
=
new
List
<
Node
>
();
59
60
61
while
(
true
)
62
{
63
if
(beginIndex
==
strObject.Length)
64
{
65
break
;
66
}
67
68
for
(
int
j
=
0
; j
<
OP_Chars.Length; j
++
)
69
{
70
if
(strObject.Length
-
beginIndex
>=
OP_Chars[j].Length)
71
{
72
if
(OP_Chars[j]
==
strObject.Substring(beginIndex, OP_Chars[j].Length))
73
{
74
//
操作符
75
Node node
=
new
Node(beginIndex, beginIndex
+
OP_Chars[j].Length
-
1
, strObject.Substring(beginIndex, OP_Chars[j].Length));
76
nodes.Add(node);
77
break
;
78
}
79
}
80
}
81
beginIndex
++
;
82
}
83
return
nodes;
84
}
85
86
///
<summary>
87
///
根据运算符结点抽取出数据结点[其中包括数据结点的位置信息]
88
///
</summary>
89
///
<param name="strObject"></param>
90
///
<returns></returns>
91
public
List
<
Node
>
BuildDataNode(
string
strObject)
92
{
93
strObject
=
ClearSpace(strObject);
94
List
<
Node
>
dataNodes
=
new
List
<
Node
>
();
95
List
<
Node
>
opNodes
=
this
.BuildOPNode(strObject);
96
97
//
考虑表达式最左边是数据结点情况, 如: A+B 表达式中的A
98
if
(opNodes.Count
>
0
&&
opNodes[
0
].startIndex
!=
0
&&
opNodes[
0
].str
!=
"
(
"
)
99
{
100
string
str
=
strObject.Substring(
0
, opNodes[
0
].startIndex);
101
if
(
this
.JudgeFigure(str)
==
false
&&
this
.IsIndexOfMath(str)
==
false
)
102
{
103
Node node
=
new
Node(
0
, opNodes[
0
].startIndex
-
1
, str);
104
dataNodes.Add(node);
105
}
106
107
}
108
109
//
根据操作运算符求得中间的一系列数据结点
110
for
(
int
i
=
0
; i
<
opNodes.Count
-
1
; i
++
)
111
{
112
if
(
this
.IsDataNodeBetweenOPNodes(opNodes[i], opNodes[i
+
1
], strObject))
113
{
114
Node node
=
new
Node(opNodes[i].endIndex
+
1
, opNodes[i
+
1
].startIndex
-
1
, strObject.Substring(opNodes[i].endIndex
+
1
, opNodes[i
+
1
].startIndex
-
opNodes[i].endIndex
-
1
));
115
dataNodes.Add(node);
116
}
117
}
118
119
//
考虑最右端是数据结点情况, 如: A+B 表达式中的B
120
if
(opNodes.Count
>
0
&&
(opNodes[opNodes.Count
-
1
].endIndex
!=
strObject.Length
-
1
))
121
{
122
string
str
=
strObject.Substring(opNodes[opNodes.Count
-
1
].endIndex
+
1
);
123
if
(
this
.JudgeFigure(str)
==
false
&&
this
.IsIndexOfMath(str)
==
false
)
124
{
125
Node node
=
new
Node(opNodes[opNodes.Count
-
1
].endIndex
+
1
, strObject.Length
-
1
, str);
126
dataNodes.Add(node);
127
}
128
}
129
return
dataNodes;
130
}
131
132
///
<summary>
133
///
判断相邻结点中间是否是数据结点
134
///
</summary>
135
///
<param name="leftNode"></param>
136
///
<param name="rightNode"></param>
137
///
<returns></returns>
138
///
根据以下定理进行判断
139
///
a. 提取的相邻字符中, 右边字符为"("操作符时, 中间不是数据变量结点.
140
///
b. 两个操作符相邻时, 其中间没有字符串, 显然也就没有数据变量结点.
141
///
c. 数据变量结点必须是字符串变量, 不能为数值.
142
///
d. 排除Math.E等常量情况(Math.E/Math.LN10/Math.LN2/Math.LOG10E/Math.LOG2E/ Math.PI/Math.SQRT1_2/Math.SQRT2).
143
private
bool
IsDataNodeBetweenOPNodes(Node leftNode, Node rightNode,
string
strObject)
144
{
145
//
条件a
146
if
(rightNode.str
==
"
(
"
)
147
{
148
return
false
;
149
}
150
151
//
条件b
152
if
(leftNode.endIndex
+
1
==
rightNode.startIndex)
153
{
154
return
false
;
155
}
156
157
//
条件c
158
if
(
this
.JudgeFigure(strObject.Substring(leftNode.endIndex
+
1
, rightNode.startIndex
-
leftNode.endIndex
-
1
))
==
true
)
159
{
160
return
false
;
161
}
162
163
if
(
this
.IsIndexOfMath(strObject.Substring(leftNode.endIndex
+
1
, rightNode.startIndex
-
leftNode.endIndex
-
1
)))
164
{
165
return
false
;
166
}
167
168
return
true
;
169
}
170
171
///
<summary>
172
///
//判断是否Math.开头 排除(Math.E/Math.LN10/Math.LN2/Math.LOG10E/Math.LOG2E/ Math.PI/Math.SQRT1_2/Math.SQRT2)等常量
173
///
</summary>
174
///
<param name="str"></param>
175
///
<returns></returns>
176
public
bool
IsIndexOfMath(
string
str)
177
{
178
if
(str.IndexOf(
"
Math.
"
)
==
0
)
179
{
180
return
true
;
181
}
182
return
false
;
183
}
184
185
///
<summary>
186
///
判断是否是数字
187
///
</summary>
188
///
<param name="str"></param>
189
///
<returns></returns>
190
private
bool
JudgeFigure(
string
str)
191
{
192
if
(str.Trim().Length
<=
0
)
193
return
true
;
194
int
dot
=
0
;
195
if
(str[
0
]
==
'
.
'
||
str[str.Length
-
1
]
==
'
.
'
)
196
return
false
;
197
for
(
int
i
=
0
; i
<
str.Length; i
++
)
198
{
199
if
(dot
>
1
)
return
false
;
200
if
(Char.IsDigit(str, i))
201
{
202
continue
;
203
}
204
if
(str[i]
==
'
.
'
)
205
{
206
dot
=
dot
+
1
;
207
continue
;
208
}
209
return
false
;
210
}
211
return
true
;
212
}
213
214
///
<summary>
215
///
返回处理后的表达式
216
///
</summary>
217
///
<param name="str"></param>
218
///
<param name="?"></param>
219
///
<returns></returns>
220
private
string
CreateClientScript(Page page,
string
strAll, List
<
Node
>
nodes)
221
{
222
string
strLeft
=
strAll.Substring(
0
, strAll.IndexOf(
"
=
"
)); ;
223
string
strRight
=
strAll.Substring(strAll.IndexOf(
"
=
"
)
+
1
);
224
225
///
<summary>
226
///
生成并注册compute方法脚本
227
///
</summary>
228
int
intNumDataNodeCount
=
nodes.Count;
229
230
//
调整方法名, 防止多个表达式运算时, 方法名冲突
231
while
(
true
)
232
{
233
//
bool flag = page.ClientScript.IsClientScriptBlockRegistered("compute" + SequenceNum.ToString());
234
if
(
!
page.ClientScript.IsClientScriptBlockRegistered(
this
.GetType(),
"
compute
"
+
SequenceNum.ToString()))
235
{
236
if
(
!
page.ClientScript.IsStartupScriptRegistered(
this
.GetType(),
"
onblur
"
+
this
.SequenceNum.ToString()))
237
{
238
break
;
239
}
240
241
}
242
SequenceNum
++
;
243
}
244
245
//
生成脚本头JS字串
246
string
strJSHead
=
"
<script language='javascript'> \n function compute
"
+
this
.SequenceNum.ToString()
+
"
() \n { \n
"
;
247
//
生成脚本体JS字串
248
string
strJSBody
=
""
;
249
for
(
int
i
=
0
; i
<
intNumDataNodeCount; i
++
)
250
{
251
strJSBody
+=
"
var
"
+
VarPreSymbol
+
nodes[i].str
+
"
= parseFloat(document.getElementById('
"
+
((Control)page.FindControl(nodes[i].str)).ClientID
+
"
')
"
+
ValueSymbol
+
"
);\n
"
;
252
}
253
strJSBody
+=
"
document.getElementById('
"
+
((Control)page.FindControl(strRight)).ClientID
+
"
')
"
+
ValueSymbol;
254
strJSBody
+=
"
=
"
;
255
256
for
(
int
i
=
0
; i
<
intNumDataNodeCount; i
++
)
257
{
258
strLeft
=
strLeft.Remove(nodes[i].startIndex, nodes[i].str.Length);
259
strLeft
=
strLeft.Insert(nodes[i].startIndex,
"
_
"
+
nodes[i].str);
260
this
.RepairNodes(
ref
nodes, i
+
1
);
261
}
262
strLeft
+=
"
;
"
;
263
strJSBody
+=
strLeft;
264
string
strJSFoot
=
"
\n }\n</script>\n\n
"
;
265
266
string
strReturnScript
=
strJSHead
+
strJSBody
+
strJSFoot;
267
this
.ComputeScript
=
strReturnScript;
268
269
270
271
///
<summary>
272
///
生成并注册onblur脚本(调用compute方法)
273
///
</summary>
274
string
strOnBlur
=
"
\n<script language='javascript'>\n
"
;
275
for
(
int
i
=
0
; i
<
nodes.Count; i
++
)
276
{
277
strOnBlur
+=
"
document.getElementById('
"
+
((Control)page.FindControl(nodes[i].str)).ClientID
+
"
')
"
+
"
.onblur=compute
"
+
this
.SequenceNum.ToString()
+
"
;\n
"
;
278
}
279
strOnBlur
+=
"
</script>
"
;
280
this
.OnblurScript
=
strOnBlur;
281
282
283
284
strReturnScript
+=
strOnBlur;
285
return
strReturnScript;
286
}
287
288
///
<summary>
289
///
重新调整数据节点集合的索引值
290
///
</summary>
291
///
<param name="nodes"></param>
292
///
<param name="index"></param>
293
private
void
RepairNodes(
ref
List
<
Node
>
nodes,
int
index)
294
{
295
for
(
int
i
=
index; i
<
nodes.Count; i
++
)
296
{
297
//
6相当于前面数据结点插入的 ".value" 的长度
298
nodes[i].startIndex
=
nodes[i].startIndex
+
VarPreSymbol.Length;
299
nodes[i].endIndex
=
nodes[i].endIndex
+
VarPreSymbol.Length;
300
}
301
}
302
303
public
string
Main(Page page,
string
strAll)
304
{
305
strAll
=
this
.ClearSpace(strAll);
306
if
(CheckParenthesesMatching(strAll)
==
false
)
307
{
308
page.Response.Write(
"
<br><br><br><br><br> 括号不匹配!
"
);
309
return
""
;
310
}
311
312
string
strLeft
=
strAll.Substring(
0
, strAll.IndexOf(
"
=
"
));
313
314
string
strEndJS_Script
=
this
.CreateClientScript(page, strAll, BuildDataNode(strLeft));
315
return
strEndJS_Script;
316
}
317
318
//
检查括号是否匹配
319
private
bool
CheckParenthesesMatching(
string
strCheck)
320
{
321
int
number
=
0
;
322
for
(
int
i
=
0
; i
<
strCheck.Length; i
++
)
323
{
324
if
(strCheck[i]
==
'
(
'
) number
++
;
325
if
(strCheck[i]
==
'
)
'
) number
--
;
326
if
(number
<
0
)
return
false
;
//
右括号不能在前面
327
}
328
if
(number
!=
0
)
329
{
330
return
false
;
331
}
332
return
true
;
333
}
334
335
//
消去空格
336
private
string
ClearSpace(
string
str)
337
{
338
return
str.Replace(
"
"
,
""
);
339
}
340
341
//
注册客户端脚本
342
public
bool
RegisterClientScript(Page page)
343
{
344
if
(
this
.OnblurScript.Length
==
0
||
this
.ComputeScript.Length
==
0
)
345
{
346
return
false
;
347
}
348
349
if
(
!
page.ClientScript.IsClientScriptBlockRegistered(
"
compute
"
+
this
.SequenceNum.ToString()))
350
{
351
page.ClientScript.RegisterClientScriptBlock(
this
.GetType(),
"
compute
"
,
this
.ComputeScript,
false
);
352
}
353
354
if
(
!
page.ClientScript.IsStartupScriptRegistered(
"
onblur
"
+
this
.SequenceNum.ToString()))
355
{
356
page.ClientScript.RegisterStartupScript(
this
.GetType(),
"
onblur
"
,
this
.OnblurScript,
false
);
357
}
358
return
true
;
359
}
360
361
}
(五). 示例代码下载
http://files.cnblogs.com/MVP33650/自动计算控件.rar
(六). 其它相关自定义控件文章
http://blog.csdn.net/ChengKing/category/288694.aspx
(七).此控件的第二个版本源码已经发布, 请看这里, 内容更精彩:
http://blog.csdn.net/ChengKing/archive/2007/04/27/1587794.aspx
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1562765