Eternalistic Designs

Pulling Data from Active Directory with the ASP.NET AJAX AutoComplete Extender

Feb 05, 2008
10 comments
Submitted By: Justin Stanley
Filed Under:
Share our glory:

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" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Phone and Email Looker Upper</title>
</head>
<body>
    <form id="form1" runat="server">
        <ajaxToolkit:ToolkitScriptManager ID="ScriptManager1" runat="server" />
        <div>
            <asp:TextBox ID="txtUserName" runat="server"></asp:TextBox>
            <asp:Button ID="btnGetInfo" runat="server" Text="Get Info" />
            <ajaxToolkit:AutoCompleteExtender 
                ID="AutoCompleteExtender1" 
                runat="server" 
                ServiceMethod="findUser" 
                ServicePath="activeDirectorySearch.asmx" 
                TargetControlID="txtUserName" 
        MinimumPrefixLength="3" 
            >
            </ajaxToolkit:AutoCompleteExtender>
            
            <asp:Label ID="lblEmail" runat="server" Text="Email Address:"></asp:Label>
            <asp:Label ID="lblEmailResult" runat="server"></asp:Label>
            <asp:Label ID="lblPhone" runat="server" Text="Phone Number:"></asp:Label>
            <asp:Label ID="lblPhoneResult" runat="server"></asp:Label></div>
    </form>
</body>
</html>
 

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
Imports System.Web.Services
Partial Class _Default
    Inherits System.Web.UI.Page
    Protected Sub btnGetInfo_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnGetInfo.Click
        Dim dictContact As New Dictionary(Of String, String)
        Dim srvSearch As activeDirectorySearch = New activeDirectorySearch
        dictContact = srvSearch.findContactInfo(txtUserName.Text)
        Dim strEmail As String
        Dim strPhone As String
        For Each line As KeyValuePair(Of String, String) In dictContact
            If line.Key = "mail" Then
                strEmail &= line.Value.ToString
            ElseIf line.Key = "telephonenumber" Then
                strPhone &= line.Value.ToString
            End If
        Next
        lblPhoneResult.Text = strPhone
        lblEmailResult.Text = strEmail
    End Sub
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
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.DirectoryServices
Imports System.Collections.Generic
<WebService(Namespace:="http://localhost/")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
<System.Web.Script.Services.ScriptService()> _
Public Class activeDirectorySearch
    Inherits System.Web.Services.WebService
    <WebMethod()> _
    Public Function findUser(ByVal prefixText As String) As String()
        Dim directory As DirectoryEntry = New DirectoryEntry("LDAP://DC=nerdliness,DC=com")
        Dim filter As String = "(&(cn=*" & prefixText & "*)(!objectClass=computer)(!objectClass=nTFRSMember))"
        Dim strCats() As String = {"cn"}
        Dim items As New List(Of String)
        Dim dirUser As DirectorySearcher = New DirectorySearcher(directory, filter, strCats, SearchScope.Subtree)
        Dim results As SearchResultCollection = dirUser.FindAll
        Dim strOut As String
        For Each result As SearchResult In results
            For Each prop As DictionaryEntry In result.Properties
                If prop.Key = "cn" Then
                    For Each individualValue As Object In prop.Value
                        items.Add(individualValue.ToString)
                    Next
                End If
            Next
        Next
        Return items.ToArray()
    End Function
    Public Function findContactInfo(ByVal strUser As String) As Dictionary(Of String, String)
        Dim directory As DirectoryEntry = New DirectoryEntry("LDAP://DC=nerdliness,DC=com")
        Dim filter As String = "(cn=*" & strUser & "*)"
        Dim strCats() As String = {"mail", "telephonenumber"}
        Dim items As New Dictionary(Of String, String)
        Dim dirUser As DirectorySearcher = New DirectorySearcher(directory, filter, strCats, SearchScope.Subtree)
        Dim results As SearchResultCollection = dirUser.FindAll
        Dim strOut As String
        For Each result As SearchResult In results
            For Each prop As DictionaryEntry In result.Properties
                If prop.Key = "mail" Or prop.Key = "telephonenumber" Then
                    For Each individualValue As Object In prop.Value
                        items.Add(prop.Key.ToString, individualValue.ToString)
                    Next
                End If
            Next
        Next
        Return items
    End Function
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



Make people smarter:

Returning Dictionary

My webmethod wants to return a dataset rather than the Dictionary.  When I add the webservice to my client project, intellisense refers to my method as returning a dataset

[

WebMethod]public SerializableDictionary<int, string> getMyActiveCustomers(int salesPersonID)SerializableDictionary<int, string> myActiveCustomers = new SerializableDictionary<int, string>();SqlConnection conn = new SqlConnection(ConfigurationManager.AppSettings["Replicator"].ToString());string sqlStr = "Select CustomerID, [Name] From Customers Where SalesPersonID = '" + salesPersonID + "'";SqlCommand cmd = new SqlCommand(sqlStr, conn);SqlDataReader dr = cmd.ExecuteReader();while (dr.Read())return myActiveCustomers;

 

{

 

 

 

 

 

conn.Open();

 

 

{

myActiveCustomers.Add(dr.GetInt32(0), dr.GetString(1));

}

 

}

Repost code?

Hey, Rob.

Having a hard time reading your code (the WYSIWYG editor loves to freak out like that some times).

Can you maybe post a link to it or something while we work on getting our side fixed?

 

LINQ to Active Directory

Lot of people viewing this post came here looking for info on pulling data out of AD. Well, lookie what Robert Shelton just posted about on his blog:

http://rshelton.com/archive/2008/02/19/free-download-linq-to-active-dire...

Side note, you should totally subscribe to his feed. Great stuff there.

Error with the Auto Complete Extender Control...

Your directions are very clear and step by step, thanks for writing this all out! I have been searching and searching on how to create an auto-updating employee list, so this is great.

I do have one problem -- which I am sure can be simply solved, but I'm stuck =). I'm running the above on Visual Web Developer 2008 Express, and am getting the message "Error Creating Control: Auto Complete Extender1. This control cannot be displayed because its TagPrefix is not registered on this Web Form."

I don't see the Auto Complete Extender in my Toolbox, althougth I did download it separately. Perhaps I missed a step in getting the separately downloaded piece into my toolbox?

Ideas? Direction? Thanks so much.

RE: Error with the Auto Complete Extender Control...

You need to download the Ajax Toolbox for 3.5 and extract somewhere.

Open a new site and on your tool box right click and select "Add Tab"

Name it what ever. Right click on the new tab and select choose items.

Click browse and browse to where you extracted the 3.5 ajax toolbox to: samplewebsite>bin>ajaxControlToolKit. click open and ok.

replace: <ajaxToolkit:ToolkitScriptManager ID="ScriptManager1" runat="server" />

with: <asp:ScriptManager ID="ScriptManager1" runat="server" />

and replace:

<ajaxToolkit:AutoCompleteExtender
                ID="AutoCompleteExtender1"
                runat="server"
                ServiceMethod="findUser"
                ServicePath="activeDirectorySearch.asmx"
                TargetControlID="txtUserName"
        MinimumPrefixLength="3"
            >
            </ajaxToolkit:AutoCompleteExtender>

with:

<cc1:AutoCompleteExtender ID="AutoCompleteExtender1" runat="server" 
                ServiceMethod="findUser"
                ServicePath="activeDirectorySearch.asmx"
                TargetControlID="txtUserName"
        MinimumPrefixLength="3"
        >
        </cc1:AutoCompleteExtender>

More Values

I have this working great,

but i would like to add more values to diplay,

e.g username, address, etc...

i know the filters needed for these (samaccountname), (streetaddress)

but i dont know how to do this.

any help would be great

Just to clarify...

So you don't want to actually search those values, you just want to display them in the results?

I want to do the same what

I want to do the same what you have done for mail, phone etc... I have managed to get a couple of values working but not some others.

This is what im using to also display the samAccountName and Mobile Number:

Public Function findContactInfo(ByVal strUser As String) As Dictionary(Of String, String)
        Dim directory As DirectoryEntry = New DirectoryEntry("LDAP://DC=interbulkgroup,DC=com")
        Dim filter As String = "(cn=*" & strUser & "*)"
        Dim strCats() As String = {"samAccountName", "mail", "telephonenumber", "mobile", "facsimileTelephoneNumber"}
        Dim items As New Dictionary(Of String, String)
        Dim dirUser As DirectorySearcher = New DirectorySearcher(directory, filter, strCats, SearchScope.Subtree)
        Dim results As SearchResultCollection = dirUser.FindAll
        Dim strOut As String
        For Each result As SearchResult In results
            For Each prop As DictionaryEntry In result.Properties

                If prop.Key = "samaccountname" Or prop.Key = "mail" Or prop.Key = "telephonenumber" Or prop.Key = "mobile" Or prop.Key = "facsimileTelephoneNumber" Then
                    For Each individualValue As Object In prop.Value
                        items.Add(prop.Key.ToString, individualValue.ToString)
                    Next
                End If
            Next
        Next
        Return items
    End Function

But the fax number doesn't display. I have also tried a couple of others like streetAddress.

Case sensitive

Looks like the DictionaryEntry keys are case sensitive, while the PropertiesToLoad of the DirectorySearcher are not.

The results that get returned appear to be all lower-case, so if you just change this:

If prop.Key = "samaccountname" Or prop.Key = "mail" Or prop.Key = "telephonenumber" Or prop.Key = "mobile" Or prop.Key = "facsimileTelephoneNumber" Then

to this:

If prop.Key = "samaccountname" Or prop.Key = "mail" Or prop.Key = "telephonenumber" Or prop.Key = "mobile" Or prop.Key = "facsimiletelephonenumber" Then

(or add prop.Key="streetaddress", etc).

You should be solid.

I'm using VS 2008 and I'm

I'm using VS 2008 and I'm getting a compile error

"Value of type '1-dimensional array of String' cannot be converted to 'String'.

My guess is that the function is typed as a string but the code in the function is returning an array? Any ideas how I can fix this?

Thanks.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <div><span><br /><blockquote><table><thead><th><tr><td><form><input><h1><h2> <h3> <h4> <h5> <h6> <img> <p> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd><i>
  • Lines and paragraphs break automatically.
  • You may post block code using <blockcode [type="language"]>...</blockcode> tags. You may also post inline code using <code [type="language"]>...</code> tags.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.

More information about formatting options