Duże listy obiektów w WPF i Silverlight – optymalizacja

W artykule „Przeszukiwanie dysku – zbieranie informacji na bieżąco” użyłem kontrolki ListBox do wyświetlania informacji o znalezionych plikach. Przypomnijmy, że były to: nazwa oraz ścieżka do pliku w postaci DataTemplate. W przypadku, gdy znajdziemy 200, 300 plików (wpisów do listy) wszystko działa jak należy. Co natomiast dzieje się jeżeli znajdziemy ich kilka tysięcy? Wszystkim programistom o słabych nerwach (jeżeli takowi istnieją ;-)) stanowczo odradzam zaglądanie do menedżera zadań.

Co się dzieje przy 10 000 wpisów?

W celach testowych do naszej listy w WPF wstawiłem wszystkie pliki z małej partycji. Było ich około 10 tysięcy. Jeżeli czytaliście artykuł o przeszukiwaniu dysku zdajecie sobie sprawę jak mały był to program. Zawierał zaledwie 2 przyciski i właśnie nieszczęsny ListBox. Oto zrzut ekranu z menedżera zadań (program SearchExample.exe):

Tak, to nie jest żadna manipulacja z mojej strony. Gwarantuję, że nie użyłem Photoshopa. Aplikacja po uruchomieniu wykorzystuje 13 000 KB. Z pewnością nie chcielibyście uruchamiać tego programu na partycji systemowej (w pół minuty ponad 0,5 GB). Zrzuty z profilera JetBrains dotTrace na końcu artykułu.

Dlaczego tak się dzieje? Nasz wspaniałomyślny ListBox tworzy automatycznie pełne obiekty – dwa TextBlocki i wszystko co związane z ListBoxItem – dla wszystkich elementów, które ma wyświetlać. W zasadzie wykonał to co do niego należy. Co więc można zmienić?

Wykorzystanie VirtualizingStackPanel

Faktycznie, ListBox musi tworzyć obiekty zgodnie z naszym DataTemplate. Czy jednak musi to robić dla wszystkich 10 000? Nie. Wystarczy aby w pamięci były przechowywane te, które są aktualnie wyświetlane. Tych z kolei jest zaledwie kilka.

Z pomocą przychodzi VirtualizingStackPanel, który w odpowiednich warunkach (o tym za chwilę) zarządza obiektami ListBoxItem. Gdy użytkownik przemieszcza się w dół lub górę listy generowane są brakujące obiekty.

Co ma wspólnego StackPanel z ListBox? Wszystkie elementy na liście umieszczane są w tzw. ItemsPanel. Może to być StackPanel, WrapPanel czy własnie VirtualizingStackPanel. Poniżej kod prezentujący jak wymusić użycie innego panelu dla itemów:

<ListBox.ItemsPanel>
	<ItemsPanelTemplate>
		<VirtualizingStackPanel />
	</ItemsPanelTemplate>
</ListBox.ItemsPanel>

Wstawienie tych 5 linijek XAMLu powoduje, że wpis w menedżerze zadań wygląda następująco:

Dodanie kolejnych 10 000 obiektów tylko nieznacznie podnosi wykorzystanie pamięci operacyjnej.

Jakie szczególne warunki muszą zajść aby to działało?

  • ListBox.ItemsSource zbindowane. Ręczne dodanie gotowych kontenerów lub wpisanie ListBoxItem w XAML nie będzie wirtualizowane.
  • utworzenie ListBox.ItemTemplate

Pełny przykład dla ListBox:

<ListBox x:Name="FileBox" ItemsSource="{Binding}">
	<ListBox.ItemsPanel>
		<ItemsPanelTemplate>
			<VirtualizingStackPanel />
		</ItemsPanelTemplate>
	</ListBox.ItemsPanel>
	<ListBox.ItemTemplate>
		<DataTemplate>
			<StackPanel>
			  <TextBlock Text="{Binding FileName}" />
			  <TextBlock Text="{Binding FilePath}" />
			</StackPanel>
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

Porównanie zrzutów pamięci z profilera

Oto jak wygląda zrzut po zastosowaniu VirtualizingStackPanel:

A tak to wygląda w przypadku domyślnym:

Zwróć uwagę na liczbę kontrolek TextBlock. W każdym z 10 000 ListBoxItem są 2.

Podsumowanie

Ten artykuł jest przestrogą przed beztroskim stosowaniem domyślnych rozwiązań w nawet tak nowoczesnych technologiach jak Silverlight czy WPF. Jeżeli coś działa dobrze dla kilkuset elementów, niekoniecznie będzie to prawdą przy większej liczbie. Zawsze należy testować swoje aplikacje w warunkach rzeczywistego wykorzystania.

Share

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Spam protection by WP Captcha-Free