Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

SemanticZoom in Windows 10

0.00/5 (No votes)
20 Jul 2015CPOL4 min read 17.5K   210  
ListView with MVVM grouping, context menu, templates in detail.

Image 1

Introduction

There are several examples for Windows 10, in my opinion all are very event oriented and hard coded instead of MVVM perspective and the have plenty of personal point of view of coding. I am preparing my first Windows 10 App for the Store and I have to realize out many things that exists in Windows 10 but are not really documented, like how the Semantic Zoom looks in Windows 10.

Background

At this moment Windows 10 has finally a SemanticZoom (SZ) in the Start Menu - All Apps section. In Windows Phone we used a LongListSelector which is now deprecated, now the full power is in the SZ.

It is recommended to know a bit of MVVM, it makes easy to read and maintain the code.

Model

I have created two classes for the model, Favorite and Category:

Category

This is the 'parent' class:

public class Category : Model, IComparer<Category>
{
 private string name;
 public String Name
 {
  get {return name; }
  set {name = value;NotifyPropertyChanged();  }
 }
  public int Compare(Category x, Category y)
 {
  return x.Name.CompareTo(y.Name);
 }
}

It is important to implement the IComparer because we will group by it is child.

Favorite

This is the 'child' class

public class Favorite : Model
{
 private string name;
 public String Name
 {
  get { return name;}
  set {name = value; NotifyPropertyChanged();}
 }


 private Category category;
 public Category Category
 {
  get { return category; }
  set {category = value; NotifyPropertyChanged();}
 }
}

ViewModel

First let's define the List of Favorites and how I like to initialize it, you can have async calls so using this way the binding is maintained and refreshed:

public class MainViewModel : ViewModel
{
 private List<Favorite> favorites;
 public List<Favorite> Favorites
 {
  get
  {
   if (favorites == null)
    InitializeFavorites();
   return favorites;
  }
  set
  {
   favorites = value;
   NotifyPropertyChanged();
  }
 }

 private async void InitializeFavorites(List<Favorite> newfavorites = null)
 {
  if (newfavorites == null)
  {
   while (favorites == null)
   {
    favorites = Factory.Settings.CachedData?.Favorites;
    await Task.Delay(60);
   }
  }
  else
  {
   favorites = newfavorites;
  }

  InitializeGrouping();

  NotifyPropertyChanged(nameof(Favorites));
 }
 
 
}

As you see if favorites is null it gets from the CachedData (internally creates the default) is just a class to initialize, and using C# 6.0 you can use 'nameof(Favorites)' instead of "Favorites" that is important in case you change the name of the property.

Now let's create the grouping section:

private CollectionViewSource favoritessource;
public CollectionViewSource FavoritesSource
{
 get
 {
  if (favorites == null)
   InitializeFavorites();
  return favoritessource;
 }
 set
 {
  if (favoritessource != value)
  {
   favoritessource = value;
   NotifyPropertyChanged();
  }
 }
}

private void InitializeGrouping()
{
 var source = Favorites?.GroupBy(p => p.Category, new CategoryComparer()).OrderBy(p => p.Key.Name);
 FavoritesSource = new CollectionViewSource() { IsSourceGrouped = true, Source = source };
}

I defined a CollectionViewSource that the XAML SZ control can automatically manage grouping.

Then you make the grouping using the CategoryComparer and I order by one of it is properties.

NOTE: I tried to use expresions like 'from ...' to compare without a good behavior because I have not found the way to compare by the name, so this way works.

As you see it is not really complex, the only point you need to realize out is the way to group using a comparer, that was the hardest part to find out.

View

here I found just one post in the Internet, that gives my some information, but there is no documentation about the path to bind the grouping, so here I explain the details.

Inside a SemanticZoom let's define the ZoomedOutView:

ZoomedOutView

Image 2

<SemanticZoom.ZoomedOutView>
 <GridView x:Name="ZoomedOutGridView"  ItemsSource="{Binding FavoritesSource.View.CollectionGroups}" HorizontalAlignment="Center" >
  <GridView.ItemsPanel>
   <ItemsPanelTemplate>
    <WrapGrid  MaximumRowsOrColumns="4" Orientation="Horizontal"/>
   </ItemsPanelTemplate>
  </GridView.ItemsPanel>
  <GridView.ItemTemplate>
   <DataTemplate>
     <ContentControl HorizontalAlignment="Left" Style="{Binding Group.Key.Name, Converter={StaticResource CategoryResourceConverter}}"/>
   </DataTemplate>
  </GridView.ItemTemplate>
 </GridView>
</SemanticZoom.ZoomedOutView>

Here there are plenty of interesting things:

ItemsSource

As you can see the way to bind the groups is binding the View.CollectionGroups from the FavoritesSource (the CollectionViewSource) from the ViewModel.

ItemsPanel

By default the ItemsPanel is not equal to the Start Menu- All Apps, to change we need to set this to a WrapGrid with the properties showed in the code.

ItemTemplate

This could be trivial, but it is not, because it involves several strict steps:

  1. I set a ContentControl because I have Canvas with Paths I created different per category
  2. The binding path is 'Group.Key' and the property of the Category
  3. Use a converter to get the resource
  4. I create a resource that returns the converter

Converter

public class CategoryResourceConverter : IValueConverter
{
 public object Convert(object value, Type targetType, object parameter, string language)
 {
  return App.Current.Resources[(String)value];
 }

 public object ConvertBack(object value, Type targetType, object parameter, string language)
 {
  throw new NotImplementedException();
 }
}

