Artykuł

freeimages.com freeimages.com
cze 18 2015
0

Implementacja dynamicznego doczytywania treści w kontrolce ListView

W architekturze WinRT możemy korzystać z dwóch bardzo przyjemnych kontrolek do prezentacji danych - ListView oraz GridView. Obie te kontrolki mają duże możliwości konfiguracyjne, posiadają sporo zdarzeń i można je również fajnie ostylować. Jednym słowem jest naprawdę dobrze;-)

Naszą piękną, idealistyczną sielankę może jednak w łatwy sposób zakłócić, pewien dość prozaiczny problem, który nazywa się nadmiar danych. Tego rodzaju sytuacja zasadniczo nie ma żadnych pozytywów. Nasza aplikacja pochłonie duże ilości pamięci, może zacząć się zacinać, a dodatkowo jeśli dane pochodzą z zewnątrz, niepotrzebnie pobierzemy dane, które być może nigdy nie zostaną wyświetlone...

W praktyce okazuje się, że można jednak łatwo wybrnąć z tej sytuacji - wystarczy bowiem w zbindowanej kolekcji zaimplementować interfejs ISupportIncrementalLoading i życie znów stanie się piękne;-)

Co chcemy osiągnąć?

Głównym naszym zadaniem będzie implementacja mechanizmu, który pozwoli na dynamiczne doładowywanie treści, w momencie gdy użytkownik będzie przesuwał się w dół naszej listy elementów. Doładowywanie ma być w pełni asynchroniczne i nie ma blokować możliwości przeglądania już pobranych elementów listy. W czasie doładowywania danych na dole strony powinien wyświetlać się również indykator, który świadczyć będzie o tym, że coś się dzieje.

Testowa aplikacja - skrócony opis

Testowa aplikacja dla systemu Windows Phone 8.1 (bardzo łatwo można przenieść przykład również na realia Windows 8.x) będzie wyświetlać proste, szare kafelki na których znajdować się będzie TextBlock z numerkiem. Numerki zostaną z kolei umieszczone w nowej klasie kolekcji, która rozszerzać będzie ObservableCollection i jednocześnie implementować interfejs ISupportIncrementalLoading. Dane będą pochodzić z dynamicznie wygenerowanej lokalnie listy intów. Żeby zasymulować proces dociągania danych z sieci, dodamy Task, który będzie wydłużać każdorazowe uderzenie o dane o 1 sekundę (czas ten można zmienić w kodzie).

Dla zwiększenia czytelności przykładu, zastosujemy pseudo ViewModel, który z kolei zostanie podpięty do kontekstu naszej strony. Tyle teorii - przejdźmy do praktyki:)

Kod całego projektu do pobrania z działu download

Na początek - XAML

Pracę rozpoczniemy od napisania kodu XAML. Poniżej kod głównej i zarazem jedynej strony w tym projekcie:

<Page
    x:Class="IncrementalLoadingTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:IncrementalLoadingTest"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" RequestedTheme="Light"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid Margin="19">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView ItemsSource="{Binding Collection}">
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                </Style>
            </ListView.ItemContainerStyle>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid Height="200" Background="#F0F0F0" Margin="0,0,0,19">
                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
                                   Text="{Binding}" FontSize="100" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <ProgressRing Visibility="{Binding LoaderVisibility}" 
                      Margin="19" Grid.Row="1" IsActive="True"/>
    </Grid>
</Page>

Tak jak pisałem wcześniej, mamy tu do czynienia z prostą listą, która wyświetla elementy składające się z Grida i TextBlocka. Każdy kafelek zajmuje pełną dostępną mu szerokość i mierzy 200 pikseli wysokości (oczywiście należy uwzględnić skalowanie systemowe na wyższych rozdzielczościach/ekranach). Na dole strony umieszczona jeszcze jest kontrolka ProgressRing, której widoczność uzależniona jest od tego czy dane aktualnie się ładują, czy też nie.

Wszystkie zbindowane właściwości znajdą się w we wspomnianym wcześniej ViewModelu.

Code-behind

W code-behind mamy proste podpięcie nowego obiektu ViewModelu do kontekstu danych strony. Liczba 100 umieszczona w konstruktorze ViewModelu informuje o tym, ile elementów trafi do wyjściowej kolekcji danych.

using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace IncrementalLoadingTest
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            this.NavigationCacheMode = NavigationCacheMode.Required;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this.DataContext = new MainPageViewModel(100);
        }
    }
}

Klasa kolekcji

Tak jak wspomniałem wcześniej, klasa kolekcji rozszerza typ ObservableCollection oraz implementuje interfejs ISupportIncrementalLoading. W tym przypadku postawiłem na generyczność i nie wskazałem tutaj na żaden konkretny typ danych.

using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.UI.Xaml.Data;

namespace IncrementalLoadingTest
{
    public class IncrementalCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
    {
        private Func<uint, Task<LoadDataResult>> loadDataTask = null;

        public IncrementalCollection(Func<uint, Task<LoadDataResult>> loadDataTask)
            :base()
        {
            this.loadDataTask = loadDataTask;
            this.HasMoreItems = true;
        }

        public bool HasMoreItems
        {
            get;
            private set;
        }

