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

使用 WPF DataGrid 扩展拖放功能以进行分组和可变大小项

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (23投票s)

2013 年 1 月 30 日

CPOL

11分钟阅读

viewsIcon

141941

downloadIcon

8101

本文描述了一个可实现拖放、分组和可变大小项的扩展 GridView 控件的实现。

引言

GridView 是一个很棒的控件,可以以多种不同方式用于在你的 Windows 应用商店应用中显示磁贴式内容。如果你最近看过任何 WinRT 应用,甚至 Microsoft 合作伙伴网站,你都会认识到在 Windows 世界的用户界面设计中,磁贴的普及性。磁贴提供了一种简单、时尚的方式来组织应用的项目列表或导航区域。也许磁贴式内容最好的例子就是 Windows 8 的开始屏幕本身。它以可调整大小的磁贴显示你的每个应用,用户可以根据自己的意愿重新排列和分组它们。

与原生应用一样,我们开发者希望在自己的应用中模拟相同的体验。这种模仿可以追溯到 Windows 的早期,并且一直是用户界面的一个持续的方法。如果你正试图在自己的 Windows 应用商店应用中模拟 Windows 8 的开始屏幕,GridView 控件是一个很好的起点。

GridView 可以显示可变大小的磁贴并为你进行分组,或者它可以显示相同大小的非分组项,并支持拖放。不幸的是,你不能默认拥有所有功能。例如,你无法为所有项面板启用拖放。如果你想要混合不同大小的项(例如,VariableSizedWrapGrid),则需要某些项面板。启用分组后也不支持拖放。

本文描述了一个扩展的 GridView 控件 GridViewEx 的实现,该控件消除了这些限制。提供的示例使你能够在支持分组和可变大小项的 GridView 中实现拖放。

如果你正在为 Windows 10 下的通用 Windows 平台 (UWP) 进行开发,请使用新文章中更新的版本:如何将扩展的 GridView 从 WinRT 升级到通用 Windows 平台 (UWP)

背景

首先,让我们看看如何在最简单的情况下启用拖放。在这里,我们有一个设置了最少属性的 GridView 和一个非常基础的 ItemTemplate。要启用拖放重新排序,你需要做三件事:

  1. AllowDrop 属性设置为 true
  2. CanReorderItems 属性设置为 true
  3. 绑定到支持数据修改(或专门是重新排序)的数据源。例如,你可以使用 ObservableCollectionIList注意:未绑定的 GridView 也支持重新排序)。
<GridView ItemsSource="{Binding}" AllowDrop="True" CanReorderItems="True">
    <GridView.ItemTemplate>
        <DataTemplate>
            <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>
    </GridView.ItemTemplate>
</GridView>

你会注意到,我们非常轻松地在 GridView 中获得了一定程度的拖放支持。

如前所述,对于绑定和未绑定场景,启用拖放都有一些主要的限制。具体来说,你不能启用分组,也不能混合使用可变大小的项。如果你查看 Windows 8 开始屏幕,你会注意到它有分组、不同大小的项和拖放。如果你真的想模仿这种体验,你将需要结合其中两到三种功能。我们如何在 GridView 中实现所有这些功能?我们需要扩展该控件以支持这些其他场景。现在让我们看一下 GridViewEx 控件。

GridViewEx 控件

GridViewEx 控件实现了常规 GridView 控件不支持情况下的拖放功能。

  • 对于 WrapGridStackPanelVirtualizingStackPanel 以外的项面板
  • 启用分组时

它还允许在用户将某个项拖放到控件的最左边或最右边边缘时,向底层数据源添加新组。

拖动代码

让我们看看控件的实现以及我们如何处理拖动项。

public class GridViewEx : GridView
{
    /// <summary>
    /// Initializes a new instance of the <see cref="GridViewEx"/> control.
    /// </summary>
    public GridViewEx()
    {
        // see attached sample
    }
 
    private void GridViewEx_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
    {
        // see attached sample
    }
 
    /// <summary>
    /// Stores dragged items into DragEventArgs.Data.Properties["Items"] value.
    /// Override this method to set custom drag data if you need to.
    /// </summary>
    protected virtual void OnDragStarting(DragItemsStartingEventArgs e)
    {
        // see attached sample
    }

该控件有几个字段,用于存储拖放过程中几个活动项的索引。OnDragStarting 事件将拖动的项存储在 DragEventArgs.Data.Properties["Items"] 值中。如果需要,你可以重写此方法以设置自定义拖动数据。

当用户拖动项时,我们需要显示提示,告知用户该项如果被放下将放置在哪里。标准的 GridView 通过将相邻项移开来处理此问题。我们将在 GridViewEx 中自己实现完全相同的行为,因为我们需要考虑 GridView 不支持放置的情况。

/// <summary>
/// Shows reoder hints while custom dragging.
/// </summary>
protected override void OnDragOver(DragEventArgs e)
{
    // see attached sample }

private int GetDragOverIndex(DragEventArgs e)
{
    // see attached sample 
}

OnDragOver 在项被拖到相邻项上方时应用重新排序提示。相邻项是从 GetIntersectingItems 方法计算的。根据每个项的位置,有五种可能的 ReorderHintStates 可以设置。

  • 无重新排序提示
  • 底部重新排序提示
  • 顶部重新排序提示
  • 右侧重新排序提示
  • 左侧重新排序提示

放置代码

接下来,让我们看看处理放置的代码。

我们必须重写 GridView.OnDrop 方法,该方法在用户将项放置到新位置时被调用。我们的重写处理标准 GridView 不支持放置的任何 ItemsPanel 的放置。

/// <summary>
/// Handles drag and drop for cases when it is not supported by the Windows.UI.Xaml.Controls.GridView control
/// </summary>
protected override async void OnDrop(DragEventArgs e)
{
    // see attached sample
} 

OnDrop 方法包含在启用分组时将项从一个组移动到另一个组,以及在用户操作请求新组创建时的逻辑。

添加新组

如果 GridView 绑定到 CollectionViewSource 并且 IsSourceGrouped 属性设置为 true,则 GridView 支持分组。这意味着分组逻辑应该在数据源级别实现,而 GridView 无法访问它。在这里,我们看到要在拖放操作期间添加新组,我们需要比标准的 Drop 事件更多一些东西。GridViewEx.BeforeDrop 事件允许我们处理这种情况,并提供更多信息,包括原始 DragEventArgs 数据。

BeforeDrop 事件在用户执行放置操作之前发生。

/// <summary>
/// Occurs before performing drop operation,
/// </summary>
public event EventHandler<BeforeDropItemsEventArgs> BeforeDrop;
/// <summary>
/// Rises the <see cref="BeforeDrop"/> event.
/// </summary>
/// <param name="e">Event data for the event.</param>
protected virtual void OnBeforeDrop(BeforeDropItemsEventArgs e)
{
    // see attached sample 
} 

BeforeDropItemEventArgs 携带有关被拖动项的重要信息,以便以后在 OnDrop 事件中访问。

/// <summary>
/// Provides data for the <see cref="GridViewEx.BeforeDrop"/> event.
/// </summary>
public sealed class BeforeDropItemsEventArgs : System.ComponentModel.CancelEventArgs
{
    /// <summary>
    /// Gets the item which is being dragged.
    /// </summary>
    public object Item
    {
        get;
    }
    /// <summary>
    /// Gets the current item index in the underlying data source.
    /// </summary>
    public int OldIndex
    {
        get;
    }
    /// <summary>
    /// Gets the index in the underlying data source where
    /// the item will be inserted by the drop operation.
    /// </summary>
    public int NewIndex
    {
        get;
    }
    /// <summary>
    /// Gets the bool value determining whether end-user actions requested
    /// creation of the new group in the underlying data source.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    /// <remarks>
    /// If this property is true, create the new data group and insert it into
    /// the groups collection at the positions, specified by the 
    /// <see cref="BeforeDropItemsEventArgs.NewGroupIndex"/> property value.
    /// Then the <see cref="GridViewEx"/> will insert dragged item
    /// into the newly added group.
    /// </remarks>
    public bool RequestCreateNewGroup
    {
        get;
    }
    /// <summary>
    /// Gets the current item data group index in the underlying data source.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    public int OldGroupIndex
    {
        get;
    }
    /// <summary>
    /// Gets the data group index in the underlying data source
    /// where the item will be inserted by the drop operation.
    /// This property only makes sense if GridViewEx.IsGrouping property is true.
    /// </summary>
    public int NewGroupIndex
    {
        get;
    }
    /// <summary>
    /// Gets the original <see cref="DragEventArgs"/> data. 
    /// </summary>
    public DragEventArgs DragEventArgs
    {
        get;
    }
} 

AllowNewGroup 属性决定了当用户将项拖放到控件的最左边或最右边边缘时是否应创建新组。这在标准 GridView 的任何情况下都不支持,因此它是 GridViewEx 类的一个不错附加功能。

/// <summary>
/// Gets or sets the value determining whether new group should be created at 
/// dragging the item to the empty space.
/// </summary>
public bool AllowNewGroup
{
    get { return (bool)GetValue(AllowNewGroupProperty); }
    set { SetValue(AllowNewGroupProperty, value); }
}
 
/// <summary>
/// Identifies the <see cref="AllowNewGroup"/> dependency property.
/// </summary>
public static readonly DependencyProperty AllowNewGroupProperty =
        DependencyProperty.Register("AllowNewGroup", typeof(bool), 
        typeof(GridViewEx), new PropertyMetadata(false));

要允许通过拖放操作创建新组,你应该将 AllowNewGroup 属性设置为 true。要处理将新组添加到数据层,你应该处理 GridViewEx.BeforeDrop 事件。事件参数有助于确定项的原始位置和目标位置。在 BeforeDrop 事件处理程序中,你可以创建新的数据组,并将其插入到参数的 NewGroupIndex 属性指定的组的集合中的位置。

添加新组功能所需的最后一件事是扩展默认的 GridView 控件模板。我们需要一个填充器或占位符,用户可以将项拖放到其中以创建新组。GridViewEx 控件模板支持在用户将项拖放到控件的最左边或最右边边缘时添加新组。因此,ItemsPresenter 两端的两个边框元素是新组的占位符。

来自 generic.xamlGridViewEx 控件模板。

<Style TargetType="local:GridViewEx">
    <Setter Property="Padding" Value="0,0,0,10" />
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="TabNavigation" Value="Once" />
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Enabled" />
    <Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" />
    <Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled" />
    <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="False" />
    <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" />
    <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" />
    <Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True" />
    <Setter Property="IsSwipeEnabled" Value="True" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:GridViewEx">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        Background="{TemplateBinding Background}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer x:Name="ScrollViewer"
                            TabNavigation="{TemplateBinding TabNavigation}"
                            HorizontalScrollMode="
                            {TemplateBinding ScrollViewer.HorizontalScrollMode}"
                            HorizontalScrollBarVisibility=
                              "{TemplateBinding 
                              ScrollViewer.HorizontalScrollBarVisibility}"
                            IsHorizontalScrollChainingEnabled=
                              "{TemplateBinding 
                              ScrollViewer.IsHorizontalScrollChainingEnabled}"
                            VerticalScrollMode="
                            {TemplateBinding ScrollViewer.VerticalScrollMode}"
                            VerticalScrollBarVisibility=
                              "{TemplateBinding 
                              ScrollViewer.VerticalScrollBarVisibility}"
                            IsVerticalScrollChainingEnabled=
                              "{TemplateBinding 
                              ScrollViewer.IsVerticalScrollChainingEnabled}"
                            IsHorizontalRailEnabled="
                            {TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
                            IsVerticalRailEnabled="
                            {TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
                            ZoomMode="{TemplateBinding 
                            ScrollViewer.ZoomMode}"
                            IsDeferredScrollingEnabled="
                            {TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
                            BringIntoViewOnFocusChange="
                            {TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}">
                        <StackPanel Orientation="Horizontal">
                            <Border Width="60" 
                            x:Name="NewGroupPlaceHolderFirst" 
                                    Background="Transparent" 
                                    Padding="{TemplateBinding Padding}" 
                                    Visibility="{Binding AllowNewGroup, 
                                    Converter={StaticResource 
                                      VisibilityConverter}, 
                                      RelativeSource={RelativeSource TemplatedParent}}"/>
                            <ItemsPresenter 
                                Header="{TemplateBinding Header}" 
                                HeaderTemplate="{TemplateBinding HeaderTemplate}"
                                HeaderTransitions="{TemplateBinding HeaderTransitions}"
                                Padding="{TemplateBinding Padding}"/>
                            <Border Width="60" 
                            x:Name="NewGroupPlaceHolderLast" 
                                    Background="Transparent" 
                                    Padding="{TemplateBinding Padding}" 
                                    Visibility="{Binding AllowNewGroup, 
                                    Converter={StaticResource 
                                      VisibilityConverter}, 
                                      RelativeSource={RelativeSource TemplatedParent}}"/>
                        </StackPanel>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

这些新的模板部分也应该在我们的代码中定义。

[TemplatePart(Name = GridViewEx.NewGroupPlaceHolderFirstName, Type = typeof(FrameworkElement))]
[TemplatePart(Name = GridViewEx.NewGroupPlaceHolderLastName, Type = typeof(FrameworkElement))]
public class GridViewEx : GridView
{
    private const string NewGroupPlaceHolderFirstName = "NewGroupPlaceHolderFirst";
    private FrameworkElement _newGroupPlaceHolderFirst;
 
    private const string NewGroupPlaceHolderLastName = "NewGroupPlaceHolderLast";
    private FrameworkElement _newGroupPlaceHolderLast;
 
    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _newGroupPlaceHolderFirst = 
          GetTemplateChild(NewGroupPlaceHolderFirstName) as FrameworkElement;
        _newGroupPlaceHolderLast = 
          GetTemplateChild(NewGroupPlaceHolderLastName) as FrameworkElement;
    }

使用和扩展 GridViewEx

现在,我们有一个拖放 GridView 解决方案,起初它看起来与标准的 GridView 相同。我们的目标是让它更像 Windows 8 的开始屏幕。让我们来讨论如何实现以下功能,这些功能在默认情况下是无法支持的。

  • 可变大小的项
  • 分组
  • 添加新组
  • 跨会话保存布局

可变大小的项

Windows 8 的开始屏幕显示各种大小的磁贴(好吧,实际上是两种大小)。如果你尝试在默认的 GridViewGridViewEx 中创建不同大小的项,它将不起作用。这是因为 GridView 使用 WrapGrid 作为其默认 ItemsPanelWrapGrid 创建一个统一的布局,其中每个项大小相同。因此,Microsoft 还包含了一个 VariableSizedWrapGrid,顾名思义,它支持不同大小的项。

GridViewEx 控件的好处在于,你可以使用 VariableSizedWrapGrid 并仍然保留拖放支持。要使用 VariableSizedWrap 网格并显示各种大小的项,你必须做两件事:

  1. GridViewEx.ItemsPanel 设置为 VariableSizedWrapGrid 的实例。
  2. 重写 GridView 上的 PrepareContainerForItemOverride 方法。在此方法中,你可以设置项的 RowSpanColumnSpan 属性来指示其大小。

这意味着我们需要用一个名为 MyGridView 的新控件来扩展 GridViewEx 控件。为什么扩展 GridViewEx 而不是简单地在 GridViewEx 类中重写 PrepareContainerForItemOverride?因为指定项大小的逻辑应该在你的数据模型中,而不是在控件本身中。也就是说,如果你想让 GridViewEx 成为一个通用的控件,可以在多个地方使用。

例如,如果你希望某些项显得更大,你会在数据项上创建一个属性,该属性返回大于 1 的整数值,并使用它来设置 RowSpanColumnSpan 属性。

public class Item
{
    public int Id { get; set; }
    public int ItemSize { get; set; }
    /* */
}

在这里,当我们创建每个项时,我们将 ItemSize 设置为 12,分别表示常规(1)或较大(2)。较大的项的 ColumnSpan 属性将设置为 2,以便它们水平占用两个项的空间。你也可以设置 RowSpan 来使项在垂直方向上也更大。

/// <summary>
/// This class sets VariableSizedWrapGrid.ColumnSpanProperty for GridViewItem controls, 
/// so that every item can have different size in the VariableSizedWrapGrid.
/// </summary>
public class MyGridView : GridViewSamples.Controls.GridViewEx
{
    // 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)
    {
        try
        {
            GridViewSamples.Samples.Item it = item as GridViewSamples.Samples.Item;
            if (it != null)
            {
                element.SetValue(
                  Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, it.ItemSize);
            }
        }
        catch
        {
            element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
        }
        finally
        {
            base.PrepareContainerForItemOverride(element, item);
        }
    }
}

现在让我们创建一个 MyGridView 实例并将其绑定到一个项的集合。

<local:MyGridView AllowDrop="True" CanReorderItems="True" 
          CanDragItems="True" IsSwipeEnabled="True"
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource ItemTemplate}" >
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid ItemHeight="160" 
            ItemWidth="160" />
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
            <Setter Property="HorizontalContentAlignment" 
            Value="Stretch"/>
            <Setter Property="VerticalContentAlignment" 
            Value="Stretch"/>
        </Style>
    </GridView.ItemContainerStyle>
</local:MyGridView>

在初始化集合的代码中,我们将为某些项(业务逻辑可以决定这一点)将 ItemSize 属性设置为 2,从而表示一个跨越两列的较大磁贴。

分组

使用 GridViewEx 控件,我们可以一起启用分组和拖放。你可以像启用标准 GridView 的分组一样对 GridViewEx 控件进行分组。事实上,你可能用它开始你的应用程序的 Grid 应用模板就使用了分组。你可以通过做两件事来实现分组:

  1. GridView 绑定到具有启用分组的数据源的 CollectionViewSource。也就是说,数据源应该包含数据组,例如每个项包含子项的集合。CollectionViewSource 作为集合类的代理来启用分组。
  2. 指定一个 GroupStyle 来确定组的显示方式。GroupStyle 包括一个 HeaderTempate 和一个 Panel,用于指定组中的子项如何排列。

可选地,你可以指定 GroupStyle.ContainerStyle。这会修改组容器的外观。例如,你可以在每个组周围添加一个边框。让我们在 GridViewExMyGridView 实现中添加分组。请记住,我们扩展了 GridViewEx 以添加可变大小项的业务逻辑。

<local:MyGridView AllowDrop="True" CanReorderItems="True" 
          CanDragItems="True" IsSwipeEnabled="True"
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource ItemTemplate}" >
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <Grid Background="LightGray" 
                    Margin="0">
                        <TextBlock Foreground="Black" 
                        Margin="10" 
                                  Style="{StaticResource 
                                  GroupHeaderTextStyle}">
                            <Run Text="{Binding Id}"/>
                            <Run Text=" group"/>
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>

            <GroupStyle.ContainerStyle>
                <Style TargetType="GroupItem">
                    <Setter Property="BorderBrush" 
                    Value="DarkGray"/>
                    <Setter Property="BorderThickness" 
                    Value="2"/>
                    <Setter Property="Margin" 
                    Value="3,0"/>
                </Style>
            </GroupStyle.ContainerStyle>

            <GroupStyle.Panel>
                <ItemsPanelTemplate>
                    <VariableSizedWrapGrid ItemHeight="160" 
                    ItemWidth="160" />
                </ItemsPanelTemplate>
            </GroupStyle.Panel>
        </GroupStyle>
    </GridView.GroupStyle>

    <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
            <Setter Property="HorizontalContentAlignment" 
            Value="Stretch"/>
            <Setter Property="VerticalContentAlignment" 
            Value="Stretch"/>
        </Style>
    </GridView.ItemContainerStyle>
</local:MyGridView>

值得注意的是,当启用分组并指定 GroupStyle 时,ItemsPanel 具有新的含义。我们将 ItemsPanelVariableSizedWrapGrid 更改为 VirtualizingStackPanel。通过分组,ItemsPanel 指的是组在 GridView 中的排列方式。由于我们希望支持每个组内不同大小的项,因此我们将 VariableSizedWrapGrid 移到了 GroupStyle.Panel 模板中。

运行示例,你会注意到我们有了分组、可变大小的项,现在还可以通过自定义 GridViewEx 控件在组之间进行拖放。

添加新组

自定义 GridViewEx 控件还增加了在用户将项拖放到控件的最左边和最右边边缘时添加新组的支持。要允许创建新组,请将 AllowNewGroup 属性设置为 true。然后,要处理将新组添加到数据层,请处理 GridViewEx.BeforeDrop 事件。事件参数有助于确定项的原始位置和目标位置。在 BeforeDrop 事件处理程序中,你可以创建新的数据组,并将其插入到参数的 NewGroupIndex 属性指定的组的集合中的位置。之所以将此留给开发人员,是因为 GridViewEx 控件对你的数据结构一无所知。

/// <summary>
/// Creates new CollectionViewSource and updates page DataContext.
/// </summary>
private void UpdateDataContext()
{
    CollectionViewSource source = new CollectionViewSource();
    source.Source = _groups;
    source.ItemsPath = new PropertyPath("Items");
    source.IsSourceGrouped = true;
    this.DataContext = source;
}
// creates new group in the data source,
// if end-user drags item to the new group placeholder
private void MyGridView_BeforeDrop(object sender, Controls.BeforeDropItemsEventArgs e)
{
    if (e.RequestCreateNewGroup)
    {
        // create new group and re-assign datasource 
        Group group = Group.GetNewGroup();
        if (e.NewGroupIndex == 0)
        {
            _groups.Insert(0, group);
        }
        else
        {
            _groups.Add(group);
        }
        UpdateDataContext();
    }
}

我们还可以使用 Drop 事件来清除任何空的组。

// removes empty groups (except the last one)
private void MyGridView_Drop(object sender, DragEventArgs e)
{
    bool needReset = false;
    for (int i = _groups.Count - 1; i >= 0; i--)
    {
        if (_groups[i].Items.Count == 0 && _groups.Count > 1)
        {
            _groups.RemoveAt(i);
            needReset = true;
        }
    }
    if (needReset)
    {
        UpdateDataContext();
    }
}

跨会话保存布局

在 Windows 8 应用中,当用户切换离开时,应用程序可能会被挂起或终止。为了获得更好的用户体验,我们的示例会在用户导航到另一页或应用程序被停用时存储当前布局。在本示例中,我们使用最简单的数据序列化为 JSON 字符串。根据你的数据结构、数据大小和需求,你可以将数据保存为其他格式并存储到其他位置。在我们的例子中,保存底层业务对象集合就足够了。

为了保存页面布局,我们重写了 LayoutAwarePage 方法(请参阅代码中的注释)。

/// <summary>
/// Populates the page with content passed during navigation.  Any saved state is also
/// provided when recreating a page from a prior session.
/// </summary>
/// <param name="navigationParameter">The parameter value passed to
/// <see cref="Frame.Navigate(Type, 
/// Object)"/> when this page was initially requested.
/// </param>
/// <param name="pageState"
/// >A dictionary of state preserved by this page during an earlier
/// session.  This will be null the first time a page is visited.</param>
protected override void LoadState(Object navigationParameter, 
	Dictionary<String, Object> pageState)
{
    base.LoadState(navigationParameter, pageState);
    if (pageState != null && pageState.Count > 0 
    && pageState.ContainsKey("Groups"))
    {
        // restore groups and items from the previously serialized state
        System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer = 
        new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<Group>));
        var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes
        	((string)pageState["Groups"]));
        _groups = (List<Group>)rootSer.ReadObject(stream);
    }
    else
    {
        // if we get here for the first time and don't have
        // serialized content, fill groups and items from scratch
        for (int j = 1; j <= 12; j++)
        {
            Group group = Group.GetNewGroup();
            for (int i = 1; i <= 7 + j % 3; i++)
            {
                group.Items.Add(new Item()
                {
                    Id = i,
                    GroupId = group.Id
                });
            }
            _groups.Add(group);
        }
    }
    UpdateDataContext();
}

/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache.  Values must conform to the serialization
/// requirements of <see cref="SuspensionManager.SessionState"/>.
/// </summary>
/// <param name="pageState">
/// An empty dictionary to be populated with serializable state.</param>
protected override void SaveState(Dictionary<String, Object> pageState)
{
    // save groups and items to JSON string so that 
    // it's possible to restore page state later
    base.SaveState(pageState);
    System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer = 
        new System.Runtime.Serialization.Json.DataContractJsonSerializer
        (typeof(List<Group>));
    var stream = new MemoryStream();
    rootSer.WriteObject(stream, _groups);
    string str = System.Text.Encoding.UTF8.GetString(stream.ToArray(), 
    		0, (int)stream.Length);
    pageState.Add("Groups", str);
}

/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes 
/// how this page was reached.  The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // restore page state
    var frameState = 
    GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
    if (frameState.ContainsKey("TilePageData"))
    {
        this.LoadState(e.Parameter, 
        (Dictionary<String, Object>)frameState["TilePageData"]);
    }
    else
    {
        this.LoadState(e.Parameter, null);
    }
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    // save page state with "TilePageData" key
    var frameState = 
    GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    this.SaveState(pageState);
    frameState["TilePageData"] = pageState;
}

上面的代码会将布局序列化到页面状态。然后,我们的示例应用程序使用 SuspensionManager 将此信息与应用状态一起保存。你可以在附加的示例中找到完整的代码。如果你需要有关保存应用状态的更多信息,请从 MSDN 文章 管理应用生命周期和状态 开始。

摘要

自定义 GridViewEx 控件使我们能够组合 GridView 控件的几个有用功能。利用更多功能可以帮助我们提供成为 Windows 应用商店应用开发新常态的用户体验。

我们还可以对 GridView 项做更多事情,使其行为更像 Windows 8 的开始屏幕。例如,开始磁贴是“活跃”的,因为它们会轮播更新的内容。你可以利用第三方磁贴库,例如 ComponentOne Tiles, 并在 GridView 中使用它们,以提供翻转和旋转以显示实时数据的动态磁贴。还附带了第二个带有动态磁贴的示例,其中展示了 C1Tile 控件与 GridViewEx 控件的结合使用。

更新历史

  • 2013 年 1 月 31 日:第一个版本
  • 2013 年 3 月 26 日:添加了最简单的数据持久性,以跨会话存储屏幕布局
  • 2013 年 5 月 16 日:增加了对组之间的新组占位符的支持。要启用它,GroupStyle.ContainerStyle 样式应定义自定义控件模板,其中包含一个名为 NewGroupPlaceHolder 的元素(或在附加示例中取消注释它)。
  • 2015 年 9 月 2 日:示例升级到 Windows 8.1
  • 2015 年 10 月 9 日:添加了指向新 UWP 版本(Windows 10)的链接
使用拖放功能扩展 GridView 以支持分组和可变大小的项 - CodeProject - 代码之家
© . All rights reserved.