And now to do not have issues with the resources they have to be defined like the following example:

Group Header Resource

XML
<Style x:Name="Friends" TargetType="ContentControl">
 <Setter Property="Template">
  <Setter.Value>
   <ControlTemplate TargetType="ContentControl">
    <Canvas  Width="29.0708" Height="32">
     <Path Fill="White" Stroke="White" StrokeThickness="0.3"
     Width="29.0708" Height="32" Data="F1M3.3822,..."/>
    </Canvas>
   </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

Where Path is the SVG converted to XAML to have vector icons in the groups.

Now we have well defined the ZoomOutView let's define the ZoomInView

ZoomInView

In this case the content is a ListView:

XML
<SemanticZoom.ZoomedInView>
 <ListView x:Name="ZoomedInListView" Margin="12,0,0,0" ItemsSource="{Binding FavoritesSource.View}"  >
 ...
 </ListView>
</SemanticZoom.ZoomedInView>

As you see the ItemsSource is binded to the View of the FavoritesSource (the CollectionViewSurce).

The ListView has the following parts:

GroupStyle

This is used to show the group header of a group of favorites (the Category)

<ListView.GroupStyle>
 <GroupStyle>
  <GroupStyle.HeaderTemplate>
   <DataTemplate>
    <ctl:NeverToggleButton  Height="42" Margin="-12,0,0,0" BorderThickness="0" Padding="0" Background="Transparent" Width="{Binding ElementName=ZoomedInListView, Path=ActualWidth}" HorizontalContentAlignment="Left">
     <Grid Margin="0,0,0,0" HorizontalAlignment="Left">
      <Grid.ColumnDefinitions>
       <ColumnDefinition Width="44"/>
       <ColumnDefinition Width="1*"/>
      </Grid.ColumnDefinitions>
      <ContentControl HorizontalAlignment="Center" Style="{Binding Key.Name, Converter={StaticResource CategoryResourceConverter}}"/>
      <TextBlock  Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,0" Text="{Binding Key.Name}" Foreground="{ThemeResource SystemControlForegroundAccentBrush}"/>
     </Grid>
    </ctl:NeverToggleButton>
   </DataTemplate>
  </GroupStyle.HeaderTemplate>
 </GroupStyle>
</ListView.GroupStyle>

In this case we have the following interesting code:

  1. The Control is a NeverToggleButton, this is because it has similar behaviour as the Photos App and I set that never gets checked.
  2. The Style of the Content Control is like the Template Binding.
  3. To Bind the properties is using Key and the property of the class.
NeverToggleButton

This is simply inheriting the ToggleButton:

public sealed class NeverToggleButton : ToggleButton
{
 public NeverToggleButton()
 {
  this.DefaultStyleKey = typeof(ToggleButton);

  this.Checked += (s, e) =>
  {
   if(IsChecked == true)
    IsChecked = false;
  };
 }
}

ItemTemplate

In this case I add a RelativePanel inside a Button to have visual response, a command and the events to show a ContextMenu (Flyout)

<ListView.ItemTemplate>
 <DataTemplate>
  <Button Background="Transparent"  Holding="Button_Holding" RightTapped="Button_RightTapped" BorderThickness="0" Command="{Binding DataContext.HyperlinkCommand, ElementName=ZoomedInListView}" CommandParameter="{Binding}" >
  <RelativePanel Margin="-18,6,6,0" >
   <Border Margin="2,0,0,0" Width="52" Height="52" x:Name="ImageBorder" >
    <Image Source="{Binding Uri, Converter={StaticResource FaviconConverter}}" Width="32" Stretch="Uniform"/>
   </Border>
   <StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="0,8,0,0" RelativePanel.RightOf="ImageBorder">
    <TextBlock VerticalAlignment="Top"  Text="{Binding Name}"/>
    <TextBlock Typography.Capitals="Titling" VerticalAlignment="Top" Text="{Binding Uri}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontSize="12" Opacity="0.5"/>
   </StackPanel>
  </RelativePanel>
  </Button>
 </DataTemplate>
</ListView.ItemTemplate>

I tried to setthe margins to zero, but I accomplish visually better with these margins.

ContextMenu

Image 3

There is a property inside the UIElements called Flyout but there is no control of it, I mean, if you set it will appear on Tapped, not when you consider, in order to create from right button or holding, I added the events:

private void Button_Holding(object sender, HoldingRoutedEventArgs e)
{
 (this.Resources["FavoriteFlyout"] as Flyout).ShowAt(sender as FrameworkElement);
}

private void Button_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
 e.Handled = true;
 (this.Resources["FavoriteFlyout"] as Flyout).ShowAt(sender as FrameworkElement);
}

Where FavoriteFlyout is a Resource:

<Flyout x:Key="FavoriteFlyout"  Placement="Right"  >
 <ToggleMenuFlyoutItem   Text="Quick Launch"  IsChecked="{Binding Quick, Mode=TwoWay}"/>
</Flyout>

Where I bind the command and the IsChecked to the favorite of the viewmodel. (Quick is a bool property)

Using the code

The solution I uploaded is an example that contains two categories with example of favorites and all is inside one project. It is easy to follow.

Points of interest

There are several parts like the Path of the binding that I only found on the internet the word Key and with that by trial and error I found how to bind it.

The way to make the layout of the margins is a bit odd, because the group headers, the stretches and the layout with the togglebutton is not the desired, if it is streched the content it should like a button but it does not.

History

v 1.0 I release this before the latest official SDK, might be there are some changes in the final first release SDK.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)