基于AJAX的自动完成
我想大家在访问某些网站的时候都曾见到过基于Ajax的自动完成功能,比如http://www.google.com,www.56.com/.
![]()
一、引出Ajax的自动完成
现在要实现一个员工信息查询的功能,即根据输入的名字检索员工的详细信息。这是一个简单的数据表查询,在ASP.NET中实现这样的功能是比较简单的.
![]()
从上面可以看出,这种员工信息查询功能还存在一些不足,比如用户可能记不全员工的名字,只记得前面几个字母是什么,这样用户只能根据记忆猜测,一遍遍地尝试。如果在用户输入的同时,输入框下方可以给出相应的提示,辅助用户输入,那么用户进行检索的速度和成功率就会大大提高.这就是基于Ajax的自动完成功能.
![]()
二、自动完成功能的实现
实现这样的功能需要按以下的步骤进行。
· 服务器端提供GetSearchItems方法给客户端,用来返回满足条件的员工列表。
· 客户端的输入框需要增加onkeydown响应函数,以便即时获取满足条件的员工列表。
· 通过客户端的JavaScript动态列出待选结果的列表,同时还要提供键盘和鼠标的响应。
三、服务器端实现
本文采用AjaxPro.NET作为Ajax开发框架,首先为使用AjaxPro.NET做一些准备工作。 添加对AjaxPro.dll的引用,修改Web.config配置文件,在system.web节点下加入如下配置:
在页面后台代码(Default.aspx.cs)的Page_Load方法中增加下面的代码:
下面定义提供给客户端调用的方法GetSearchItems(),参数query为模糊查询的关键字值:
四、客户端实现
相对于服务器端的方法而言,客户端的处理要复杂得多。首先来分析如何根据服务器端返回的ArrayList对象展示结果。这里用到了Web编程中“层”(div)的概念,通过JavaScript和DOM创建一个新的层div,将ArrayList中的每一个条目都作为其子节点加入到div中,而每一个条目也被看作是一个div,其中具体的文本内容则是一个span对象。
除了显示待选的结果之外,下拉区域还要对键盘、鼠标事件做出响应;为了实时地显示待选结果,还需要定时更新待选结果的列表。这些功能都封装在lookup.js中。下面是lookeu.js的定义:
于此,一个基于 Ajax的自动完成功能就实现了。
本文借鉴于《ajax web2.0快速入门与项目实践》。
这本书上还使用了控件将该功能进行了封装,这样要实现Ajax的自动完成功能就更加方便了。在此就不做过多解说。
本文示例代码下载:AutoComplete.rar
---------------------------------------------------------------------------------------------------------

一、引出Ajax的自动完成
现在要实现一个员工信息查询的功能,即根据输入的名字检索员工的详细信息。这是一个简单的数据表查询,在ASP.NET中实现这样的功能是比较简单的.

从上面可以看出,这种员工信息查询功能还存在一些不足,比如用户可能记不全员工的名字,只记得前面几个字母是什么,这样用户只能根据记忆猜测,一遍遍地尝试。如果在用户输入的同时,输入框下方可以给出相应的提示,辅助用户输入,那么用户进行检索的速度和成功率就会大大提高.这就是基于Ajax的自动完成功能.

二、自动完成功能的实现
实现这样的功能需要按以下的步骤进行。
· 服务器端提供GetSearchItems方法给客户端,用来返回满足条件的员工列表。
· 客户端的输入框需要增加onkeydown响应函数,以便即时获取满足条件的员工列表。
· 通过客户端的JavaScript动态列出待选结果的列表,同时还要提供键盘和鼠标的响应。
三、服务器端实现
本文采用AjaxPro.NET作为Ajax开发框架,首先为使用AjaxPro.NET做一些准备工作。 添加对AjaxPro.dll的引用,修改Web.config配置文件,在system.web节点下加入如下配置:
1
<httpHandlers>
2
<!-- Register the ajax handler -->
3
<add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro" />
4
</httpHandlers>

2

3

4

在页面后台代码(Default.aspx.cs)的Page_Load方法中增加下面的代码:
1
protected void Page_Load(object sender, EventArgs e)
2
{
3
AjaxPro.Utility.RegisterTypeForAjax(typeof(_Default));
4
}

2

3

4

