none
Периодические очистка и заполнение ObservableCollection очень ощутимо подвешивает UI RRS feed

  • Вопрос

  • В настоящий момент, я пытаюсь использовать для биндинга ObservableCollection, привязав её к WPF DataGrid. Количество записей в коллекции варьируется от 0 до 100. Это немного. Но обновляется коллекция часто - приблизительно раз 60 - 100 миллисекунд.

    Я выполняю это следующими шагами.

    Сначала, я определил производный от неё класс:

    /// <summary>
    /// Класс ObservableOpenBook представляет содержимое агрегированного стакана,
    /// предназначенное для визуализации в окне приложения.
    /// </summary>
    public class ObservableOpenBook : ObservableCollection<OpenBookRecord>
    {
        /// <summary>
        /// Создаёт экземпляр класса ObservableOpenBook.
        /// </summary>
        public ObservableOpenBook()
             : base()
        {
        }
    }

    Затем определяю экземпляр этого класса, как данное-член в классе главного окна приложения:

    /// <summary>
    /// Представление стакана по выбранному для торгов инструменту,
    /// содержимое которого предназначено для визуализации в окне приложения.
    /// </summary>
    private static ObservableOpenBook displayedOpenBook = new ObservableOpenBook();

    Затем, в классе главного окна приложения, определил свойство, возвращающее ObservableOpenBook:

    /// <summary>
    /// Возвращает стакан по выбранному торгуемому инструменту
    /// для отображения его содержимого в окне приложения.
    /// </summary>
    public static ObservableOpenBook GetDisplayedOpenBook
    {
       get { return displayedOpenBook; }
    }

    Затем, в классе ReplicationDataProvider возвращаю ObservableOpenBook для разметки XAML:

    /// <summary>
    /// Класс ReplicationDataProvider предоставляет данные из потоков репликации
    /// для их визуального отображения в окнах приложения.
    /// </summary>
    public class ReplicationDataProvider
    {
       /// <summary>
       /// Предоставлякт данные из агрегированного стакана по инструменту.
       /// </summary>
       /// <returns></returns>
       public ObservableOpenBook GetDisplayedOpenBook()
       {
                return MainWindow.GetDisplayedOpenBook;
       }
    }

    В XAML-разметке главного окна пишу:

    <Window.Resources>
            <ObjectDataProvider x:Key="ReplicationDataProvider" ObjectType="{x:Type local:ReplicationDataProvider}"/>
            <ObjectDataProvider x:Key="OpenbookRecords" ObjectInstance="{StaticResource ResourceKey=ReplicationDataProvider}" MethodName="GetDisplayedOpenBook"/>
    </Window.Resources>

    а ниже там, где в XAML определяется DataGrid пишу:

    <!--Табличная сетка для представления данных из агрегированного стакана-->
    <DataGrid Name="dgOrdersAggr" Grid.Row="0" Grid.Column="0" Margin="10" AutoGenerateColumns="False"
                              IsReadOnly="True" CanUserAddRows="False" CanUserDeleteRows="False"
                              CanUserResizeRows="False" CanUserReorderColumns="False"
                          ItemsSource="{Binding Source={StaticResource ResourceKey=OpenbookRecords}}">
       <DataGrid.Columns>
           <DataGridTextColumn Header="Цена" Binding="{Binding Path=Price}"/>
           <DataGridTextColumn Header="Объём" Binding="{Binding Path=Volume}"/>
       </DataGrid.Columns>
    </DataGrid>

    Теперь, по событию, отрабатываемому в другом треде, наполняю коллекцию данными:

    // Найти стакан, соответствующий выбранному торгуемому инструменту
    OpenBook tmpOb = totalOpenBook.GetOpenBookByInstrument(this.futTradedInstrumentID);
    // Если стакан найден,
    if (tmpOb != null)
    {
        // Если его содержимое изменилось во время последней транзакции,
        if (tmpOb.IsChanged)
        {
            // Если стакан содержит записи:
            if (tmpOb.OpenBookRecords.Count > 0)
            {
                // Вывести на экран содержимое стакана по выбранному инструменту.
                this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate()
                {
                    // Очистить предыдущее содержимое стакана.
                    displayedOpenBook.Clear();
                    // Заполнить стакан в окне приложения.
                    for (int i = 0; i < tmpOb.OpenBookRecords.Count; i++)
                        displayedOpenBook.Add(tmpOb.OpenBookRecords[i]);
                 });
                 // После очередного наполнения экранного буфера стакана записями,
                 // сбрасываем флаг выполнения изменений в стакане.
                 // Стакан после этого считается консистентным, пока не будет
                 // изменён во время следующей транзакции.
                 tmpOb.IsChanged = false;
            }
            else
            {
                // Очистить предыдущее содержимое стакана.
                this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate()
                {
                     displayedOpenBook.Clear();
                });
                tmpOb.IsChanged = false;
            }
        }
    }

    Отображение данных в окне здорово тормозит работу UI. Чем тут можно помочь?

    24 сентября 2012 г. 14:09

Ответы

  • избавьтесь от статической привязки данных

    для этого можно сделать оболочку класс с открытым свойством ObservableOpenBook после чего поставить этот класс как DataContext для вашего контролла/окна и привязаться к нему тогда не нужно будет использоваться статическую привязку

    • Помечено в качестве ответа TownSparrow 27 сентября 2012 г. 8:56
    25 сентября 2012 г. 19:21
  • // Очистить предыдущее содержимое стакана.
    // Заполнить стакан в окне приложения.
    // Разрешить сигнализацию приёмнику привязки об изменениях в стакане
    // Показать пользователю изменения в стакане.
    
    Вот этого как раз и не должно быть. У вас размерность коллекции не поменялась. Значит, можно переиспользовать старые элементы. Изменяйте их. А вы меняете полностью коллекцию. Вот и тормозит все.
    • Предложено в качестве ответа Abolmasov Dmitry 26 сентября 2012 г. 13:26
    • Помечено в качестве ответа TownSparrow 27 сентября 2012 г. 8:55
    26 сентября 2012 г. 13:24
    Отвечающий

Все ответы

  • попробуйте вместо добовления и удаления записей создавать коллекцию по новой

     // Заполнить стакан в окне приложения.
                   // for (int i = 0; i < tmpOb.OpenBookRecords.Count; i++)
                      //  displayedOpenBook.Add(tmpOb.OpenBookRecords[i]);
    displayedOpenBook = new ObservableOpenBook (tmpOb);



    25 сентября 2012 г. 9:31
  • Может быть с такой частотой обновления вам не стоит использовать ObservableCollection, а лучше самому контролировать привязку/отвязаку данных. Т.е. когда данные изменились и подготовлены к отображению, то тогда выполнять привязку данных (т.е. грубо говоря использовать обычный List например), чтобы не было перестройки графического интерфейса по мере заполнения вами ObservableCollection после полной очистки, ведь каждое изменение коллекции отображается на интерфейс.

    Для связи [mail]

    25 сентября 2012 г. 13:58
  • Здравствуйте, Brash_O. Вы пишете: попробуйте вместо добовления и удаления записей создавать коллекцию по новой displayedOpenBook = new ObservableOpenBook (tmpOb);

    А как же тогда синтаксис в XAML-разметке:

    ItemsSource="{Binding Source={StaticResource ResourceKey=OpenbookRecords}}">

    Ведь StaticResource берётся один раз, при запуске, как я понимаю, приложения. И если я поменяю ссылку в процессе работы приложения, как вы пишете:

    displayedOpenBook = new ObservableOpenBook (tmpOb);

    то разве эта измененная ссылка будет ли видна через StaticResource? Будет выполняться отображение данных в DataGrid? Может быть для этого, в разметке в XAML каким-нибудь образом поменять синтаксис привязки? Но как? Посоветуйте, пожалуйста. Что использовать в этом случае вместо StaticResource? Я пробовал подобное динамическое изменение источника в коде C#, устанавливал на него св-во DataContext датагрида в коде C#, но отображения в датагриде не было.

    P.S. Можете смеятся надо мной или ругать меня, но меня эта привязка скажу так "бьёт по шее". Т.е. никак не могу в ней натаскаться - набраться опыта иными словами.





    • Изменено TownSparrow 25 сентября 2012 г. 14:16
    25 сентября 2012 г. 14:02
  • К моему ответу - вот взгяните ответ в теме WPF: what's the most efficient/fast way of adding items to a ListView?

    Для связи [mail]

    25 сентября 2012 г. 14:13
  • Спрасибо, Димитрий.

    А скажите, пожалуйста, - BindingList, о котором написано в рекомендуемом вами посте, может использоваться в качестве источника данных для WPF DataGrid? Мне в сети попадалась статья, что он (DataGrid) работает быстрее, чем ListView и ListBox, Поэтому я его и выбрал в качестве приёмника првязки. Завтра попробую сделать так, как там советуется.

    • Изменено TownSparrow 25 сентября 2012 г. 14:36
    25 сентября 2012 г. 14:28
  • К моему ответу - вот взгяните ответ в теме WPF: what's the most efficient/fast way of adding items to a ListView?

    Для связи [mail]


    Сделал как рекомендовано, но всё равно UI подвешивается - тормозит, хотя немного поменьше, но всё равно тормозит. Как бы решить этот вопрос?
    • Изменено TownSparrow 25 сентября 2012 г. 16:10
    25 сентября 2012 г. 16:07
  • Здорово подвешивает UI после нескольких минут работы Практически также как OdservableCollection.
    • Изменено TownSparrow 25 сентября 2012 г. 16:50
    25 сентября 2012 г. 16:39
  • избавьтесь от статической привязки данных

    для этого можно сделать оболочку класс с открытым свойством ObservableOpenBook после чего поставить этот класс как DataContext для вашего контролла/окна и привязаться к нему тогда не нужно будет использоваться статическую привязку

    • Помечено в качестве ответа TownSparrow 27 сентября 2012 г. 8:56
    25 сентября 2012 г. 19:21
  • попробуйте вместо добовления и удаления записей создавать коллекцию по новой

     // Заполнить стакан в окне приложения.
                   // for (int i = 0; i < tmpOb.OpenBookRecords.Count; i++)
                      //  displayedOpenBook.Add(tmpOb.OpenBookRecords[i]);
    displayedOpenBook = new ObservableOpenBook (tmpOb);



    Спасибо,  Brash_O. Я попробую избавиться от статической привязки данных и сделать, как вы предложили ранее, пересоздание коллекции всякий раз когда  её содержимое меняется. Я ещё раз извиняюсь - выше, вы говорили, что пересоздание коллекции заново будет в меньшей степени тормозить UI, чем повторные заполнения уже существующей коллекции? Это действительно так?

    К слову, парни, а нельзя ли непосредственно (безо всякой привязки) вставлять записи в WPF DataGid или в WPF ListView, как это делалось раньше, например, в гридах и листбоксах Delphi?



    • Изменено TownSparrow 26 сентября 2012 г. 6:55
    26 сентября 2012 г. 6:49
  • да потому что когда вы удаляете элемент это вызывает перерисовку интерфейса, и когда добовляете то опять же перерисовуваете интерфейс, если пересоздавать то происходит однократная перерисовка

    26 сентября 2012 г. 6:56
  • Вы все сделали по примеру, выставляете отключение события как в коде ниже?
    // disable UI updation
    list.RaiseListChangedEvents = false;
    for each(string s in MyCollection){
       list.Add(s);
    }
    // after all.. update the UI with following
    list.RaiseListChangedEvents = true;


    Для связи [mail]

    26 сентября 2012 г. 7:45
  • http://sdrv.ms/UTFlez

    Создал небольшой тестовый проект. В первом гриде - обновляются полностью данные. Во втором - только отдельные свойства.

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

    В частности - наверняка ведь у вас не раз в 0,1 секунду меняется размерность коллекции. А значит можно одну и туже коллекцию использовать много раз и свести задачу ко второму случаю. Можно даже воспользоваться ObservableCollection и менять количество элементов в коллекции только когда нужно, а значения перезаписывать. Главное, это сократить количество полных пересозданий всех строк в гриде.

    26 сентября 2012 г. 7:48
    Отвечающий
  • Да, Антон. Я делаю всё так как там показывалось. Вот мой код из проекта:

    // Найти стакан, соответствующий выбранному торгуемому инструменту
    OpenBook tmpOb = totalOpenBook.GetOpenBookByInstrument(this.futTradedInstrumentID);
    // Если стакан найден,
    if (tmpOb != null)
    {
       // Если его содержимое изменилось во время последней транзакции,
       if (tmpOb.IsChanged)
       {
          // Запретить сигнализацию приёмнику привязки об изменениях в стакане                       
          displayedOpenBook.RaiseListChangedEvents = false;
          // Если стакан содержит записи:
          if (tmpOb.OpenBookRecords.Count > 0)
          {
              // Вывести на экран содержимое стакана по выбранному инструменту.
              this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate()
              {
                 // Очистить предыдущее содержимое стакана.
                 displayedOpenBook.Clear();
                 // Заполнить стакан в окне приложения.
                 for (int i = 0; i < tmpOb.OpenBookRecords.Count; i++)
                       displayedOpenBook.Add(tmpOb.OpenBookRecords[i]);
                 // Разрешить сигнализацию приёмнику привязки об изменениях в стакане
                 displayedOpenBook.RaiseListChangedEvents = true;
                 // Показать пользователю изменения в стакане.
                 displayedOpenBook.ResetBindings();
              });
          }
          // После очередного наполнения экранного буфера стакана записями,
          // сбрасываем флаг выполнения изменений в стакане. Стакан после этого
          // считается консистентным, пока не будет изменён во время следующей транзакции.
          tmpOb.IsChanged = false;
       }
    }

    Именно размерность коллекции меняется реже, чем раз в 100 миллисекунд. Именно размерность (т.е. кол-во элементов в ней) Но значения её элементов (являющихся экземплярами класса OpenBookRecord), см.ниже, могут меняться и чаще, чем раз в 100 миддисекунд. Я допускаю здесь что иногда может быть даже раз в 10 миллисекунд. Ниже - класс OpenBookRecord (извиняюсь за название пространства имён, но у меня так проект называется):

    using System; using System.Collections.Generic; using System.ComponentModel; namespace Balalaika { /// <summary> /// Класс OpenBookRecord представляет запись /// из таблицы агрегированного стакана. /// </summary> public class OpenBookRecord : IComparable<OpenBookRecord>, INotifyPropertyChanged { #region Events /// <summary> /// Событие "Изменилось значение свойства" /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion #region Fields /// <summary> /// Значение ReplID записи. /// </summary> private Int64 replId; /// <summary> /// Значение ReplRev записи. /// </summary> private Int64 replRev; /// <summary> /// Цена котировки. /// </summary> private Double price; /// <summary> /// Объем агрегированной котировки. /// </summary> private Int64 volume; /// <summary> /// Направление котировки (покупка/продажа). /// </summary> private SByte dir; /// <summary> /// Ссылка на стакан, содержащий эту запись /// и поле кода инструмента. /// </summary> private OpenBook residence; #endregion //* #region PropertyChanged Event Raiser /// <summary> /// Вызывает событие "Изменилось значение свойства". /// </summary> /// <param name="propertyName"></param> private void NotifyPropertyChanged(String propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #endregion //*/ #region Properties /// <summary> /// Возвращает или задаёт значение ReplID записи. /// </summary> public Int64 ReplId { get { return replId; } set { if (replId != value) { replId = value; NotifyPropertyChanged("ReplId"); } } } /// <summary> /// Возвращает или задаёт значение ReplRev записи. /// </summary> public Int64 ReplRev { get { return replRev; } set { if (replRev != value) { replRev = value; NotifyPropertyChanged("ReplRev"); } } } /// <summary> /// Возвращает или задаёт цену котировки. /// </summary> public Double Price { get { return price; } set { if (price != value) { price = value; NotifyPropertyChanged("Price"); } } } /// <summary> /// Возвращает или задаёт объем агрегированной котировки. /// </summary> public Int64 Volume { get { return volume; } set { if (volume != value) { volume = value; NotifyPropertyChanged("Volume"); } } } /// <summary> /// Возвращает или задаёт /// </summary> public SByte Dir { get { return dir; } set { if (dir != value) { dir = value; NotifyPropertyChanged("Dir"); } } } /// <summary> /// Возвращает или задаёт ссылку на стакан, содержащий эту запись /// и поле кода инструмента. /// </summary> public OpenBook Residence { get { return residence; } set { if (residence != value) { residence = value; NotifyPropertyChanged("Residence"); } } } #endregion #region Methods /// <summary> /// Предикат, указывающий порядок сортировки по умолчанию записей в стакане. /// Согласно этому предикту - записи сортируются по возрастанию значения /// в поле ReplID /// </summary> /// <param name="cmpRecord"></param> /// <returns></returns> public int CompareTo(OpenBookRecord cmpRecord) { return replId.CompareTo(cmpRecord.replId); } #endregion

    }

    }

    Я прошу прощенья, но я уже весь в мыле от того, что никамк не слажу с проблемой.

    Я сейчас в своём приложении занимаюсь проверкой кода, который реализует отправку заявок на биржу, а завтра скорее всего опять к стакану вернусь. Сделать-то его надо.


    • Изменено TownSparrow 26 сентября 2012 г. 11:28
    26 сентября 2012 г. 11:22
  • // Очистить предыдущее содержимое стакана.
    // Заполнить стакан в окне приложения.
    // Разрешить сигнализацию приёмнику привязки об изменениях в стакане
    // Показать пользователю изменения в стакане.
    
    Вот этого как раз и не должно быть. У вас размерность коллекции не поменялась. Значит, можно переиспользовать старые элементы. Изменяйте их. А вы меняете полностью коллекцию. Вот и тормозит все.
    • Предложено в качестве ответа Abolmasov Dmitry 26 сентября 2012 г. 13:26
    • Помечено в качестве ответа TownSparrow 27 сентября 2012 г. 8:55
    26 сентября 2012 г. 13:24
    Отвечающий
  • // Очистить предыдущее содержимое стакана.
    // Заполнить стакан в окне приложения.
    // Разрешить сигнализацию приёмнику привязки об изменениях в стакане
    // Показать пользователю изменения в стакане.
    Вот этого как раз и не должно быть. У вас размерность коллекции не поменялась. Значит, можно переиспользовать старые элементы. Изменяйте их. А вы меняете полностью коллекцию. Вот и тормозит все.

    Т.е. как я понял - не очищать стакан, а переприсваивать значения уже имеющихся элементов в нём. Но не делать вот этого:

    // Запретить сигнализацию приёмнику привязки об изменениях в стакане                       
    displayedOpenBook.RaiseListChangedEvents = false;

    перед очередным заполнением стакана, пускай путём переиспользования старых элементов.

    А после окончания его очередного заполнения (переприсваивания значений старых элементов) не делать этого:

    // Разрешить сигнализацию приёмнику привязки об изменениях в стакане
    displayedOpenBook.RaiseListChangedEvents = true;
    // Показать пользователю изменения в стакане.
    displayedOpenBook.ResetBindings();

    Так? Я правильно понял? Но я извиняюсь - кол-во элементов в стакане тоже может меняться, пускай не так часто, а скажем раз в 300 - 500 миллисекунд. Вобщем - кто его знает, но скакать может. В этом случае, просто буду сравнивать новый объём со старым и если старый больше, то после записи новых значений буду удалять лишние элементы, например с помощью RemoveAt();, а если новый больше - то придётся добавлять разницу с помощью Add();. И в этом случае BindingList<T> будет тормозить UI в ощутимой для пользователя степени или нет? Т.е. в частности скроллинг содержимого того-же стакана, скроллинг отрисовываемых в томже окне графиков будет тормозиться?

    P.S. Я извиняюсь - у меня вечером уже голова не соображает. Я с утра так ничего себе работник, а вечером - никудышный. Всем пока, до завтра.





    • Изменено TownSparrow 26 сентября 2012 г. 15:21
    26 сентября 2012 г. 15:08
  • Да, примерно так и будет. Тормозить не будет сильно, если вообще будет, поскольку добавляться/удаляться будет пара-тройка элементов, а не все объекты. А обновление пары десятков значений вообще не будет ощущаться.
    26 сентября 2012 г. 15:46
    Отвечающий
  • Да, Антон, похоже действительно так. Я сейчас пробую с ObservableCollection и всё идёт нормально - по крайней мере заметно не тормозится. Спасибо большое, вопрос я закрываю. Огромное спасибо, ребята, всем за помощь.
    27 сентября 2012 г. 8:52