支持 AJAX 的名称选择器用户控件





5.00/5 (37投票s)
一个完全支持 AJAX 的用户控件,用于以下拉格式从数据库中选择姓名。
- 下载源代码 - 33.6 KB
(你需要配置 web.config 才能使此示例正常工作。)
引言
我越是使用 Facebook 这样的网站,就越对它们使用的控件着迷。一个让我着迷的控件是用来查找其他用户的控件。当你输入时,一个格式非常好的下拉列表会显示带有其他信息的姓名。选择其中一个姓名会触发更多的服务器端工作。本文是我尝试使用 Microsoft 技术栈开发类似体验的尝试:SQL 用于数据库,ASP.NET 2.0 用于代码,AJAX 控件工具包用于增强效果,以及一点 JavaScript,因为这是不可避免的。
背景
我以前尝试过这种类型的控件,但对结果非常不满意。我过去尝试中最大的问题是它使用了 **CALLBACK** 技术,该技术严重依赖 JavaScript 并通过网络发送文本。这种方法有很多问题……虽然不可否认它更精简,但提供的控制比我想要的要少。相反,我想要一种方法,让用户体验感觉是在客户端,但实际上我能够在服务器上进行操作并提供有意义且格式良好的结果。我唯一能想到的实现这一目标的方法是使用 **异步回发 (Asynchronous Postbacks)**。然而,使用回发的最大缺点是控件在回发(异步或不异步!)后往往会失去焦点,这对于用户期望能够自由输入而无需重新选择文本框的文本框来说是灾难性的,所以我还需要克服这一点。
此控件的另一个目标是使其在我的应用程序中完全可移植。此控件设计用于放置在我的应用程序的任何页面上的任何位置,无需任何代码配置。
要点 #1 - 数据库部分
当然,这样的搜索可以在很多场景下工作。本文附带的示例侧重于人物。在 SQL Server 2005 中,我有三个表。
- 一个 Person 表,包含 Firstname、Lastname、JobTitleID 和 LocationID。
- 一个 JobTitle 表,包含 JobTitleID 和 JobTitle。
- 一个 Location 表,包含 LocationID 和 Location。
然后,我使用以下存储过程来搜索匹配的人。此存储过程中重要的部分是 `WHERE
` 子句。我通过名字、或者姓氏、或者名字空格姓氏、或者姓氏逗号空格名字来匹配用户输入的字符串。
CREATE PROCEDURE usp_searchPeople (@namecontains nvarchar(20))
AS
SELECT
P.FirstName,
P.LastName,
J.JobTitle,
L.Location
FROM
Person as P
INNER JOIN JobTitle as J on P.JobTitleID = J.JobTitleID
INNER JOIN Location as L on P.LocationID = L.LocationID
WHERE
@namecontains <> ''
AND
P.LastName like N'%' + @namecontains + '%'
OR P.Firstname like N'%' + @namecontains + '%'
or ((P.LastName + ', ' + P.FirstName) like N'%' + @namecontains + '%')
OR ((P.FirstName + ' ' + P.LastName) like N'%' + @namecontains + '%')
ORDER BY
P.Lastname, P.FirstName
要点 #2 - 类
我非常喜欢在设计器页面上使用 `ObjectDataSource`。使用它们会迫使你将数据库交互抽象成类,这在面向对象的编程世界中是件好事。所以,这是我的两个 People 类:一个是 `Person` 类,而 `People` 类负责获取数据并返回 `DataSet`(这是我们希望 `ObjectDataSource` 能够检索并最终填充 `GridView` 的要求)。这些类没什么突破性的。但是,在 `People` 类中,我使用了 `Application.Data` DLL 来从 SQL Server 获取数据。如果你不熟悉……你真的应该了解。它非常好。另外,我在 `Web.Config` 中设置了数据库连接(如果你要测试这个,你需要调整一下)。
Public Class Person
Private _Firstname As String
Public Property Firstname() As String
Get
Return _Firstname
End Get
Set(ByVal value As String)
_Firstname = value
End Set
End Property
Private _Lastname As String
Public Property Lastname() As String
Get
Return _Lastname
End Get
Set(ByVal value As String)
_Lastname = value
End Set
End Property
Private _JobTitle As String
Public Property JobTitle() As String
Get
Return _JobTitle
End Get
Set(ByVal value As String)
_JobTitle = value
End Set
End Property
Private _Location As String
Public Property Location() As String
Get
Return _Location
End Get
Set(ByVal value As String)
_Location = value
End Set
End Property
End Class
Imports Microsoft.ApplicationBlocks.Data
Public Class People
Public Function SearchPeople(ByVal searchstr As String) As DataSet
Dim strSQL As String = "usp_SearchPeople"
Dim params(0) As SqlClient.SqlParameter
params(0) = New SqlClient.SqlParameter("@namecontains", searchstr)
Try
PeopleDS = SqlHelper.ExecuteDataset(_
ConfigurationManager.AppSettings("MyDataBase"), _
CommandType.StoredProcedure, strSQL, params)
Catch ex As Exception
_ErrMsg = ex.Message
End Try
Return PeopleDS
End Function
Private _ErrMsg As String
Public Property ErrMsg() As String
Get
Return _ErrMsg
End Get
Set(ByVal value As String)
_ErrMsg = value
End Set
End Property
Private _PeopleDS As DataSet = New DataSet()
Public Property PeopleDS() As DataSet
Get
Return _PeopleDS
End Get
Set(ByVal value As DataSet)
_PeopleDS = value
End Set
End Property
Private _Person As Person
Public Property Person() As Person
Get
Return _Person
End Get
Set(ByVal value As Person)
_Person = value
End Set
End Property
End Class
要点 #3 - 用户控件设计器页面
到目前为止,我们已经有了创建用户控件设计器页面的足够信息。在我们完成之前,页面上会有很多东西,但以下是重要部分:
- 搜索框 - 一个用户将输入文本的框,附带一些 JavaScript 引用。我将在第 4 部分讨论这些函数。
<asp:TextBox runat="server" ID="txtSearchStr"
Width="260" style="border:none;"
onfocus="startSearching(this.id)" onblur="endSearching()" />
<asp:UpdatePanel runat="server" ID="upPersonSearch" UpdateMode="conditional">
<Triggers>
<asp:AsyncPostBackTrigger ControlID="txtSearchStr" EventName="TextChanged" />
</Triggers>
<ContentTemplate>
<asp:ObjectDataSource runat="server" ID="odsPeopleList"
TypeName="ASPNETPersonSearch.People" SelectMethod="SearchPeople">
<SelectParameters>
<asp:ControlParameter ControlID="txtSearchStr"
Name="namecontains" DefaultValue="@@##$$" />
</SelectParameters>
</asp:ObjectDataSource>
<asp:GridView runat="server" ID="gvPeopleList"
DataSourceID="odsPeopleList" AutoGenerateColumns="false"
ShowFooter="false" ShowHeader="False"
DataKeyNames="PeopleID" >
<Columns>
<asp:TemplateField>
<ItemTemplate>
<div>
<asp:LinkButton runat="server"
style="text-decoration:none;" ID="btn1"
SkinID="plain" CommandName="Select" />
</div>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<EmptyDataTemplate>
<asp:Label runat="server" ID="lbl1" />
</EmptyDataTemplate>
</asp:GridView>
让我们在此停止并检查我们所拥有的。在页面上,我们有一个 `TextBox`、一个 `UpdatePanel`、一个 `ObjectDataSource` 和一个网格。我们可以像这样运行页面,它也会……在某种程度上奏效。我需要克服的最大问题是 `TextBox` 的 `TextChanged` 属性 **仅在** 用户按下 <Enter> 或 <Tab> 键时 **才触发**。但那不是我想要的。我希望它在我键入时触发。唉……JavaScript。
要点 #4 - JavaScript
要实现那种即时键入并看到结果的感觉,你需要某种 JavaScript。我的第一个想法是使用 `OnKeyUp` 或 `OnKeyPress` JavaScript 函数……事实上,我最初也是这样尝试的。但有一个问题。如果一个人输入速度很快,并且你触发了回发和数据更新,那么用户输入的字符和提交的搜索字符串之间会有延迟。你可以将所有 JavaScript 包装在一个计时器函数中,该函数跟踪按键之间的时间,如果用户继续输入,则重置时钟……但随着我对该方法的测试越多,它的工作方式就越奇怪。这时我找到了以下方法:
- 在文本控件的 `OnFocus` 时启动某个函数,并在 `OnBlur` 时停止它。
- 然后,不是响应用户的按键,而是每秒钟左右触发一次回发。
当然,这可能看起来有点过分,所以我设置了一个变量来保存上次使用的搜索值。如果它与搜索框中的内容匹配(也就是说,用户输入了一个值,搜索已触发,并且他们在那儿坐了 20 秒来查看姓名),JavaScript 就会跳过回发,为我们节省一次与服务器的往返。
以下是 JavaScript 的要点:
- 全局变量 - 在函数中用作引用。实际上,我并没有将 `prm` 变量放在项目中的 `PeopleSearch.js` 文件里。相反,我把它放在用户控件设计器页面上。我还将其包装在检查它是否已存在的逻辑中——无论是为了其他目的而创建的,还是因为我在同一页面上放置了两个这样的用户控件。这是因为我需要引用已渲染页面的 DOM 中的某个内容,而不是创建一个新对象。
//global variables defined in the PeopleSearch.vb file
var intervalTimerId = 0;
var currenttext = "";
//script reference at the top of the PeopleSearch.ascx page
<script language="javascript" type="text/javascript">
//in case there are two User controls on the same page,
//we only set this variable once
if(!prm) {var prm = Sys.WebForms.PageRequestManager.getInstance();}
</script>
this.id
`)。这对于能够在任何地方以及在同一页面上多次重用此控件至关重要。function startSearching(cid) {
intervalTimerId = setInterval ( "doPostBackAsync( '" + cid + "', '' )", 1000 );
}
function endSearching() {
clearInterval (intervalTimerId) ;
}
此函数中的真正亮点是以下代码:
prm._asyncPostBackControlClientIDs.push(eventName);
我通过谷歌搜索发现了这个,它很天才。这就是如何从 JavaScript 触发 `UpdatePanel`。`.push` 方法模拟了 `__doPostBack` 事件。这是完整的代码:
function doPostBackAsync( eventName, eventArgs )
{ var tbox = document.getElementById(eventName);
if(tbox.value.length > 2 && tbox.value != currenttext )
{
if( !Array.contains( prm._asyncPostBackControlIDs, eventName) )
{prm._asyncPostBackControlIDs.push(eventName);}
if( !Array.contains( prm._asyncPostBackControlClientIDs, eventName) )
{
prm._asyncPostBackControlClientIDs.push(eventName);
}
__doPostBack( eventName, eventArgs );
currenttext = tbox.value;
}
}
好的,我们快完成了。最后一部分是注册所有这些 JavaScript。我选择在控件的设计器页面上使用 `ScriptManagerProxy`。这允许你在需要的地方引用和注册 `.js` 文件的函数,而不是将引用放在使用页面(`.aspx` 页面)上,或者更糟糕的是,放在母版页中。
<asp:ScriptManagerProxy runat="server" ID="sm1">
<Scripts>
<asp:ScriptReference Path="~/js/EmployeeSearch.js" />
</Scripts>
</asp:ScriptManagerProxy>
要点 #5 - 后台代码 (Code-Behind)
现在我们已经做了所有这些工作来允许我们在服务器端控制事物……让我们看看我们可以做什么。首先,由于这是一个用户控件,所以可以合理地假设你希望绑定数据并将数据传回给使用它的页面。所以,你必须导入 `Web.UI` 和 `ComponentModel` 命名空间。
Imports System.Web.UI
Imports System.ComponentModel
接下来,让我们考虑我们的 `GridView` 中的结果会是什么样子。我可以只保留名字和姓氏,但这很无聊。相反,当行绑定时,我选择获取搜索字符串,检查其中是否包含逗号(用户正在输入姓氏,名字),然后显示带有高亮显示的结果,以向他们展示与他们的搜索匹配的内容。这似乎很费力,但它使得用户体验非常自然。再次查看文章顶部的循环图片。请注意,当我输入我的名字,然后是姓氏时,结果会随着我的输入而显示。然后,当我输入姓氏,逗号,名字时,结果会切换到我输入搜索的方式。另外,我在此方法中使用 `People` 对象,这使得处理姓名变得轻而易举。最后,我选择仅显示 `People.SearchPeople` 方法返回的结果。你可以在此方法中轻松添加图片、链接或其他有关此人的信息,以增强搜索结果。
Private Sub gvPeopleList_RowDataBound(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
Handles gvPeopleList.RowDataBound
'retrieve the text entered by the user
Dim searchStr As String = txtSearchStr.Text
If e.Row.RowType = DataControlRowType.DataRow Then
'create new person object
Dim peep As New Person()
'when we are done inspecting the input text,
'this will hold the string to show users
'in the result box
Dim Namestr As String = ""
'if they person is entering Lastname, Firstname
Dim strArr As String() = Split(searchStr, ",")
'if there is a comma in the search text
If UBound(strArr) > 0 Then 'there was a comma
'if the Last name text is more than just white space,
'show the matches using the Highlight function
peep.Lastname = IIf(Trim(strArr(0)).Length > 0, _
Utilities.Highlight(Trim(strArr(0)), _
e.Row.DataItem("LastName")), _
e.Row.DataItem("LastName"))
'if the first name text is more than just white space,
'show the matches using the Highlight function
peep.Firstname = IIf(Trim(strArr(1)).Length > 0, _
Utilities.Highlight(Trim(strArr(1)), _
e.Row.DataItem("Firstname")), _
e.Row.DataItem("Firstname"))
'set the presentation variable
Namestr = peep.Lastname & ", " & peep.Firstname
Else
'if there was no comma, then search for a space
strArr = Split(searchStr)
If UBound(strArr) > 0 Then
'if there was a space....
peep.Lastname = IIf(Trim(strArr(1)).Length > 0, _
Utilities.Highlight(Trim(strArr(1)), _
e.Row.DataItem("LastName")), _
e.Row.DataItem("LastName"))
peep.Firstname = IIf(Trim(strArr(0)).Length > 0, _
Utilities.Highlight(Trim(strArr(0)), _
e.Row.DataItem("Firstname")), _
e.Row.DataItem("Firstname"))
Namestr = peep.Firstname & " " & peep.Lastname
'if all the other options fail, just highlight
'the First and Last names from the search
'results and set the presentation variable
Else
peep.Firstname = Utilities.Highlight(searchStr, _
e.Row.DataItem("Firstname"))
peep.Lastname = Utilities.Highlight(searchStr, _
e.Row.DataItem("LastName"))
Namestr = peep.Lastname & ", " & peep.Firstname
End If
End If
'set the persons location
peep.Location = e.Row.DataItem("Location")
'if the person has a job title, set it
peep.JobTitle = IIf(e.Row.DataItem("JobTitle") _
Is DBNull.Value, "", e.Row.DataItem("JobTitle"))
'Find the link button in the grid
Dim btn As LinkButton = TryCast(e.Row.FindControl("btn1"), LinkButton)
'set the text of the link button
btn.Text = "<b>" & Namestr & "</b>" & _
"<br />" & peep.JobTitle & _
" - " & peep.Location
ElseIf e.Row.RowType = DataControlRowType.EmptyDataRow Then
'If there person has entered more than two characters,
'show them that there were no search results
If txtSearchStr.Text.Length > 2 Then
Dim lbl As Label = TryCast(e.Row.FindControl("lbl1"), Label)
lbl.Text = "No matching records were found for the search string: <i>" & _
searchStr & "</i>."
'otherwise, do not show the empty row template
Else
e.Row.Visible = False
End If
End If
End Sub
好的,所以结果看起来不错。现在,让我们考虑一下我们希望如何处理用户点击某个结果。首先,我喜欢设置一个 `Bindable` 属性,它总是从 `GridView` 的选定行返回 `DataKey` `PeopleID`。这是一种优雅的方式,可以将 `gvPeopleList GridView` 的选定值传递给使用它的页面。理论上,你也可以将一个值传递给 `PeopleID`,但我在这篇帖子中没有涵盖这种情况。
Private _PeopleID As Integer = 0
<Bindable(True)> <Browsable(True)> _
Public Property PeopleID() As Integer
Get
Return CType(gvPeopleList.DataKeys(_
gvPeopleList.SelectedIndex).Item("PeopleID"), Integer)
End Get
Set(ByVal value As Integer)
_PeopleID = value
End Set
End Property
现在我们可以告诉使用它的页面“值”已被选中,但这还不够好。我们还需要触发一个公共事件,使用它的页面可以监听并做出反应——就像我们的用户控件是一个“普通”的 ASP 控件一样,例如 `DropDownList` 的 `SelectedIndexChanged` 事件。在这种情况下,我已将此公共事件(`PersonSelected`)与 `GridView` 的 `SelectedIndexChanged` 事件连接起来。
Public Event PersonSelected(ByVal sender As Object, ByVal e As System.EventArgs)
Private Sub gvPeopleList_SelectedIndexChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles gvPeopleList.SelectedIndexChanged
RaiseEvent PersonSelected(sender, e)
End Sub
呼。就是这样。这就是控件。你可以疯狂地在后台代码中添加各种花哨的功能。事实上,我对这个控件有几个想法。但是,如广告所示,我们现在有了一个用户控件,可以将其放入我解决方案中的任何页面中来搜索人员并返回匹配的 ID。关于此控件的最后一件事:我不喜欢从使用页面的标题中引用控件。相反,我更喜欢将控件添加到我的 `web.config` 中,如下所示:
<system.web>
<pages>
<controls>
<add tagPrefix="uc1"
src="~/UserControls/PersonSearch.ascx"
tagName="PersonSearch"/>
</controls>
</pages>
</system.web>
要点 #6 - 使用控件
如果你查看我附加到此帖子的解决方案,你会发现这个控件涉及很多文件。
- Classes/People.vb, Classes/Person.vb
- css/default.css
- js/PersonSearch.js
- UserControls/PeopleSearch.ascx, UserControls/PeopleSearch.ascx.vb
这看起来工作量很大,但实际上并非如此。最棒的是,现在我们可以在一个“普通”的 ASPX 页面中轻松地使用这个相当复杂的项目集合。这是一个示例设计器和后台代码页面来“使用”此控件:
<%@ Page Language="vb" AutoEventWireup="false"
CodeBehind="Default.aspx.vb" Inherits="ASPNETPersonSearch._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<link href="css/default.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div>
<table>
<tr>
<td>Choose a Person</td>
<td><uc1:PersonSearch runat="server" id="ps1" /></td>
</tr>
</table>
</div>
</form>
</body>
</html>
非常简洁……对吧?还有后台代码:
Partial Public Class _Default
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
End Sub
Private Sub ps1_PersonSelected(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles ps1.PersonSelected
Dim pid As Integer = ps1.PeopleID
'do stuff with the selected person ID
End Sub
End Class
天哪,你甚至可以有一个 `UpdatePanel` 并使用 `PersonSelected` 事件作为触发器,如下所示:
<table>
<tr>
<td>Choose a Person</td>
<td>
<uc1:PersonSearch runat="server" id="ps1" />
</td>
</tr>
</table>
<asp:UpdatePanel runat="server" ID="upResult" UpdateMode="conditional">
<Triggers>
<asp:AsyncPostBackTrigger ControlID="ps1" EventName="PersonSelected" />
</Triggers>
<ContentTemplate>
<asp:Label runat="server" ID="lblresult" />
</ContentTemplate>
</asp:UpdatePanel>
你应该知道,如果你只复制此帖子附带的解决方案并尝试运行它,它将 **不起作用**。你 **必须** 更改 `web.config` 文件中的数据库引用。如果你只尝试运行此项目,你可能会遇到此错误:
The DataSet in data source 'odsPeopleList' does not contain any tables.
此错误意味着 `People.SearchPeople` 方法中的 SQL 没有填充 `DataSet`(而不是返回不匹配的结果)。这只会在查询本身有问题时发生。
结论
就这样。一个非常便携、美观、易于实现的 ComboBox 搜索控件,使用了 AJAX、ASP.NET 和一点 JavaScript。在我使用此控件时发现的一件事是,如果你将控件本身放在 `UpdatePanel` 中,你的用户会失去对控件的焦点,除非你配置“父” `UpdatePanel` 为:`ChildrenAsTriggers="false"
`。但是,你可以将此控件添加到任何 `.aspx` 页面,或任何子页面的 `
有一件事我在这篇文章中没有涉及,那就是网格中结果的样式。我已将 `.css` 文件包含在示例项目中,但它根本不涉及网格。