Incremental update item data for ListViewBase controls in windows 8.1

Generally Listview and Gridview which based on ListViewBase class should process a large amount of data items. It may heavily impact the performance if we can’t handle it properly. We can utilize UI virtualization to solve parts of memory and performance issues. But we will still encounter the problem that the data items can’t be displayed smoothly when we pan the screen if the data items are complex. It will bring bad user experience. To improve the user experience, there are two major improvements for ListViewBased controls in windows 8.1

  1.  Add a placeholder for every data item. Before the data loading is finished, a placeholder is shown for each data item. You can enable or disable this feature by setting ShowsScrollingPlaceholders property.
  2.  Add a new event ContainerContentChanging for ListViewBase controls. This event will be trigger when the content of the ListViewBase controls is modifying. So you can add code to optimize the data item displaying in the event handler.

The first method is very simple. You don’t need to write any code because by default ShowsScrollingPlaceholders is enabled. In this article, I will focus on the second method:

ContainerContentChanging event will be raised when each data item is loading. You can choose to load the content of the data item step by step in this event handler. For example, there are three elements in my GridViews’ data item template:

<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>

We can set to load Name first, then TakenTime, last Photo image. If ShowsScrollingPlaceholders is true, a grey placeholder is added automatically before the data item is loaded. If you dislike the grey placeholder, you can also choose to set this property false, and then manually add a custom placeholder at the first step. Then the data template includes four elements:

<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"/>

To implement the phased content loading, you should get the sub element’s instance in your ContainerContentChanging event handler, then show the different contents according to the value of args.Phase property. You need to call RegisterUpdateCallback function to enter the next phase after your finished the content displaying in the current phase. Here is the sample code of ContainerContentChanging event handler:

        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;

      

Then when we pan the GridView, the first step is to show the placeholder:

 

Next is to hide the placeholder and show photo name:

 

 

Next is the TakenTime:

 

 

Last it shows all the content including images:

 

Above code implements the incremental loading for ListViewBase based controls. But it brings another problem. The code heavily depends on the structure of data template. You should get the Xaml element by name to set its property. So the code is fragile and hard to port. Fortunately, Blend for Visual Studio 2013 provides a useful behavior: IncrementalUpdateBehavior. It can help to implement the same function in XAML. Actually, if you reflector the code of IncrementalUpdateBehavior, you can see that its implementation is also based on ContainerContentChanging, just use a more generic style:

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));            }

    }

}

Here ElementCacheRecord maintains the elements associated to Phase. For each Phase matched element, the runtime calls ThawAndShow to display it.

We don't’ need to care too much about the internal of IncrementalUpdateBehavior. Just make sure that you know how to use it. The usage of IncrementalUpdateBehavior is very simple, just open your project in Blend and then drag and drop IncrementalUpdateBehavior for each element in DataTemplate you want to display, and set Phase property accordingly. Blend will help to generate following code:

<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>

Please take care that Phase property starts from 1 instead of 0. Compared with ContainerContentChanging, IncrementalUpdateBehavior has some limitation. Firstly, it can handle the custom placeholder. So you should always set ShowsScrollingPlaceholders to true if you want it. Secondly IncrementalUpdateBehavior is only valid when there is incremental update. Unlike ContainerContentChanging, it will not be called when the page start to load. But in most cases, I believe it’s your best choice.