Windows 8.1下ListView和GridView的数据分步显示
通常基于ListViewBase类的ListView和GridView需要处理大量的数据项(date Item),如果处理不好的话,会对应用的性能产生严重影响。虽然UI virtualization帮助解决了显示大量Item时内存占用过多及初始化时间过长的问题,在用户进行平移操作的时候,如果每一个Item的内容比较复杂,仍然会出现Item加载过慢的问题,就好像幻灯片一样,每个Item一个一个的显示出来,使应用的流畅度大打折扣。为了进一步提高用户体验,Windows 8.1下为这两个控件的显示效果作了优化,主要表现在两个方面:
- 为基于ListViewBase类的控件的每个Item都添加了占位符,也就是说,当显示Item的时候,在数据没有加载完成之前,系统首先为每个Item都自动显示一个缺省的图标。您可以使用ShowsScrollingPlaceholders属性来启用或禁止这项功能。
- 为基于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都是可以满足你的需求的。
Comments
Anonymous
November 14, 2013
Would you please give a sample link to this issue? Thanks so much.Anonymous
November 14, 2013
All the necessary code is included in the article? what's your specific problem?Anonymous
November 20, 2013
Thanks Han, I have created a demo by using your code. Thanks so much. Cheers! You are great!Anonymous
January 23, 2014
能给个例子吗?我看前部风没有问题,可是使用Behavior时我有点看不明白。还有,这种方案可以用于wp8吗?