none
WPF обновление ObservableCollection при большом количестве одновременных добавлений RRS feed

  • Вопрос

  • На клиенте есть ObservableCollection (а точнее ObservableDictionary с DrWPF.com), забинденная на грид. Новые записи для этой коллекции приходят с сервера постепенно, и их добавление я маршализую потоку UI через Dispatcher.BeginInvoke. Но что делать в случае, когда приходит сразу много записей? При моей схеме, при приходе нескольких десятков тысяч записей, у меня подвисает UI на время, пока идет построчное добавление в ObservableCollection, а это занимает довольно много времени. Как этого избежать? Я попытался добавлять записи в другую коллекцию такого же типа, а потом в потоке диспетчера просто присваивать забинденной коллекциии ссылку на эту другую (уже заполненную) коллекцию, но перестает работать биндинг.

    Иначе говоря, в случае, когда сервер присылает мне сразу десять тысяч записей, мне не надо их поочередно визуально отрисовывать в UI с огромной скоростью (как происходит сейчас). Что можно сделать? Наверняка, эта проблема имеет стандартное решение.

    19 февраля 2011 г. 20:31

Ответы

  • Да, все верно. Если Вам нужна индексация и поиск (в случаях, когда LINQ to objects неприемлем), можно решить эту задачу одним из трех способов:

    • унаследоваться от стандартной реализации ObservableCollection<T> или BindingList<T> и дополнить его функционалом словаря;
    • расширить функционал стандартного словаря, реализовав в нем интерфейс IRaiseItemChangedEvents или INotifyCollectionChanged;
    • реализовать полностью новый класс-обертку над Dictionary<T,U> или HashTable с реализацией одного из интефейсов.
    Я бы выбрал последний вариант, исключительно из личных предпочтений к такому стилю проектирования.
    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:32
    20 февраля 2011 г. 21:55
  • А, у Вас же уже есть ObservableDictionary , да еще и с исходниками. Тогда лучше использовать его.

    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:34
    20 февраля 2011 г. 21:57
  • Собирать пришедшее от сервера в буфер из N item-ов, и добавлять пачками. В датагриде по умолчанию включена виртуализация, так что тормозит скорее всего на накладных расходах.

    My blog
    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:32
    20 февраля 2011 г. 22:42
    Модератор
  • А привязка производится к элементу DataGrid? Можете привести соответствующий код разметки? Возможно, там используются шаблоны ItemTemplate? Я добивался того, чтобы 10 миллионов строк добавлялись в таблицу в течение трех с половиной секунд (рабочий нетоповый ноутбук), поищу в исходниках проекта.
    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    21 февраля 2011 г. 19:48
  • Давайте, возьмем за основу какой-нибудь базовый пример, после чего попытаемся улучшить его производительность.

    Создайте новый WPF-проект с именем MyWpfApplication. Замените содержимое файла MainWindow.xaml следующим:

    <Window x:Class="MyWpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
      <Grid>
        <DataGrid AutoGenerateColumns="True" Height="184" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="479" />
        <Button Content="Fill" Height="23" HorizontalAlignment="Left" Margin="12,202,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
        <Button Content="Bind" Height="23" HorizontalAlignment="Left" Margin="12,231,0,0" Name="button2" VerticalAlignment="Top" Width="75" Click="button2_Click" />
        <Label Height="28" HorizontalAlignment="Left" Margin="93,201,0,0" Name="label1" VerticalAlignment="Top" />
        <Label Height="28" HorizontalAlignment="Left" Margin="93,230,0,0" Name="label2" VerticalAlignment="Top" />
      </Grid>
    </Window>
    
    

    и содержимое файла MainWindow.xaml.cs следующим:

    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows;
    
    namespace MyWpfApplication
    {
      public partial class MainWindow : Window
      {
        private const int RECORDS_COUNT = 1 * 1000 * 1000;
    
        public MainWindow()
        {
          InitializeComponent();
        }
    
        public List<Entity> EntitiesList { get; set; }
    
        private void button1_Click(object sender, RoutedEventArgs e)
        {
          Stopwatch stopwatch = new Stopwatch();
          stopwatch.Start();
          EntitiesList = new List<Entity>();
          for (int index = 0; index < RECORDS_COUNT; index++)
            EntitiesList.Add(new Entity { FirstProperty = "FirstProperty of Record " + index.ToString(), SecondProperty = "SecondProperty of Record " + index.ToString(), ThirdProperty = index });
          stopwatch.Stop();
          label1.Content = stopwatch.Elapsed.TotalSeconds;
        }
    
        private void button2_Click(object sender, RoutedEventArgs e)
        {
          Stopwatch stopwatch = new Stopwatch();
          stopwatch.Start();
          dataGrid1.ItemsSource = EntitiesList;
          stopwatch.Stop();
          label2.Content = stopwatch.Elapsed.TotalSeconds;
        }
      }
    
      public class Entity
      {
        public string FirstProperty { get; set; }
        public string SecondProperty { get; set; }
        public int ThirdProperty { get; set; }
      }
    }
    
    
    Запустите приложение и замерьте продолжительность операции, которая выполняется при нажатии на кнопку Bind.

    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    22 февраля 2011 г. 8:56
  • У Вас даже быстрее, чем у меня :)

    Таким образом, привязка одного миллиона записей из трех полей в этом примере занимает 100 миллисекунд, а в Вашем приложении 3000 записей из 10 полей привязываются около 5 секунд?

    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    22 февраля 2011 г. 10:34
  • Алексей, большое спасибо за пример. Я, в меру своих сил, пытался выявить источник проблемы, и мои выводы, что он в биндинге, были сделаны после того, как в проверочном коде:

     public class Element
     {
     public long Number { get; set; }
     public string Description { get; set; }
     }
    
     private ObservableDictionary<long, Element> ElementsForUI { get; set; }
    
     private void RefreshUI(List<Element> elements = null)
     {
     _mainWindow.Dispatcher.BeginInvoke(new ThreadStart(() =>
     {
     if (elements != null && elements.Count > 0)
     {
     try
     {
     elements.ForEach(delegate(Element element)
     {
     ElementsForUI.Add(element.Number, element);
     });
     }
     catch (Exception)
     {
     { }
     throw;
     }
     _mainWindow.dgrElements.ScrollIntoView(_mainWindow.dgrElements.Items[_mainWindow.dgrElements.Items.Count - 1]); //скролл на последний добавленный элемент
     }
     }));
    

    ...если я заменял строку добавления в забинденную коллекцию ElementsForUI.Add(element.Number, element); (ObservableDictionary<long, Element>) на строку добавления в обычный словарь, ни на что не забинденнный, (new Dictionary<long, Element>();), то я получал время его заполнения в разы меньше (к примеру, добавление тысячи записей в первом случае занимало 700 мс, во втором - 50 мс). Сейчас, благодаря Вашему примеру, стал смотреть дальше, и выяснил, что проблема действительно не в биндинге, а в том, что это так медленно идет добавление в ObservableDictionary (то есть медленно, даже если не ставить его в ItemsSource).

    Попробовал ObservableDictionary c codeplex.com по Вашей ссылке, он работает существенно быстрее той реализации ObservableDictionary, которую я использовал, и всего раз в 2-4 медленнее обычного словаря, что вполне приемлемо. Добавление ста тысяч записей занимает полсекунды.

    Эта проблема решена, еще раз спасибо.

    • Помечено в качестве ответа PashaPashModerator 22 февраля 2011 г. 17:45
    22 февраля 2011 г. 16:05

Все ответы

  • Вариант с подменой коллекции целиком должен был сработать. Только присваивать нужно было не забинденной коллекции, а свойству ItemsSource/DataSource того контрола, который отображает коллекцию.
     

    My blog
    19 февраля 2011 г. 20:43
    Модератор
  • Возможно, вместо ObservableCollection<T> получится использовать BindingList<T> ?
    19 февраля 2011 г. 20:53
  • PashaPash, да, можно и без подмены, сначала все получить, а потом только биндить. Проблема только в том, что узнать, когда будут получены все "исторические" данные, невозможно. Сервер просто выдает таблицу данных последовательно по одной строчке.

    20 февраля 2011 г. 20:48
  • Алексей, из описания не смог понять, как это может помочь в моей ситуации.
    20 февраля 2011 г. 20:50
  • Посмотрите описание свойства RaiseListChangedEvents .
    20 февраля 2011 г. 21:00
  • Вообще, мне нужен именно словарь, так как очень много элементов и постоянно нужен поиск по ним с константным временем.

    Но идею вроде-бы понял, при формировании подавлять событие изменения коллекции, чтобы не было отрисовки, типа как описано тут http://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/

    20 февраля 2011 г. 21:47
  • Да, все верно. Если Вам нужна индексация и поиск (в случаях, когда LINQ to objects неприемлем), можно решить эту задачу одним из трех способов:

    • унаследоваться от стандартной реализации ObservableCollection<T> или BindingList<T> и дополнить его функционалом словаря;
    • расширить функционал стандартного словаря, реализовав в нем интерфейс IRaiseItemChangedEvents или INotifyCollectionChanged;
    • реализовать полностью новый класс-обертку над Dictionary<T,U> или HashTable с реализацией одного из интефейсов.
    Я бы выбрал последний вариант, исключительно из личных предпочтений к такому стилю проектирования.
    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:32
    20 февраля 2011 г. 21:55
  • А, у Вас же уже есть ObservableDictionary , да еще и с исходниками. Тогда лучше использовать его.

    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:34
    20 февраля 2011 г. 21:57
  • Собирать пришедшее от сервера в буфер из N item-ов, и добавлять пачками. В датагриде по умолчанию включена виртуализация, так что тормозит скорее всего на накладных расходах.

    My blog
    • Помечено в качестве ответа Qwester33 21 февраля 2011 г. 7:32
    20 февраля 2011 г. 22:42
    Модератор
  • Посмею предложить небольшое улучшение: добавлять не при заполнении буфера, а по таймеру.
    20 февраля 2011 г. 22:50
  • Попробовал дождаться получения всей "истории", помещая ее в буфер, а затем добавить содержимое этого буфер в Observable, с подавлением OnCollectionChanged во время добавления элементов. Использовал вот такой метод добавления:

     public void AddRange(IDictionary<TKey, TValue> dictionary)
     {
     if (dictionary == null)
     throw new ArgumentNullException("dictionary");
    
     _suppressNotification = true;
    
     foreach (var item in dictionary)
     {
     Add(item.Key, item.Value);
     }
     _suppressNotification = false;
     OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
     }
    

    Разницы в быстродействии, по сравнению с прямым добавлением каждой записи в Observable - ноль. Грубо говоря, добавление 3000 записей занимает 5 секунд, независимо от того, сколько раз сработает OnCollectionChanged = один раз или три тыщи раз.

    Еще раз опишу проблему:

    Допустим, мне пришла таблица на 3 тыщи строк. У меня в UI есть датагрид, который должен показывать последние 10 строк этой таблицы. Задача - поместить эти три тыщи строк в этот грид, и сделать это быстро.

    Вот мой метод заполнения Observable коллекции, к которому я в итоге вернулся, но который делает заполнение медленно:

        public class Element
        {
          public long Number { get; set; }
          public string Description { get; set; }
        }
    
        private ObservableDictionary<long, Element> ElementsForUI { get; set; }
    
        private void RefreshUI(List<Element> elements = null)
        {
          _mainWindow.Dispatcher.BeginInvoke(new ThreadStart(() =>
          {
            if (elements != null && elements.Count > 0)
            {
              try
              {
                elements.ForEach(delegate(Element element)
                {
                  Element elementOut;
                  if (ElementsForUI.TryGetValue(element.Number, out elementOut)) //если элемент с таким номером существует, то значит в апдейте изменение элемента
                  {
                    elementOut.Number = element.Number;
                    elementOut.Description = element.Description;
                  }
                  else //если элемент с таким номером не существует, то значит в апдейте новый элемент и его надо добавить
                    ElementsForUI.Add(element.Number, element);
                });
              }
              catch (Exception)
              {
                { }
                throw;
              }
              _mainWindow.dgrElements.ScrollIntoView(_mainWindow.dgrElements.Items[_mainWindow.dgrElements.Items.Count - 1]); //скролл на последний добавленный элемент
            }
          }));
        }
    21 февраля 2011 г. 16:58
  • А привязка производится к элементу DataGrid? Можете привести соответствующий код разметки? Возможно, там используются шаблоны ItemTemplate? Я добивался того, чтобы 10 миллионов строк добавлялись в таблицу в течение трех с половиной секунд (рабочий нетоповый ноутбук), поищу в исходниках проекта.
    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    21 февраля 2011 г. 19:48
  • Элемент имеет штук 20 свойств, из них штук 10 забиндены. Привязка свойств элемента сделана так:

    Binding="{Binding Path=Value.Number}"
    

    Вы это имели в виду?  ItemTemplate не используется. Я пробовал отключать все форматирование (стили и триггеры), это не оказывало видимого влияния на производительность.

    По скорости мне бы хотелось, чтобы было примерно ну секунд 10 для ста тысяч записей. То есть миллионы за секунды не требуется.

    21 февраля 2011 г. 20:42
  • Давайте, возьмем за основу какой-нибудь базовый пример, после чего попытаемся улучшить его производительность.

    Создайте новый WPF-проект с именем MyWpfApplication. Замените содержимое файла MainWindow.xaml следующим:

    <Window x:Class="MyWpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
      <Grid>
        <DataGrid AutoGenerateColumns="True" Height="184" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="479" />
        <Button Content="Fill" Height="23" HorizontalAlignment="Left" Margin="12,202,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
        <Button Content="Bind" Height="23" HorizontalAlignment="Left" Margin="12,231,0,0" Name="button2" VerticalAlignment="Top" Width="75" Click="button2_Click" />
        <Label Height="28" HorizontalAlignment="Left" Margin="93,201,0,0" Name="label1" VerticalAlignment="Top" />
        <Label Height="28" HorizontalAlignment="Left" Margin="93,230,0,0" Name="label2" VerticalAlignment="Top" />
      </Grid>
    </Window>
    
    

    и содержимое файла MainWindow.xaml.cs следующим:

    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows;
    
    namespace MyWpfApplication
    {
      public partial class MainWindow : Window
      {
        private const int RECORDS_COUNT = 1 * 1000 * 1000;
    
        public MainWindow()
        {
          InitializeComponent();
        }
    
        public List<Entity> EntitiesList { get; set; }
    
        private void button1_Click(object sender, RoutedEventArgs e)
        {
          Stopwatch stopwatch = new Stopwatch();
          stopwatch.Start();
          EntitiesList = new List<Entity>();
          for (int index = 0; index < RECORDS_COUNT; index++)
            EntitiesList.Add(new Entity { FirstProperty = "FirstProperty of Record " + index.ToString(), SecondProperty = "SecondProperty of Record " + index.ToString(), ThirdProperty = index });
          stopwatch.Stop();
          label1.Content = stopwatch.Elapsed.TotalSeconds;
        }
    
        private void button2_Click(object sender, RoutedEventArgs e)
        {
          Stopwatch stopwatch = new Stopwatch();
          stopwatch.Start();
          dataGrid1.ItemsSource = EntitiesList;
          stopwatch.Stop();
          label2.Content = stopwatch.Elapsed.TotalSeconds;
        }
      }
    
      public class Entity
      {
        public string FirstProperty { get; set; }
        public string SecondProperty { get; set; }
        public int ThirdProperty { get; set; }
      }
    }
    
    
    Запустите приложение и замерьте продолжительность операции, которая выполняется при нажатии на кнопку Bind.

    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    22 февраля 2011 г. 8:56
  • Если запустить из студии и нажать Bind, то покажет значение примерно 0,0005-0,00055

    22 февраля 2011 г. 9:39
  • Fill выполняется примерно секунд 25, Bind после этого выполняется 0,1

    22 февраля 2011 г. 9:45
  • У Вас даже быстрее, чем у меня :)

    Таким образом, привязка одного миллиона записей из трех полей в этом примере занимает 100 миллисекунд, а в Вашем приложении 3000 записей из 10 полей привязываются около 5 секунд?

    • Помечено в качестве ответа Qwester33 22 февраля 2011 г. 16:07
    22 февраля 2011 г. 10:34
  • Алексей, большое спасибо за пример. Я, в меру своих сил, пытался выявить источник проблемы, и мои выводы, что он в биндинге, были сделаны после того, как в проверочном коде:

     public class Element
     {
     public long Number { get; set; }
     public string Description { get; set; }
     }
    
     private ObservableDictionary<long, Element> ElementsForUI { get; set; }
    
     private void RefreshUI(List<Element> elements = null)
     {
     _mainWindow.Dispatcher.BeginInvoke(new ThreadStart(() =>
     {
     if (elements != null && elements.Count > 0)
     {
     try
     {
     elements.ForEach(delegate(Element element)
     {
     ElementsForUI.Add(element.Number, element);
     });
     }
     catch (Exception)
     {
     { }
     throw;
     }
     _mainWindow.dgrElements.ScrollIntoView(_mainWindow.dgrElements.Items[_mainWindow.dgrElements.Items.Count - 1]); //скролл на последний добавленный элемент
     }
     }));
    

    ...если я заменял строку добавления в забинденную коллекцию ElementsForUI.Add(element.Number, element); (ObservableDictionary<long, Element>) на строку добавления в обычный словарь, ни на что не забинденнный, (new Dictionary<long, Element>();), то я получал время его заполнения в разы меньше (к примеру, добавление тысячи записей в первом случае занимало 700 мс, во втором - 50 мс). Сейчас, благодаря Вашему примеру, стал смотреть дальше, и выяснил, что проблема действительно не в биндинге, а в том, что это так медленно идет добавление в ObservableDictionary (то есть медленно, даже если не ставить его в ItemsSource).

    Попробовал ObservableDictionary c codeplex.com по Вашей ссылке, он работает существенно быстрее той реализации ObservableDictionary, которую я использовал, и всего раз в 2-4 медленнее обычного словаря, что вполне приемлемо. Добавление ста тысяч записей занимает полсекунды.

    Эта проблема решена, еще раз спасибо.

    • Помечено в качестве ответа PashaPashModerator 22 февраля 2011 г. 17:45
    22 февраля 2011 г. 16:05