Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Виртуализация данных в WPF

Anna | 17.06.2014 | нет комментариев
Доброго времени суток.

Меня давным-давно волновал вопрос написания своего класса для оптимальной загрузки информации из базы данных, скажем когда число записей больше 10 млн. записей.
Отложенная загрузка информации, применение нескольких источников данных и пр.

Не обнаружил на прогре пост посвященный данной теме, следственно представляю вам свой перевод статьи Пола МакКлина, которая стала отправной точкой в решении поставленных задач.

Оригинал статьи: тут
Начальные файлы плана: тут

Дальше по тексту я буду писать от имени автора.

Вступление

WPF предоставляет некоторые увлекательные вероятности виртуализации пользовательского интерфейса для результативной работы с крупными коллекциями, по крайней мере с точки зрения пользовательского интерфейса, но не предусматривает всеобщего способа для виртуализации данных. В то время, как во многих постах на форумах идет обсуждение виртуализации данных, никто (насколько мне вестимо) не опубликовал решение. Статья представляет одно из таких решений.

Предпосылки

Виртуализация пользовательского интерфейса

Когда элемент управления WPF ItemsControl связан с огромный коллекцией начальных данных с включенной настройкой виртуализации UI, элемент управления создает визуальные контейнеры только для видимых элементов (плюс несколько сверху и снизу). Традиционно это малая часть начальной коллекции. Когда пользователь прокручивает список, новые визуальные контейнеры создаются тогда, когда элементы становятся видимыми, а ветхие контейнеры уничтожаются в тот момент, когда элементы становятся заметными. При повторном применении визуальных контейнеров, мы снижаем убыточные расходы на создание и разрушение объектов.

Виртуализация пользовательского интерфейса обозначает, что элемент управления может быть связан с огромный коллекцией данных и занимать при этом меньший объем памяти в связи с малым числом видимых контейнеров.

Виртуализация данных

Виртуализация данных — термин, тот, что обозначает достижение виртуализации для объекта данных связанного с ItemsControl. Виртуализация данных не предусмотрена в WPF. Для касательно маленьких коллекций базовых объектов потребление памяти не имеет значения. Впрочем для крупных коллекций потребление памяти может стать дюже существенным. Помимо того, приобретение информации из базы данных либо создание объектов может занять много времени, исключительно при сетевых операциях. По этим причинам желанно применять какой-то механизм виртуализации данных, Дабы ограничить число объектов данных, которые обязаны быть извлечены из источника и помещены в памяти.

Решение

Обзор

Это решение базируется на том, что когда элемент управления ItemsControl связан с реализацией IList, а не IEnumerable, следственно он не перечисляет каждый список, а взамен этого предоставляет только выборку элементов, нужных для показа. Он использует качество Count для определения размера коллекции, для установки размера полосы прокрутки. В грядущем он будет перебирать экранные элементы через индексатор списка. Таким образом, дозволено сделать IList, тот, что может известить, что он имеет огромное число элементов, а получать элементы только по мере необходимости.

IItemsProvider<T>

Для того Дабы применять данное решение, базовый источник должен уметь предоставлять информацию о числе элементов в коллекции, и предоставлять малую часть (либо страницу) из каждой коллекции. Эти требования выражены в интерфейсе IItemsProvider.

/// <summary>
/// Представляет подрядчика деталей коллекции of collection details.
/// </summary>
/// <typeparam name="T">Тип элемента в коллекции</typeparam>
public interface IItemsProvider<T>
{
    /// <summary>
    /// Получить всеобщее число доступных элементов
    /// </summary>
    /// <returns></returns>
    int FetchCount();

    /// <summary>
    /// Получить диапазон элементов
    /// </summary>
    /// <param name="startIndex">НачTimes[key]).TotalMilliseconds > PageTimeout )
        {
            _pages.Remove(key);
            _pageTouchTimes.Remove(key);
        }
    }
}

Страницы хранятся в словаре (Dictionary), в котором индекс применяется в качестве ключа. Также словарь применяется для хранения информации о времени последнего применения. Это время обновляется при всяком обращении к странице. Оно применяется способом CleanUpPages() для удаления страниц, к которым не было обращения за существенное число времени.

protected virtual void LoadPage(int pageIndex)
{
    PopulatePage(pageIndex, FetchPage(pageIndex));
}