        public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
            return Task.Run<LoadMoreItemsResult>(async () => {
                return await this.LoadMoreItemsInternalAsync(count);
            }).AsAsyncOperation<LoadMoreItemsResult>();
        }

        private async Task<LoadMoreItemsResult> LoadMoreItemsInternalAsync(uint count)
        {
            var loadDataResult = await this.loadDataTask(count);
            this.HasMoreItems = loadDataResult.HasMoreItems;

            return new LoadMoreItemsResult { Count = loadDataResult.LoadedItemsCount };
        }
    }
}

Właściwość HasMoreItems robi dokładnie to co mówi, czyli informuje kolekcję, czy w danej chwili są jeszcze jakieś elementy do pobrania. Żeby cały mechanizm w ogóle wystartował, ustawiamy tą właściwość na True w konstruktorze klasy, dzięki czemu już na samym starcie nastąpi próba pobrania pierwszego elementu.

Dane pobierane są za pomocą metody (jak łatwo się domyślić) LoadMoreItemsAsync, która przyjmuje uint jako parametr. Parametr ten mówi, ile elementów ma zostać pobranych przy aktualnym wywołaniu metody (można ten parametr brzydko mówiąc olać, ale jeśli tylko jest możliwość, to warto z niego skorzystać i pobierać tyle danych ile sugeruje system).

Sama metoda działa asynchronicznie. Podczas jej działania kolekcja powinna zostać uzupełniona o dane (o ile takowe istnieją), a wynikiem jej działania powinien być obiekt LoadMoreItemsResult do którego trafia informacja o liczbie pobranych elementów (możemy żądać 20 elementów, ale jeśli nasze źródło danych powie że mamy ich tylko 15, to też tyle ich zostanie zwróconych).

Podczas działania tej metody koniecznie musimy również określić aktualny status flagi HasMoreItems. Ja większość kodu odpowiedzialnego za aktualizację danych, wrzuciłem do Taska, który leży z kolei w ViewModelu. Pora mu się przyjrzeć;-)

ViewModel

Naszą wisienką na torcie jest ViewModel, którego kod znajdziecie poniżej:

using PropertyChanged;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.UI.Core;
using Windows.UI.Xaml;

namespace IncrementalLoadingTest
{
    [ImplementPropertyChanged]
    public class MainPageViewModel
    {
        private int alreadyGet = 0;
        private int collectionSize = 0;
        private IEnumerable<int> sourceData = null;

        public MainPageViewModel(int collectionSize)
        {
            this.collectionSize = collectionSize;
            this.sourceData = GetData(this.collectionSize);
            this.Collection = new IncrementalCollection<int>(this.LoadData);
        }

        public IncrementalCollection<int> Collection { get; private set; }

        public Visibility LoaderVisibility { get; private set; }

        public async Task<LoadDataResult> LoadData(uint count)
        {
            var items = this.sourceData.Skip(this.alreadyGet).Take((int)count).ToList();
            int itemsNo = items.Count();
            this.alreadyGet += itemsNo;     

            var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
            await dispatcher.RunAsync(
                CoreDispatcherPriority.Normal,
                async () =>
                {
                    this.LoaderVisibility = Visibility.Visible;
                    foreach (int n in items)
                    {
                        this.Collection.Add(n);
                    }
                    await Task.Delay(1000);
                    this.LoaderVisibility = Visibility.Collapsed;
                });

            return new LoadDataResult
            {
                HasMoreItems = this.alreadyGet < this.collectionSize,
                LoadedItemsCount = (uint)itemsNo
            };
            
        }

        private static IEnumerable<int> GetData(int collectionSize)
        {
            return Enumerable.Range(1, collectionSize).OrderBy(c => c).ToList();
        }
    }
}

Przystąpmy do analizy, ponieważ dzieje się tu kilka ciekawych rzeczy. Po pierwsze, ponownie skusiłem się na opisywaną przeze mnie jakiś czas temu bibliotekę Fody, która rozwiązała problem notyfikacji o zmianie stanu właściwości (atrybut ImplementPropertyChanged).

W konstruktorze ViewModelu (19-24) inicjalizujemy naszą strukturę danych, a także tworzymy nowy obiekt naszej kolekcji, do którego przekazujemy referencję do Taska ładującego dane.

Sercem ViewModelu jest wspomniany wyżej Task LoadData, który nasza kolekcja wykorzystuje do załadowania danych (Task został specjalnie umieszczony w ViewModelu, dzięki czemu kolekcja może być łatwo re-użyta do innych danych). Wewnątrz metody wyciągamy wskazaną przez mechanizm doładowywania liczbę elementów. Ponieważ na potrzeby przykładu korzystamy z lokalnej listy intów, musimy gdzieś zapisać liczbę aktualnie pobranych z niej elementów (34). Same elementy pobieramy za pomocą łańcuchowego wywołania metod Skip oraz Take (32) - LINQ FTW!

Warto również zwrócić uwagę na dispatchera, z którego korzystamy w liniach 36-48. Konstrukcja ta jest potrzebna, ponieważ zmiana właściwości LoaderVisibility oraz dodanie elementów do kolekcji ma wpływ na UI.

Poniżej screen z działającej aplikacji.

Materiały

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus