Этот пост меня побудило практически полное отсутствие описание того, как же на платформе WP8 делать виртуализацию длинных списков. Методы, использующиеся в дектопной Windows 8 тут не работают. Например, тот же ISupportIncrementalLoading попросту отсутствует на WP8.
А так, как я (в свободное от работы менеджером время 🙂 ) делаю приложение, где такая виртуализация жизненно необходима, решил поделиться своим решением. Сразу скажу, что не претендую на идеальность, это просто работающий вариант, который может сэкономить вам часы гугления и тестов.
PS Сейчас я перешел на iOS под Monotouch и подобных проблем нет совсем, поэтому решил достать статью из черновиков. Мало ли кому окажется полезным.
Изложение будет в виде туториала, чтобы его могли воспроизвести даже те, кто еще мало знаком с платформой и .net приложениями.
О чем я говорю
- У нас есть список.
- В списке есть over 100500 пунктов. Для полноты задачи — каждый из пунктов отображает картинку.
- Мы хотим их отображать так, чтобы телефон не умер от нехватки памяти. И не просто отображать, а полноценно с ними работать
Что же нужно для этого сделать
Создаем в XAML LongListSelector
Именно лонглистселектор является контролом, официально рекомендованным MS для разработки списков. ListBox настоятельно рекомендовано более не использовать. Что ж, не будем.
<phone:LongListSelector Width="480" DataContext="{Binding}" Name="List_ListSelector" ItemTemplate="{StaticResource List_ListSelectorItemDataTemplate}" />
Создаем в App.xaml DataTemplate с шаблоном для нашего LongListSelector.
В моем случае это просто текст, который отображает номер элемента и картинку.
<Application.Resources> <DataTemplate x:Key="List_ListSelectorItemDataTemplate"> <StackPanel Margin="0,0,0,27" Height="400"> <TextBlock Text="{Binding Path=ID}" /> <Image Source="{Binding Path=ImageToShow}", Name="ListImage"></Image> </StackPanel> </DataTemplate> </Application.Resources>
Создаем хэлперный класс, который будет оберткой для нашего листа, коллекции и данных. Назовем его LongVirtualList.
class LongVirtualList { public LongListSelector List; // это сам список public ObservableCollection<BaseListElement> Collection; //это коллекция, которая служит ресурсом для списка public DataSource DataSource;// это источник данных для коллекции. Основная задача - по номеру элемента коллекции отдать нам какую-то информацию. В данном случае просто заглушка, умеющая отдавать картинки. public LongVirtualList(LongListSelector longListSelector) { this.List = longListSelector; this.Collection = new ObservableCollection<BaseListElement>(); this.DataSource = new DataSource(); this.InitializeCollection(this.DataSource); // Этот метод заполняет коллекцию пустыми элементами в количестве, maxCount от источника данных. Каждому элементу присваивается постоянный номер. this.List.ItemsSource = this.Collection; longListSelector.ItemRealized+=this.longListSelector_ItemRealized; longListSelector.ItemUnrealized+=this.longListSelector_ItemUnrealized; } private void InitializeCollection(DataSource dataSource) { for (int i = 0; i < dataSource.Count; i++) { this.Collection.Add(new ListTestElement(i)); //ListTestElement это наследник-заглушка класса BaseListElement. } }
Это заготовка под класс. Важно, что к контролу мы привязываем целую коллекцию. Это позволяет обеспечить плавную прокрутку, отсутствие дергания элементов (некоторые реализации динамических списков так же подразумевают применение короткой коллекции и повторное использование элементов. Это не наш вариант). Коллекция с пустыми элементами не вызывает проблем с памятью даже при очень большом объеме (я проверял на миллионе и все было нормально).
Теперь идет самое интересное, собственно то, ради чего я тут все это пишу.
MS представило следующие события:
ItemRealized и ItemUnrealized
Первое из них срабатывает тогда, когда List хочет загрузить в себя новый итем. Второе срабатывает тогда, когда данный итем требуется выгрузить.
Очень важное дополнение: Управлять вызовом этих событий вы не можете. Они вызываются автоматически, когда телефон «чувствует», что ему скоро потребуются данные. Как он это понимает? По тому, сколько элементов списка помещается на экране + чуть-чуть предыдущих и следующих. И тут прячется интересный подводный камень, который я выяснил опытным путем, убив на это несколько часов. Количество элементов списка на экране он определяет до рендеринга. Элементы с динамическим размером (например, картинки) игнорируются, если только не задавать их размер вручную.
Например, если вы укажете в XAML высоту StackPanel Height=«400», то событие ItemRealized будет вызвано последовательно для ~6 элементов списка. Если же в этом же примере вы не укажете высоту, то внешний результат будет тем же (если вы используете большую картинку), однако движок попробует загрузить уже штук 50 элементов и велика вероятность схватить ошибку переполнения памяти.
Итак:
public void longListSelector_ItemUnrealized(object sender, ItemRealizationEventArgs e) { BaseListElement item = (BaseListElement)e.Container.Content; if (item != null) { item.NullCache(); } } public void longListSelector_ItemRealized(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e) { BaseListElement item = (BaseListElement)e.Container.Content; if (item != null) { if (item.Cached == false) { item.FillCache(); } } }
Настало время пройтись по самим элементам списка.
Базовым элементом списка является класс BaseListElement. В этот же самый список можно добавлять любых потомков базового класса.
class BaseListElement : PropertyHelper //обратите внимание, мы наследуем PropertyChangedEventHandler от другого класса. Это позволяет обрабатывать изменения как базовых свойств BaseListElement, так и свойств его потомков с помощью одного EventHandler. В классах-потомках от BaseListElement наследовать PropertyHelper уже не нужно. { public int ID; public bool Cached; private BitmapImage imageToShow; public BitmapImage ImageToShow { get { return this.imageToShow; } set { this.imageToShow = value; NotifyChange("ImageToShow"); } } public BaseListElement(int id) { this.ID = id; this.Cached = false; } public virtual void NullCache() { this.Cached = false; if (this.ImageToShow != null) { this.ImageToShow = null; GC.Collect(); } } public virtual void FillCache() { this.Cached = true; // this.ImageToShow = DataSource.LoadImage(this.ID); тут любой метод загрузки картинки, у меня он реализован в дочерних классах // например, такой BitmapImage bi = new BitmapImage(new Uri("Assets/test.jpg", UriKind.Relative)); bi.DecodePixelWidth = 400; bi.CreateOptions = BitmapCreateOptions.IgnoreImageCache; this.ImageToShow = bi; } //Ничто не мешает нам так же сделать асинхронную загрузку, и использовать этот метод как основной. public virtual async Task FillCacheAsync() { this.FillCache(); } }
Думаете, все? Как бы не так. Код с подобной реализацией класса умрет через несколько сотен загруженных картинок. Все потому, что WP8 очень «своевольно» (не то слово!) обращается с кэшем BitmapImage данных и не выгружает картинки самостоятельно ни в какую!
Поэтому модифицируем методы NullCache() и FillCache(). Теперь они требуют для работы ссылки на контрол Image, которые можно передать им из методов. Мы получим эту ссылку из контейнера e.Container методов ItemUnrealized и ItemRelized.
Итак, правильное кэширование картинок:
public virtual void NullCache(Image image) { if (this.ImageToShow != null) { //Обнулений потребуется не одно, а сразу несколько. BitmapImage bitmapImage = image.Source as BitmapImage; bitmapImage.UriSource = null;//обнуляем само изображение image.Source = null;//обнуляем привязку, иначе это изображение останется навсегда в кэше контрола. DisposeImage(this.ImageToShow)// Обнуляем переменную в данном классе, переопределяя ее заранее заданным маленьким изображением. Просто обнуление =null ничего не даст, переменная при привязке помечается как статическая и мусорщик на ней не работает. GC.Collect(); } this.Cached = false; } public virtual void FillCache(Image image) { this.Cached = true; BitmapImage bi = new BitmapImage(new Uri("Assets/test.jpg", UriKind.Relative)); bi.DecodePixelWidth = 400; bi.CreateOptions = BitmapCreateOptions.IgnoreImageCache; this.ImageToShow = bi; //при обнулении кэша контрола image мы убили ему source, поэтому придется привязывать ресурс динамически при каждом заполнении контрола. А привязку в XAML можно вообще убрать. Binding ImageValueBinding = new Binding("ImageToShow"); ImageValueBinding.Source = this; args.ImageControl.SetBinding(Image.SourceProperty, ImageValueBinding); } public static void DisposeImage(BitmapImage image) { Uri uri= new Uri("oneXone.png", UriKind.Relative);//ссылка на картинку 1x1, которая загружена в проект StreamResourceInfo sr=Application.GetResourceStream(uri); try { using (Stream stream=sr.Stream) { image.DecodePixelWidth=1; //Крайне важный пункт. Именно от него зависит, сколько картинка потребует места для хранения. Если на него "забить", то картинка растянется на изначальный размер BitmapImage и отожрет кучу памяти. Как сделать так, чтобы использованные картинки вообще не занимали места в WP8, я не нашел. (т.е. как их убить полностью, не используя хаков прямой работы с данными). image.SetSource(stream); } } catch {} }
Откуда мы возьмем Image для наших методов подгрузки/выгрузки элементов?
Вот отсюда:
public void longListSelector_ItemRealized(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e) { BaseListElement item = (BaseListElement)e.Container.Content; Image img= FindChild<Image>(e.Container, "ListImage"); if (item != null) { if (item.Cached == false) { item.FillCache(); } } } public static T FindChild<T>(DependencyObject parent, string childName) where T : DependencyObject { if (parent == null) { return null; } T foundChild = null; int childrenCount = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < childrenCount; i++) { DependencyObject child = VisualTreeHelper.GetChild(parent, i); // If the child is not of the request child type child var childType = child as T; if (childType == null) { // Рекурсивно идем вниз по дереву foundChild = FindChild<T>(child, childName); if (foundChild != null) { break; } } else if (!string.IsNullOrEmpty(childName)) { var frameworkElement = child as FrameworkElement; // Если задано имя потомка if (frameworkElement != null && frameworkElement.Name == childName) { foundChild = (T)child; break; } // Если мы нашли элемент, но он содержит еще вложения с тем же типом и именем foundChild = FindChild<T>(child, childName); } else { foundChild = (T)child; break; } } return foundChild; }
Осталась самая малость, покажу реализацию хэлперного класса PropertyHelper, у нас ведь подробный туториал:
public abstract class PropertyHelper:INotifyPropertyChanged { protected void NotifyChange(string args) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(args)); } } public event PropertyChangedEventHandler PropertyChanged; }
Наличие событий PropertyChanged в свойствах элементов списка гарантирует нам обновление элементов даже если они уже загружены в список, без дополнительных телодвижений. Это очень удобно. Например, мы можем сменить в настройках приложения язык и при обновлении ресурсов списка, его элементы обновятся сами собой, без перезагрузки страницы.
Последний момент и все готово.
public MainPage() { InitializeComponent(); LongVirtualList virtualList = new LongVirtualList(List_ListSelector); }
На эту основу можете прикручивать навороты, например дополнительный кэш или что-то, что вы еще хотите сделать.
Данный список у меня работает с тестовой коллекцией из тысяч картинок 1600*1200, обеспечивая их плавную прокрутку и своевременную подгрузку.
Вопрос асинхронной подгрузки данных я затрагивать тут не стал.
Рад, если кому-то все это будет полезным. Во всяком случае, перерыв весь английский интернет, какого-либо сборного рецепта, подобного этому, не нашел, пришлось все изобретать самому.
ссылка на оригинал статьи http://habrahabr.ru/post/164543/
Добавить комментарий