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

如何将扩展的 GridView 从 WinRT 升级到通用 Windows 平台(UWP)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (14投票s)

2015 年 10 月 8 日

CPOL

8分钟阅读

viewsIcon

24667

downloadIcon

890

本文描述了通用 Windows 平台 (UWP) 的扩展 GridView 控件的实现。

简介: 使用 GridView 显示磁贴式内容

Windows 8 开始屏幕引入了一种新的组织项目列表或应用程序导航区域的方法。Microsoft 的 GridView 控件是显示此类磁贴式内容的绝佳方式。GridView 可以显示不同大小的磁贴并为您对它们进行分组,或者它可以显示相同大小的非分组项并支持拖放。不幸的是,并非所有功能都可以默认启用。例如,并非所有项面板都支持拖放。如果您想要混合使用不同大小的项(即 VariableSizedWrapGrid),则需要某些项面板。此外,启用分组时不支持拖放。

这是对扩展 GridView 控件 GridViewEx 实现的第二次审视,该控件消除了这些限制。第一次尝试是针对 WinRT 平台,并提供了对启用分组和可变大小项的拖放功能的支持。您可以在此处找到之前的文章: 为分组和可变大小项扩展 GridView 的拖放功能

本文介绍了将 GridViewEx 控件和示例应用程序从 WinRT 升级到通用 Windows 平台 (UWP)。大多数拖放功能保持不变,因此我只介绍特定于平台的更改和新增功能。如果您对拖放的更多详细信息感兴趣,请阅读之前的文章并查看附加代码。

extended GridView with grouping, variable sized items and drag and drop support

将现有的 WinRT 应用程序升级到通用 Windows 平台 (UWP)

创建 UWP 应用程序

有许多博客文章和 MSDN 文章介绍了迁移现有项目,例如这篇文章。在许多情况下,从新的空白 UWP 项目开始会更容易。 然后,您可以决定要使用旧应用程序的哪些部分。 我们旧的示例使用了 LayoutAwarePageSuspensionManager 类,这些类已包含在 Windows 应用商店项目模板中。如果您有几个基于这些模板构建的项目,那么您可能希望进行最少的更改并重新使用其余代码。 因此,让我们向新的 UWP 应用程序添加一些旧的通用类。

  • Common/VisibilityConverter.cs
  • Common/LayoutAwarePage.cs
  • Common/SuspensionManager.cs

此外,让我们重新使用相同的数据和示例,因此请添加 DataModel 和 Samples 文件夹中的所有文件。

布局和导航中的更改

VisibilityConverterSuspensionsManager 类在使用 UWP 时无需更改。让我们看看布局和导航逻辑中发生了什么变化。

随着新的外形尺寸和广泛支持的设备,Microsoft 已弃用 ApplicationViewState 处理。UWP 平台提供了其他方法,例如AdaptiveTriggers 来构建自适应布局。因此,我们可以安全地删除所有关于处理 ApplicationViewStates 的代码。没有它,LayoutAwarePage 的名称对于剩余功能来说并不完美,但为了兼容性,我们可以将其保留原样。

在 WinRT 和 UWP 中,Microsoft 都建议在采用分层导航模式的应用中使用后退按钮。在桌面 WinRT 应用程序中,您需要将此按钮添加到自己的 XAML。在 UWP 中,您可以在桌面模式下启用标题栏后退按钮,并将其用法与移动或平板设备上的硬件后退按钮相同。您可以在此处找到 UWP 后退按钮指南:这里。UWP 方法看起来非常通用,不需要任何自定义 XAML,因此相关的代码是包含在不同 XAML 页面的基类中的一个不错的选择。让我们在 LayoutAwarePage 类中执行此操作。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // subscribe on Back button event
    if (IsWindowsPhoneDevice())
    {
        // use hardware button
        Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
    }
    else
    {
        // enable/disable window back button depending on navigation state
        var currentView = SystemNavigationManager.GetForCurrentView();
        currentView.AppViewBackButtonVisibility = this.Frame != null && this.Frame.CanGoBack ?
            AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed;
        currentView.BackRequested += backButton_Tapped;
    }
    ...
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    // unsubscribe from Back button event
    if (IsWindowsPhoneDevice())
    {
        Windows.Phone.UI.Input.HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
    }
    else
    {
        // unsubscribe from window back button
        var currentView = SystemNavigationManager.GetForCurrentView();
        currentView.BackRequested -= backButton_Tapped;
    }
    ...
// handle Back button events
private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
    if (this.Frame != null && this.Frame.CanGoBack)
    {
        e.Handled = true;
        this.Frame.GoBack();
    }
}
private void backButton_Tapped(object sender, BackRequestedEventArgs e)
{
    this.GoBack(this, new RoutedEventArgs());
}

设备后退按钮的处理仅包含在 **Windows 移动扩展程序 for UWP** 中,因此您需要将此引用添加到项目中。现在所有导航都由 LayoutAwarePage 处理,派生页面无需担心。

对该类的最后一个小补充是一个用于在运行时检测设备类型的静态方法。

public static bool IsWindowsPhoneDevice()
{
    if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
    {
        return true;
    }	
    return false;
}

其他与平台相关的更改

1. 如果您希望您的应用程序符合其他 Windows 10 应用程序的外观,请使用 Windows 10 ThemeResources

2. 与 Windows 8 版本相比,Microsoft 在 GridView 控件方面进行了一些更改,https://msdn.microsoft.com/en-us/library/windows/apps/mt188204.aspx#gridview。其中最主要的是将默认方向从水平更改为垂直。这需要更改控件模板和默认属性设置。 

3. 我没有找到任何关于此的说明,但似乎 VariableSizedWrapGrid 面板的行为也发生了变化。在所有示例中,您都应将其 Orientation 设置为 Horizontal,否则它将始终将所有项显示为单列。

4. 我在 VariableSizedWrapGrid (或者可能是 Microsoft 的 GridView 实现) 中发现的另一个关键更改是,它不像 Windows 8 那样自动根据项大小设置列和行跨度。现在这是应用程序的责任。这是在 Windows 8 中有效但在 Windows 10 中无效的 XAML。

<GridView  Grid.Row="1" Grid.Column="1" Margin="10" AllowDrop="True" CanReorderItems="True" CanDragItems="True" IsSwipeEnabled="True">
                <GridView.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VariableSizedWrapGrid/>
                    </ItemsPanelTemplate>
                </GridView.ItemsPanel>
                <Rectangle Height="100" Width="200" Fill="Blue" />
                <Rectangle Height="100" Width="100" Fill="Red" />
                <Rectangle Height="100" Width="100" Fill="Yellow" />
                <Rectangle Height="100" Width="100" Fill="Green" />

我找到的最佳解决方案是设置项上的 VariableSizedWrapGrid 附加属性,并将它们传播到派生自 GridView 的自定义控件中的 ListViewItemPresenter 元素。

/// <summary>
/// This class sets VariableSizedWrapGrid.ColumnSpanProperty for GridViewItem controls,
/// so that every item can have different size in the VariableSizedWrapGrid.
/// Also it sets VerticalContentAlignment and HorizontalContentAlignment to Stretch.
/// </summary>
public class GridViewTiled : GridView
{
    // set ColumnSpan according to the business logic (maybe some GridViewSamples.Samples.Item or group properties)
    protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
    {
        element.SetValue(ContentControl.HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch);
        element.SetValue(ContentControl.VerticalContentAlignmentProperty, VerticalAlignment.Stretch);
        UIElement el = item as UIElement;
        if (el != null)
        {
            int colSpan = Windows.UI.Xaml.Controls.VariableSizedWrapGrid.GetColumnSpan(el);
            int rowSpan = Windows.UI.Xaml.Controls.VariableSizedWrapGrid.GetRowSpan(el);
            if (rowSpan > 1)
            {
                // only set it if it has non-defaul value
                element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.RowSpanProperty, rowSpan);
            }
            if (colSpan > 1)
            {
                // only set it if it has non-defaul value
                element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, colSpan);
            }
        }
        base.PrepareContainerForItemOverride(element, item);
    }
}

这是在 UWP 中工作的 XAML(附加的 GridView 演示中的 Unbound 页面使用了它)。

<controls:GridViewTiled Grid.Row="1" Grid.Column="1" Margin="10" AllowDrop="True" CanReorderItems="True" CanDragItems="True" >
    <controls:GridViewTiled.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid ItemHeight="100" ItemWidth="100" Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </controls:GridViewTiled.ItemsPanel>
    <Rectangle VariableSizedWrapGrid.ColumnSpan="2" VariableSizedWrapGrid.RowSpan="2" Fill="Blue" />
    <Rectangle Fill="Red" />
    <Rectangle Fill="Yellow" />
    <Rectangle Fill="Green" />

NewGroupPlaceholder 控件

GridViewEx 控件的 WinRT 版本使用简单的边框作为新组的占位符。它不允许在拖放操作期间更改其外观。为了使其更用户友好并在拖动时高亮显示放置区域,让我们添加 NewGroupPlaceholder 控件,该控件具有在拖动时切换视觉状态的非常简单的逻辑。

GridViewEx - NewGroupPlaceholder

代码非常简单;您可以在附加的示例中找到它。默认控件模板使用系统强调色来高亮显示放置区域。

<Style TargetType="local:NewGroupPlaceholder">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="Margin" Value="8" />
    <Setter Property="Height" Value="32" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:NewGroupPlaceholder">
                <Border x:Name="root" Background="{TemplateBinding Background}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="DragStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="DragOver">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="dragOverElement"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Border x:Name="dragOverElement" Background="{ThemeResource SystemControlHighlightListAccentLowBrush}" Opacity="0"/>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

GridViewEx 控件中的更改

现在让我们看看为了让 GridViewEx 控件在 UWP 下工作,我们需要对其进行哪些更改。

GridViewEx 控件的大部分代码在 UWP 中的工作方式与 WinRT 完全相同。唯一关键的是在 OnDragOver 重写中设置 DragEventArgs.AcceptedOperation 属性的必要性。显然,UWP 中默认的 GridView 实现会将此属性重置为 None 以支持非项面板。因此,如果您不在 OnDragOver 重写中再次设置它,Drop 事件将永远不会发生。 幸运的是,这只有一行代码。