protected IList<T> FetchPage(int pageIndex)
{
    return ItemsProvider.FetchRange(pageIndex*PageSize, PageSize);
}

В заключении, FetchPage() исполняет приобретение страницы из ItemsProvider, и способ LoadPage() изготавливает работу по вызову способа PopulatePage(), размещающего страницу в словаре c заданным индексом.

Может показаться, что в коде много несущественных способов, но они были разработаны таким образом по определенным причинам. Всякий способ исполняет ровно одну задачу. Это помогает сберечь код читаемым, а также делает простым растяжение и модификацию функциональности в производных классах, как будет отслеживаться дальше.

Класс VirtualizingCollection< T> достигает основной цели по осуществению виртуализации данных. К сожалению, в процессе применения данный класс имеет один значительный недочет — все способы приобретения данных выполняются синхронно. Это обозначает, что они запускаются потоками пользовательского интерфейса, что в итоге допустимо затормаживают работу приложения.

AsyncVirtualizingCollection< T>

Класс AsyncVirtualizingCollection< T> унаследован от VirtualizingCollection< T>, и переопределяет способ Load() для реализации асинхронной загрузки данных. Ключевой спецификой асинхронного источника данных является то, что в момент приобретения данных он должен оповестить через свою связку (data binding) пользовательский интерфейс. В обыкновенных объектах это решается применением интерфейса INotifyPropertyChanged. Для реализации коллекций нужно применять его близкого родственника INotifyCollectionChanged. Данный интерфейс применяется классом ObservableCollection< T>

public event NotifyCollectionChangedEventHandler CollectionChanged;

protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    NotifyCollectionChangedEventHandler h = CollectionChanged;
    if (h != null)
        h(this, e);
}

private void FireCollectionReset()
{
    NotifyCollectionChangedEventArgs e = 
      new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
    OnCollectionChanged(e);
}

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
    PropertyChangedEventHandler h = PropertyChanged;
    if (h != null)
        h(this, e);
}

private void FirePropertyChanged(string propertyName)
{
    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
    OnPropertyChanged(e);
}

Класс AsyncVirtualizingCollection< T> реализует оба интерфейса INotifyPropertyChanged и INotifyCollectionChanged для предоставления максимальной эластичности связки. В этой реализации нечего подметить.

protected override void LoadCount()
{
    Count = 0;
    IsLoading = true;
    ThreadPool.QueueUserWorkItem(LoadCountWork);
}

private void LoadCountWork(object args)
{
    int count = FetchCount();
    SynchronizationContext.Send(LoadCountCompleted, count);
}

private void LoadCountCompleted(object args)
{
    Count = (int)args;
    IsLoading = false;
    FireCollectionReset();
}

В переопределенном способе LoadCount(), приобретение вызывается асинхронно через ThreadPool. По заключении, будет установлено новое число и вызван способ FireCollectionReset() обновляющий пользовательский интерфейс через InotifyCollectionChanged. Подметьте, что способ LoadCountCompleted вызывается из потока пользовательского интерфейса вследствие применению SynchronizationContext. Качество SynchronizationContext устанавливается в конструкторе класса, с предположением, что экземпляр коллекции будет сделан в потоке пользовательского интерфейса.

protected override void LoadPage(int index)
{
    IsLoading = true;
    ThreadPool.QueueUserWorkItem(LoadPageWork, index);
}

private void LoadPageWork(object args)
{
    int pageIndex = (int)args;
    IList<T> page = FetchPage(pageIndex);
    SynchronizationContext.Send(LoadPageCompleted, new object[]{ pageIndex, page });
}

private void LoadPageCompleted(object args)
{
    int pageIndex = (int)((object[]) args)[0];
    IList<T> page = (IList<T>)((object[])args)[1];

    PopulatePage(pageIndex, page);
    IsLoading = false;
    FireCollectionReset();
}

Асинхронная загрузка данных страницы действуют по тем же правилам, и вновь способ FireCollectionReset() применяется для обновления пользовательского интерфейса.

Подметим также качество IsLoading. Это примитивный флаг, тот, что может быть использован пользовательским интерфейсом для индикации загрузки коллекции. Когда качество IsLoading изменяется, способ FirePropertyChanged() вызывает обновление пользовательского интерфейса через механизм INotifyProperyChanged.

