내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-07-10 02:41

제목

[WPF] DataGrid, ListView 컨트롤 스크롤링시 데이터 갱신 01 - Virtualization of the data in WPF


DataGrid, ListView 같은 Collection 타입의 데이터를 바인딩 하고 표시 하는 컨트롤은 IList 인터페이스를 참조하며, 컨트롤이 화면에 렌더링 될때 화면에 보이는 영역의 데이터를 인터페이스 IList의 인덱서를 통해 데이터를 표시 하게 되어 있다.
(기본적으로 DataGrid및 ListView 컨트롤은 Virtualization이 지원되어 화면에 보이는 영역만 데이터를 표시한다.)

즉 화면에 0 ~ 20 까지의 인덱스 데이터를 표시 할 경우

object List [int index]
cs

위 인덱서를 호출하고
추상클래스 CustomTypeDescriptor 

public override PropertyDescriptorCollection GetProperties()
cs

메서드를 통해 속성 정보를 얻어와 데이터를 표시한다.
따라서 직접 IList를 상속받아 Collection타입 클래스를 구현하여 DataGrid및 ListView 컨트롤 스크롤링시 데이터를 비동기 적으로 갱신해 줄 수 있다.

다음은 구글에서 검색한 특정 사이트의 코드다.

Kind time of days.

Me the question of writing of the class for optimal loading of the information from a database, for example when a record count more than 10 million records for a long time interesting.
The postpon loading of the information, usage of several data sources and so forth.

Doing not find on habre a post devot to the g subject, therefore I represents you Paul Makklina's translation of the article which becoming a starting point in the decision of tasks in view.

Original of article: here
Source files of the project: here

Further in the text I will write on behalf of the author.

Introduction


WPF gave some interesting possibilities of virtualization of the user interface for effective operation with the big collections, at least with the point of view of the user interface, but did not provide the common method for virtualization of the data. While in many posts at forums there are an arguing of virtualization of the data, anybody (as far as I know) doing not publish the decision. Article represented one of such decisions.

Premises


Virtualization of the user interface


When the control item of WPF ItemsControl are connect with the big collection of the initial data with switched on adjustment of virtualization of UI, the control item created visual containers only for visible elements (plus a little on top and from below). Normally it are a small part of the initial collection. When the user scrolled the list, new visual containers formed when elements become visible, and old containers was destroy while elements become invisible. At a reuse of visual containers, we cut an overhead charge for creation and destruction of objects.

Virtualization of the user interface meant that the control item could be connect with the big collection of the data and occupy thus smaller memory size in connection with a small amount of visible containers.

Virtualization of the data


Virtualization of the data ? the term who meant achievement of virtualization for object of the data connect with ItemsControl. Virtualization of the data are not provid in WPF. For rather small collections of basic objects consumption of storage had no value. However for the big collections consumption of storage could become very much to the considerable. Besides, obtaining of the information from a database or creation of objects could occupy a lot of time, especially at network operations. For these reasons it are desirable to use any mechanism of virtualization of the data to restrict an amount of objects of the data which should be deriv from a source and was allocated in storage.

Decision


Review


It are the decision are based that when the control item of ItemsControl are connect with implementation of IList, instead of IEnumerable, hence it did not enumerate all list, and instead gave only sampling of the elements necessary for show. It used property of Count for determination of the size of a collection, for setting of the size of scroll bar. In the future it will sort out screen elements through the indexer of the list. Thus, it are possible to create IList which could inform that it had a considerable quantity of elements, and to receive elements only as required.

IItemsProvider <T>


To use the g decision, the basic source should be able to give the information on an amount of elements in a collection, and to give a small part (or page) from all collection. These requirements was express in the IItemsProvider interface.

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

/// <summary>
/// Получить диапазон элементов
/// </summary>
/// <param name="startIndex">Начальный индекс</param>
/// <param name="count">Количество элементов для получения</param>
/// <returns></returns>
IList<T> FetchRange(int startIndex, int count);
}