protected override void OnDragOver(DragEventArgs e)
{
    int newIndex = GetDragOverIndex(e);
    if (newIndex >= 0)
    {
        e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;

如果现在构建应用程序,您会看到大量关于已弃用的 ItemContainerGenerator 方法的编译器警告。修复它很容易,只需改用 ItemsControl 类的相应方法即可。

为了解决 VariableSizedWrapGrid 的限制,请添加上面描述的相同的 PrepareContainerForItemOverride 方法。

最后要做的就是使用新的垂直方向和 NewGroupPlaceholder 控件更新默认的 GridViewEx 控件样式。您可以在附加示例的 generic.xaml 文件中找到它。

使其更方便

在为本文编写示例时,我发现了标准 GridView 控件中的另一个扩展点。如果您需要自定义单个项的 UI 元素,在许多情况下,您应该继承自 GridView 并重写 PrepareContainerForItemOverride 方法。为了避免这种情况,我在 GridViewEx 控件中添加了新的 PreparingContainerForItem 事件。此事件的事件参数同时包含数据对象和其 UI 容器,因此您可以根据需要调整 UI 元素属性,例如:

/// <summary>
/// Set column spans depending on group id.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void gve_PreparingContainerForItem(object sender, GridViewEx.PreparingContainerForItemEventArgs e)
{
    try
    {
        Item it = e.Item as Item;
        if (it != null)
        {
            e.Element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, it.GroupId % 2 + 1);
        }
    }
    catch
    {
        e.Element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
    }
}

最后一点是将 GridViewEx 控件和相关的可重用类(NewGroupPlaceholderVisibilityConverter)打包成一个自定义控件程序集。

特定于设备的 UI

有几种方法可以实现自适应布局。在此示例中,我主要想做的是更改移动版本的项大小。不幸的是,我找不到使用自适应触发器更改 GridViewItemsPanelTemplate 中使用的 VariableSizedWrapGrid ItemHeightItemWidth 属性的方法。可以包含 2 个具有不同设置的 GridView 实例,并根据屏幕大小更改它们的可见性;您可以尝试这样做。我个人认为,使用特定于设备的 XAML 页面更正确,因为它允许应用程序仅下载特定于设备的 XAML,从而节省一些资源。在这种情况下,您可以自由地对布局进行比自适应触发器更多的更改。因此,让我们在项目中创建 Samples\DeviceFamily-Mobile 文件夹,并在其中为 BoundUnboundGrouped 示例添加具有略微不同外观的 XAML。Grouped 示例显示单个 GridView 控件,因此移动页面可以更改磁贴大小和一些填充,仅此而已。

BoundUnbound 示例并排显示 2 个 GridView 控件。在小屏幕上,它看起来太拥挤,并且无法很好地显示所有细节。让我们使用 Pivot 控件一次只显示一个 GridView 控件,并允许在 2 个控件之间进行导航。您可以在附加的示例中找到源 XAML。请注意,代码隐藏文件对于桌面和移动页面是相同的,您无需执行任何特殊操作即可运行其中一个或另一个 XAML,环境会自动为您处理。

对于 Customized 示例,让我们尝试一种不同的方法,通过为磁贴大小添加依赖属性。

public double TileSize
{
    get { return (double)GetValue(TileSizeProperty); }
    set { SetValue(TileSizeProperty, value); }
}
public static readonly DependencyProperty TileSizeProperty =
    DependencyProperty.Register(nameof(TileSize), typeof(double), typeof(Customized), new PropertyMetadata(100));
public Customized()
{
    if (IsWindowsPhoneDevice())
    {
        TileSize = 72;
    }
    this.InitializeComponent();
}

GridViewGridViewEx 控件部分可以从 XAML 绑定到此属性。

<GroupStyle.Panel>
    <ItemsPanelTemplate>
        <VariableSizedWrapGrid ItemHeight="{Binding TileSize, ElementName=pageRoot}"
                               ItemWidth="{Binding TileSize, ElementName=pageRoot}"
                               Orientation="Horizontal" MaximumRowsOrColumns="10"/>
    </ItemsPanelTemplate>
</GroupStyle.Panel>

已知问题

1. Windows SDK 10.0.10240.0 中安装的移动模拟器在触摸交互方面存在问题。附加示例在使用鼠标时(无论模拟器输入设置如何)效果很好,但使用真实触摸交互拖动东西非常困难。默认的 GridView 设置也存在同样的问题,所以这显然是 Microsoft GridView 实现或模拟器问题中的一个问题。也许这与此处描述的相同问题有关:https://github.com/Microsoft/Windows-universal-samples/issues/131

2. 如果将 GridViewEx 设置为值类型对象的集合,则在从 DragEventArgs 中提取数据时会收到 System.ExecutionEngineException。如果您确实需要值类型,请将其包装在某些引用类型中。例如,WinRT 的 Bound 示例使用整数集合。

ObservableCollection<int> coll = new ObservableCollection<int>();
 for (int i = 1; i <= 31; i++)
 {
     coll.Add(i);
 }

在 DataTemplate 中使用项

<DataTemplate x:Key="ItemTemplate">
    <Border BorderBrush="Aqua" BorderThickness="1" Background="Peru">
        <Grid Margin="12">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock Text="{Binding}"/>
            <TextBlock Grid.Row="1">item</TextBlock>
        </Grid>
    </Border>
</DataTemplate>

在 UWP 版本中,我们可以将其替换为匿名类型。

ObservableCollection<object> coll = new ObservableCollection<object>();
for (int i = 1; i <= 31; i++)
{
    coll.Add(new { Id = i }); // add anonymous type object
}

更新后的 XAML

<DataTemplate x:Key="ItemTemplate">
    <Border BorderBrush="Aqua" BorderThickness="1" Background="Peru">
        <Grid Margin="12">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock Text="{Binding Id}"/>
            <TextBlock Grid.Row="1">item</TextBlock>
        </Grid>
    </Border>
</DataTemplate>

摘要

自定义的 GridViewEx 控件使我们能够结合 GridView 控件的几个有用功能。利用更多功能使我们能够提供用户体验,这些用户体验正成为通用 Windows 平台应用程序开发的新常态。

上面描述的附加 GridView demo 最初是为了研究目的而制作的,看起来不太真实。带有动态磁贴的第二个示例显示了不同大小的旋转磁贴中的图像。要运行它,您需要下载新的 ComponentOne 的 UWP Beta 版。非常感谢 Flickr 没有要求 API 密钥并使其成为可能。

live tiles

历史

·         2015 年 10 月 9 日:第一个版本。

·         2015 年 10 月 22 日:添加了控件程序集、PreparingContainerForItem 事件、带有动态磁贴的示例。

 

© . All rights reserved.