A Simple SemanticZoom

I'm Confused

I'm a big fan of simplicity.  There are times that I look at sample code and cannot figure out what the developer was thinking when he wrote it - I'm sure many of you have felt the same way.  That's how I felt about the official SemanticZoom sample - overly complicated, too much stuff being done for me, rather than a clear sample setting me up for understanding how I am supposed to work with the control and get a real feel for it.  For this reason, I set out to write a very simplified sample of the SemanticZoom control and I want to share it with you.

 

My Goal

I want to be able to look at the code for a SemanticZoom control and understand the following things:

  1. Why does it look the way it does?
  2. Where does the data come from?
  3. How does it switch between the views?

In this effort, I want the code that I present to you to be as basic as possible, but still have all of the necessary elements to be a feature-rich SemanticZoom control.  It should also be clear enough so that you can modify the sample easily and make it your own. 

The View

The Semantic Zoom consists of two parts:

ZoomedOutView - This is the high-level overview of the content displayed by the SemanticZoom.  It should allow you to quickly navigate to a collection of items with the same characteristic  - for example: all items that start with the same letter. In my app, I am showing a list of people and the languages that they speak.  However, in the ZoomedOutView, I am only showing the languages available, along with the number of people who speak that language:     
    
    
   
The XAML code that goes along with this is rather simple:  a gridview bound to a CollectionViewSource that hosts the grouped data:

 <SemanticZoom.ZoomedOutView> <GridView x:Name="MySZ_ZoomedOutGridView" SelectionMode="None" IsItemClickEnabled="True" VerticalAlignment="Top"> <GridView.ItemTemplate> <DataTemplate> <Border BorderBrush="White" BorderThickness="1"> <StackPanel Margin="10"> <TextBlock Text="{Binding Group.Language}" FontSize="22" /> <StackPanel Orientation="Horizontal" MinWidth="150"> <TextBlock Text="No. Available:&#160;"/> <TextBlock Text="{Binding Group.Speakers.Count}"/> </StackPanel> </StackPanel> </Border> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </GridView.ItemsPanel> </GridView> </SemanticZoom.ZoomedOutView>

  
You can see that the ZoomedOutView does not use any group controls, but it does bind to the groups inside the CollectionViewSource, which contains grouped data.  We'll come back to the data.
 

ZoomedInView - This is the more detailed of the two views.  It displays all of the groupings, along with all of the items in that group:
 

The XAML code for this is slightly more complicated - we use not only the data for the items, but we also must work with the groups and the headers for those groups: 

 <SemanticZoom.ZoomedInView> <GridView x:Name="MySZ_ZoomedInGridView" 
 ItemsSource="{Binding Source={StaticResource MyCollectionViewSource}}"  SelectionChanged="MySZ_ZoomedInGridView_SelectionChanged" VerticalAlignment="Top"> <GridView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Name}" MinWidth="150" /> </StackPanel> </DataTemplate> </GridView.ItemTemplate> <GridView.GroupStyle> <GroupStyle> <GroupStyle.HeaderTemplate> <DataTemplate> <Border BorderBrush="White" BorderThickness="1" MinWidth="150"> <TextBlock Text="{Binding Language}" FontSize="22" Margin="10"/> </Border> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </GridView.GroupStyle> </GridView>
</SemanticZoom.ZoomedInView>

Again - nothing groundbreaking here, and that's exactly the way that I want it.  This code simply pastes a header cell at the top of each group with the name of that group, then iterates through each item in each group and displays it.   
   

The Data
To be honest, this is where I really would get confused in other SemanticZoom samples.  It would get a collection of data together, then it would pass through some magical Linq (as found in the official sample), then it would be good for the SemanticZoom control.  That's not what I wanted to do - I want to understand how the data is formatted to be usable by the SemanticZoom control.  This is the data structure we're using:

  • Speaker- the customer is a class (implements INotifiyPropertyChanged) that consists of a name (string), and a collection of languages (list<string>) known by that customer.  This is a sample Customer:
      Customer:
        Name: Johnny
        Languages: Spanish
  • LanguageGroup- the LanguageGroup contains the name of the language of that group (string) and collection of members of that group(ObservableCollection<Speaker>).  In this app, I add a new group (Speakers) for every language, and add every Speaker who knows that language.  This is a sample LanguageGroup :     
      LanguageGroup:     
        Language: Spanish     
        Speakers:
               Customer  - Johnny
               Customer - Jose
  • AllLanguageGroups- this is the final collection (ObservableCollection) which contains all of the LanguageGroups.  This is the datasource for our CollectionViewSource.
      AllLanguageGroups:
             LanguageGroup: Spanish
             LanguageGroup: Arabic
             

The most important thing we have to realize in binding the data is that the GridViews actually have difference datasources.  This is how I set the datasources in the code:

