Saturday, March 10, 2012

Win8 Metro controls and data grouping


In Windows 8, the primary collection controls are ListView and GridView, which are items controls that derive from a common ListViewBase class. There’s also the FlipView, which binds to data collections as well but displays one item at a time. ListView and GridView have built-in support for data grouping, which makes a lot of very common scenarios a breeze to implement. In this post I’ll zoom in on the grouping feature in Metro list controls.
image
Suppose you have an app that displays a list of courses. You can use the Grid Application project template in Visual Studio to generate the skeleton for you. Then you need to create your own data source in place of the SampleDataSource provided in the project. In my example, I read course data from an XML file that I add to the project:
   1: <courses>
   2:   <course>
   3:     <title>Windows Phone 7 Hands-On</title>
   4:     <description>This course covers many real-world issues developers face when building Windows Phone applications</description>
   5:     <author>Yacine Khammal</author>
   6:     <level>Intermediate</level>
   7:     <duration>03:04:56</duration>
   8:     <published>04/18/2011</published>
   9:     <track>WP7</track>
  10:     <image>Data/Images/wphandson.png</image>
  11:     <modules>
  12:       <module>
  13:         <title>WP application architecture</title>
  14:         <description>A look at building an MVVM application for Windows Phone 7 application. We examine phone-specific choices and constraints faced when structuring a data backed business app for the phone.</description>
  15:         <image>Data/Images/wcfriaservices.png</image>
  16:         <clips>
  17:           <clip title="Intro" videouri="section1.wmv" />
  18:           <clip title="Overview of MVVM" videouri="section3.wmv" />
  19:           <clip title="Windows Phone 7 considerations" videouri="section4.wmv" />
  20:           <clip title="Sample requirements" videouri="section2.wmv" />
  21:           <clip title="Hands-on : application structure" videouri="section6.wmv" />
  22:           <clip title="Hands-on : viewmodel classes" videouri="section7.wmv" />
  23:           <clip title="Hands-on : the main view" videouri="section8.wmv" />
  24:           <clip title="Hands-on : view hierarchy" videouri="section9.wmv" />
  25:           <clip title="Navigation topics" videouri="section10-0.wmv" />
  26:           <clip title="Hands-on : navigation" videouri="section10.wmv" />
  27:           <clip title="Commanding" videouri="section11-0.wmv" />
  28:           <clip title="Hands-on : adding commands - 1" videouri="section11.wmv" />
  29:           <clip title="Hands-on : adding commands - 2" videouri="section12.wmv" />
  30:           <clip title="Takeaways" videouri="section13.wmv" />
  31:         </clips>
  32:       </module>
  33:       <module> ... </module>
  34:     </course>
  35:     <course> ... </course>
  36: </courses>

