views:

2094

answers:

4

I need to retrieve a set of Widgets from my data access layer, grouped by widget.Manufacturer, to display in a set of nested ASP.NET ListViews.

The problem is that (as far as I can tell) the nested ListView approach requires me to shape the data before using it, and I can't figure out the best approach to take. The best I've been able to come up with so far is to put a LINQ query in my data access layer like so:

var result = from widget in GetAllWidgets(int widgetTypeID)
             group widget by widget.Manufacturer into groupedWidgets
             let widgets = from widgetGroup in groupedWidgets
                           select widgetGroup
             select new { Manufacturer = groupedWidgets.Key, Widgets = widgets };

Of course, anonymous types can't be passed around, so that doesn't work. Defining a custom class to enclose data seems like the wrong way to go. Is there some way I can perform the grouping on the ASP.NET side of things? I'm using ObjectDataSources to access the DAL.

Updated: OK, I'm not creating an anonymous type anymore, and instead my DAL passes an IEnumerable<IGrouping<Manufacturer, Widget>> to the ASP.NET page, but how can I use this in my ListViews? I need to render the following HTML (or something pretty much like it)

<ul>
  <li>Foo Corp.
    <ol>
      <li>Baz</li>
      <li>Quux</li>
    </ol>
  </li>
  <li>Bar Corp.
    <ol>
      <li>Thinger</li>
      <li>Whatsit</li>
    </ol>
  </li>
</ul>

Originally, I had a ListView within a ListView like so:

<asp:ListView ID="ManufacturerListView">
    <LayoutTemplate>
        <ul>
            <asp:Placeholder ID="itemPlaceholder" runat="server" />
     </ul>
    </LayoutTemplate>
    <ItemTemplate>
        <li><asp:Label Text='<%# Eval("Manufacturer.Name") %>' />
        <li>
     <asp:ListView ID="WidgetsListView" runat="server" DataSource='<%# Eval("Widgets") %>'>
         <LayoutTemplate>
             <ol>
                 <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </ol>
        </LayoutTemplate>
        <ItemTemplate>
            <li><asp:Label Text='<%# Eval("Name") %>'></li>
        </ItemTemplate>
     </asp:ListView>
     </li>
    </ItemTemplate>
</asp:ListView>

Note how the DataSource property of WidgetsListView is itself databound. How can I duplicate this functionality without reshaping the data?

This is getting kind of complicated, sorry if I should have just made a separate question instead.

+3  A: 

When you're using Linq to group, you can get a strongly typed object without that shaping:

List<int> myInts = new List<int>() { 1, 2, 3, 4, 5 };
IEnumerable<IGrouping<int, int>> myGroups = myInts.GroupBy(i => i % 2);
foreach (IGrouping<int, int> g in myGroups)
{
  Console.WriteLine(g.Key);
  foreach (int i in g)
  {
    Console.WriteLine("  {0}", i);
  }
}
Console.ReadLine();

In your case, you'd have:

  IEnumerable<IGrouping<Manufacturer, Widget>> result =
    GetAllWidgets(widgetTypeId).GroupBy(w => w.Manufacturer);

This will let you return the result from the method.

David B
Thanks for the advice. I updated my original question with more info.
Brant Bobby
+7  A: 

Ok, I'm going to contradict my prior statement. Since eval wants some kind of property name in the nested control, we should probably shape that data.

public class CustomGroup<TKey, TValue>
{
  public TKey Key {get;set;}
  public IEnumerable<TValue> Values {get;set;}
}

// and use it thusly...

IEnumerable<CustomGroup<Manufacturer, Widget>> result =
  GetAllWidgets(widgetTypeId)
  .GroupBy(w => w.Manufacturer)
  .Select(g => new CustomGroup<Manufacturer, Widget>(){Key = g.Key, Values = g};

/// and even later...

<asp:ListView ID="ManufacturerListView">
<LayoutTemplate>
    <ul>
        <asp:Placeholder ID="itemPlaceholder" runat="server" />
    </ul>
</LayoutTemplate>
<ItemTemplate>
    <li><asp:Label Text='<%# Eval("Key.Name") %>' />
    <li>
    <asp:ListView ID="WidgetsListView" runat="server" DataSource='<%# Eval("Values") %>'>
        <LayoutTemplate>
            <ol>
                <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
            </ol>
       </LayoutTemplate>
       <ItemTemplate>
           <li><asp:Label Text='<%# Eval("Name") %>'></li>
       </ItemTemplate>
    </asp:ListView>
    </li>
</ItemTemplate>
</asp:ListView>
David B
A: 

This question helped me greatly, excellent!! +1 and favourite!

Colin
comments should be entered as comments to the question up top.
Atømix
A: 

I've just spent quite a while on this. Eventually found the solution and it's so simple.


var enumerableData = myData.Tables[0].AsEnumerable();
var groupedData = enumerableData.GroupBy(x => x["GroupingColumn"]);

myParentRepeater.DataSource = groupedData;
myParentRepeater.DataBind();


<asp:Repeater ID="myParentRepeater" runat="server">
  <ItemTemplate>
    <h3><%#Eval("Key") %></h3>
    <asp:Repeater ID="myChildRepeater" runat="server" DataSource='<%# Container.DataItem %>'>
      <ItemTemplate>
        <%#((DataRow)Container.DataItem)["ChildDataColumn1"] %>
        <%#((DataRow)Container.DataItem)["ChildDataColumn2"] %>
      </ItemTemplate>
      <SeparatorTemplate>
        <br />
      </SeparatorTemplate>
    </asp:Repeater>
  </ItemTemplate>
</asp:Repeater>


Eval("Key") returns the grouped value. When retrieving child info, Container.DataItem is of type IGrouping but you simply cast it to the correct type.

Hope this helps someone else.

In your item template, you can replace `<%#((DataRow)Container.DataItem)["ChildDataColumn1"] %>` with `<%# Eval("ChildDataColumn1") %>`.
Atømix