Building a LinkedIn Style Address Book with the ListView and LinqDataSource Controls
LinkedIn has a nice looking address book widget that displays all of your connections partitioned by last name. To the left of the contact listing is an index that allows you to jump right to a section. This control isn't overly fancy, but I was curious what HTML/CSS/JavaScript was used to create it. So I took it apart using the IE Developer Toolbar and rebuilt it using ASP.NET 3.5 - making use primarily of LINQ to XML, the LinqDataSource and a couple of ListView's. Feel free to browse the source code by following the download link. And, of course I recommend checking out the live demo as well. If you are interested in how I created the sample, a quick description of the implementation details can be found just below the screen shot. And the best part - excluding the CSS, only 115 lines of markup/code are required to build this sample.
Live Demo (IE6, IE7, FF, Opera 9.24) | Download (.NET 3.5)
Creating the LinqDataSource
The demo makes use of a single LinqDataSource that feeds all three ListView controls found on the page. This datasource returns a record for each of the letters of the alphabet - {A, B, ... Z}. Additionally, each of these 26 records contains a collection of all of the contacts whose last name starts with the current letter. If no contacts have last names starting with a given letter, an empty collection is returned. The table below shows this structure - notice the empty Contacts collection for letters 'C' and 'Z'.
| First Letter of Last Name | Contacts | ||||||||
| A |
| ||||||||
| B |
| ||||||||
| C |
| ||||||||
| ... | ... | ||||||||
| Z |
|
To generate this structure, I use LINQ to extract the contact records from an XML file and join them by the first letter of their last name to an in-memory character array that contains all of the letters in the alphabet. After these structures are joined together, I select out the attributes required to build the interface. The resulting structure is similar to one that could have been created by grouping the contacts by the first letter of their last name. I used the join instead because I want to make sure I include all letters - even those that do not have any associated contacts (I use all of the letters to build the index piece of the address book).
This LINQ query is wired to a LinqDataSource by placing the code into the datasources Selecting event handler.
Contact Listing ListView
After the datasource was created, I moved on to adding the ListView that will generate the listing of contacts for the address book. The core portion of the address book displays contacts grouped by last name. I used two ListViews to generate an OL structure for grouping the contacts. Each of the LI elements within the OL contains a TABLE element that has a row for each contact within the current group. Before the TABLE element is a H2 element that contains the heading for the current group. The screen shot below shows how this general structure would be applied to elements listed in the table above.
The outer ListView binds directly to the LinqDataSource we previously created. This outer ListView generates the grouping information contained within the outer OL element. Within the ItemTemplate of this ListView is another nested ListView that binds to the Employees property and generates the nested TABLE that contains items for each of the contacts within the current group.
Contact Index ListView
Next, I went ahead and created the markup structure for generating the contact index. If a letter in the index (like Z or C) does not have any associated contacts, I still want to render the letter. If there are associated contacts, I want to render an anchor element that will scroll the page automatically to the contacts for the selected index. Again, I used the ListView to create a OL/LI structure for these items. If a letter doesn't have any associated contacts, I render the letter within a SPAN. Otherwise, I render an anchor element and set the href to the ID of the grouping header element I created using the previous ListView.
Some CSS Trickery
The most complex part of the addressbook widget is the CSS that controls how the index and grouping headers are aligned - both accomplish this by making use of negative margins. The index DIV floats left and has a negative margin set, making sure it displays to the left of the addressbook container.
The 2px solid line that appears before each grouping of contacts is actually the top border of the table (the table is shifted up by setting the top margin to a negative value). It is shifted up just enough that it aligns with the middle of the H2 element above it.
That's it. Enjoy!
Comments
Awesome!
I like all your blogs. Beautifully explained and the components are also usable. Thanks for your good contribution to the World Wide Web.
Bye
Bjorn
The Netherlands
You always have amazing examples that are a great show of simplistic beauty, where form does not overshadow function.
You make my day!
Thank you for all your examples that you have done. This one is one of the best i think.
Jeremy
Thats a pretty neat implementation!
Wow, this is a clever implementation. I like.
I also find it quite interesting to "take sites apart" to see how certain aspects work. Its the best way to learn new tricks. Well, it was the best way to learn until this blog came along ;)
@Matt: just curious to know.. lets say if i want to have hyperlink on name and when i click on the name i want the new window open or open in a same window?
is that possible?
thanks.
This is some sweet stuff. Im gonna see if I can create something similar for my site.
Thanks a bunch.
Roshawn
I couldnt get this to work in VB.NET. None of the code converters on the web could convert it correctly. Any ideas?
This is good stuff!
I have one issue that I am having troubling resolving with your code. Maybe you can help...
I have replaced all the Eval() with Literal controls. The literals are being populated during the lvContacts.ItemDataBound event. However, I am unable to access the literal that I have placed in the "lvItems" listview. You have nested this listview inside the "lvContacts" listview. I have tried all different types of variations using the FindControl method, but nothing seems to help.
Do you have any ideas on how to access it?
Thanks,
David
Hey Matt,
Great stuff! Question though. This looks great for small address books but how would you handle 2000 + records?
Todd
I love your blog and its contents, is there anyway for you to include VB into your samples for guys like me who doesn't know anything about C#. Thanks much and keep up the good work
Hello,
I am trying to convert your Linq To Xml example here into Linq To SQL, at the same time translating from C# into VB. The VB compiler is complaining about the first Into clause (Into groups) - saying that it expected the end of the statement before the Into clause.
Any help would be much appreciated.
Dim tConnection As SqlConnection = New SqlConnection("ConnectionString")
Dim tContext As DataContext = New DataContext(tConnection)
'Dim tAlphabet As Char() = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
Dim tAlphabetTable As Table(Of Alphabet) = tContext.GetTable(Of Alphabet)()
Dim tLinksTable As Table(Of Link) = tContext.GetTable(Of Link)()
tArgs.Result = From a In tAlphabetTable _
Join l In tLinksTable On _
a.Letter Equals l.CategoryName.Chars(0) Into groups
I had trouble with converting to use SQL and VB, i dont know very much about LINQ but the following seem to work
Dim myQuery = From myLetters In myAlphabet Join myProfiles In myProfileQuery On myLetters Equals myProfiles.LastName.Chars(0) _
Group By myLetters Into myGroup = Group _
Order By myLetters _
Select New With _
{ _
.Key = myLetters, _
.Count = myGroup.Count, _
.Contacts = From C In myGroup _
Order By C.myProfiles.LastName _
Select New With _
{ _
.FirstName = C.myProfiles.LastName _
} _
}