1

I have two controls on my form: a listbox with a list of workers and a panel which acts as a container for showing the details (cards) about their work. When a user clicks on the worker's name, I display cards on the panel. A card is a usercontrol with some fairly simple UI (2 groupboxes, 3 textboxes and several labels) and simple logic (setting forecolor of labels).

The cards are created in runtime. Previous cards are removed from the panel and the new ones get added - the number of cards per worker is 1 to 4. It gets interesting here. Everything works fine until approx. the fifth click on the workers. It seems that GC kicks in and it takes about two seconds (0.3s x number of previously removed cards) for the old cards (previously removed) to get disposed and new ones shown. If moving between workers works great before, it gets painfully slow at that point. After some exploring I've located the problem to lay in Dispose method of my usedcontrol. Call base.Dispose() takes about 0.3s.

Here’s my code:

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();
}

What I've tried so far:

  • hiding cards instead of disposing - it works faster when moving between workers, but the penalty is paid when closing the form
  • hiding the cards and have a separate thread which disposes them - no change
  • test with adding 10 cards and disposing them immediately - slow
  • test with adding 10 cards and disposing them immediately in the constructor - FAST!
  • replaced DevExpress controls with “normal” – no change
  • manually disposing old cards instead of removing them when changing worker - every move between workers gets slower: for (var ix = panelControl1.Controls.Count - 1; ix >= 0; --ix) { panelControl1.Controls[ix].Dispose();}
  • profiling it - that's how I've found the problem in Dispose. I can trace it down to Control.DestroyHandle
  • calling Controls.Clear() in Dispose method of my control - super strange behaviour and exceptions
  • removed all of the controls from my usercontrol - a little bit faster, but still slow
  • hiding panelControl1 when removing and adding cards - no change
  • turned background GC off - no change
  • adding cards with AddRange

Since the same functionality works fast when called from the constructor, I belive the reason must be somewhere in the (control) handles.

I just can't find the reason for this strange behaviour. I would appreciate any ideas....

UPDATE: While researching connection between GC and Control.Dispose I found this excellent answer

Community
  • 1
  • 1
Marko Juvančič
  • 5,792
  • 1
  • 25
  • 41
  • Disposing doesn't have anything to do with the garbage collector. You are using Controls.Clear() in a very dangerous way, it doesn't dispose controls. Run Taskmgr.exe, Processes tab. View + Select Columns and tick USER Objects and GDI Objects. Ensure these values are stable for your app and don't climb without bound. Contact devexpress for more support. – Hans Passant May 22 '12 at 13:06
  • @HansPassant Thank you for your comment & ideas. The number of USER and GDI objects does not change. What do you mean with "dangerous way"? I just want them removed from my container. – Marko Juvančič May 22 '12 at 13:38

2 Answers2

1

After [unsuccessful support from DevExpress], I did some testing and playing with the code and finally found the solution.

The trick is in clearing the controls on the UserControl before disposing.

One can modify Dispose method on the UC (this solution worked in some cases, but not in all of them) or hide the UC instead of removing it form panel and cleraring its Controls

Solution 1:

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

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

  base.Dispose(disposing); 
  }

Solution 2:

Add a new method to the UC:

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

and in my original question replace this line

panelControl1.Controls.Clear();

with this:

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

It works (at least) 20 times faster, which is good enough.

Marko Juvančič
  • 5,792
  • 1
  • 25
  • 41
  • I'm afraid this is not a solution at all... This is dangerous way, because it have an control handles leak (you should always dispose all unused controls). Take a look at my answer. I believe it can helps. – DmitryG May 28 '12 at 04:44
0

The cause of the issue is related neither to DevExpress nor standard controls. But, it is related to creating and destroying control handles. To improve your cards' implementation, avoid these operations when it is possible. I suggest you use caching for your cards:

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;
}

The next step in optimizing cards is reducing the total number of used control handles. Since you are using DevExpress controls, the best option for you is XtraLayoutControl. The use of XtraLayoutControl allows you to significantly reduce the total number of control handles. It only creates 4 handles for the layout you've described (3 editors with labels within a couple of group boxes) instead of 8 handles when the standard controls are used. XtraLayoutControl does not create handles for an editor's labels, groups, tabs. Please also take a look at XtraGrid LayoutView - it provides benefits of using the grid's databinding architecture and cards' layout virtualization without any additional coding..

DmitryG
  • 17,677
  • 1
  • 30
  • 53