If the basic data source are database request it are possible to implement rather easily the IItemsProvider interface, us an aggregate function of COUNT (), or expressions of OFFSET and LIMIT g by the majority of suppliers of databases.

VirtualizingCollection <T>


It are implementation of the IList interface which carried out virtualization of the data. VirtualizingCollection

divided all space of a collection into a row of pages. If needed pages booted in storage, and was destroy at uselessness.

The interesting moments will be was consider more low. For particulars address, please, to source codes enclos to g article.

The first aspect of implementation of IList are an implementation of property of Count. It are us by a control item of ItemsControl for an estimation of the size of a collection and otrisovka of scroll bar.

Private int _count = -1;

public virtual int Count
{
get
{
if (_count == -1)
{
LoadCount();
}
return _count;
}
protected set
{
_count = value;
}
}

protected virtual void LoadCount()
{
Count = FetchCount();
}

protected int FetchCount()
{
return ItemsProvider.FetchCount();
}


Property of Count are implement with usage of a template of the postpon or lazy loading (lazy load). It used special value-1 to show that value are not load yet. At the first reversal property will load an actual amount of elements from ItemsProvider.

Other prominent aspect of the IList interface are implementation of the indexer.

public T this[int index]
{
get
{
// определить какая страница и смещение внутри страницы
int pageIndex = index / PageSize;
int pageOffset = index % PageSize;

// запросить главную страницу
RequestPage(pageIndex);

// если обратились более чем к 50% тогда запросить следующую страницу
if ( pageOffset > PageSize/2 &&pageIndex < Count / PageSize)
RequestPage(pageIndex + 1);

// если обратились менее чем к 50% тогда запросить предшествующую страницу
if (pageOffset < PageSize/2 &&pageIndex > 0)
RequestPage(pageIndex - 1);

// удалить устаревшие страницы
CleanUpPages();

// защитная проверка в случае асинхронной загрузки
if (_pages[pageIndex] == null)
return default(T);

// вернуть запрошенный элемент
return _pages[pageIndex][pageOffset];
}
set { throw new NotSupportedException(); }
}


The indexer represented the most unique part of the decision. In the first, it should define, what page possessed an interrogated element (pageIndex) and offset in page (pageOffset). Then the method of RequestPage (), return page are caus.

Then there are a loading of the following or prior page on the basis of variable pageOffset. It are based on that assumption that if users viewed page 0, that is a high probability that they will scroll downwards for review of page 1. Data acquisition did not cause in advance passes of the data at display on the screen.

CleanUpPages () are caus for cleaning (or outswappings) not us pages.

At last, protectional check on presence of page. This check are necessary on a case if the method of RequstPage () did not work in the synchronous mode, as at usage of a derivative class of AsyncVirtualizingCollection

.

private readonly Dictionary<int, IList<T>> _pages = 
new Dictionary<int, IList<T>>();
private readonly Dictionary<int, DateTime> _pageTouchTimes =
new Dictionary<int, DateTime>();

protected virtual void RequestPage(int pageIndex)
{
if (!_pages.ContainsKey(pageIndex))
{
_pages.Add(pageIndex, null);
_pageTouchTimes.Add(pageIndex, DateTime.Now);
LoadPage(pageIndex);
}
else
{
_pageTouchTimes[pageIndex] = DateTime.Now;
}
}

protected virtual void PopulatePage(int pageIndex, IList<T> page)
{
if (_pages.ContainsKey(pageIndex))
_pages[pageIndex] = page;
}

public void CleanUpPages()
{
List<int> keys = new List<int>(_pageTouchTimes.Keys);
foreach (int key in keys)
{
// page 0 is a special case, since the WPF ItemsControl
// accesses the first item frequently
if ( key != 0 &&(DateTime.Now -
_pageTouchTimes[key]).TotalMilliseconds > PageTimeout )
{
_pages.Remove(key);
_pageTouchTimes.Remove(key);
}
}
}


