Building a Grouping Grid with the ASP.NET 3.5 LinqDataSource and ListView Controls
I haven't played too much with ASP.NET 3.5's new LinqDataSource control yet - so I figured it was about time I did a little research and checked it out. Typically, I like to document my research by creating a small prototype application focused on a handful of the new features that I am investigating. I find building a reference application is helpful because it tends to drive out some of those nasty implementation details that somehow remain hidden if I just browse the documentation or read a reference book.
So for a recent research project, I decided I would look into the LinqDataSource's control GroupBy and OrderGroupBy attributes to see if they could be used to help me build a grid that supports grouping. Below is the final screen shot for my reference application. The grid shows the rows in the Northwind Orders table - grouped by the customer that placed the order. The group of orders for each customer can be expanded/collapsed by clicking on the plus/minus icon (no postback required). Read on if you are interested in the details. If you download the code, make sure to update the connection string in the web.config to point to your Northwind database.
Live Demo (IE6, IE7 and FF) | Download
The LinqDataSource Control
Below is what MSDN has to say about the LinqDataSource control. In a nutshell, the control is a nice way to leverage the Linq features in your web apps.
Creating the Northwind DataContext
The LinqDataSource requires a context object that represents the data source. For my scenario I want to access the rows in the Orders table of the Northwind database, so my first step was to use Visual Studio's designer tools to create the context object for my Northwind database. To do this I opened the 'Add New Item' dialog and selected the 'LINQ to SQL Classes' template. I named my template Northwind.dbml and clicked Add.
If you connect Visual Studio's Server Explorer to your database, it becomes really easy to create entities from your the tables in your database. So once the designer was opened for the Northwind.dbml file, I opened the Server Explorer (CTRL + ALT + S), navigated out to my local SQL Server where I have my Northwind database. I expanded the Tables node in the Northwind tree and dragged the Orders table onto the design surface.
Configuring the LinqDataSource
After the Northwind DataContext was setup, I next moved on to configuring my LinqDataSource that I am using to fill the grid. I want to display all of the orders grouped by the customer that placed the order. To help me accomplish this, I configured the LinqDataSource as follows
The ContextTypeName points to the Northwind DataContext object that I created in the previous step and the TableName attribute is set to the table within the DataContext that I want to be fetching records from - Orders for this sample. The Select and GroupBy attributes is where it gets a little more interesting. Like I mentioned earlier, I was to group the orders by the customer that placed the order. So I set the GroupBy attribute to the customerid column. If you look again at the screen shot for my grid, I am also displaying the customerid and the number of orders in the group using the following format: '{0} ({1} Orders)' where {0} is the customerid and {1} is the number of orders they placed. I want to use these values in my databinding expressions so I need to make sure to use them in the select statement. You can reference the grouped by property in the select statement by using the well known Key property. So when the orders for ALFKI are bound to the grid, Eval('Key') resolves to 'ALFKI' and Eval('Count') resolves to '6'.
The other interesting item in my Select statement is the use of the It keyword. The It keyword provides you with access to the individual items of the group - so in this scenario the orders for the individual groups. So for customer ALFKI, here are the values of the Key, Count and Items properties.
| Key | Count | Items |
| ALFKI | 6 | Order { ID='10643', OrderedDate='08/25/1997' ... } Order { ID='10692', OrderedDate='10/31/1997' ... } Order { ID='10702', OrderedDate='11/24/1997' ... } Order { ID='10835', OrderedDate='02/12/1998' ... } Order { ID='10952', OrderedDate='04/27/1998' ... } Order { ID='11011', OrderedDate='05/07/1998' ... } |
Configuring the ListView
After my LinqDataSource was all setup I started working on creating the ListView and its data binding expressions that are used to fill my grid. I thought the easiest way to produce the markup for the grouping was to use two ListView's, one nested inside the other. The outer ListView is responsible for rendering the grouping header rows and the nested ListView renders the orders belonging to each of the groups.
So I first created the outer ListView as follows. I set the DataSourceID to point to the LinqDataSource we configured in the previous section. Then I used the LayoutTemplate to define the header columns of the table and the ItemTemplate to render the header rows for each of the groups. As you can see, I am referencing the Key and Count properties of the LinqDataSource using the standard DataBinding syntax.
Next, I added a nested ListView to the outer ListView for rendering the individual orders. For the nested ListView, I bound the DataSource property to the Items property of the LinqDataSource and I used regular DataBinding expressions to reference the individual properties of each of the orders.
Expanding and Collapsing the Groups
Finally, to put the finishing touches on the grid, I need a way to expand/collapse the groups by clicking on the plus/minus icon. To do this, I added a JavaScript function to the page called toggleGroup. This function applies and removes a css class called hidden the rows to control their visibility.
When the grid first renders, the hidden class is applied to all of the data rows. When the user clicks the plus icon, the following bit of JavaScript is run and uses the addCssClass/removeCssClass ASP.NET AJAX functions to add or remove the hidden class from the rows. Finally, the function takes care of updating the plus/minus icon.
That's it. Enjoy!
Comments
Bookmarked!
This is technical ques, can you group it by date? I mean, just like how your blog archives were group by Month then by Year like so,
January 2008
December 2007
...
I can't do it using the OrderDate then by Total Orders :(
More Power to Matt!
Should I give you an easier way.
Use the AjaxControlToolkit accordion control and stick the grid into the template.
And what you get is a grid shown at a time. And if you want to see another grid the first one closes. So you use the maximum real estate of the page.
Nicely done! I like this.
Great article as usual!
Deepak, You cannot dynamically populate an accordion control's panes then bind a gridview to that pane.
Let me know if you have tried it.
Nice article Matt.
Hey Now Matt,
Great post, very informative & useful. The end result for the gridview functionality is really nice to have.
Thx 4 the info,
Catto
Does LinqDataSource allow to limit queries?
Thanks in advance
Looks great, but its running quite slow when paging? or its just me?
Cheers
thank you for the article
You also can't forget about the GroupTemplate. I can't tell if all of this could be done using it, but it's a useful feature anyway..
Great article.I really like your documantion style.It's very clean.Thanks for sharing your knowledge with us..
Hi PEN,
I tried to put the code in here but its just not showing correctly here. ie only part of the code is getting pasted. But I will try to point out the important features.
Code Page
1) Add an accordion control to the page. which supports OnItemDataBound="AccordionDisplay_ItemDataBound"
2) In the Content template add a grid with ID="grdDetails"
In the code behind
1) Somewhere you will bind the data to the accordion control
AccordionDisplay.DataSource = _SomeCollection;
AccordionDisplay.DataBind();
This will instigate a call to the following procedure
2) you will implement AccordionDisplay_ItemDataBound(object sender, AjaxControlToolkit.AccordionItemEventArgs e)
This gets called for each header and template
if (e.ItemType == AjaxControlToolkit.AccordionItemType.Content)
{
GridView grdDetails = (GridView) e.AccordionItem.FindControl("grdDetails");
// Once you got the grid bind it to your data source.
grdDetails.DataSource = _SomeData;
grdDetails.DataBind();
)
Hope this helps. I will keep a check on this blog for any more questions you might have.
It sure would be cool if you could do a tutorial on how to make a grid like the one from the extjs.com framework. They have the ability to click on the column header and then choose which columns you want to show in the grid. You can show/hide columns - I can't find anything like it on the web for asp.net and I don't want to use the extjs framework.
Awesome post. Makes me wish we were using 3.5 for my current project. Thanks a heap for this
You should by default set the row to visible and on load add the css class 'hidden' with javascript. In this way your page shows up nicely in browsers which don't support javascript or have it disabled. And you'll add some extra functionality for browsers that do support javascript.
Cheers,
Wes
wonderfully written article as it is so well explained, however unless I am missing a key point the LinqdataSource is a great RAD tool but useless for the n-Tier apps that most of use write.
Matt RULEZ!!!
Great article!
I was wondering how can I display the CustomerName instead of the CustomerId?
What would be the value of my Select= ?
Thanks!
I have changed the row content in the second listview "lvItems" with "linkbuton" controls, and now I would like to get the values of each cellule in the row when I click on this row
I've tried that by using ;
void lvv_SelectedIndexChanging(Object sender, ListViewSelectEventArgs e)
{
using (ListViewDataItem listViewDataItem = (ListViewDataItem)lv.Items[e.NewSelectedIndex])
{
if (listViewDataItem != null)
{
ListView lvv = (ListView)listViewDataItem.FindControl("lvItems");
ListViewItem item1 = (ListViewItem)lvv.Items[e.NewSelectedIndex];
LinkButton l1 = (LinkButton)item1.FindControl("LinkButton1");
Message.Text = l1.Text;
}
}
}
and;
asp:ListView ID="lvItems" runat="server"
DataSource=''
OnSelectedIndexChanging="lvv_SelectedIndexChanging" >"
thanks for your help
Awesome site and contributions as usual.
I'm trying to implement a nested ListView. I'm having a problem determing which ListView method in which to databind the nested ListView. I've been going through the MSDN ListView Members documentation and seem close but not quite there.
Here is my relevant ASPX code:
<asp:ListView ID="lvCartonsPackages" runat="server" ItemPlaceholderID="ipCarton" OnItemDataBound="lvCartonsPackages_ItemDataBound"> <LayoutTemplate> <table class="yui-datatable-theme"> <thead> <tr> <th>Tracking #</th> <th>Received</th> <th>Number Pkgs</th> <th>Expected On</th> <th>Carton Receipt Conditions</th> </tr> </thead> <tbody id="ipCarton" runat="server"></tbody> </table> </LayoutTemplate> <ItemTemplate> <tr> <td><%# Eval("in_track_num")%></td> <td><%# Eval("receive_date")%></td> <td><%# Eval("exp_num_packages")%></td> <td><%# Eval("exp_receive_date")%></td> <asp:ListView ID="lvCartonConditions" runat="server" ItemPlaceholderID="ipCartonConditions" DataKeyNames="condition_type_cde"> <LayoutTemplate> <td id="ipCartonConditions" runat="server" /> </LayoutTemplate> <ItemTemplate> <td><%# Eval("condition_type") %></td> </ItemTemplate> </asp:ListView> </tr> </ItemTemplate> </asp:ListView>And here is my relevant C# codebehind:
protected void lvCartonsPackages_ItemDataBound(object sender, ListViewItemEventArgs e) { if (e.Item.ItemType == ListViewItemType.DataItem) { ListViewDataItem currentItem = (ListViewDataItem)e.Item;///NCA 27-Feb-08: Internal framework getting user object.
string sUsername = Page.User.Identity.Name;
Foo.User oUser = (Foo.User)Cache[sUsername];
BOTypes oTypes = new BOTypes(oUser);
///NCA 27-Feb-08: Basic two column result set here...
DataTable dtCartonConditionTypes = oTypes.GetAllConditionTypes();
ListView lvCartonConditions = (ListView)currentItem.FindControl("lvCartonConditions");
lvCartonConditions.DataSource = dtCartonConditionTypes;
lvCartonConditions.DataBind();
}
}
Any direction would be greatly appreciated, meanwhile it's back to MSDN. Thanks again for the contributions here!
Clay
Should mention that I realize ItemDataBound isn't the place to do this, just used to test syntax.
Excellent Post Thank you
However paging causes the following error
Sys.WebForms.PageRequestManagerServerErrorException
This occurs with SQL 2000
SQL Express works fine. I have searched for a workaround.
Very nice code, thanks.
Is there a way to nest 3 ListView controls in order to group by yet another property, for instance the month of the order?
I can see where the results are pointed to the itemplaceholder of the listview; however, I am not sure how the nested list view places its results right under the its corresponding parent result.
In other words since there is only one how are the second batch of results placed right under its parent ?
Where is the second placeholder?
Thanks
Erik
Hi. I am playing with your code and using two tables to get data. In one table I have CategoryId and in second CategoryName. Normally with linq I would use
Eval("Table2.CategoryName") to show CategoryName in a listview
but in nested ListView it does not work, probably because the nested ListView's datasource is DataSource=Eval("Items").
I can use a function to make it work but in Linq there should be a direct way for this.
Any idea how to get CategoryName?
Thanks
Raja
Hi Matt, i really liked this article.
Currently I'm trying to use an ObjectDataSource instead of a LinqDataSource but it doesn't have a "items" proprty that I can use in the nested listview datasource.
Do you have any suggestion or idea that i can further investigate?
Thank you?
What needs to be done in order to change the Group by order to Count instead of the customer?
Since Count is not part of the data context it does not allow me select as a field.
Any thoughts!
Thanks,
Awesome article
Dear All, I am seeking your help in a problem that I am facing. The problem is, I have got your sample project working great but I can not customize it with my needs. There are many causes for that:1- I am connecting to a remote Oracle DB Which is not supported. (possible solution Oracle Data Access Components "ODAC")
2- My query is so complicated that I can not get the count of the category in which I want to group.
3- I am not using a link data source but an SQL data source(again counting and ordering).
So, is there a workaround that I can follow to solve these issues. I have been struggling for more than four weeks.
Hi, matt! How can I sort the nested table . For example ,When I click the "ID", it will sort by "ID". However, when I use the ,I got a warning message.
Eager for your reply, thank u.
Is it possible to do same this you did in the article using asp.net 2.0
Building a Grouping Grid with the ASP.NET 3.5 LinqDataSource and ListView Controls
Thank You
Prahlad
@prahlad : Yes, you could use a listview or repeater and use the itemdatabound event to inject javascript onclick events.
@matt : you could alter the html to use tbody tags and hide/show the tbody element which will hide the rows with it.
Hi
Your projects are great!
I Have a question how can I make this grid to load with expanded nodes?
Thanks in advance!