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
<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:
- I set a ContentControl because I have Canvas with Paths I created different per category
- The binding path is 'Group.Key' and the property of the Category
- Use a converter to get the resource
- 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
<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:
<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:
- The Control is a NeverToggleButton, this is because it has similar behaviour as the Photos App and I set that never gets checked.
- The Style of the Content Control is like the Template Binding.
- 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
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.