public bool IsLoading
{
    get
    {
        return _isLoading;
    }
    set
    {
        if ( value != _isLoading )
        {
            _isLoading = value;
            FirePropertyChanged("IsLoading");
        }
    }
}

Демонстрационный план

Для того, Дабы продемонстрировать данное решения, я сотворил примитивный демонстрационный план (входит в начальные коды плана).

Во-первых, была сделана реализация класса IItemsProvider, которая предоставляет фальшивые данные с остановкой потока для симуляции задержки приобретения данных с диска либо по сети.

public class DemoCustomerProvider : IItemsProvider<Customer>
{
    private readonly int _count;
    private readonly int _fetchDelay;

    public DemoCustomerProvider(int count, int fetchDelay)
    {
        _count = count;
        _fetchDelay = fetchDelay;
    }

    public int FetchCount()
    {
        Thread.Sleep(_fetchDelay);
        return _count; 
    }

    public IList<Customer> FetchRange(int startIndex, int count)
    {
        Thread.Sleep(_fetchDelay);

        List<Customer> list = new List<Customer>();
        for( int i=startIndex; i<startIndex count; i   )
        {
            Customer customer = new Customer {Id = i 1, Name = "Customer "   (i 1)};
            list.Add(customer);
        }
        return list;
    }
}

Вездесущий объект Customer использован в качестве элемента коллекции.

Примитивное окно WPF с элементом управления ListView было сделано, Дабы дозволить пользователю поэкспериментировать с разными реализациями списка.

<Window x:Class="DataVirtualization.DemoWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Data Virtualization Demo - By Paul McClean" Height="600" Width="600">

    <Window.Resources>
        <Style x:Key="lvStyle" TargetType="{x:Type ListView}">
            <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
            <Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Recycling"/>
            <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="True"/>
            <Setter Property="ListView.ItemsSource" Value="{Binding}"/>
            <Setter Property="ListView.View">
                <Setter.Value>
                    <GridView>
                        <GridViewColumn Header="Id" Width="100">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Id}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Header="Name" Width="150">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Name}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsLoading}" Value="True">
                    <Setter Property="ListView.Cursor" Value="Wait"/>
                    <Setter Property="ListView.Background" Value="LightGray"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid Margin="5">

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <GroupBox Grid.Row="0" Header="ItemsProvider">
            <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                <TextBlock Text="Number of items:" Margin="5" 
                  TextAlignment="Right" VerticalAlignment="Center"/>
                <TextBox x:Name="tbNumItems" Margin="5" 
                  Text="1000000" Width="60" VerticalAlignment="Center"/>
                <TextBlock Text="Fetch Delay (ms):" Margin="5" 
                  TextAlignment="Right" VerticalAlignment="Center"/>
                <TextBox x:Name="tbFetchDelay" Margin="5" 
                  Text="1000" Width="60" VerticalAlignment="Center"/>
            </StackPanel>
        </GroupBox>

        <GroupBox Grid.Row="1" Header="Collection">
            <StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                    <TextBlock Text="Type:" Margin="5" 
                      TextAlignment="Right" VerticalAlignment="Center"/>
                    <RadioButton x:Name="rbNormal" GroupName="rbGroup" 
                      Margin="5" Content="List(T)" VerticalAlignment="Center"/>
                    <RadioButton x:Name="rbVirtualizing" GroupName="rbGroup" 
                      Margin="5" Content="VirtualizingList(T)" 
                      VerticalAlignment="Center"/>
                    <RadioButton x:Name="rbAsync" GroupName="rbGroup" 
                      Margin="5" Content="AsyncVirtualizingList(T)" 
                      IsChecked="True" VerticalAlignment="Center"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                    <TextBlock Text="Page size:" Margin="5" 
                      TextAlignment="Right" VerticalAlignment="Center"/>
                    <TextBox x:Name="tbPageSize" Margin="5" 
                      Text="100" Width="60" VerticalAlignment="Center"/>
                    <TextBlock Text="Page timeout (s):" Margin="5" 
                      TextAlignment="Right" VerticalAlignment="Center"/>
                    <TextBox x:Name="tbPageTimeout" Margin="5" 
                      Text="30" Width="60" VerticalAlignment="Center"/>
                </StackPanel>
             </StackPanel>
        </GroupBox>

        <StackPanel Orientation="Horizontal" Grid.Row="2">
            <TextBlock Text="Memory Usage:" Margin="5" 
              VerticalAlignment="Center"/>
            <TextBlock x:Name="tbMemory" Margin="5" 
              Width="80" VerticalAlignment="Center"/>

            <Button Content="Refresh" Click="Button_Click" 
              Margin="5" Width="100" VerticalAlignment="Center"/>

            <Rectangle Name="rectangle" Width="20" Height="20" 
                     Fill="Blue" Margin="5" VerticalAlignment="Center">
                <Rectangle.RenderTransform>
                    <RotateTransform Angle="0" CenterX="10" CenterY="10"/>
                </Rectangle.RenderTransform>
                <Rectangle.Triggers>
                    <EventTrigger RoutedEvent="Rectangle.Loaded">
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetName="rectangle" 
                                   Storyboard.TargetProperty=
                                     "(TextBlock.RenderTransform).(RotateTransform.Angle)" 
                                   From="0" To="360" Duration="0:0:5" 
                                   RepeatBehavior="Forever" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Rectangle.Triggers>
            </Rectangle>

            <TextBlock Margin="5" VerticalAlignment="Center" 
              FontStyle="Italic" Text="Pause in animation indicates UI thread stalled."/>

        </StackPanel>

        <ListView Grid.Row="3" Margin="5" Style="{DynamicResource lvStyle}"/>

    </Grid>