MyCollectionViewSource.Source = AllLanguageGroups;; MySZ_ZoomedOutGridView.ItemsSource = MyCollectionViewSource.View.CollectionGroups;
We are setting the source of the CollectionViewSource to AllLanguageGroups- the set of languages and all of its Speakers.  The ItemsSource of the ZoomedInView is in turn set to the CollectionViewSource - we can't directly set the ItemsSource to the AllLanguageGroupsor we will lose the grouping capabilities that CollectionViewSources provide.

Next, we set the ItemsSource of the ZoomedOutView to the collection of Groups inside the CollectionViewSource - this is because the ZoomedOutView only uses the details of the groups themselves.  Notice the XAML bindings we used in the ZoomedOutView:

 <TextBlock Text="{Binding Group.Language}" FontSize="22" />
<TextBlock Text="{Binding Group.Speakers.Count}"/>

The bound information is actually the collection of groups.  Along with that, we can dive into each group and get the individual details inside each group.

Switching Between the Views
This turns out to be the most basic of the things that I want to understand.  To begin with, the default view is the ZoomedInView.  However, we can tell the SemanticZoom which view is active on startup using the IsZoomedInViewActive property.  This is a Boolean value, simple enough to set in code. 

There are several of ways you can switch between views:

  1. Pinch/Spread gesture - this is the default way of getting back and forth.

  2. Group Selection - when the ZoomedOutView is active, you can click on the header to go specifically to that group section in the ZoomedInView.

  3. Via code - I put a button in my app that toggles the IsZoomedInViewActive property:     

         

 private void ChangeViewButton_Click(object sender, RoutedEventArgs e) { MySemanticZoom.IsZoomedInViewActive = !MySemanticZoom.IsZoomedInViewActive; }

 

Gotchas
I noticed that when I started my app, the ZoomedInGridView.SelectionChanged event always fires.  You'll see that the first item in the first group is always selected on app startup:
 

I can't figure out how to prevent that - and IIRC, it's this way in the sample as well.  Since I am bound to the SelectionChanged event in the ZoomedInView's GridView, I made a decision to unsubscribe from that event before assigning the datasource, and resubscribe after the datasource is bound:

 MySZ_ZoomedInGridView.SelectionChanged -= MySZ_ZoomedInGridView_SelectionChanged; MyCollectionViewSource.Source = AllLanguageGroups; MySZ_ZoomedInGridView.SelectionChanged += MySZ_ZoomedInGridView_SelectionChanged;

This fixes the problem for now.  I will investigate this for future reference.

Phone
It's almost really cool - this source code *nearly* works perfect on the phone as well, but there are a few changes I made since the form factor is different:

  1. I needed to change the orientation of the views to vertical instead of horizontal - This is purely cosmetic.

  2. There's a "by-design feature" of the phone's SemanticZoom:  The ZoomedInView is always visible.  This is what I see after changing the orientation of the app for the phone:     

    I asked our internal folks about this issue - the ZoomedInView is "permanent" - it should show up underneath the ZoomedOutView - and the ZoomedOutList is the "popup" for the list.  To work around this situation, I binded the ZoomedInView's GridView.Visibilityproperty to the IsZoomedInViewActive property of the SemanticZoom.  However, since these are different types, I created a converter to convert the Boolean type of IsZoomedInViewActive to the Visibility enumeration used by the GridView.  This seems to work well:

     <local:VisibilityConverter x:Key="VisibilityConverter" />
    

     <GridView x:Name="MySZ_ZoomedInGridView" 
     ItemsSource="{Binding Source={StaticResource MyCollectionViewSource}}" 
     SelectionChanged="MySZ_ZoomedInGridView_SelectionChanged" 
     VerticalAlignment="Top" 
     Visibility="{Binding ElementName=MySemanticZoom, 
     Path=IsZoomedInViewActive, Converter={StaticResource VisibilityConverter}}">
    

     public class VisibilityConverter : IValueConverter {  public object Convert(object value, Type targetType, 
     object parameter, string language) { bool IsZoomedInViewActive = (bool)value; return (IsZoomedInViewActive ? Windows.UI.Xaml.Visibility.Visible 
     : Windows.UI.Xaml.Visibility.Collapsed); } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } }
    


       

3. Lastly, I found a weird exception occurring in the MessageDialog code:

 var MyMessageDialog = new MessageDialog(SpeakerInformation); try {  await MyMessageDialog.ShowAsync(); } catch { }

The interesting thing is that an exception is thrown at MyMessageDialog.ShowAsync().  I don't know why, but it's something that I am going to file a bug on - this works perfectly fine in Windows 8.1, and there's literally nothing that I can change about this code to fix it.

 

Conclusion
I hope that you've found this article as enlightening as I found to be while I was writing it.  The source code used here is directly derived from code I received from a fellow Microsoftie, Jag Dua, who passed it on to me while I was searching for the answers to these questions.

Comments are encouraged, and feel free to find me in the forums, tweet to me at WinDevMatt, or use my team’s handle WSDevSol.

SuperSimpleSemanticZoom.zip