.Net TableLayoutPanel – Clearing Controls is Very Slow - c#

This is really simple.
I have a TableLayoutPanel that is populated with controls (just Labels, Buttons, and some Panels with buttons) based on a database query. When the data needs to be refreshed, I use TableLayoutPanel.Controls.Clear(). Unfortunately, this is a very slow operation. I would expect it to be faster than the code populating the table, but it is at least 3 or 4 times slower.
I definitively proved that the slowness is when executing Controls.Clear() by executing this as the single thing done to the TableLayoutPanel after a message box is displayed (then the procedure returns). The controls visibly disappear from the bottom up. When the recordset is used to repopulate the TableLayoutPanel, the speed of the controls appearing from top to bottom is almost faster than I can see.
I'm already doing TableLayoutPanel.SuspendLayout() and ResumeLayout().
Using this.DoubleBuffered = true on the form doesn't appear to do anything.
I could just Dispose the entire control and recreate it through code, but this is a big pain and makes having a nice form designer GUI pointless. I would have to dig into every property I've set on the control and create a line of code for it (though I guess I could get this out of the designer code itself, it still feels wrong).
Any ideas on how to do the job faster? I'm even open to using other methods besides a TableLayoutPanel... I just need the freedom to put multiple buttons per cell or barring that to be able to span columns in the table header.
Can C# at least freeze the whole form while it redraws and then paint all at once?

I've run into issues with slowness using TableLayoutPanels as well. Rather than setting the DoubleBuffered property on the form, the best solution I have found is to create a new class that inherits from TableLayoutPanel, and in that class' constructor, enable double-buffering:
public class DoubleBufferedTableLayoutPanel : TableLayoutPanel
{
public DoubleBufferedTableLayoutPanel()
{
DoubleBuffered = true;
}
}
Then, use the DoubleBufferedTableLayoutPanel wherever you would normally use a TableLayoutPanel.

This seems to work for my uses:
tableLayoutPanel.Visible = false;
tableLayoutPanel.Clear();
/* Add components to it */
tableLayoutPanel.Visible = true;

There is no need to subclass TableLayoutPanel as in Chris Ryan's answer. I had the same problem and solved it by setting the property through reflection:
typeof(TableLayoutPanel)
.GetProperty("DoubleBuffered",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.SetValue(myTableLayoutPanel, true, null);

If i'm going to built up some dynamic gui i'm always going to do so in code. But at a starting point i just start with the designer on a dummy form and style each control the way i (or better the customer) like(s). Afterwards i take a look into the Designer.cs file and copy the necessary property settings out of it into some factory function like
private TextBox CreateTextBox(string name, /* maybe other parameters */)
{
var textBox = new TextBox();
textBox.Name = name;
//Other settings from given parameters...
//Further settings which are all the same for these kind of control
textBox.KeyDown += (sender, e) => {};
return textBox;
}
So i make sure that every control feels and looks the same on my GUI. This will be done on each level within my surface (starting with the small controls like TextBox and goes up to the containers like GroupBox or TableLayoutPanel.
In some cases this leads to a point where a factory function calls several other factory functions. If this is becoming true it's time to think about encapsulating these controls into a single UserControl, but as always it depends if this is needed or not.
From my side i can only encourage you to move your code out of the designer into a self-written function. At the beginning it is (as always) more work, but afterwards it is easier to make even bigger changes to the layout.

Related

Setting the control's parent in form designer

This MSDN article offers some good advice about manually customising the form designer to improve performance:
Reduce the number of method and property calls on controls during startup. For example, Controls.Bounds is a better option than calls to Control.Location and Control.Size.
Create the form from the top down. In nested control hierarchies, set the parent property of containers (using the above rule) before adding controls to the container. As in the BigForm application, the panels had their parent property set to the form before the 40 controls were connected to the panel. If further containers exist lower in the hierarchy, the same changes should be applied.
I have followed the 1st bit of advice, replacing:
this.MyControl.Location = new System.Drawing.Point(5, 5);
this.MyControl.Size = new System.Drawing.Size(630, 90);
with:
this.MyControl.Bounds = new System.Drawing.Rectangle(5, 5, 630, 90);
This resulted in a super 20% (about 200ms) speed-up on one form. I'm trying to follow the 2nd bit of advice and not quite sure how to proceed. The Designer.cs file contains code like this:
this.Controls.Add(this.pnlHeader);
but not the code I was expecting (according to the example):
this.pnHeader.Parent = this; // Not in the Designer
The code this.Controls.Add(this.pnlHeader); appears at the bottom of InitializeComponent. Is the advice suggesting moving the code to the top or something else entirely?
EDIT #2
i found this msdn article which explain the issue as following:
Another method for improving performance is to initialize the controls
in the control tree top-down. For example, if you have a panel control
with many controls in it, create the panel first, and then add the
controls to the panel. Also, setting the parent property of the
control instead of adding to the Controls collection can improve
performance.
For example, consider adding a textbox to a panel's control collection:
Before optimization:
// Create a new panel and textbox control
Panel panel1 = new Panel();
TextBox textBox1 = new TextBox();
// Set the Text property of the TextBox control
textBox1.Text = "My Text";
// Add the TextBox to the Panel's control collection
panel1.Controls.Add(this.textBox1);
// Add the Panel to the Form's control collection
this.Controls.Add(panel1);
//... subsequent controls
Optimizing this code snippet using the top-down and parenting techniques results in the following snippet:
After optimization:
// Create a new panel and textbox control
Panel panel1 = new Panel();
TextBox textBox1 = new TextBox();
// set parents from top to down
this.panel1.Parent = this;
this.textBox1.Parent = this.panel1;
// Set properties of child control (cause repainting only once)
textBox1.Text = "My Text";
//... subsequent controls
This can make a big difference with a deeply nested control hierarchy.
Optimizing the code in the InitializeComponent method by creating
the controls top-down and re-parenting them resulted in a performance
improvement of about 50% over the default Forms Designer generated
code!
Following on from the answer from S.Serp here are some observations to be aware of:
Replacing Location and Size with Bounds resulted in an average boost of ~15% for form loading.
Replacing Controls.Add with Parent resulted in a further boost of ~5-10% (for an impressive total of ~20-25%).
As Sefe points out, manually editing the Designer.cs file is usually only appropriate where you are not making changes in the Designer very often. Any changes made in the Designer will overwrite your manual code. Be warned! This is not a disaster, you simply lose the boost. Either live with the slower form loading or redo the manual changes.
Be careful to put the Bounds call after setting Multiline = true; on TextBox controls (if enabled). If you set it before, your control will (unhelpfully) be resized to a single line.
Be careful with ensuring each child control has the parent set correctly! Open all forms in the Visual Studio Designer after manually editing InitializeComponent to see that everything is kosher...but don't edit anything otherwise your changes will be deleted.
The lines...
this.Controls.Add(this.pnlHeader);
...and...
this.pnHeader.Parent = this;
...are equivalent. The control trees they produce are identical. And you should not change the auto-generated code. Your changes will be overwritten the next time the designer writes its updates.
You usually don't have to worry about the designer-generated code. You can concentrate on your part and assume the designer is correct.

A Form(?) appears briefly outside of Form1 when adding a control to it

I have a program which creates new Controls (actually - Form's with TopLevel = false) and puts them on a Panel which is on Form1.
For some reason, this is usually followed by what seems like a Form appearing very briefly outside of Form1. It's hard to tell exactly what's happening because it's so brief. Perhaps it's Form1 moving there (and changing its size) and then returning. Or perhaps it's the Form that's being put on Form1 that appears there (though how could that happen at all? It's out of Form1!).
I couldn't find anything in the code that might cause it, and the whole code is way too long to post here. (The relevant code is simply: MyPanel.Controls.Add(newForm);.)
Is there any known bug that might cause this?
I can sort of guess what the code looks like. The problem with adding a Form to the Controls collection with TopLevel = false is that you have to explicitly make it visible. This is an odd quirk (aka bug), normally a control automatically becomes visible when you add it to a Controls collection.
So it probably looks like this:
var newform = new Form1();
newform.Visible = true; // or newform.Show()
newform.TopLevel = false;
MyPanel.Controls.Add(newForm);
And yes, that can make it visible for a very brief moment, still as a top-level window, if you do something else that causes messages to be dispatched. Fix it by swapping the Visible and TopLevel property assignments.
If this doesn't help then post a code snippet.
So you tried adding a Form to a Form's Controls collection and received the exception telling you that you can't add top level controls. To "fix" it, you set the TopLevel property of that Form to false.
That's not the correct fix.
Use a Panel (or one of its derivatives) instead.

What is the cleanest way of removing a control from a container?

I'm having a WinForms performance issue that might be related to the fact that I dynamically add and then remove hundreds of controls.
EDIT {
The application displays a timeline which consists of controls representing historical events. Controls are added, removed or moved, depending on the time you jump to. The performance issues are not only during the addition and removal of controls (this I can live with), but even after I jump to a time with no historical events (meaning no controls are currently displayed). After jumping around and getting to a time where there are no events on the timeline, some activities in the GUI still take a long amount of time to complete, such as opening menus or opening dialog boxes. The strange thing is that other GUI activities, such as pressing buttons, do not stall. }
Although the memory consumption is perfectly stable, can it still be that there is an issue with freeing resources?
In order to remove a control, I do two things:
Unregister callbacks from all events,
Call containerPanel.Controls.Remove(control).
Thanks!
As you already observed, it isn't a memory problem. My guess is, that the problem is the simple fact, that your program needs to refresh the screen that often. If you remove and add those "hundreds of controls" in one batch, you can try to disable screen refresh until you are done.
You can do this using SuspendLayout and ResumeLayout:
SuspendLayout();
for(...)
AddControl(...);
ResumeLayout();
and
SuspendLayout();
for(...)
RemoveControl(...);
ResumeLayout();
You might have trouble due to GC pressure, that is that the garbage collector is running often due to many objects beeing created and then freed. when the GC runs all threads are stopped in their tracks (almost) and the app looks like its freezing
i dont think you're doing anything wrong with your removal code, but perhaps you can cache the controls somehow? can you tell us a bit more about you scenario?
-edit-
Based on your scenario, i'd suggest sidestepping the whole issue with removing controls and adding new ones and if possible reusing the controls that are already in the view, but switching out their data contexts (binding them to diffrent data) when the view changes. In wpf a common name for this approach is UI-virtualization but it can be applied to any ui framework, at least in principle
Another way around the problem might be to have empty place holder controls for the for all the positions in the timeline that you can scroll to immediately and then add content to as its loaded from disk or whereever. That way you would not have to affect the layout of the whole time line, you'd just fill in the particular slot the user is viewing. This would be even more effective if all the time-line-event-controls are all the same size, then the layout of the entire timeline would be completley unaffected)
Removing lots of controls one at a time is really not something that WinForms is designed to do well.
Each call to ControlCollection.Remove results in a call to ArrayList.RemoveAt. If you are removing the last item in the collection this not too bad. If you are removing an item from the middle of the collection Array.Copy will get called to shuffle all of the items after that element in the ArrayList's internal array down to fill the empty spot.
There are a couple of approaches you could try:
Remove all the controls then add back the ones you want to keep
ArrayList l = new ArrayList();
foreach (Control c in Controls){
if (ShouldKeepControl(c))
l.Add(c);
else
UnhookEvents(c);
}
SuspendLayout();
Controls.Clear();
Controls.AddRange((Control[])l.ToArray(typeof(Control)));
ResumeLayout();
Remove last to first
/* Example assumes your controls are in the best possible
order for this technique. If they were mostly at the end
with a few in the middle a modified version of this
could still work. */
int i = Controls.Count - 1;
bool stillRemoving = true;
SuspendLayout();
while (stillRemoving && i >= 0){
Control c = Controls[i];
if (ShouldRemoveControl(c)){
UnhookEvents(c);
Controls.RemoveAt(i);
i--;
}else{
stillRemoving = false;
}
}
ResumeLayout();
The effectiveness of either approach will depend on how many controls you are keeping after removing a batch of controls and the order of the controls in the collection.
Since Control implements IDisposable you should also Dispose the control after removing it from its container.
containerPanel.Controls.Remove(control);
control.Dispose();
When doing hundreds of small updates to the UI of a WinForm app there might be performance issues when the UI thread over and over again redraws the interface. This especially occurs if the updates are pushed from a background thread.
If this is the problem it can render the UI totally unusable for a while. The solution is to make the updates in a way that the UI doesn't redraw until all of the pending updates are done.
Okay,
this look funny but for me, the only solution which works fine for me was
For i = 0 To 3 ' just to repeat it !!
For Each con In splitContainer.Panel2.Controls
splitContainer.Panel2.Controls.Remove(con)
con.Dispose()
'con.Visible = False
Next
Next
using suspendLayout() and resumeLayout() methods !!!

Dynamically Populated TableLayoutPanel Performance Degredation

I have a user control that contains a 2-column TableLayoutPanel and accepts commands to dynamically add rows to display details of an item selected in a separate control. So, the user will select a row in the other control (a DataGridView), and in the SelectedItemChanged event handler for the DataGridView I clear the detail control and then regenerate all the rows for the new selected item (which may have a totally different detail display from the previously selected item). This works great for a while. But if I keep moving from one selected item to another for quite a long time, the refreshes become VERY slow (3-5 seconds each). That makes it sound like I'm not disposing everything properly, but I can't figure out what I'm missing. Here's my code for clearing the TableLayoutPanel:
private readonly List<Control> controls;
public void Clear()
{
detailTable.Visible = false;
detailTable.SuspendLayout();
SuspendLayout();
detailTable.RowStyles.Clear();
detailTable.Controls.Clear();
DisposeAndClearControls();
detailTable.RowCount = 0;
detailTable.ColumnCount = 2;
}
private void DisposeAndClearControls()
{
foreach (Control control in controls)
{
control.Dispose();
}
controls.Clear();
}
And once I get finished loading up all the controls I want into the TableLayoutPanel for the next detail display here's what I call:
public void Render()
{
detailTable.ResumeLayout(false);
detailTable.PerformLayout();
ResumeLayout(false);
detailTable.Visible = true;
}
I'm not using anything but labels (and a TextBox very rarely) inside the TableLayoutPanel, and I add the Labels and TextBoxes to the controls list (referenced in DisposeAndClearControls()) when I create them. I tried just iterating over detailTable.Controls and disposing them that way, but it seemed to miss half the controls (determined by stepping through it in the debugger). This way I know I get them all.
I'd be interested in any suggestions to improve drawing performance, but particularly what's causing the degradation over multiple selections.
Just use a custom control that inherits from TableLayoutPanel and set the DoubleBuffered property on true, works great... especially when you dynamically add or remove rows.
public CustomLayout()
{
this.DoubleBuffered = true;
InitializeComponent();
}
I had a similar issue with TableLayout. If I used TableLayout.Controls.Clear() method, the child controls never got disposed but when I simply dropped the TableLayout without clearing it, the leak stopped. In retrospect, it's funny I used the Clear method to prevent some kind of leak.
Apparently, Clear method does not explicitly dispose of the controls (which makes sense, because the fact that you removed them from the TableLayout does not mean you are done with them) and removing the child controls from the TableLayout prevents the cleanup routine to dispose of the children when the LayoutTable itself gets disposed (it simply does not know about them anymore).
My recommendation: Delete the detailTable.Controls.Clear(); line, remove the detailTable itself from the parent's Controls collection and dispose it, then create a brand new TableLayout for the next round. Also lose the DisposeAndClearControls method entirely since you won't need it. In my experience, it worked nicely.
This way, you won't have to recycle your entire user control anymore but only the TableLayout within.
Unfortunately, the only advice I can offer is to take care of the placement of your controls yourself. In my experience the .NET TableLayoutPanel, while very useful, is leaking SOMETHING and becomes unusably slow as it grows (and it doesn't take an unreasonable number of cells to get to this point, either). This behavior can be seen in the designer as well.
I changed the containing form to just construct a new version of my user control on each selection change. It disposes the old one and constructs a new one. This seems to perform just fine. I'd originally gone with reusing just one for performance reasons anyway. Clearly that doesn't improve the performance. And the performance isn't a problem if I dispose the old one and create a new one.
Unfortunate that the TableLayoutPanel leaks like that, though.
I faced the same problem and found a good way without changing too much:
in VB.net
Dim tp As Type = tlpMyPanel.GetType().BaseType
Dim pi As Reflection.PropertyInfo = _
tp.GetProperty("DoubleBuffered", _
Reflection.BindingFlags.Instance _
Or Reflection.BindingFlags.NonPublic)
pi.SetValue(tlpMyPanel, True, Nothing)
or in C#:
Type tp = tlpMyPanel.GetType().BaseType;
System.Reflection.PropertyInfo pi =
tp.GetProperty("DoubleBuffered",
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
pi.SetValue(tlpMyPanel, true, null);
TableLayoutPanel.Controls.Clear() works fine for me, maybe its because i clear it from a different tab than its displayed in.
List<Control> controls = new List<Control>();
foreach (Control control in tableLayoutPanelEnderecoDetalhes.Controls)
{
controls.Add(control);
}
foreach (Control control in controls)
{
control.Dispose();
}

How bad is the following snippet?

My question is simple: how bad is the following snippet of code? How would you do it?
CancelEventHandler _windowClosing;
private CancelEventHandler WindowClosing
{
set
{
clearEventHandlerList();
this.Closing += value;
_windowClosing = value;
/*
* if calling the method with null parameters,
* it will set up itself as the primary control on the Window
*/
_windowClosing(null,null);
}
get
{
return _windowClosing;
}
}
private readonly CancelEventHandler[] CONTROLS = null;
private int current = 0;
public InitializerForm()
{
InitializeComponent();
/*
* these are the handlers for the different controls,
* in the order of appereance to the user
*/
STATES = new CancelEventHandler[] { handler1, handler2, etc. };
WindowClosing = CONTROLS[0];
}
private void clearEventHandlerList()
{
foreach (CancelEventHandler c in CONTROLS)
{
this.Closing -= c;
}
}
private void handler1(object obj, CancelEventArgs e)
{
if (obj == null)
{
//hide every other control, but this one, also set up if necessary
}
else
{
//do something
WindowClosing = CONTROLS[++current]; // set the next control to show
e.Cancel = true;
}
}
The point would be that the code wouldn't close a form, but instead show another component on it, and the set the way to handle that (this is mobile platform, so clicking OK button on the top generates a closing event). This is because showing several forms (4 or 5) one after another to the user makes the app blink, and also very annoying, while replacing just components is much smoother. This model works, but seems very nasty, and I would like a cleaner way to handle this.
Update:
I updated the code sample so that variable names are somewhat speaky. Still, I'm convinced this is awful, (a) but not sure how much, and more importantly, (b) how to do it better.
Update 2:
So, it seems that the code is still a bit mysterious.
Now here's what the problem is:
I show the user a form, which instructs him what to do in several languages. He proceeds by clicking OK on the window. Next, I ask for his language, and then a few questions (where his/her GPS is, etc.) like this. After he could answer the questions (this shouldn't take more than a few seconds each), I show him a splash screen (I load stuff in a separate thread meanwhile), which has a picture. Showing these forms one after another makes the whole application start slow, and filled with UI lags.
Here's what I do to work around the lags: I put the content of the windows into panels, and put those panels one on another, and hide every one of them but the one that should be visible to the user. (current variable) Each of the windows does different things, so I need to change handler of the window closing event in addition. In this code the part which enables the panel is in the same function (handler1, handler2, etc.) with the part which handles the window closing event. If the arguments are null, it does the former, if it isn't (that means it was triggered by the user) it does the latter.
I need an extensible solution to this so that I can insert and remove dialogs anytime I want (the order and the pointers to the functions are stored in the CONTROLS field, and this seems to be very convenient, if you actually understand it. Although it is never easy to change the entire content of a form, there ought to be a simpler way to do this, as well a nicer one, that is what I'm looking for.
I hope this time I could explain how the model works.
I think it might be theoretically possible to make that code more delightfully diverting, perilously puckish, jovially jeopardous, cheerily chancy and unwarily whimsical but it would require some serious thought.
somehow your code makes me want to cry, i´m sorry. i read it twice and all i know about it is that it "doesStuff" with "STATES".
if you really want some help on this one you will have to work on it yourself first...
Use, XML! It's human-readable!
More seriously-
It seems like you're trying to create some sort of configuration wizard, so I'd start by researching that. Regarding your particular solution, I generally advocate very strongly against the "layered panel" approach. I do so because I maintain apps written by people who found this approach, or the related "hidden tabs on a tab control" approach, to be a good idea. It's not, and maintainers will curse your name for years to come.
That being said, what alternatives are there? Well, one alternative is what you've already dismissed because of its "flicker". I'd say that, in general, the flicker isn't that big of a deal for a quick and dirty application. It might be a good idea to make sure that your new window is called up before closing the old one, for example. (I'm assuming this is possible, I haven't developed on a mobile device.)
Another possibility might be a less-evil version of your layered panels. Instead of throwing a half-dozen panels into one form, create a separate user control for each wizard page and then add/remove the user controls to a containing form. This can avoid your flicker and will prove to be much easier to maintain because each page is in a different control. This might also ease any subsequent "Back" button functionality and make your data structures more naturally defined because those user controls will be associated with a specific logical bit of data. It's still not ideal, but it's probably good enough for a one-off solution.
A third technique, if you foresee extensive wizard modification as the product matures, might be to generalize the creation of your user controls by defining them in a more logical/declarative manner (e.g. via XML). If you dynamically generate sensible controls based on XML, then modifying the panels might be as easy as diving into your XML and doing something like:
<Questions>
<Question type="Text"> <!-- generate a textbox for the answer field -->
Favorite Color:
</Question>
<Question type="Number" range="0-255"> <!-- Maybe this is a spinner -->
The answer to life, the universe, and everything:
</Question>
</Questions>
That's just off the top of my head, and completely overkill for any one-off application, but it's a possibility.
Now, let me caveat this by saying this might work, but it may not be the answer to your real problem - that of a slow and unresponsive UI when you have a lot of forms. The real answer may be to just go ahead and do all separate forms, but have each form load its child forms in a background thread while the user is staring at the first form.
But assuming you're still set on this, I'd start off by making a separate class just to handle the Panel stacking/hierarchy. Call it PanelManager. You would instantiate the PanelManager and associate it with the main form, then add Panels to it (perhaps keyed to a String) and set the order. In the main form, have the closing handler call PanelManager.CloseCurrentPanel() and if there are no more Panels to show then it's time to close the main form.
Time for pseudo-code! Here's a quick idea for the class, i'll leave it to you to implement it:
public class PanelManager {
// constructor
public PanelManager (Form ownerForm);
// short-cut properties
public Panel this[int idx]
{ get; set; }
public int Index
{ get; set; }
// main functionality
public int AddPanel (Panel p);
public void SetPanelOrder (Panel p, int idx);
public void RemovePanel (Panel p);
public void RemovePanelAt (int idx);
// shows the first Panel
public void Show ();
// shows Panel[idx]
public void Show (int idx);
// adds the panel to the top of the stack and displays it
// returns the index of the panel
public int AddPanelAndShow (Panel p);
// hides the current panel, displays the one underneath it
// returns false if there are no more panels
public bool HideCurrentPanel ();
}
in the constructor for the main form, instantiate it by new PanelManager (this), then in the closing event handler, call panelManager.HideCurrentPanel () and then figure out whether or not you need to close it after that.

Categories

Resources