Pages was stor in the dictionary (Dictionary) in which the index are us as a key. Also the dictionary are us for information storage about time of the last usage. It are time it are updat at each reversal to page. It are us by a method of CleanUpPages () for deleting of pages to whom there were no reversal for the significant amount of time.

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

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


In completion, FetchPage () fulfilled obtaining of page from ItemsProvider, and the method of LoadPage () produced operation on a method call of PopulatePage (), allocat page in the dictionary of a C the given index.

Could seem that in the code there was a lot of insignificant methods, but they develop thus for the particular reasons. Each method carried out exactly one task. It helped to save the code readable, and also did simple the extension and modification of multifunction in derivative classes as it will be watch further.

The class of VirtualizingCollection< T> reached an overall objective on osushchestveniye of virtualization of the data. Unfortunately, in the course of usage this class had an one essential lack ? all methods of data acquisition was fulfill synchronously. It meant that they was launch by flows of the user interface that as a result potentially zatormazhivayut operation of application.

AsyncVirtualizingCollection <T>


The class of AsyncVirtualizingCollection< T> are legacy from VirtualizingCollection< T>, and redefined a method of Load () for implementation of asynchronous loading of the data. A key singularity of asynchronous data source are that at the moment of data acquisition it should notify the user interface through the sheaf (data binding). In normal objects it dared usage of the INotifyPropertyChanged interface. For implementation of collections it are necessary to use it the near relation of INotifyCollectionChanged. This interface are us by a class of 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);
}


The class of AsyncVirtualizingCollection< T> implemented both the interface INotifyPropertyChanged and INotifyCollectionChanged for provision of the maximum flexibility of a sheaf. In this implementation there are nothing to mark.

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


In the redefin method of LoadCount (), obtaining are caus asynchronously through ThreadPool. On termination of, the new amount will be install and the method of FireCollectionReset () the updat user interface through InotifyCollectionChanged are called. Note that the method of LoadCountCompleted are caus from a flow of the user interface thanks to SynchronizationContext. Property of SynchronizationContext are install in the designer of a class, with the assumption that the copy of a collection will be creat in a flow of the user interface.

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


Asynchronous loading of the data of page operated by the same rules, and again the method of FireCollectionReset () are us for update of the user interface.

Let's mark also property of IsLoading. It are a simple flag who could be us by the user interface for indication of loading of a collection. When property of IsLoading changed, the method of FirePropertyChanged () caused update of the user interface through the INotifyProperyChanged mechanism.

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


Demonstration project


To show g decisions, I creating the simple demonstration project (entered into source codes of the project).

At first, implementation of a class of IItemsProvider who gave the fictitious data with a stop of a flow for simulation of a time delay of data acquisition with a disk or on a network were creat.

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


The ubiquitous object of Customer are us as an element of a collection.

The simple window of WPF with a control item of ListView were creat to allow the user to experiment with various implementations of the list.

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


It is not necessary to go into details XAML. The only thing that should be mark ? it are usage of the given styles of ListView for change of a back background and the cursor of the mouse in reply to change of property of 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);
}
}
}


Breadboard model of a window simple enough, but sufficient for demonstration of the decision.

The user could adjust an amount of elements in a copy of DemoCustomerProvider and time of a simulator of a time delay.

Demonstration allowed users to compare standard implementation of List (T), implementation with the synchronous loading of the data of VirtualizingCollection (T) and implementation with asynchronous loading of the data of AsyncVirtualizingCollection (T). At usage of VirtualizingCollection (T) and AsyncVirtualizingCollection (T) the user could set page size and taymaut (set time through which page it should be preempt from storage). They should be select according to characteristics of an element and an expect template of usage.

Virtualization of the data in WPF

For comparing of different types of collections, in a window the total amount of the us storage also are display. Animation of a rotat square are us for visualization of a stop of a flow of the user interface. In completely asynchronous decision, animation should not zatormazhivatsya or stop.

출처1

출처2