Медленная утилизация UserControl

В моей форме есть два элемента управления: список со списком работников и панель, которая действует как контейнер для отображения сведений (карточек) об их работе. Когда пользователь нажимает на имя работника, я отображаю карточки на панели. Карта — это пользовательский элемент управления с довольно простым пользовательским интерфейсом (2 групповых поля, 3 текстовых поля и несколько меток) и простой логикой (настройка переднего цвета меток).

Карты создаются во время выполнения. Предыдущие карты убираются с панели и добавляются новые - количество карт на одного рабочего от 1 до 4. Тут становится интересно. Все работает нормально до ок. пятый щелчок по рабочим. Кажется, что GC срабатывает, и требуется около двух секунд (0,3 с x количество ранее удаленных карт) для удаления старых карт (ранее удаленных) и отображения новых. Если раньше перемещение между работниками работало отлично, то в этот момент оно становится болезненно медленным. После некоторого изучения я обнаружил, что проблема заключается в методе Dispose моего используемого элемента управления. Вызов base.Dispose() занимает около 0,3 с.

Вот мой код:

private void ShowCards(List<Work> workItems) {
  var y = 5;
  panelControl1.SuspendLayout();
  panelControl1.Controls.Clear();

  foreach (var work in workItems) {
    var card = new Components.WorkDisplayControl(work);
    card.Top = y;
    card.Left = 10;

    y += card.Height + 5;

    panelControl1.Controls.Add(card);
  }

  panelControl1.ResumeLayout(true);
  Application.DoEvents();
}

Что я пробовал до сих пор:

  • скрытие карт вместо выбрасывания - работает быстрее при перемещении между воркерами, но штраф оплачивается при закрытии формы
  • скрыть карты и иметь отдельный поток, который их распоряжается - без изменений
  • тест с добавлением 10 карт и их немедленной утилизацией - медленный
  • тест с добавлением 10 карт и размещением их сразу в конструкторе - БЫСТРО!
  • заменены элементы управления DevExpress на «нормальные» — без изменений
  • ручное удаление старых карт вместо их удаления при смене воркера - каждый переход между воркерами становится медленнее: for (var ix = panelControl1.Controls.Count - 1; ix >= 0; --ix) { panelControl1.Controls[ix].Dispose();}
  • профилировать - вот как я нашел проблему в Dispose. Я могу проследить это до Control.DestroyHandle
  • вызов Controls.Clear() в Dispose методе моего управления - супер странное поведение и исключения
  • удалил все элементы управления из моего пользовательского контроля - немного быстрее, но все же медленно
  • скрытие panelControl1 при удалении и добавлении карт - без изменений
  • отключил фоновый GC - без изменений
  • добавление карт с AddRange

Поскольку та же функциональность работает быстро при вызове из конструктора, я считаю, что причина должна быть где-то в (управляющих) дескрипторах.

Я просто не могу найти причину такого странного поведения. Буду признателен за любые идеи....

ОБНОВЛЕНИЕ: при исследовании связи между GC и Control.Dispose я нашел отличный ответ


person Marko Juvančič    schedule 22.05.2012    source источник
comment
Удаление не имеет ничего общего со сборщиком мусора. Вы используете Controls.Clear() очень опасным способом, он не удаляет элементы управления. Запускаем Taskmgr.exe, вкладка «Процессы». View + Select Columns и отметьте USER Objects и GDI Objects. Убедитесь, что эти значения стабильны для вашего приложения и не растут без ограничений. Свяжитесь с devexpress для получения дополнительной поддержки.   -  person Hans Passant    schedule 22.05.2012
comment
@HansPassant Спасибо за ваши комментарии и идеи. Количество объектов USER и GDI не меняется. Что вы имеете в виду под опасным способом? Я просто хочу, чтобы они были удалены из моего контейнера.   -  person Marko Juvančič    schedule 22.05.2012


Ответы (2)


После [неудачной поддержки от DevExpress] я немного потестировал и поиграл с кодом и, наконец, нашел решение.

Хитрость заключается в очистке элементов управления на UserControl перед утилизацией.

Можно изменить метод Dispose на UC (это решение работало в некоторых случаях, но не во всех из них) или скрыть UC вместо того, чтобы удалять его с панели формы и очищать его Controls

Решение 1.

protected override void Dispose(bool disposing) {
  if (disposing && (components != null)) {
    components.Dispose();
  }

  Controls.Clear(); // <--- Add this line

  base.Dispose(disposing); 
  }

Решение 2.

Добавьте новый метод в UC:

public void ClearControls() {
  Controls.Clear();
}

и в моем исходном вопросе замените эту строку

panelControl1.Controls.Clear();

с этим:

for (var ii = panelControl1.Controls.Count - 1; ii >= 0; --ii) { 
  var wdc = panelControl1.Controls[ii] as Components.WorkDisplayControl;
  wdc.Visible = false;
  wdc.ClearControls();
}

Он работает (как минимум) в 20 раз быстрее, что вполне достаточно.

person Marko Juvančič    schedule 24.05.2012
comment
Боюсь, это вообще не решение... Это опасный способ, потому что он имеет утечку управляющих дескрипторов (вы всегда должны удалять все неиспользуемые элементы управления). Взгляните на мой ответ. Я верю, что это может помочь. - person DmitryG; 28.05.2012

Причина проблемы не связана ни с DevExpress, ни со стандартными элементами управления. Но это связано с созданием и уничтожением дескрипторов управления. Чтобы улучшить реализацию ваших карт, избегайте этих операций, когда это возможно. Я предлагаю вам использовать кэширование для ваших карт:

void ShowCards(List<Work> workItems) {
    cardsPanel.SuspendLayout();
    CacheCards(cardsPanel.Controls);
    int y = 5;
    foreach(var work in workItems) {
        var card = GetCardFromCache(work);
        card.Top = y;
        card.Left = 10;
        y += card.Height + 5;
        cardsPanel.Controls.Add(card);
    }
    cardsPanel.ResumeLayout(true);
}
//
Stack<WorkDisplayControl> cache;
void CacheCards(Control.ControlCollection controls) {
    if(cache == null)
        cache = new Stack<WorkDisplayControl>();
    foreach(WorkDisplayControl wdc in controls)
        cache.Push(wdc);
    controls.Clear();
}
WorkDisplayControl GetCardFromCache(Work data) {
    WorkDisplayControl result = (cache.Count > 0) ?
        cache.Pop() : new WorkDisplayControl();
    result.InitData(data);
    return result;
}

Следующим шагом в оптимизации карточек является уменьшение общего количества используемых дескрипторов управления. Поскольку вы используете элементы управления DevExpress, лучшим вариантом для вас будет XtraLayoutControl. Использование XtraLayoutControl позволяет значительно сократить общее количество дескрипторов управления. Он создает только 4 дескриптора для описанного вами макета (3 редактора с метками в нескольких групповых полях) вместо 8 дескрипторов при использовании стандартных элементов управления. XtraLayoutControl не создает дескрипторы для меток, групп и вкладок редактора. Также обратите внимание на XtraGrid LayoutView — он обеспечивает преимущества использования архитектуры привязки данных сетки и карточек. ' виртуализация макета без дополнительного кодирования.

person DmitryG    schedule 28.05.2012