thanks for reading.
I have a C#.NET form with buttons that switch controls in a main panel. I didn't have any issues until I upgraded to Visual Studio 2012 and Advanced Installer. Target Framework is 4.0, not 4.5.
When I change controls, I dispose and remove the previous before adding the new one, but I'm getting an error when there aren't any controls yet (ie, when the first one loads).
The original loop crashed with something about iterating while modifying the collection, so now I'm trying to just remove one control after ensuring it's there.
This errors with: Index 0 is out of range.
This all works fine on the dev machine, and it wasn't an issue using the old built-in VS installer.
Any ideas? 4.0 framework issue? Missing reference not being deployed?
Thanks!
panelMain.SuspendLayout();
int control_count = panelMain.Controls.Count;
if (control_count > 1) {
Log.Write("More than one control found in main panel.", ErrorLevel.Error);
}
if (control_count > 0) {
Control current_ctrl = panelMain.Controls[0];
current_ctrl.Dispose();
panelMain.Controls.Remove(current_ctrl);
}
//foreach (Control ctrl in panelMain.Controls) {
// ctrl.Dispose();
// panelMain.Controls.Remove(ctrl);
//}
The issue with the foreach loop that you've commented out is that you cannot add items to or remove items from a collection that you are currently enumerating. That means that if you want to loop through a collection and remove items then you must use a for loop. If you want to remove multiple items then you must loop backwards.
The issue with the second if statement is that disposing a control automatically removes it from its parent's Controls collection. That means that, as soon as you call Dispose on the control, there is no longer an item in the Controls collection so the Remove call fails.
So, the moral of the story is that you should use a for loop, loop backwards and use just Dispose to destroy and remove.
Here is a simple recursive method to dispose of controls if anyone is interested. Using the advice of jmcilhinney above.
NOTE: make sure you read all comments about Visible property and setting it back to true.
// Start by calling a parent control containing the controls you want to
// destroy such as a form, groupbox or panel
private void DisposeControls(Control ctrl)
{
// Make control disappear first to avoid seeing each child control
// disappear. This is optional (if you use - make sure you set parent
// control's Visible property back to true if you create controls
// again)
ctrl.Visible = false;
for (int i = ctrl.Controls.Count - 1; i >= 0; i--)
{
Control innerCtrl = ctrl.Controls[i];
if (innerCtrl.Controls.Count > 0)
{
// Recurse to drill down through all controls
this.DisposeControls(innerCtrl);
}
innerCtrl.Dispose();
}
}
Related
How do I make my Custom controls draggable and droppable on a grid let's say?
I want to drag a panel(custom control) and drop it somewhere on my screen, in the best case in a grid, for example how it's done in Visual Studio, you can grab the solution explorer let's say and drop it somewhere, but how exactly do I do that?
You need to build your project and then it will be automatically available in the Toolbox when you are in the XAML designer. Just like the common controls.
For Drag and Drop at runtime look and the official WPF documentation. Also I suggest you look at the GongSolutions.WPF.DragDrop library in GitHub it is open source so you can see how they implemented it if the functionality it provides does not do what you want.
I found this post with a very similar conclusion to this one. the difference being scale.
the following example will swap the parent container
int i = 0;
void swapLocations()
{
foreach(var formObject in objList) //objList == a list or array on all objects you want to move from one container to another
{
if (i % 2 == 0)
{
// catch current position
Point moveLocation = new Point(formObject.Location.X + formObject.Parent.Location.X,formObject.Location.Y + formObject.Parent.Location.Y);
// remove this object
formObject.Parent.Controls.Remove(formObject);
// add this object to the form
this.Controls.Add(formObject);
// set location
formObject.Location = moveLocation;
formObject.SendToBack();
}
else
{
formObject.BringToFront();
}
}
++i;
}
Is there a way to make a control dependent on another control? I have a combo box and a button, and I need the button to be enabled if and only if there is an item selected in the combo box.
I know I can set the Enabled property of the button inside the SelectedIndexChanged callback, but then it will require some code, and besides there's an issue with what initial state the button would have. So I'm looking for something that wouldn't require manually handing events, is this possible?
Thanks!
No, there is no way in winforms to do this without code. What I usually do is to collect all such state-setting code into one specific method:
private void SetControlStates()
{
theButton.Enabled = theComboBox.SelectedIndex >= 0;
// code for other controls follow here
}
Then I trigger this method from all over the place, as soon as there is an interaction that may lead to the state changing (including the last thing I do when the form has finished loading; that takes care of initial state). If you want to avoid unnecessary assignments, just add code to check the value first:
private void SetControlStates()
{
bool buttonEnabled = theComboBox.SelectedIndex >= 0;
if (theButton.Enabled != buttonEnabled) theButton.Enabled = buttonEnabled;
// code for other controls follow here
}
Are there any other methods of bringing a control to the front other than control.BringToFront()?
I have series of labels on a user control and when I try to bring one of them to front it is not working. I have even looped through all the controls and sent them all the back except for the one I am interested in and it doesn't change a thing.
Here is the method where a label is added to the user control
private void AddUserLabel()
{
var field = new UserLabel();
userContainer.Controls.Add(field);
SendLabelsToBack(); // Send All labels to back
userContainer.Controls[field.FieldName].BringToFront();
}
Here is the method that sends all of them to the back.
private void SendLabelsToBack()
{
foreach (var label in userContainer.Controls);
label.SendToBack();
}
Yeah, there's another way. The Controls.SetChildIndex() also changes Z-order. The one with index 0 is the one on top. Doesn't buy you anything though, BringToFront() uses this method.
Your SendLabelsToBack() method as given cannot work, it will also send the label to added to the back. But your next statement fixes that again.
Okay, that doesn't work, which means the BringToFront() method doesn't get executed. Look in the Output window for a "first chance exception" notification. As written, your SendLabelsToBack() will cause an exception if the user control contains any control other than a UserLabel. Also, set a breakpoint after the BringToFront() call and check the value of userContainer.Controls[0].Name when it breaks.
Controls' z-index is per-container.
If you call BringToFront on a control that is inside a container (such as a Panel), it will not bring the container to the front.
Therefore, the control will only go in front of other controls in that container.
To see what containers your controls are in, you can use the Document Outline pane in the View menu.
EDIT: Your userContainer control is probably behind a different control.
Have you tried Invalidate() after BringToFront()? BringToFront does not raise the Paint event
try this:
private void SendLabelsToBack()
{
foreach (var label in userContainer.Controls)
{
label.SendToBack();
label.Invalidate();
}
}
I think you just need to change your last line:
userContainer.Controls[field.FieldName].BringToFront();
to this:
userContainer.Controls[field.Name].BringToFront();
When you use a string as the indexer for the Controls collection, it goes by the Name property of the control (not the FieldName property).
Since you're just trying to bring the most recently-added control to the top, this would also work:
userContainer.Controls[userContainer.Controls.Count - 1].BringToFront();
From my experience looks like windows puts all controls belonging to one graphic container(pane, group box...etc) in a software collection. The collection is ordered by child index which is a property of every control in that container.
The trick is that children with the same index can and do exists. In this case windows will paint those children ordered relative to others but between them it will paint them in the reverse order they had been added to the container.
Long story short: for one container-you need to make sure controls have different indexes by changing ALL NOT just SOME of the indexes when you want to change the z-order. For example:
foreach (Control newControl in TopControl.Controls)
{
TopControl.Controls.SetChildIndex(newControl,indexlogic(newControl));
}
where indexLogic(newControl ) is your method of calculation of the index of particular control.
I have one form called:
MyControlContainerForm ccf
and a main form called:
SolidForm sf
and I am adding all the controls inside an instance of new MyControlContainerForm () to SolidForm, using:
sf.Controls.Add ( Control )
but when I remove them using:
sf.Controls.Remove ( Control )
they are gone from MyControlContainerForm instance as well.
Why? And how do I prevent this?
I want to be able to add MyControlContainerForm controls whenever I want, without initializing MyControlContainerForm every time, just once.
The reason this is happening is not that you're removing the controls from form2, but rather that you're adding them. Controls can't be shared between forms. If you look at the reflected code of the form2.Controls.Add() on the Control Collection enumerator, we can see what's happening here:
...
if (value.parent == this.owner)
{
value.SendToBack();
}
else
{
if (value.parent != null)
{
value.parent.Controls.Remove(value);
}
base.InnerList.Add(value);
...
As you can see here it check the parent of the incoming control, if it's not the owner of the collection, then it simply runs a value.parent.controls.Remove(value) to strip the control from it's originating form, so it can be added to the current one.
Controls are not intended to be on 2 Forms at the same time. Im surprised you got way with that, probably because you do not Show MyControlContainerForm .
Note that Control has a Parent property (= in who's Controls collection am I?), singular.
Edit:
In fact, when button1 is on panel1, it is part of panel1.Controls. But the statement
panel2.Controls.Add(button1);
removes button1 from panel1.Controls.
You can use a List<Control> as a store. That would also keep them alive just fine.
I've got a list view that I'm populating with 8 columns of user data. The user has the option to enable auto refreshing, which causes the ListView to be cleared and repopulated with the latest data from the database.
The problem is that when the items are cleared and repopulated, the visible area jumps back to the top of the list. So if I'm looking at item 1000 of 2000, it's very inconvenient to get back to that item.
Basically, what I'm asking is, how do I get the current scroll distances (x and y) and then restore them?
I just wanted to provide some information for those who desperately try to use the buggy ListView.TopItem property:
You MUST set the TopItem property AFTER calling ListView.EndUpdate
The items of the ListView control MUST have their Text property set to something other
than String.Empty, or the property won't work.
Setting the ListView.TopItem throws null reference exceptions intermittently. Always keep this line of code inside a Try...Catch block.
Of course, this will cause the ListView's scrollbar to jump to 0 and back to the location of the top item, which is annoying. Please update this question if you find a workaround to this problem.
I used the following successfully:
int topItemIndex = 0;
try
{
topItemIndex = listView1.TopItem.Index;
}
catch (Exception ex)
{ }
listView1.BeginUpdate();
listView1.Items.Clear();
//CODE TO FILL LISTVIEW GOES HERE
listView1.EndUpdate();
try
{
listView1.TopItem = listView1.Items[topItemIndex];
}
catch (Exception ex)
{ }
I had the same problem with a while ago and I ended up implementing an algorithm to compare the model with the list, so I only added/removed elements that had changed. This way if there were no massive changes the list didn't jump to the beginning. And the main thing I wanted to achieve was the efficiency (so that the list doesn't blink).
The TopItemIndex property on ListView is what you are looking for, however it has some confirmed bugs that should have been addressed in VS2010 release.. not sure (haven't checked).
Anyway, my workaround for making this work is to do this:
listViewOutput.TopItemIndex = outputList.Count - 1;
listViewOutput.TopItemIndex = myNewTopItemIndex;
For some reason setting it directly does not update it, but setting it to the last item and then the one I want works reliably for me.
Look at the ListView.TopItem property. It has an index, which should contain its position in the list. Find that index in the new list, and set TopItem to that item, and it should do the scrolling automatically.
Unfortunately you will need to use some interop to scroll to the exact position in the ListView. Use GetScrollInfo winapi function to get the existing scroll position and SendMessage to scroll to the position.
There in an article on CodeProject named Scrolling to a group with a ListView that might guide you to the solution.
My solution to maintaining scroll position:
Form level variable:
private static int scrollSpot = 0;
Inside listview refresh (ie Timer,button) to store the current spot:
scrollSpot = this.listView1.TopItem.Index;
refreshTheForm();
Inside refreshTheForm method to show the stored spot (put at very end of method):
if (scrollSpot <= 1)
{
listView1.Items[scrollSpot].Selected = true;
}
else
{
listView1.Items[scrollSpot - 2].Selected = true;
}
listView1.TopItem = listView1.SelectedItems[0];
I was having sort-of the same problem. I have a listView that I populate every 1/2 sec and when I set the TopItem to an ListItem whose index > visible items, then the list jumped between the topItem and back 2 spots.
So, to correct the problem, I set the TopIterm AFTER the call to EndUpdate.
lvB.EndUpdate();
lvI.EndUpdate();
lvR.EndUpdate();
if (lstEntryInts.Items.Count > 0)
lstEntryInts.TopItem = lstEntryInts.Items[iTopVisIdx];
if (lstEntryBools.Items.Count > 0)
lstEntryBools.TopItem = lstEntryBools.Items[iTopVisIdx];
if (lstEntryReals.Items.Count > 0)
lstEntryReals.TopItem = lstEntryReals.Items[iTopVisIdx];
In my tests, you did not even need the TopItem, although I used a int to save the selected item. Also TopItem throws an exception if you are using View.Tile or View.LargeIcon.
This code does not move the scroll bars:
listView1.BeginUpdate();
listView1.Items.Clear();
// loop through your add routine
listView1.Items.Add(lvi);
listView1.EndUpdate();