Windows 10 中的语义缩放





0/5 (0投票)
带 MVVM 分组、上下文菜单和模板的 ListView 详解。
引言
有几个关于 Windows 10 的示例,我认为它们都非常面向事件且硬编码,而不是从 MVVM 的角度来看,并且有很多个人编码观点。我正在准备我的第一个 Windows 10 应用商店应用,我需要实现许多 Windows 10 中存在但没有真正文档化的功能,例如 Semantic Zoom 在 Windows 10 中的样子。
背景
目前 Windows 10 的“开始”菜单 - “所有应用”部分终于有了 SemanticZoom (SZ)。在 Windows Phone 中,我们使用了一个已弃用的 LongListSelector,现在全部功能都体现在 SZ 中。
建议了解一些 MVVM 知识,这会使代码易于阅读和维护。
模型
我创建了两个模型类:Favorite 和 Category
类别
这是“父”类
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); } }
实现 IComparer 很重要,因为我们将按其子项进行分组。
Favorite
这是“子”类
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
首先,我们定义 Favorites 列表以及我喜欢初始化它的方式,您可以进行异步调用,这样可以保持绑定并刷新。
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)); } }
正如您所见,如果 favorites 为 null,则会从 CachedData(内部创建默认值)获取。这只是一个用于初始化的类,并且使用 C# 6.0,您可以使用 'nameof(Favorites)' 而不是“Favorites”,这在更改属性名称时很重要。
现在,让我们创建分组部分
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 }; }
我定义了一个 CollectionViewSource,XAML SZ 控件可以自动管理分组。
然后,您可以使用 CategoryComparer 进行分组,并按其属性之一进行排序。
注意:我尝试使用类似“from ...”的表达式进行比较,但行为不佳,因为我还没有找到按名称比较的方法,所以这种方式有效。
正如您所见,这并不复杂,您需要实现的一点是分组的方法,这正是最难找到的部分。
视图
在这里,我在网上只找到了一篇文章,它提供了一些信息,但关于绑定分组路径的文档却很少,所以在这里我将详细解释。
在 SemanticZoom 内部,我们定义 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>
这里有很多有趣的东西
ItemsSource
正如您所见,绑定组的方式是将 ViewModel 中的 FavoritesSource(即 CollectionViewSource)的 View.CollectionGroups 绑定起来。
ItemsPanel
默认情况下,ItemsPanel 不等于“开始”菜单 - “所有应用”,要更改它,我们需要将其设置为 WrapGrid,并具有代码中显示的属性。
ItemTemplate
这可能微不足道,但实际上并非如此,因为它涉及几个严格的步骤。
- 我设置了一个 ContentControl,因为我有一个 Canvas 和 Path,我为每个类别创建了不同的 Canvas 和 Path。
- 绑定路径是“Group.Key”以及 Category 的属性。
- 使用转换器来获取资源。
- 我创建了一个返回转换器的资源。
转换器
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(); } }
现在,为了避免出现资源问题,它们必须按照以下示例进行定义。
组标题资源
<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>
其中 Path 是 SVG 转换为 XAML,以便在组中使用矢量图标。
现在我们已经很好地定义了 ZoomOutView,让我们来定义 ZoomInView。
ZoomInView
在这种情况下,内容是一个 ListView。
<SemanticZoom.ZoomedInView>
<ListView x:Name="ZoomedInListView" Margin="12,0,0,0" ItemsSource="{Binding FavoritesSource.View}" >
...
</ListView>
</SemanticZoom.ZoomedInView>
正如您所见,ItemsSource被绑定到 FavoritesSource(即 CollectionViewSurce)的 View。
ListView 包含以下部分
GroupStyle
这用于显示一组收藏夹(即 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>
在这种情况下,我们有以下有趣的代码。
- 控件是 NeverToggleButton,这是因为它与 Photos App 的行为相似,并且我设置了它永远不会被选中。
- Content Control 的 Style 类似于 Template Binding。
- 绑定属性是使用 Key 和类的属性。
NeverToggleButton
这只是继承了 ToggleButton。
public sealed class NeverToggleButton : ToggleButton { public NeverToggleButton() { this.DefaultStyleKey = typeof(ToggleButton); this.Checked += (s, e) => { if(IsChecked == true) IsChecked = false; }; } }
ItemTemplate
在这种情况下,我在 Button 内添加了一个 RelativePanel 以获得视觉响应、一个命令以及显示 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>
我尝试将边距设置为零,但通过这些边距在视觉上效果更好。
ContextMenu
UIElements 中有一个名为 Flyout 的属性,但无法控制它,我的意思是,如果您设置了它,它会在 Tapped 时出现,而不是在您考虑的时候,为了从右键单击或长按创建,我添加了事件。
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); }
其中 FavoriteFlyout 是一个资源。
<Flyout x:Key="FavoriteFlyout" Placement="Right" > <ToggleMenuFlyoutItem Text="Quick Launch" IsChecked="{Binding Quick, Mode=TwoWay}"/> </Flyout>
其中我将命令和 IsChecked 绑定到 ViewModel 的 favorite。(Quick 是一个布尔属性)
使用代码
我上传的解决方案是一个示例,其中包含两个类别和收藏夹示例,所有内容都在一个项目中。易于理解。
关注点
有几个部分,例如绑定的 Path,我只在网上找到了“Key”这个词,通过反复试验,我找到了如何绑定它。
边距布局的方式有点奇怪,因为组标题、拉伸和带 togglebutton 的布局不是我想要的,如果拉伸内容,它应该看起来像一个按钮,但它不是。
历史
v 1.0 我在此最新官方 SDK 之前发布了它,最终的第一个发布 SDK 可能有一些变化。