Oh and by the way, in case you’re interested, for lack of better ideas, here’s the approach I’ve used to read the data from the xml :


   1: private async Task GetCoursesAsync()
   2: {
   3:     try
   4:     {
   5:         var storageFolder = await Package.Current.InstalledLocation.GetFolderAsync("Data");
   6:         var xmlFile = await storageFolder.GetFileAsync("CourseData.xml");
   7:         var stream = await xmlFile.OpenAsync(FileAccessMode.Read);
   8:  
   9:         XmlDocument xmlDoc = await XmlDocument.LoadFromFileAsync(xmlFile);
  10:         XDocument doc = XDocument.Parse(xmlDoc.GetXml());
  11:  
  12:         courseList = (from course in doc.Descendants("course")
  13:                       select new Course
  14:                       {
  15:                           Title = course.Element("title").Value,
  16:                           Description = course.Element("description").Value,
  17:                           Author = course.Element("author").Value,
  18:                           Level = course.Element("level").Value,
  19:                           Duration = TimeSpan.Parse(course.Element("duration").Value),
  20:                           Published = DateTime.Parse(course.Element("published").Value),
  21:                           Track = course.Element("track").Value,
  22:                           Image = new BitmapImage(new Uri(projectBaseUri, course.Element("image").Value)),
  23:                           Modules = (from module in course.Element("modules").Descendants("module")
  24:                                      select new Module
  25:                                      {
  26:                                          Title = module.Element("title").Value,
  27:                                          Description = module.Element("description").Value,
  28:                                          Image = new BitmapImage(new Uri(projectBaseUri, module.Element("image").Value)),
  29:                                          Clips = (from clip in module.Element("clips").Descendants("clip")
  30:                                                   select new Clip
  31:                                                   {
  32:                                                       Title = clip.Attribute("title").Value,
  33:                                                       VideoUri = new Uri(projectBaseUri, "Data/Video/" + clip.Attribute("videouri").Value),
  34:                                                       Transcript = clip.HasElements ? clip.Element("transcript").Value : ""
  35:                                                   }).ToList()
  36:                                      }).ToList()
  37:                       }).ToList();
  38:  
  39: ...
  40: }

So now I have a data source. Of course I’ve previously defined a Course class, a Module class, a Clip class, each with appropriate data properties. If your app also needs to update data as well as display it, then you should have your data classes extend the BindableBase class included in the Common directory of the VS project templates, and have your property setters call SetProperty() to raise UI-bound change notifications.

We could just go ahead and bind a list control to our data source, just like we’ve done in my previous post. However we want to first group the data, so the first level of listing is groups. So first, we define a grouping construct, which IS a list of items or EXPOSES a list of items. For example :


   1: public class CourseTrack
   2: {
   3:     public CourseTrack()
   4:     {
   5:         Courses = new ObservableCollection<Course>();
   6:     }
   7:  
   8:     public string TrackName { get; set; }
   9:     public ObservableCollection<Course> Courses { get; set; }
  10: }

There we have it, a CourseTrack grouped concept that exposes a list of courses as well as a group name called TrackName. Here’s an alternative approach :


   1: public class CourseTrack : List<Course>
   2: {
   3:     public string TrackName { get; set; }
   4:     public new IEnumerator<object> GetEnumerator()
   5:     {
   6:         return (IEnumerator<object>)base.GetEnumerator();
   7:     }
   8:  
   9:     public CourseTrack(string trackName, IEnumerable<Course> courses)
  10:     {
  11:         TrackName = trackName;
  12:         base.AddRange(courses);
  13:     }
  14: }

Here the CourseTrack IS a list of courses since it extends a collection. This approach is actually required if you want to use semantic zoom.  And here’s an example of how you actually group the data :


   1: internal void GroupCoursesByTrack()
   2: {
   3:     CoursesByTrack = new List<CourseTrack>();
   4:  
   5:     var query = from course in courseList
   6:                 orderby course.Track
   7:                 group course by course.Track into g
   8:                 select new CourseTrack(g.Key, g);
   9:  
  10:     CoursesByTrack = query.ToList();
  11:  
  12: }

You got it, CoursesByTrack is a list of groups, that is a list of CourseTracks each representing a list of groups.

Then you bind a CollectionViewSource to the list  :


   1: <CollectionViewSource
   2:     x:Name="tracksViewSource"
   3:     Source="{Binding CourseTracks}"
   4:     IsSourceGrouped="true" />

Your sharp eye probably didn’t miss the IsSourceGroup property, which as the name suggests indicates the data source is grouped, i.e. the elements in the list are groups. As a result, any ListViewBase control that binds to the CollectionViewSource will display the root elements as groups using the group-related templates you specify :


   1: <GridView ItemsSource="{Binding Source={StaticResource tracksViewSource}}"
   2:           ItemTemplate="{StaticResource TrackListItemTemplate}">
   3:     ...
   4:     <GridView.GroupStyle>
   5:         <GroupStyle>
   6:             <GroupStyle.HeaderTemplate>
   7:                 <DataTemplate>
   8:                     ...
   9:                 </DataTemplate>
  10:             </GroupStyle.HeaderTemplate>
  11:             <GroupStyle.Panel>
  12:                 ...
  13:             </GroupStyle.Panel>
  14:         </GroupStyle>
  15:     </GridView.GroupStyle>
  16: </GridView>

Within the GroupStyle property you can specify the group header through the HeaderTemplate, and the panel to use for displaying the list of groups using the Panel template.

Note that if you used approach 1 above, that is you define your group class, CourseTrack, as EXPOSING a collection of courses as a property as opposed to deriving from a collection type, then in your CollectionViewSource, in addition to setting IsGroupedSource to true, you need to indicate the path to the actual groups within the data source using the ItemsPath property :


   1: <CollectionViewSource
   2:     x:Name="tracksViewSource"
   3:     Source="{Binding CourseTracks}"
   4:     IsSourceGrouped="true"
   5:     ItemsPath="Courses"/>

If you’ve worked with the Developer Preview, you probably remember you had to have your group classes implement an IGroupInfo interface for this to work. Now all you need to do is extend or expose a collection property. So much simpler and more intuitive! Grouping your data source has really become a breeze.

Happy grouping !

1 comment: