Windows 8.1下ListView和GridView的数据分步显示

通常基于ListViewBase类的ListView和GridView需要处理大量的数据项(date Item),如果处理不好的话,会对应用的性能产生严重影响。虽然UI virtualization帮助解决了显示大量Item时内存占用过多及初始化时间过长的问题,在用户进行平移操作的时候,如果每一个Item的内容比较复杂,仍然会出现Item加载过慢的问题,就好像幻灯片一样,每个Item一个一个的显示出来,使应用的流畅度大打折扣。为了进一步提高用户体验,Windows 8.1下为这两个控件的显示效果作了优化,主要表现在两个方面:

  1. 为基于ListViewBase类的控件的每个Item都添加了占位符,也就是说,当显示Item的时候,在数据没有加载完成之前,系统首先为每个Item都自动显示一个缺省的图标。您可以使用ShowsScrollingPlaceholders属性来启用或禁止这项功能。
  2.  为基于ListViewBase类的控件添加了一个新的事件(Event):ContainerContentChanging。这个Event会在加载的内容被改变的时候触发。您可以在这个Event的事件处理函数中加入代码来优化对Item的显示。

第一种方法的实现很简单,这里就不多说了,这里我就第二种方法做进一步的介绍。

ContainerContentChanging会在每一个Item被加载的时候触发,这时候你可以选择分步加载Item中的内容。比如说,我的GridView中有三个加载项:

<DataTemplate x:Key="ItemDataTemplate">

    <Grid>

     <Grid.RowDefinitions

          <RowDefinition Height="*"/>

          <RowDefinition Height="160"/>

          <RowDefinition Height="*"/>

      </Grid.RowDefinitions>

      <TextBlock x:Name="tbkName" Text="{Binding Name}" />

      <Image x:Name="imgPhoto" Source="{Binding ImageUri}" Grid.Row="1" />

       <TextBlock x:Name="tbkTakenTime" Text="{Binding TakenTime}"Grid.Row="2"/>

    </Grid>

</DataTemplate>

那么我们可以设定第一步先加载Name,第二步再加载TakenTime,最后一步才加载Photo。如果ShowsScrollingPlaceholders设为True的话,那么实际上在第一步之前系统会自动为每个Item加上一个灰色的占位符。如果你觉得这个占位符不好看,你也可以选择将这个属性设为False,然后手动的在ContainerContentChanging的第一步中加入占位符。那么这时候加载项就变为四个:

<Image x:Name="imgPlaceholder" Grid.RowSpan="3" Source="ms-appx:///Placeholder.jpg" />

<TextBlock x:Name="tbkName" Text="{Binding Name}" />

<Image x:Name="imgPhoto" Source="{Binding ImageUri}" Grid.Row="1" />

<TextBlock x:Name="tbkTakenTime" Text="{Binding TakenTime}"Grid.Row="2"/>

 要实现Item内容的分步加载,你需要在ContainerContentChanging中得到每个Item上的子控件,然后根据args.Phase值的不同确定要显示的内容。在每一个Phase中通过注册回调函数来进入下一个Phase。示例代码如下:

        private void ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)

        {

            Grid templateRoot =

               (Grid)args.ItemContainer.ContentTemplateRoot;

            var photoItem = (PhotoViewModel)args.Item;

            Image imgPlaceholder = (Image)templateRoot.FindName("imgPlaceholder");

            TextBlock tbkName =  (TextBlock)templateRoot.FindName("tbkName");

            Image imgPhoto = (Image)templateRoot.FindName("imgPhoto");

            TextBlock tbkTakenTime = (TextBlock)templateRoot.FindName("tbkTakenTime");

            if (args.InRecycleQueue == true)

            {

                tbkName.ClearValue(TextBlock.TextProperty);

                imgPhoto.ClearValue(Image.SourceProperty);

                tbkTakenTime.ClearValue(TextBlock.TextProperty);

            }

            else if (args.Phase == 0)

            {

                imgPlaceholder.Opacity = 1;

                tbkName.Opacity = 0;

                imgPhoto.Opacity = 0;

                tbkTakenTime.Opacity = 0;

                args.RegisterUpdateCallback(ContainerContentChangingDelegate);

            }

            else if (args.Phase == 1)

            {

                imgPlaceholder.Opacity = 0;

                tbkName.Text = photoItem.Author;

                tbkName.Opacity = 1;

                args.RegisterUpdateCallback(ContainerContentChangingDelegate);

            }

            else if (args.Phase == 2)

            {

                tbkTakenTime.Text = photoItem.TakenTime;

                tbkTakenTime.Opacity = 1;

                args.RegisterUpdateCallback(ContainerContentChangingDelegate);

            }

            else if (args.Phase == 3)

            {

                imgPhoto.Source = new BitmapImage(photoItem.ImageUri);

                imgPhoto.Opacity = 1;

            }

            args.Handled = true;

        }

 

        private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> ContainerContentChangingDelegate

        {

            get

            {

                if (_delegate == null)

                {

                    _delegate = new TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs>(OnContainerContentChanging);

                }

                return _delegate;

            }

        }

        private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> _delegate;

      

这样,当我们将GridView的Item项向平移时,如果要显示的内容还没有加载,那么根据代码中的设定,第一步会显示占位符:

 

接下来会显示图片名称:

 

 

然后是拍摄时间:

 

 

最后是图片:

 

这样我们就实现了对基于ListViewBase类的控件的内容的分步加载。但是这又带来了另一个问题,上述代码需要在code behind中访问XAML中的控件,代码的耦合性太强,并不利于代码的移植。理论上说,上述代码是对于UI的优化,如果能够不写code behind代码,在XAML中直接完成就完美了。那么,有没有这样的方法呢?幸运的是,Blend for Visual Studio 2013专门为此提供了一个behaivor:IncrementalUpdateBehavior。它可以用来帮助我们完成该功能。实质上,如果我们反编译IncrementalUpdateBehavior的实现代码,可以看到其内部也是基于ContainerContentChanging实现的,只不过以一种更通用的实现方式来实现,其关键代码如下:

private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs e)

{

    ElementCacheRecord record;

    UIElement element = e.ItemContainer.ContentTemplateRoot;

    if (this.elementCache.TryGetValue(element, out record))

    {

        if (!e.InRecycleQueue)

        {

            foreach(var phasedElementRecords in record.ElementsByPhase)

            {

                foreach (var phasedElementRecord in phasedElementRecords)

                {

                    phasedElementRecord.FreezeAndHide();

                }

            }

                       

            if (record.Phases.Count > 0)

            {

 

                e.RegisterUpdateCallback((uint)record.Phases[0], new TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs>(this.OnContainerContentChangingCallback));            }

 

            ((FrameworkElement)element).DataContext = e.Item;

        }

        else

        {

            element.ClearValue(FrameworkElement.DataContextProperty);

 

            foreach (var phasedElementRecords in record.ElementsByPhase)

            {

                foreach (var phasedElementRecord in phasedElementRecords)

                {

                    phasedElementRecord.ThawAndShow();

                }

            }

                       

        }

        e.Handled = true;

    }

}

private void OnContainerContentChangingCallback(ListViewBase sender, ContainerContentChangingEventArgs e)

{

    ElementCacheRecord record;

    UIElement element = e.ItemContainer.ContentTemplateRoot;

    if (this.elementCache.TryGetValue(element, out record))

    {

        int num = record.Phases.BinarySearch((int)e.Phase);

        if (num >= 0)

        {

            foreach(var phasedElementRecord in record.ElementsByPhase[num])

            {

                phasedElementRecord.ThawAndShow();

            }

                       

            num++;

        }

        else

        {

            num = ~num;

        }

        if (num < record.Phases.Count)

        {

            e.RegisterUpdateCallback((uint)record.Phases[num], new TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs>(this.OnContainerContentChangingCallback));            }

    }

}

这里面ElementCacheRecord维护了与Phase相关联的Element,对于每一个与Phase相匹配的element,代码都会去调用ThawAndShow将其显示出来。

当然,如果仅仅是使用IncrementalUpdateBehavior的话,我们并不需要关心它是如何实现的,只需要知道如何使用就可以了,IncrementalUpdateBehavior的使用方法相当简单,你只需要在DataTemplate中为对应的Element 添加IncrementalUpdateBehavior,并设置合适的Phase就可以了,你可以在Blend中为每个Element拖拉一个IncrementalUpdateBehavior并设置了相应的Phase,Blend生成的代码如下:

<TextBlock x:Name="tbkAuthor" Text="{Binding Author}">

    <Interactivity:Interaction.Behaviors>

        <Core:IncrementalUpdateBehavior Phase="1"/>

    </Interactivity:Interaction.Behaviors>

</TextBlock>

<Image x:Name="imgPhoto" Source="{Binding ImageUri}" Grid.Row="1" >

    <Interactivity:Interaction.Behaviors>

        <Core:IncrementalUpdateBehavior Phase="3"/>

    </Interactivity:Interaction.Behaviors>

</Image>

<TextBlock x:Name="tbkTakenTime" Text="{Binding TakenTime}" Grid.Row="2">

    <Interactivity:Interaction.Behaviors>

        <Core:IncrementalUpdateBehavior Phase="2"/>

    </Interactivity:Interaction.Behaviors>

</TextBlock>

注意这边的Phase是从1开始的。另外,比起实现ContainerContentChanging的事件响应函数,使用IncrementalUpdateBehavior还是有一些限制的,首先这种方式不能使用占位符等自定义的行为,其次就是IncrementalUpdateBehavior只有在增量加载的时候有效,也就是说在UI第一次加载的时候并不会使用上述设置。当然,我相信在大多数情况下,IncrementalUpdateBehavior都是可以满足你的需求的。