Intro
From time to time, I get the chance to take off the "database guy" hat I usually wear around the office and pretend I'm a developer. A good deal of that coding involves some sort of web-based tool or another so, being a Microsoft shop, I figured it was time for me to get up to speed on their implementation of AJAX. For the last couple of weeks, I've been playing around with the AJAX Control Toolkit for ASP.NET (http://www.asp.net/ajax/) and today had the chance to put one of those controls (the AutoComplete extender) to work.
I needed to create a web-based company address/phone book to throw on our intranet site. We've nearly doubled the number of employees on the payroll over the past several months, and with everybody spread across about a dozen or so field offices, it can be a pain finding somebody when you need them, especially when you don't know their full name. If only there was some way to allow users to type in the parts they knew and dynamically generate a list of users with names that fit that criteria... something that might automatically complete their search string...
In case you don't know (or know it by some other name, if you're a non-Microsoft person), the AutoComplete extender attaches to a TextBox control and, as the user begins to type, gives a list of suggestions he can click if he's too lazy to type in the whole phrase. You've seen it in the Google toolbar, etc. Pretty damn handy tool, if you ask me. You can see a sample of it (along with a list of properties) at http://www.asp.net/AJAX/AjaxControlToolkit/Samples/AutoComplete/AutoComplete.aspx.
That ASP.net website is chock full of well done, bite-sized how-to videos to help you learn to use the various Microsoft AJAX controls. In fact, that's where I started and it's the first place I go when I want to learn about a new piece of the puzzle. The videos are typically about 10-30 minutes long, with just enough information to give point you in the right direction. Perfect for those of us with short atten... Oh, look! Something shiny!
Sorry. Anyway... All the information people needed to access was in Active Directory (names, phone numbers, and email addresses), so really all we needed to do was give our users a way to search and access the data they wanted. Unfortunately, the sample "How Do I..." video for that control doesn't really delve into populating your control with data from a simple database, much less from Active Directory. Had to do a little digging for that. It did, though, give me the basic foundation I needed to set up the control and create a web service to supply it with info of some kind.
Description and Caveats
Ok, so enough of the intro crap. Let's get to the goods. We're going to create a simple ASP.NET website that consists of one TextBox control with a linked AutoComplete extender, a button to kick off our email/phone search event, and a couple of labels to display the results.
When someone types a few letters in the TextBox, our AutoComplete extender will send those characters to our web service. That web service will perform the Active Directory lookup using the System.DirectoryServices namespace and return the collection of values back to that control. After someone selects the user they want, they can hit our button to search Active Directory for that person's email address and phone number (we'll use the web service for that lookup, too). And if they exist, they'll see them in our labels.
Couple of caveats here. I'm using Visual Studio 2005 with the AJAX Control Toolkit installed seperately. I'm assuming you already have a similar setup on your rig, otherwise you'll need to follow the instructions on the ASP.net site to get your ducks in a row. Watch the first two videos here if you need a hand: http://www.asp.net/learn/ajax-videos/
Second caveat is that, to look up Active Directory info, well, you'll need access to an Active Directory domain. Definitely beyond the scope of this article. I'm also assuming you'll be able to figure out your own LDAP path. My example is going to be pretty basic and assumes you'll want to search your entire Active Directory domain from the root on down.
Finally, I'm going to assume that you have basic knowledge of Visual Studio in general. That is, you know how to create a new website, how to add controls from the Toolbox, where the Solution Explorer is, etc. I'll throw in a link to my project files at the end if you're lazy.
Default.aspx
Ok, let's get on with it. First of all, let's create our site, our blank web service, and add our various references.
In my case, since I was starting from scratch, I just chose to create a new web site using the AJAX Control Toolkit Web Site template added to Visual Studio when I installed the AJAX Control Toolkit. Piece of cake.
In my Default.aspx page, I added the controls I mentioned above. Here's the source for that page, if you just want to copy and paste:
<%@ Page Language="VB" AutoEventWireup="true" CodeFile="Default.aspx.vb" Inherits="_Default" %><br /> <br /> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><br /> <html xmlns="http://www.w3.org/1999/xhtml"><br /> <head runat="server"><br /> <title>Phone and Email Looker Upper</title><br /> </head><br /> <body><br /> <form id="form1" runat="server"><br /> <ajaxToolkit:ToolkitScriptManager ID="ScriptManager1" runat="server" /><br /> <div><br /> <asp:TextBox ID="txtUserName" runat="server"></asp:TextBox><br /> <asp:Button ID="btnGetInfo" runat="server" Text="Get Info" /><br /> <ajaxToolkit:AutoCompleteExtender <br /> ID="AutoCompleteExtender1" <br /> runat="server" <br /> ServiceMethod="findUser" <br /> ServicePath="activeDirectorySearch.asmx" <br /> TargetControlID="txtUserName" <br /> MinimumPrefixLength="3" <br /> ><br /> </ajaxToolkit:AutoCompleteExtender><br /> <br /><br /> <asp:Label ID="lblEmail" runat="server" Text="Email Address:"></asp:Label><br /> <asp:Label ID="lblEmailResult" runat="server"></asp:Label><br /><br /> <asp:Label ID="lblPhone" runat="server" Text="Phone Number:"></asp:Label><br /> <asp:Label ID="lblPhoneResult" runat="server"></asp:Label></div><br /> </form><br /> </body><br /> </html><br />
Really straight-forward if you've done any ASP.NET. Take a second to look closer at the AutoCompleteExtender if this is your first time playing with it. Couple of those properties you'll want to keep in mind:
ServiceMethod="findUser"
This is the name of the WebMethod we'll create in our web service that will ultimately perform our Active Directory lookup and return the results. If you name your webmethod something else, make sure you change this property to match.
ServicePath="activeDirectorySearch.asmx"
The URL for our web service. Mine's hosted locally, so no problem using that relative path. Of course, if you put it somewhere else you'll need to modify this URL, too.
TargetControlID="txtUserName"
The name of the control we're binding this extender to. When someone types in txtUserName, this AutoComplete extender will work its mojo.
MinimumPrefixLength="3"
The minimum number of characters your user has to type before the web service is called. I've found that my Active Directory searches can be fairly slow, so I don't want to look for a ridiculously short string. At the same time, there are plenty of people with three letter names, so I want to make sure they get returned.
There are a bunch of other properties that might be worth your time. Check out http://www.asp.net/AJAX/AjaxControlToolkit/Samples/AutoComplete/AutoComplete.aspx for more info.
Default.aspx.vb
Our code behind file will be pretty simple, too. After all, there's just the one button to deal with (the rest of our logic is in the web service). Mine looks like this:
Imports System.Collections.Generic<br /> Imports System.Web.Services<br /> <br /> Partial Class _Default<br /> Inherits System.Web.UI.Page<br /> <br /> Protected Sub btnGetInfo_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnGetInfo.Click<br /> Dim dictContact As New Dictionary(Of String, String)<br /> Dim srvSearch As activeDirectorySearch = New activeDirectorySearch<br /> dictContact = srvSearch.findContactInfo(txtUserName.Text)<br /> Dim strEmail As String<br /> Dim strPhone As String<br /> <br /> For Each line As KeyValuePair(Of String, String) In dictContact<br /> If line.Key = "mail" Then<br /> strEmail &= line.Value.ToString<br /> ElseIf line.Key = "telephonenumber" Then<br /> strPhone &= line.Value.ToString<br /> <br /> End If<br /> <br /> Next<br /> <br /> lblPhoneResult.Text = strPhone<br /> lblEmailResult.Text = strEmail<br /> End Sub<br /> End Class
Not much to it, but a couple of things we need. First, make sure you import System.Collections.Generic and System.Web.Services. The former is needed for the dictionary objects I'm creating to hold the user info, and the latter is needed to make the call to our web service (activeDirectorySearch).
All we're doing here is taking the string in the txtUsername.Text property and feeding that to the findContactInfo() webmethod we'll be creating. That webmethod returns a dictionary (hash table, key/value pairs, whatever you wanna call it) with the email address and phone number (if any) in Active Directory for the person whose name appears in the text box. It then iterates through the dictionary and creates a string to hold our results. Those are then assigned to the Text properties of our result labels. Admittedly, the formatting is kinda hinky, but we're going more for functionality here.
activeDirectorySearch.vb
Ok, let's create our web service. If you haven't done this before, it's pretty simple: just right-click your project in Solution Explorer, choose Add New Item..., and select Web Service. Give it a name, click Add, and you're set. In my case, the name is activeDirectorySearch, but if you name yours differently make sure you edit your code behind file above to match.
This is probably the most complex part of the project, and even it isn't that hard. Here's the code:
Imports System.Web<br /> Imports System.Web.Services<br /> Imports System.Web.Services.Protocols<br /> Imports System.DirectoryServices<br /> Imports System.Collections.Generic<br /> <br /> <WebService(Namespace:="http://localhost/")> _<br /> <WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _<br /> <Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _<br /> <System.Web.Script.Services.ScriptService()> _<br /> Public Class activeDirectorySearch<br /> Inherits System.Web.Services.WebService<br /> <br /> <WebMethod()> _<br /> Public Function findUser(ByVal prefixText As String) As String()<br /> Dim directory As DirectoryEntry = New DirectoryEntry("LDAP://DC=nerdliness,DC=com")<br /> Dim filter As String = "(&(cn=*" & prefixText & "*)(!objectClass=computer)(!objectClass=nTFRSMember))"<br /> <br /> Dim strCats() As String = {"cn"}<br /> Dim items As New List(Of String)<br /> Dim dirUser As DirectorySearcher = New DirectorySearcher(directory, filter, strCats, SearchScope.Subtree)<br /> Dim results As SearchResultCollection = dirUser.FindAll<br /> Dim strOut As String<br /> For Each result As SearchResult In results<br /> For Each prop As DictionaryEntry In result.Properties<br /> If prop.Key = "cn" Then<br /> For Each individualValue As Object In prop.Value<br /> items.Add(individualValue.ToString)<br /> Next<br /> End If<br /> Next<br /> Next<br /> <br /> Return items.ToArray()<br /> <br /> End Function<br /> <br /> Public Function findContactInfo(ByVal strUser As String) As Dictionary(Of String, String)<br /> Dim directory As DirectoryEntry = New DirectoryEntry("LDAP://DC=nerdliness,DC=com")<br /> Dim filter As String = "(cn=*" & strUser & "*)"<br /> <br /> Dim strCats() As String = {"mail", "telephonenumber"}<br /> Dim items As New Dictionary(Of String, String)<br /> Dim dirUser As DirectorySearcher = New DirectorySearcher(directory, filter, strCats, SearchScope.Subtree)<br /> Dim results As SearchResultCollection = dirUser.FindAll<br /> Dim strOut As String<br /> For Each result As SearchResult In results<br /> For Each prop As DictionaryEntry In result.Properties<br /> If prop.Key = "mail" Or prop.Key = "telephonenumber" Then<br /> For Each individualValue As Object In prop.Value<br /> items.Add(prop.Key.ToString, individualValue.ToString)<br /> Next<br /> End If<br /> Next<br /> Next<br /> <br /> Return items<br /> <br /> End Function<br /> <br /> End Class
First, notice that we added two other namespaces at the top: System.DirectoryServices and System.Collections.Generic. As before, System.Collections.Generic is needed for our item and dictionary objects. System.DirectoryServices, on the other hand, gives us the ability to pull our info out of Active Directory and iterate through it.
NOTE: You can't just add that "Imports System.DirectoryServices" line and call it good. If you try it, you'll get a wonderful little warning:
"Namespace or type specified in the Imports 'System.DirectoryServices' doesn't contain any public member or cannot be found. Make sure the namespace or the type is defined and contains at least one public member. Make sure the imported element name doesn't use any aliases."
To get around that, you need to add a reference to System.DirectoryServices. Just right-click your project in Solution Explorer and choose Add Reference... Scroll down the list in the .NET tab and look for that component name. Select it, click Add, and carry on.
Another gotcha... make sure you add that line that's immediately above "Public Class activeDirectorySearch", the one that says:
<System.Web.Script.Services.ScriptService()> _
Without it, this whole thing breaks down.
Finally, you'll need to customize the Namespace:="http://localhost/" line and the two "Dim directory as DirectoryEntry("LDAP://DC=nerdliness,DC=com" lines to match your situation. As mentioned, my simple example is looking at the top of the mythical nerdliness.com Active Directory domain and searching from there. You'll need to figure out your own LDAP path for your situation.
Ok, other than that, we're really just got two webmethods named findUser and findContactInfo. The first is the one we told our AutoComplete extender to use when we created our Default.aspx page. The second is the one we referenced in our code behind file to find the contact info for a specific person when someone clicks our button.
They're both pretty similar, with a couple of minor differences. They both take a string as input, for instance, but while the first returns an array of strings, the second returns a dictionary. While you could modify the second one to return the data however you like (provided you make the appropriate changes to your code behind file, of course), you're stuck with the first one. The inputs and outputs for the web method used by the AutoComplete extender are VERY specific, down to the name of the input string. Change that from prefixText to something else and you'll find things not working very quickly.
Pretty much all the code in both methods is simply drilling down through the various collections returned from Active Directory until you get to the data you actually want. Experiment around with that and you'll find you have access to a great deal of info kept in Active Directory.
And that's really it... You should be able to fire up that application let rock the house. You might want to play with that filter part a little bit if you find that the query returns too many non-user results, but other than that everything should be solid.
While I'm thinking about it, I need to give some love to RayV. I bastardized the code he posted in this MSDN forum thread nearly two years ago to get my code working:
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=372979&SiteID=1