</Window>

Не стоит вдаваться в подробности XAML. Исключительное что стоит подметить — это применение заданных жанров ListView для метаморфозы заднего фона и курсора мыши в результат на метаморфоза свойства IsLoading.

public partial class DemoWindow
{
    /// <summary>
    /// Initializes a new instance of the <see cref="DemoWindow"/> class.
    /// </summary>
    public DemoWindow()
    {
        InitializeComponent();

        // use a timer to periodically update the memory usage
        DispatcherTimer timer = new DispatcherTimer();
        timer.Interval = new TimeSpan(0, 0, 1);
        timer.Tick  = timer_Tick;
        timer.Start();
    }

    private void timer_Tick(object sender, EventArgs e)
    {
        tbMemory.Text = string.Format("{0:0.00} MB", 
                             GC.GetTotalMemory(true)/1024.0/1024.0);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // create the demo items provider according to specified parameters
        int numItems = int.Parse(tbNumItems.Text);
        int fetchDelay = int.Parse(tbFetchDelay.Text);
        DemoCustomerProvider customerProvider = 
                       new DemoCustomerProvider(numItems, fetchDelay);

        // create the collection according to specified parameters
        int pageSize = int.Parse(tbPageSize.Text);
        int pageTimeout = int.Parse(tbPageTimeout.Text);

        if ( rbNormal.IsChecked.Value )
        {
            DataContext = new List<Customer>(customerProvider.FetchRange(0, 
                                                   customerProvider.FetchCount()));
        }
        else if ( rbVirtualizing.IsChecked.Value )
        {
            DataContext = new VirtualizingCollection<Customer>(customerProvider, pageSize);
        }
        else if ( rbAsync.IsChecked.Value )
        {
            DataContext = new AsyncVirtualizingCollection<Customer>(customerProvider, 
                              pageSize, pageTimeout*1000);
        }
    }
}

Макет окна доволно примитивный, но довольный для демонстрации решения.

Пользователь может настроить число элементов в экземпляре DemoCustomerProvider и время симулятора задержки.

Демонстрация разрешает пользователям сравнить стандартную реализацию List(T), реализацию с синхронной загрузкой данных VirtualizingCollection(T) и реализацию с асинхронной загрузкой данных AsyncVirtualizingCollection(T). При применении VirtualizingCollection(T) и AsyncVirtualizingCollection(T) пользователь может задать размер страницы и таймаут (задает время через которое страница должна быть выгружена из памяти). Они обязаны быть выбраны в соответствии с колляциями элемента и ожидаемым образцом применения.

Для сопоставления различных типов коллекций, в окне также отображается всеобщий объем примененной памяти. Анимация вращающегося квадрата применяется для визуализации остановки потока пользовательского интерфейса. В всецело асинхронном решении, анимация не должна затормаживаться либо останавливаться.

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB