65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 10 中的语义缩放

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015 年 7 月 20 日

CPOL

5分钟阅读

viewsIcon

17908

downloadIcon

248

带 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

这可能微不足道,但实际上并非如此,因为它涉及几个严格的步骤。

  1. 我设置了一个 ContentControl,因为我有一个 Canvas 和 Path,我为每个类别创建了不同的 Canvas 和 Path。
  2. 绑定路径是“Group.Key”以及 Category 的属性。
  3. 使用转换器来获取资源。
  4. 我创建了一个返回转换器的资源。

转换器

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>

在这种情况下,我们有以下有趣的代码。

  1. 控件是 NeverToggleButton,这是因为它与 Photos App 的行为相似,并且我设置了它永远不会被选中。
  2. Content Control 的 Style 类似于 Template Binding。
  3. 绑定属性是使用 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 可能有一些变化。

© . All rights reserved.