下面定义提供给客户端调用的方法GetSearchItems(),参数query为模糊查询的关键字值:
1
[AjaxPro.AjaxMethod()]
2
public ArrayList GetSearchItems(string query)
3
{
4
ArrayList items = new ArrayList();
5
StringBuilder queryString = new StringBuilder();
6
queryString.Append("select employeeid,lastname,firstname,title,titleofcourtesy from dbo.Employees");
7
queryString.Append(" where firstname like '%" + query + "%'");
8![]()
9
DataSet ds = DataBase.Instance.ReturnDataSet(queryString.ToString());
10
for (int i = 0; i < ds.Tables[0].Rows.Count; i++)
11
{
12
items.Add(ds.Tables[0].Rows[i][2].ToString());
13
}
14
return items;
15
}
GetSearchItems方法返回一个ArrayList对象,它将包含所有以用户输入字符串的员工名字。
2

3

4

5

6

7

8

9

10

11

12

13

14

15

四、客户端实现
相对于服务器端的方法而言,客户端的处理要复杂得多。首先来分析如何根据服务器端返回的ArrayList对象展示结果。这里用到了Web编程中“层”(div)的概念,通过JavaScript和DOM创建一个新的层div,将ArrayList中的每一个条目都作为其子节点加入到div中,而每一个条目也被看作是一个div,其中具体的文本内容则是一个span对象。
除了显示待选的结果之外,下拉区域还要对键盘、鼠标事件做出响应;为了实时地显示待选结果,还需要定时更新待选结果的列表。这些功能都封装在lookup.js中。下面是lookeu.js的定义:
1
// 下拉区背景色
2
var DIV_BG_COLOR = "#EEE";
3
// 高亮显示条目颜色
4
var DIV_HIGHLIGHT_COLOR = "#C30";
5
// 字体
6
var DIV_FONT = "Arial";
7
// 下拉区内补丁大小
8
var DIV_PADDING = "2px";
9
// 下拉区边框样式
10
var DIV_BORDER = "1px solid #CCC";
11![]()
12![]()
13
// 文本输入框
14
var queryField;
15
// 下拉区id
16
var divName;
17
// IFrame名称
18
var ifName;
19
// 记录上次选择的值
20
var lastVal = "";
21
// 当前选择的值
22
var val = "";
23
// 显示结果的下拉区
24
var globalDiv;
25
// 下拉区是否设置格式的标记
26
var divFormatted = false;
27![]()
28
/**
29
InitQueryCode函数必须在<body onload>事件的响应函数中调用,其中:
30
queryFieldName为文本框控件的id,
31
hiddenDivName为显示下拉区div的id
32
*/
33
function InitQueryCode (queryFieldName, hiddenDivName)
34
{
35
// 指定文本输入框的onblur和onkeydown响应函数
36
queryField = document.getElementById(queryFieldName);
37
queryField.onblur = hideDiv;
38
queryField.onkeydown = keypressHandler;
39![]()
40
// 设置queryField的autocomplete属性为"off"
41
queryField.autocomplete = "off";
42![]()
43
// 如果没有指定hiddenDivName,取默认值"querydiv"
44
if (hiddenDivName)
45
{
46
divName = hiddenDivName;
47
}
48
else
49
{
50
divName = "querydiv";
51
}
52
53
// IFrame的name
54
ifName = "queryiframe";
55
56
// 100ms后调用mainLoop函数
57
setTimeout("mainLoop()", 100);
58
}
59![]()
60
/**
61
获取下拉区的div,如果没有则创建之
62
*/
63
function getDiv (divID)
64
{
65
if (!globalDiv)
66
{
67
// 如果div在页面中不存在,创建一个新的div
68
69
if (!document.getElementById(divID))
70
{
71
var newNode = document.createElement("div");
72
newNode.setAttribute("id", divID);
73
document.body.appendChild(newNode);
74
}
75![]()
76
// globalDiv设置为div的引用
77
globalDiv = document.getElementById(divID);
78![]()
79
// 计算div左上角的位置
80
var x = queryField.offsetLeft;
81
var y = queryField.offsetTop + queryField.offsetHeight;
82
var parent = queryField;
83
while (parent.offsetParent)
84
{
85
parent = parent.offsetParent;
86
x += parent.offsetLeft;
87
y += parent.offsetTop;
88
}
89![]()
90
// 如果没有对div设置格式,则为其设置相应的显示样式
91
if (!divFormatted)
92
{
93
globalDiv.style.backgroundColor = DIV_BG_COLOR;
94
globalDiv.style.fontFamily = DIV_FONT;
95
globalDiv.style.padding = DIV_PADDING;
96
globalDiv.style.border = DIV_BORDER;
97
globalDiv.style.width = "100px";
98
globalDiv.style.fontSize = "90%";
99![]()
100
globalDiv.style.position = "absolute";
101
globalDiv.style.left = x + "px";
102
globalDiv.style.top = y + "px";
103
globalDiv.style.visibility = "hidden";
104
globalDiv.style.zIndex = 10000;
105![]()
106
divFormatted = true;
107
}
108
}
109![]()
110
return globalDiv;
111
}
112![]()
113
/**
114
根据返回的结果集显示下拉区
115
*/
116
function showQueryDiv(resultArray)
117
{
118
// 获取div的引用
119
var div = getDiv(divName);
120
121
// 如果div中有内容,则删除之
122
while (div.childNodes.length > 0)
123
div.removeChild(div.childNodes[0]);
124![]()
125
// 依次添加结果
126
for (var i = 0; i < resultArray.length; i++)
127
{
128
// 每一个结果也是一个div
129
var result = document.createElement("div");
130
// 设置结果div的显示样式
131
result.style.cursor = "pointer";
132
result.style.padding = "2px 0px 2px 0px";
133
// 设置为未选中
134
_unhighlightResult(result);
135
// 设置鼠标移进、移出等事件响应函数
136
result.onmousedown = selectResult;
137
result.onmouseover = highlightResult;
138
result.onmouseout = unhighlightResult;
139![]()
140
// 结果的文本是一个span
141
var result1 = document.createElement("span");
142
// 设置文本span的显示样式
143
result1.className = "result1";
144
result1.style.textAlign = "left";
145
result1.style.fontWeight = "bold";
146
result1.innerHTML = resultArray[i];
147
148
// 将span添加为结果div的子节点
149
result.appendChild(result1);
150
151
// 将结果div添加为下拉区的子节点
152
div.appendChild(result);
153
}
154![]()
155
// 如果结果集不为空,则显示,否则不显示
156
showDiv(resultArray.length > 0);
157
}
158![]()
159
/**
160
用户点击某个结果时,将文本框的内容替换为结果的文本,
161
并隐藏下拉区
162
*/
163
function selectResult()
164
{
165
_selectResult(this);
166
}
167![]()
168
// 选择一个条目
169
function _selectResult(item)
170
{
171
var spans = item.getElementsByTagName("span");
172
if (spans)
173
{
174
for (var i = 0; i < spans.length; i++)
175
{
176
if (spans[i].className == "result1")
177
{
178
queryField.value = spans[i].innerHTML;
179
lastVal = val = escape(queryField.value);
180
mainLoop();
181
queryField.focus();
182
showDiv(false);
183
return;
184
}
185
}
186
}
187
}
188![]()
189
/**
190
当鼠标移到某个条目之上时,高亮显示该条目
191
*/
192
function highlightResult()
193
{
194
_highlightResult(this);
195
}
196![]()
197
function _highlightResult(item)
198
{
199
item.style.backgroundColor = DIV_HIGHLIGHT_COLOR;
200
}
201![]()
202
/**
203
当鼠标移出某个条目时,正常显示该条目
204
*/
205
function unhighlightResult()
206
{
207
_unhighlightResult(this);
208
}
209![]()
210
function _unhighlightResult(item)
211
{
212
item.style.backgroundColor = DIV_BG_COLOR;
213
}
214![]()
215
/**
216
显示/不显示下拉区
217
*/
218
function showDiv (show)
219
{
220
var div = getDiv(divName);
221
if (show)
222
{
223
div.style.visibility = "visible";
224
}
225
else
226
{
227
div.style.visibility = "hidden";
228
}
229
//adjustiFrame();
230
}
231![]()
232
/**
233
隐藏下拉区
234
*/
235
function hideDiv ()
236
{
237
showDiv(false);
238
}
239![]()
240
/**
241
调整IFrame的位置,这是为了解决div可能会显示在输入框后面的问题
242
*/
243
function adjustiFrame()
244
{
245
// 如果没有IFrame,则创建之
246
if (!document.getElementById(ifName))
247
{
248
var newNode = document.createElement("iFrame");
249
newNode.setAttribute("id", ifName);
250
newNode.setAttribute("src", "javascript:false;");
251
newNode.setAttribute("scrolling", "no");
252
newNode.setAttribute("frameborder", "0");
253
document.body.appendChild(newNode);
254
}
255![]()
256
iFrameDiv = document.getElementById(ifName);
257
var div = getDiv(divName);
258![]()
259
// 调整IFrame的位置与div重合,并在div的下一层
260
try
261
{
262
iFrameDiv.style.position = "absolute";
263
iFrameDiv.style.width = div.offsetWidth;
264
iFrameDiv.style.height = div.offsetHeight;
265
iFrameDiv.style.top = div.style.top;
266
iFrameDiv.style.left = div.style.left;
267
iFrameDiv.style.zIndex = div.style.zIndex - 1;
268
iFrameDiv.style.visibility = div.style.visibility;
269
}
270
catch (e)
271
{
272
}
273
}
274![]()
275
/**
276
文本输入框的onkeydown响应函数
277
*/
278
function keypressHandler (evt)
279
{
280
// 获取对下拉区的引用
281
var div = getDiv(divName);
282
283
// 如果下拉区不显示,则什么也不做
284
if (div.style.visibility == "hidden")
285
{
286
return true;
287
}
288![]()
289
// 确保evt是一个有效的事件
290
if (!evt && window.event)
291
{
292
evt = window.event;
293
}
294
var key = evt.keyCode;
295![]()
296
var KEYUP = 38;
297
var KEYDOWN = 40;
298
var KEYENTER = 13;
299
var KEYTAB = 9;
300
301
// 只处理上下键、回车键和Tab键的响应
302
if ((key != KEYUP) && (key != KEYDOWN) && (key != KEYENTER) && (key != KEYTAB))
303
{
304
return true;
305
}
306![]()
307
var selNum = getSelectedSpanNum(div);
308
var selSpan = setSelectedSpan(div, selNum);
309
310
// 如果键入回车和Tab,则选择当前选择条目
311
if ((key == KEYENTER) || (key == KEYTAB))
312
{
313
if (selSpan)
314
{
315
_selectResult(selSpan);
316
}
317
evt.cancelBubble = true;
318
return false;
319
}
320
else //如果键入上下键,则上下移动选中条目
321
{
322
if (key == KEYUP)
323
{
324
selSpan = setSelectedSpan(div, selNum - 1);
325
}
326
if (key == KEYDOWN)
327
{
328
selSpan = setSelectedSpan(div, selNum + 1);
329
}
330
if (selSpan)
331
{
332
_highlightResult(selSpan);
333
}
334
}
335![]()
336
// 显示下拉区
337
showDiv(true);
338
return true;
339
}
340![]()
341
/**
342
获取当前选中的条目的序号
343
*/
344
function getSelectedSpanNum(div)
345
{
346
var count = -1;
347
var spans = div.getElementsByTagName("div");
348
if (spans)
349
{
350
for (var i = 0; i < spans.length; i++)
351
{
352
count++;
353
if (spans[i].style.backgroundColor != div.style.backgroundColor)
354
{
355
return count;
356
}
357
}
358
}
359![]()
360
return -1;
361
}
362![]()
363
/**
364
选择指定序号的结果条目
365
*/
366
function setSelectedSpan(div, spanNum)
367
{
368
var count = -1;
369
var thisSpan;
370
var spans = div.getElementsByTagName("div");
371
if (spans)
372
{
373
for (var i = 0; i < spans.length; i++)
374
{
375
if (++count == spanNum)
376
{
377
_highlightResult(spans[i]);
378
thisSpan = spans[i];
379
}
380
else
381
{
382
_unhighlightResult(spans[i]);
383
}
384
}
385
}
386![]()
387
return thisSpan;
388
}
389![]()
InitQueryCode函数必须在页面的onload响应中执行,该函数最后调用setTimeout方法执行了mainLoop方法。注意,mainLoop方法并没有在lookup.js中定义,必须在包含lookup.js文件的页面文件中增加该函数的定义。于此,我们就需要在Default.aspx页面上加入如下定义:
2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

1
<script language="javascript" src="lookup.js"></script>
2
<script language="javascript">
3
mainLoop = function()
4
{
5
val = escape(queryField.value);
6
if (lastVal != val)
7
{
8
var response = _Default.GetSearchItems(val);
9
showQueryDiv(response.value);
10
lastVal = val;
11
}
12
setTimeout('mainLoop()', 100);
13
return true;
14
}
15
</script>
由上述代码可以看到mainLoop函数每隔100ms会执行一次,它会判断当前文本输入框的值和上次提交查询的值是否相同,如果不同,它会重新向服务器发送请求进行查询,并且更新下拉区域的显示。
2

3

4

5

6

7

8

9

10

11

12

13

14

15

于此,一个基于 Ajax的自动完成功能就实现了。
本文借鉴于《ajax web2.0快速入门与项目实践》。
这本书上还使用了控件将该功能进行了封装,这样要实现Ajax的自动完成功能就更加方便了。在此就不做过多解说。
本文示例代码下载:AutoComplete.rar
---------------------------------------------------------------------------------------------------------