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();
Related
Right I have got a list box which contains a list of tracks and when the track is pressed it moves to a second list box, what I now need to happen is have the first item that's in the second list box move automatic to a text box.
This is my current code for the first move
private void genreListBox_DoubleClick(object sender, EventArgs e)
{
playlistListBox.Items.Add(genreListBox.SelectedItem);
}
I am thinking it should be something like this
presentlyPlayingTextBox.AppendText(playlistListBox.);
But I'm not sure how to add the first line without clicking it.
I have tried this but I get an error for the value.
presentlyPlayingTextBox.Text = playlistListBox.SelectedItem.Value;
Normally you would use something like the 'SelectedIndexChanged' or 'SelectedValueChanged' action of a listbox, otherwise events like DoubleClick will fail if the keyboard is used to change the selection.
Also that event should be triggered on the second listbox when it is changed by the first.
EDIT:
On a standard Listbox, there is no .SelectedItem.Value, only .SelectedItem.
However as a listbox holds objects, not just text, you need to say you want the text value.
presentlyPlayingTextBox.Text = playlistListBox.SelectedItem.ToString();
I don't know if I fully understand your question, but if you're just attempting to move the first item from ListBox_2 to a textbox, would the following work?
presentlyPlayingTextBox.Text = playlistListBox.Items[0]?.Value; // C# >= 6
presentlyPLayingTextBox.Text = ((playlistListBox.Items == null) ? playlistListBox.Items[0].Value : "" ); // C# < 6
You could also create your own delegate/event that would fire when the user did some action.
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();
}
}
I have a System Windows Forms ListView in C# (VS 2008) with AutoArrange=false and LargeIconView. I let the user change the position of an item in my ListView and save all position changes in a txt file. Then I load it back from txt file and I verified that my information is allright but anyway if I try to set the position back to my ListViewItems it won't change anything. If I directly address just one item with hard-coded values it seems to work. I have no idea what's going on. How can I set the saved positions back to my ListViewItems?
I moved the loading part from Form_Load to Form_Shown event just for a case to ensure the ListView would be surely loaded while I try to assign the positions, but it did not helped on this matter. The listViewItem positions won't change to the saved ones, but they have like a default align. I have checked that I have autoarrange set to false and I found nothing more what I could do here. Maybe you know? Maybe another setting plays a role?
If I do just this to the one item it changes without a problem, but the loaded values don't apply.
listView1.Items[0].Position = new Point(300, 400);
Here I load my values in the .Tag of my ListViewItem and get the POINT-Data.
The POINT-Data is valid and is really there, I verified it. But it doesn't work somehow.
The items stay arranged in the default alignment and the position doesn't change.
private void Form1_Shown(object sender, EventArgs e)
{
foreach (ListViewItem LVI in listView1.Items)
{
Point PNT1 = (Point)LVI.Tag;
int x = PNT1.X;
int y = PNT1.Y;
LVI.Position = new Point(x, y);
}
}
Update after further testing:
Since I add about 25000 items to my ListView I use the AddRange Method. The AddRange Method seems to drop my Position previously set to all ListViewItems in the List of ListViewItems.
With AddRange this items are being added in less than 1-2 minutes, with the .Add-Method Position is preserved, but I can't use just .Add. It looks like a bug of Microsoft in place here.
What can I do instead? The .Add-Method is out of question, adding the items would take here 1 hour and 47 minutes instead of 1-2 minutes.
List<ListViewItem> L1 = new List<ListViewItem>();
//...Load data from file - the data is valid loaded (checked / debugged)
ListViewItem LV1 = new ListViewItem();
LV1.Tag = P1;
LV1.Text = Text1;
LV1.ImageKey = Name1;
LV1.ToolTipText = ToolTip1;
LV1.ForeColor = ForeColor1;
LV1.BackColor = BackColor1;
LV1.Position = Position1; //here comes the problem, that seems to be lost
L1.Add(LV1);
//listView1.Items.Add(LV1); //this would work but it is to slow - AddRange required
}
listView1.Items.AddRange(L1.ToArray()); //here the position seems to get lost
Well, what you're doing there is you're taking each item and getting the X/Y value from it (when it's already been put into the list) and assigning that same X/Y value back to it, resulting in no net change.
You need to load the point data from your file and then assign that to the items in the view. For example:
for(int I = 0; I < listView1.Items.Count(); I++)
{
listView1.Items[I].Position = new Point(GetYourXForIndex(I),GetYourYForIndex(I));
}
Where GetYourXForIndex(int) is a function you defined which returns the proper X value for a given index. Same for GetYourYForIndex(int).
The one answer which I received on that topic doesn't meet my problem / has nothing to do with my problem, so I can't choose it. I found the solution myself. Microsoft doesn't serialize the position property of a ListViewItem and it doesn't take the property into account when the .AddRange method got executed on a ListViewItem-List. It can be considered as a bug / limitation with fault at Microsoft (my personal opinion). I totally recoded the structure and made some overrides to the ListView to fix that stuff. Since the "fix code" is about several pages and not compatible to the code in my question it won't fit in here, but I tell what to do with the problem. For serializing
I wrote an own type ListViewItem2 (this type I marked to be serializable) which stores all the properties which I want have to be saved included the position. Then I override the OnItemAdded event in such way that the position of an item will be kept when it has been added by the AddRange method. I hope this helps to those who encounter similar difficulties. The autoarrange-property of the ListView must be also off to set the positions.
We display our data on datagrids, bound to a dataset, which is in turn fed from a Progress database on the server. During processing, we need to make a change to the data-set and refresh it's value from the server. So far, all well and good and no problems.
The problem is that when we come back with the new data, we want the selection in the datagrid to remain on the same row it was on before. We've managed this with the following code:
int iPostingPos = dgridPostings.CurrentRow.Index;
// process data on server
dataContTranMatch.RunBoProcedure(dataContTranMatch.BoProcedure,
transactionMatchingDataSet);
// Reload Data
LoadData();
if (iPostingPos > ttPOSTingsRowBindingSource.Count)
{
iPostingPos = ttPOSTingsRowBindingSource.Count;
}
if (ttPOSTingsRowBindingSource.Count > 0)
{
ttPOSTingsRowBindingSource.Position = iPostingPos;
dgridPostings.Rows[iPostingPos].Selected = true;
}
This works, but we get the selected line jumping about on the screen, which is really annoying the users.
For example, if you select row 7, then run this code, you have row 7 selected, selection then jumps to row 0, then jumps back to row 7. This isn't acceptable.
In an attempt to fix this, we've tried enclosing the above code in the following additional lines:
chTableLayoutPanel1.SuspendLayout();
*DO CODE*
chTableLayoutPanel1.ResumeLayout();
But this didn't help.
So far, the most acceptable solution that we've been able to reach is to change the colour on the selection so that you can't see it, letting it leap about and then putting the colours back as they should be. This makes the flicker more acceptable.
dgridPostings.RowsDefaultCellStyle.SelectionBackColor =
SystemColors.Window;
dgridPostings.RowsDefaultCellStyle.SelectionForeColor =
SystemColors.ControlText;
DO CODE
dgridPostings.RowsDefaultCellStyle.SelectionBackColor =
SystemColors.Highlight;
dgridPostings.RowsDefaultCellStyle.SelectionForeColor =
SystemColors.HighlightText;
We beleive that the issue is caused by the binding source being temporarily empty as the dataset is refreshed, we then re-navigate one it's got data in it again.
Can anyone offer any ideas on how we can prevent this unpleasent flicker from occuring?
Many thanks
Colin
It may be a bit heavy handed but one option would be to suspend painting of the control. A user asked how to achieve this here: How Do I Suspend Painting For a Control and Its' Children. I've used the selected answer there to achieve something similar.
Woot, first Stack Overflow post! I've been asked to work on a desktop application to improve an inventory process for my company. I dabbled with WPF in school and I figured I'd start there. After researching some, I learned about MVVM, put a design together, and forged ahead. Finally, I'm stuck and looking for some help and also a sanity check to see if I'm on the right path.
I have single-column DataGrid bound to an observable collection. Users of the application use a scan gun to enter values in. One potential value that I catch in my "Cell" model object is a "MoveNextColumn" value. This raises a custom event in my model that is handled in the View Model. The handler is supposed to simulate blank entries for all remaining rows in that column, set focus on the last row, and wait for input before moving on. So here is what I have so far:
private void dummyCell_MoveToNextColumn(object sender, RoutedEventArgs e) {
e.Handled = true;
// Cell is the model object containing the parsing rules and raising events
var lSender = sender as Cell;
var gridItems = ViewGridReference.Items;
var lastItem = gridItems[gridItems.Count - 1];
if (lSender == lastItem) {
// We are at the bottom of the column
// Move the program on to the next column
CurrentColumn++;
OnPropertyChanged("ItemPositions");
} else {
// Simulate "empty position" input for this cell and all cells down the column
// Cells are validating themselves as the simulation progresses
foreach (Cell item in ViewGridReference.Items) {
item.ActualItemCode = string.Empty;
}
// ViewGridReference is a reference to my DataGrid set from the view
ViewGridReference.Focus();
ViewGridReference.SelectedIndex = gridItems.Count - 1;
ViewGridReference.CurrentCell = new DataGridCellInfo(lastItem, ViewGridReference.Columns[0]);
((DataGridCell)ViewGridReference.SelectedItem).Focus();
}
}
All of this seems to be working as expected: all rows receive blank input and are validated (I use color properties in the cell to which the view binds to signify the validity of the entry).
Unfortunately, though the focus is on the last row as desired, it is not editable and the user cannot submit another "MoveNextColumn" value which would move the program on. The goal here is to minimize any keyboard interaction. Everything should be done with scan guns and barcodes.
Any ideas on how to make the selected cell editable after this code executes?
Any "hey, your design sucks" feedback would be cool too. This is new to me and I'm open to constructive criticism.
I have made some progress with this. The entire grid was left at an uneditable state in the code above. This now leaves focus on the last cell in my column and allows me to submit input with the scan gun.
This seems to work, but I'd still appreciate some feedback on whether there is a better way.
private void dummyCell_MoveToNextColumn(object sender, RoutedEventArgs e) {
e.Handled = true;
// Cell is the model object containing the parsing rules and raising events
var lSender = sender as Cell;
var gridItems = ViewGridReference.Items;
var lastItem = gridItems[gridItems.Count - 1];
if (lSender == lastItem) {
// We are at the bottom of the column
// Move the program on to the next column
CurrentColumn++;
OnPropertyChanged("ItemPositions");
} else {
// Simulate "empty position" input for this cell and all cells down the column
// Cells are validating themselves as the simulation progresses
foreach (Cell item in ViewGridReference.Items) {
item.ActualItemCode = string.Empty;
}
ViewGridReference.SelectedIndex = gridItems.Count - 1;
ViewGridReference.CurrentCell = new DataGridCellInfo(lastItem, ViewGridReference.Columns[0]);
(ViewGridReference.ItemsSource as ListCollectionView).EditItem(ViewGridReference.SelectedItem);
((DataGridCell)ViewGridReference.SelectedItem).Focus();
}
}
Updated 12/2/2010
Hey, there is an important update to this. The first thing to note is that text entry is being done with a scan gun in my scenario, so 'Enter' keys are sent down with each pull of the trigger. It shoots down each character followed by the Enter key all at once.
WPF sees this enter and wants to set the focus to the DataGridCell directly beneath the cell in which the Enter key input was received. The code above sets the focus to the last cell, but then the Enter key event still fires and is handled by DataGrid after this code is run. The effect is that the focus is reset back to the subsequent cell, not the last cell like I want.
So I need to either figure out how to eat the Enter key for just that scan, or I need to break how WPF handles Enter keys. The last line up there actually throws an exception. We are trying to use a Model class (Class.cs) as a DataGridCell, and there is nothing to handle that cast. Because of that, the Focus() method tries to operate on a null object and we get a NullReferenceException. This was really confusing me because Visual Studio 2010 would sometimes break to alert me about this, but sometimes it wouldn't. However, if I run the executable outside of Visual Studio, it works just fine. That's because unhandled, non-fatal exceptions are ignored and the Enter key behavior fails to operate as normal.
So it works, but in a pretty gross way. I either need to figure out how to do one-time handling of the Enter key and override the default WPF handler, or just leave it like it is and grimace.