Asp.Net MVC 扩展联想控件
在web中,为改善用户体验,我们常会将一些文本输入框做成智能联想,以让用户更快更准确的输入内容。大概是这样的:当用户开始在文本框输入时,客户端脚本ajax向服务端发起请求,服务端从数据库读取返回数据,客户端解析数据附加在文本框的下拉div中供用户选择参考。
在MVC中我们可以通过扩展HtmlHelper来封装自己写的控件,以便在整个项目中像使用 Html.TextBox("") 一样来使用自定义控件。
扩展代码如下
View Code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Mvc; using System.Web.Mvc.Html; namespace HtmlHelperExt { public static class SuggestBoxExtensions { #region SuggestBox 联想控件 /// <summary> /// 联想控件 /// </summary> /// <param name="htmlHelper"></param> /// <param name="name">name(id)</param> /// <param name="value">value</param> /// <param name="controller">controller</param> /// <param name="action">action</param> /// <param name="action">fieldName 要在下拉框显示的DataTable中的字段名</param> /// <param name="action">callBack 当选择值后的回调脚本函数</param> /// <param name="htmlAttributes"></param> /// <returns></returns> public static string SuggestBox(this HtmlHelper htmlHelper, string name, object value, string controller, string action,string fieldName,string callBack,IDictionary<string, object> htmlAttributes) { return htmlHelper.SuggestBox(name, value, controller, action,"", fieldName, fieldName, "", "", "",callBack, htmlAttributes); } /// <summary> /// 联想控件 /// </summary> /// <param name="htmlHelper"></param> /// <param name="name">name(id)</param> /// <param name="value">value</param> /// <param name="controller">controller</param> /// <param name="action">action</param> /// <param name="headerText">下拉选框的头部文字(要显示多列用 ';'隔开)</param> /// <param name="displayFields">要在下拉框显示的DataTable中的字段名(要显示多列用 ';'隔开)</param> /// <param name="valueField">要赋文本框的字段(只能是一个,且包含在displayFields中)</param> /// <param name="action">callBack 当选择值后的回调脚本函数</param> /// <param name="htmlAttributes"></param> /// <returns></returns> public static string SuggestBox(this HtmlHelper htmlHelper, string name, object value, string controller, string action, string headerText, string displayFields, string valueField, string callBack, IDictionary<string, object> htmlAttributes) { return htmlHelper.SuggestBox(name, value, controller, action, headerText, displayFields, valueField, "", "", "",callBack, htmlAttributes); } /// <summary> /// 联想控件 /// </summary> /// <param name="htmlHelper"></param> /// <param name="name">name(id)</param> /// <param name="value">value</param> /// <param name="controller">controller</param> /// <param name="action">action</param> /// <param name="headerText">下拉选框的头部文字(要显示多列用 ';'隔开)</param> /// <param name="displayFields">要在下拉框显示的DataTable中的字段名(要显示多列用 ';'隔开)</param> /// <param name="valueField">要赋文本框的字段(只能是一个,且包含在displayFields中)</param> /// <param name="keyField">选择行的主键</param> /// <param name="keyTextBoxName">将主键值保存在以此命名的隐藏的文本控件中,可供其他地方使用</param> /// <param name="keyTextBoxValue">初始化时主键文本控件中的值</param> /// <param name="htmlAttributes"></param> /// <returns></returns> public static string SuggestBox(this HtmlHelper htmlHelper, string name, object value, string controller, string action, string headerText, string displayFields, string valueField, string keyField, string keyTextBoxName, string keyTextBoxValue,string callBack,IDictionary<string, object> htmlAttributes) { var sb = new StringBuilder(); if (htmlAttributes == null) htmlAttributes = new Dictionary<string, object>(); string styleStr = ""; if (htmlAttributes.ContainsKey("style")) styleStr = htmlAttributes["style"].ToString(); string boxId = name.ToUpper() + "_SUGBOX"; if (styleStr.Length > 0) sb.Append(htmlHelper.TextBox(name, value, new { style = styleStr, autocomplete = "off" })); else sb.Append(htmlHelper.TextBox(name, value, new { autocomplete = "off" })); sb.Append("<script type=\"text/javascript\">"); sb.AppendFormat("$('{0}').suggest({{boxId:'{1}',controller:'{2}',action:'{3}',headerText:'{4}',displayFields:'{5}',valueField:'{6}',keyField:'{7}',keyTextBoxName:'{8}',callBack:'{9}'}})", "#" + name, boxId, controller, action, headerText, displayFields, valueField, keyField, keyTextBoxName,callBack); sb.Append("</script>"); if (keyTextBoxName != "") { sb.Append(htmlHelper.Hidden(keyTextBoxName, keyTextBoxValue)); } return sb.ToString(); } #endregion } }
通过Controller读取、解析、返回数据。将从数据库(或XML)读取的数据存入DataTable,然后转换为Json字符串再返回给客户端。本Demo中模拟数据在XML文件中。
Controller代码如下:
View Code
public class SuggestBoxController : Controller { public ActionResult Demo() { return View(); } public string Suggest() { string searchText = ""; if (Request["param"] == null) { return ""; } searchText = Request["param"].ToString(); DataSet ds = new DataSet(); ds.ReadXml(Server.MapPath("~/KeyWords.xml")); DataRow[] drs = ds.Tables[0].Select("name like '%" + searchText + "%'"); DataTable dt = new DataTable(); dt.Columns.AddRange(new DataColumn[] { new DataColumn("id"), new DataColumn("name") }); int len = drs.Length; for (int i = 0; i < len; i++) { DataRow dr = dt.NewRow(); dr[0] = drs[i][0]; dr[1] = drs[i][1]; dt.Rows.Add(dr); } return CreateJsonStr(dt); } #region CreateJsonStr /// <summary> /// 将DataTable数据转换为Json字符串 /// </summary> /// <param name="dt"></param> /// <returns></returns> public string CreateJsonStr(DataTable dt) { StringBuilder JsonString = new StringBuilder(); JsonString.Append("{ "); JsonString.Append("\"Data\":[ "); if (dt != null && dt.Rows.Count > 0) { for (int i = 0; i < dt.Rows.Count; i++) { JsonString.Append("{ "); for (int j = 0; j < dt.Columns.Count; j++) { if (j < dt.Columns.Count - 1) { JsonString.Append("\"" + dt.Columns[j].ColumnName.ToString() + "\":" + "\"" + dt.Rows[i][j].ToString() + "\","); } else if (j == dt.Columns.Count - 1) { JsonString.Append("\"" + dt.Columns[j].ColumnName.ToString() + "\":" + "\"" + dt.Rows[i][j].ToString() + "\""); } } if (i == dt.Rows.Count - 1) { JsonString.Append("} "); } else { JsonString.Append("}, "); } } } JsonString.Append("]}"); return JsonString.ToString(); } #endregion }
主要核心还是在客户端的脚本中,脚本通过ajax访问服务端,并加载绑定返回数据,响应反馈用户的操作。
View Code
(function($) { var itemIndex = 0; $.fn.suggest = function(options) { var params = { boxId: "suggestBox", boxWidth: 250, boxHeight: 200, controller: "", action: "", headerText: "", displayFields: "", valueField: "", keyField: "", keyTextBoxName: "", callBack: "" }; var ops = $.extend(params, options); var headerTextArr = new Array(); var displayFieldsArr = new Array(); headerTextArr = ops.headerText.split(';'); displayFieldsArr = ops.displayFields.split(';'); var headerStr = ""; var headerLen = headerTextArr.length; if (headerLen == 1 || headerLen == 0) { var textBox = $(this); ops.boxWidth = textBox.css("width"); } var box = ''; if (ops.headerText.length == 0) { box = '<div id="' + ops.boxId + '" style="display:none;width:' + ops.boxWidth + ';height:' + ops.boxHeight + '"><ul class="suggestBoxItems"></ul></div>'; } else { for (var i = 0; i < headerLen; i++) { if (i == headerLen - 1) { headerStr += '<span class="headerTextShort">' + headerTextArr[i] + '</span>' } else { headerStr += '<span class="headerTextLong">' + headerTextArr[i] + '</span>' } } box = '<div id="' + ops.boxId + '" style="display:none;width:' + ops.boxWidth + ';height:' + ops.boxHeight + '"><div class = "headerText">' + headerStr + '</div><ul class="suggestBoxItems"></ul></div>'; } $(this).after(box); var itemCount = 0; $(this).bind('keyup', function(e) { var value = $.trim($(this).val()); if (value.length >= 1) { var position = $(this).position(); $('#' + ops.boxId).css({ 'display': 'block', 'background': 'white', 'color': 'black', 'position': 'absolute', 'border': "1px solid #D5D5D5", 'left': position.left, 'top': position.top + 22 }); var pVal = $(this).val() + ""; if (pVal.search('&') >= 0) { pVal = pVal.replace('&', '%26'); } if (e.keyCode != 38 && e.keyCode != 40 && e.keyCode != 13 && e.keyCode != 9) { var sugTextBox = $(this); var dataUrl = "/" + ops.controller + "/" + ops.action; if (pVal != "") { $.ajax({ type: "post", async: true, url: dataUrl, data: "param=" + pVal, dataType: "json", cache: false, timeout: 5000, beforeSend: loading(ops.boxId), error: function(XMLHttpRequest, textStatus, errorThrown) { alert(textStatus); $('#' + ops.boxId).slideUp("slow"); $('#' + ops.boxId + ' ul').html(''); }, success: function(data) { initBox(ops.boxId, sugTextBox, data, displayFieldsArr, ops.valueField, ops.keyField, ops.keyTextBoxName); } }); } itemIndex = 0; } var itemCount = $('#' + ops.boxId + ' ul li').length; switch (e.keyCode) { case 38: if (itemIndex == 0) { itemIndex = itemCount + 1; } if (itemIndex > 1) { $('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').css({ 'background': 'white', 'color': 'black' }); itemIndex--; } $('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').css({ 'background': '#7AADEB', 'color': 'white' }); $(this).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('font').text()); if (ops.keyTextBoxName != "") { $('#' + ops.keyTextBoxName).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('div').text()); } break; case 40: if (itemIndex < itemCount) { $('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').css({ 'background': 'white', 'color': 'black' }); itemIndex++; } $('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').css({ 'background': '#7AADEB', 'color': 'white' }); $(this).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('font').text()); if (ops.keyTextBoxName != "") { $('#' + ops.keyTextBoxName).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('div').text()); } break; case 13: if (itemIndex > 0 && itemIndex <= itemCount) { $(this).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('font').text()); if (ops.keyTextBoxName != "") { $('#' + ops.keyTextBoxName).val($('#' + ops.boxId + ' ul li:nth-child(' + itemIndex + ')').find('div').text()); } $('#' + ops.boxId).slideUp("fast"); $('#' + ops.boxId + ' ul').html(''); eval(ops.callBack); } break; default: break; } } else { $('#' + ops.boxId).slideUp("fast"); $('#' + ops.boxId + ' ul').html(''); } }); $(this).blur(function() { var tempLi = $('#' + ops.boxId + ' ul li:nth-child(1)'); if (itemIndex == 0 && tempLi != undefined) { $(this).val(tempLi.find('font').text()); if (ops.keyTextBoxName != "") { $('#' + ops.keyTextBoxName).val(tempLi.find('div').text()); } itemIndex = 1; } if ($('#' + ops.boxId + ' ul').html() != '') { eval(ops.callBack); } $('#' + ops.boxId).slideUp("fast"); $('#' + ops.boxId + ' ul').html(''); }); }; function loading(boxId) { $('#' + boxId + ' ul').html('<img alt="loading" src="/Scripts/SuggestBox/loading.gif"/>'); } function initBox(boxId, obj, data, displayFieldsArr, valueField, keyField, keyTextBoxName) { var str = ""; if (data == undefined || data.Data == undefined || data.Data.length == 0) { $('#' + boxId + ' ul').html('<div class="noRecordsTip">No records found<div>'); } else { for (var i = 0; i < data.Data.length; i++) { var fieldStr = ""; for (var j = 0; j < displayFieldsArr.length; j++) { if (displayFieldsArr[j] == valueField) { if (j == 0 || j != displayFieldsArr.length - 1) { fieldStr += "<font class='singleField'>" + data.Data[i][displayFieldsArr[j]] + "</font>"; } else { fieldStr += "<font>" + data.Data[i][displayFieldsArr[j]] + "</font>"; } } else { var tempValue = data.Data[i][displayFieldsArr[j]]; if (tempValue.length > 16) { tempValue = tempValue.substr(0, 16) + "..."; } fieldStr += "<span class='commonFields'>" + tempValue + "</span>"; } } if (keyField != "") { fieldStr += "<div style = 'display:none;'>" + data.Data[i][keyField] + "</div>"; } str += "<li>" + fieldStr + "</li>"; } $('#' + boxId + ' ul').html(str); } if (data != undefined && data.Data != undefined && data.Data.length == 1) { var tempLi = $('#' + boxId + ' ul li'); obj.val(tempLi.find('font').text()); if (keyTextBoxName != "") { $('#' + keyTextBoxName).val(tempLi.find('div').text()); } itemIndex = 1; } $('#' + boxId + ' ul li').each(function() { $(this).bind('click', function() { obj.val($(this).find('font').text()); if (keyTextBoxName != "") { $('#' + keyTextBoxName).val($(this).find('div').text()); } eval(ops.callBack); $('#' + boxId).slideUp("fast"); }); }); $('#' + boxId + ' ul li').each(function() { $(this).hover( function() { $('#' + boxId + ' ul li:nth-child(' + itemIndex + ')').css({ 'background': 'white', 'color': 'black' }); itemIndex = $('#' + boxId + ' ul li').index($(this)[0]) + 1; $(this).css({ 'background': '#7AADEB', 'color': 'white' }); obj.val($(this).find('font').text()); if (keyTextBoxName != "") { $('#' + keyTextBoxName).val($(this).find('div').text()); } }, function() { $(this).css({ 'background': 'white', 'color': 'black' }); } ); }); }; })(jQuery);
在View里面需要Import我们写的扩展类所在的命名空间,<%@ Import Namespace="HtmlHelperExt" %>
以及引入相关的js、css(extension.suggestbox.js 和 jquery-1.4.1.js 和 SugBoxStyle.css)
View Code
<div> <% IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(); htmlAttributes.Add("style", "width:198px"); %> 简单单列 <%=Html.SuggestBox("MySuggestBox01", "", "SuggestBox", "Suggest","name","", htmlAttributes)%> <br /><br /><br /><br /> 带表头单列 <%=Html.SuggestBox("MySuggestBox02", "", "SuggestBox", "Suggest","关键字","name","name","", htmlAttributes)%> <br /><br /><br /><br /> 带表头双列 <%=Html.SuggestBox("MySuggestBox03", "", "SuggestBox", "Suggest","编号;关键字","id;name","name","", htmlAttributes)%> <br /><br /><br /><br /> 带表头双列+回调函数 <%=Html.SuggestBox("MySuggestBox04", "", "SuggestBox", "Suggest", "编号;关键字", "id;name", "name", "id", "MySuggestBox04_ID", "0", "afterSelect()", htmlAttributes)%> <span id="tip"></span> <script type="text/javascript"> var id = document.getElementById("MySuggestBox04_ID").value; document.getElementById("tip").innerHTML = "当前选择的编号是: <font color='red'>" + id + "</font>"; function afterSelect() { var id = document.getElementById("MySuggestBox04_ID").value; document.getElementById("tip").innerHTML = "当前选择的编号是: <font color='red'>"+id+"</font>"; } </script> </div>
结果演示一:简单单列
结果演示二:带表头单列
结果演示三:带表头双列
结果演示四:带表头双列+回调函数(选择一值时将key值赋给指定的Hidden中)
初写博客,还请大牛们多多指教