I'm using a Windows Forms ListView control to display a list of items, up to 1000 items in total, though only about 20 are visible within the listview at one time (listview is using Details view).
I'm frequently adding new items to the bottom of the listview, and scroll to the newly added item automatically (using item.EnsureVisible()) so the user can see the most recent data. When the list size is above 1000 items, I remove the oldest list item (i.e. index 0, the one at the top of the listview) from the list to keep it at 1000 items.
Now to my problem:
Whenever the selection in the listview changes, additional details associated with the item are shown elsewhere in the form. When this selection change occurs, I stop the auto-scroll-to-new-items so the user's selected item stays where it is (i.e. the list doesn't scroll to newest items when an item in it is selected), and only re-enable the auto-scroll-to-newest when the user dismisses the additional details part of the form.
This all works fine, except when I remove the oldest item from the listview (to ensure the list doesn't grow beyond 1000 items): When I remove that oldest item, the listview scrolls everything up by 1 automatically (i.e. nothing I've done programatically does this scrolling). I realise that if the selected item is one of the earliest 20 events (which makes the earliest 20 events the visible ones), it will have no choice but to scroll the visible items when it removes the earliest, but if the selection is, say, midway through the list, it should have no need to scroll the listview items.
Is there any way I can prevent the listview automatically scrolling up by one when I remove the oldest item? Or will I have to work around it by making sure the visible items remain in the position they were before I removed the oldest item, after removing it (which seems a real hassle)?
Ok, this is my not-ideal (but at least mostly working) solution, in C# (converted from VB.NET so StackOverflow's syntax highlighter can display it properly, so apologies for any typos!).
Any better solutions, please do suggest them!
// loop through every item in the list from the top, until we find one that is
// within the listview's visible area
private int GetListViewTopVisibleItem()
{
foreach (ListViewItem item In ListView1.Items)
{
if (ListView1.ClientRectangle.IntersectsWith(item.GetBounds(ItemBoundsPortion.Entire)))
{
// +2 as the above intersection doesn't take into account the height
// of the listview's header (in Detail mode, which my listview is)
return (item.Index + 2);
}
}
// failsafe: return a valid index (which won't be used)
return 0;
}
private void RemoveOldestItem()
{
// pause redrawing of the listview while we fiddle...
ListView1.SuspendLayout();
// if we are not auto-scrolling to the most recent item, and there are
// still items in the list:
int TopItemIndex = 0;
if (!AllowAutoScroll && (ListView1.SelectedItems.Count > 0))
TopItemIndex = GetListViewTopVisibleItem();
// remove the first item in the list
ListView1.Items.RemoveAt(0);
// if we are not auto-scrolling, and the previous top visible item was not
// the one we just removed, make that previously top-visible item visible
// again. (We decrement TopItemIndex as we just removed the first item from
// the list, which means all indexes have shifted down by 1)
if (!AllowAutoScroll && (--TopItemIndex > 0))
ListView1.Items(TopItemIndex).EnsureVisible();
// allow redraw of the listview now
ListView1.ResumeLayout()
}
(This assumes, of course, that the selected item is currently visible otherwise it doesn't make a whole lot of sense to do; it always is visible in my scenario though, unless the selected event is the one being removed from the top of the list (in which case nothing is selected anymore so the issue goes away))
Related
I implemented some logic for the user to be able to copy selected values from a ListView to the clipboard. While playing playing around with it, I noticed that ListView.SelectedItems returns the items in a different order depending on how you shilft-select items.
For simplicity, let's assume my List has 5 items, each containing a string with a single number in ascending order, e.g. first item = "1", second item = "2", etc.
Scenario 1: Click on item 2, hold shift, click on item 4 -> SelectedItems has the expected order of elements "2,3,4".
Scenario 2: Click on item 4, hold shift, click on item 2 -> SelectedItems has an unexpected order of elements "4,2,3".
Am I missing something really simple here? If I select a range using shift "from the bottom to the top", the first item clicked is moved to the first position of SelectedItems, then following all the others in correct order.
Is the ListView noticing that the item previously selected is also within the range? So to say remembering it, leaving it at the first position in SelectedItems and just "adding" all the remaining items? Wouldn't bother me if you'd only shift-select from the top to bottom, but the other way around it messes up the order... :/
Any help/idea is appreciated!
I'm running into very strange WinForms behavior that seems to be a framework bug.
Situation:
On an empty form, put 2 ListViews next to each other. Disable HideSelection to make the selection visible while the control does not have focus. The View property (details, large icon) does not seem to matter, but I find details makes it easier to click (in which case, add one column).
It also does not matter whether or not MultiSelect is enabled or not.
(button1 is not involved)
In the form constructor, put some items into the lists:
this.listView1.Items.Add("item1-1");
this.listView1.Items.Add("item1-2");
this.listView1.Items.Add("item1-3");
this.listView2.Items.Add("item2-1");
this.listView2.Items.Add("item2-2");
Now, when the user selects something in listView1, we want the item with the same list index to become selected in listView2. E.g. user selects item1-1 in the left list view, we want to select item2-1 in the right list view and so on.
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
// (*1)
this.listView2.SelectedIndices.Clear();
if (this.listView1.SelectedIndices.Count != 0 && this.listView1.SelectedIndices[0] < this.listView2.Items.Count)
{
// (*1) can also be put here; it makes no difference with regards to the bug (it makes a functional difference though)
this.listView2.SelectedIndices.Add(this.listView1.SelectedIndices[0]);
}
}
The bug sometimes occurs in the following scenario (I can only reproduce if I carry these steps out very quickly (as in, less than a second overall)):
Click item 1 (starting to count at 1) in left list
Item 1 in right list becomes selected automatically
Click item 1 in right list
Click item 2 in left list
Item 2 in right list becomes selected automatically
Click item 2 in right list
Click item 1 in left list
Item 1 in right list becomes selected automatically
Wait a while (ca. 300 ms)
Item 2 in right list becomes selected automatically (Incorrect!)
More generally (this is my conjecture):
Click an item in the left list
Click the item in the right list which just got automatically selected
Quickly select a different item in the left list
The correct item is automatically selected in the right list for a brief moment
The selection jumps back to the previous item after a short delay
I can also observe the invalid selection change in the right list with this event handler, but I can't get any use out of the breakpoint - there doesn't seem to be anything suspicious to me. In the faulty case, the call stack only contains framework internal methods (except for the topmost frame, which is the event handler, of course), so the bogus selection change comes from the framework itself.
private void listView2_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.listView1.SelectedIndices.Count == 0 || this.listView2.SelectedIndices.Count == 0)
{
return;
}
if (this.listView2.SelectedIndices[0] != this.listView1.SelectedIndices[0])
{
// Unless the user MANUALLY selects a DIFFERENT item in the right list view, this should never happen, but it does!
int j = 5; // BREAKPOINT HERE
}
}
The bug does not always happen and seems to be rather timing sensitive, but after some "practice" I can reproduce it about 50 % of the time now.
My guess is that there's some really dumb failsafe mechanism in there. When the user clicks on an item, WinForms just decides to check a short while later if that item really got selected, and if it's not selected, it selects it again (even though the selection was changed programmatically in the meantime). But that alone can't be it, because the bug does not occur unless you explicitly click on the automatically selected item right after the selection changed automatically.
Can anyone reproduce this, and how could I work around this?
This happens to me on Win 10 x64, with .NET frameworks 4.5.2 and 4.7
It turns out you have to set the "focused item" as well, either via ListViewItem.Focused or ListView.FocusedItem, e.g.:
this.listView2.SelectedIndices.Clear();
if (this.listView1.SelectedIndices.Count != 0 && this.listView1.SelectedIndices[0] < this.listView2.Items.Count)
{
var item_we_want_to_select = this.listView2.Items[this.listView1.SelectedIndices[0]];
item_we_want_to_select.Selected = true;
item_we_want_to_select.Focused = true;
}
However all this sounds fishy and I would not use a ListView for such a GUI again. I cannot comfortably tell (nor quote the documentation) about what any of that exactly does, whether it is the correct approach, and whether it has any unintended side effects. It stills seems to me as if the original observation is a Windows bug and this is a workaround that may or may not work.
I need to make an item transition like pivot control, but my problem is that pivot control loads all the elements from its binding and it has a loop effect(from the last element navigates to the first and viceversa). Things I tried:
I tried to make an implementation where I just have 3 pivot items and I change the content of them according to the elements i want to show. The problem that i have there is that I need to change the its ItemTemplate from source, because some of my items are not always the same and I was not able to correctly change the SelectedIndex to navigate across the items.
I tried to implement my own transitions (swiping an element and loading an usercontrol inside), but the results are not very good, there is a delay till it finish loading all the stuff.
Here is a code sample of how I'm trying to change the current ítem.
private void PivotControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if ((CurrentItem == ListaElem.Count - 1) && (LastPos == 0))
{
PivotControl.SelectedIndex = 0;
CurrentItem = 0;
}
}
The control that I want to replicate is Android v4 FragmentManager.
Updated:
I need a control to be able to navigate throw items and if I'm on item 1, the item 0 and item 2 are preloaded, if I move to item 2, the item 0 should be unloaded for avoid memory problems (each pivot could have 1 or more images) and it preloads the item 3
Any help would be aprreciated.
I've got a ListView bound to an ObservableCollection that implements ISupportIncrementalLoading. Everything is working nicely however when new items are added to the collection via LoadMoreItemsAsync() the ListView jumps to the top of the list.
The ListView should of course carry on scrolling from the current position. Any ideas what could be wrong?
UPDATE:
There is no selected item and individual items are added one at a time i.e.: 10 items added results in 10 collection changed events. It shouldn't be the last item in the list after its been updated, but the first updated item.
list.ScrollIntoView(i);
where i is the i-th selected item.
With respect to a Virtual ListView control in a Winforms App, what are ALL the functions of RetrieveVirtualItem event?
Scenario: There is a Dictionary object which acts as my ListView cache. And its items are displayed on the ListView. On click of a column, the cache dictionary is sorted in memory and the ListView is Refresh()ed. The effect on UI, the ListView is sorted well. :)
But I want to understand what the role of RetrieveVirtualItem Event is, which gets triggered on Refresh().
In the RetrieveVirtualItem event handler [ Someone else's code, :( ], following are done:
Based on RetrieveVirtualItemEventArgs.ItemIndex, get the message from Cache
Set RetrieveVirtualItemEventArgs.Item = retreived item above
It appears that what's done in the event handler is important because, if I take it out, ListView cries. What is the significance of this event?
EDIT
Or let me re-phrase the question... My concern is, Why, after Sorting (and RetrieveVirtualItem event handler), the selected item remains at the location where it was before sorting. I.e, If I select item #5 and sort, and if sorting makes this item as the last item, I would prefer the LAST item to be selected after Sort. But in my case 5th item is selected after sort. So what is the default behavior of RetrieveVirtualItem WRT selected item?
A virtual ListView should only call RetreiveVirtualItem for the rows currently visible on the screen.
As you navigate in the ListView, for example, you press the page down key, the ListView will calculate what should now be the index of the top row and will then call RetrieveVirtualItem so that your code can provide the item to use at each row index.
Unless you cache or otherwise store the items you are providing via RetrieveVirtualItem, they will no longer exist once they are scrolled out of the listview.
This is what the Virtual in VirtualListView means - there aren't any real rows, the rows are virtual. That is how it could display a list containing hundreds of thousands of rows - because it will ever only actually contain how ever many rows are visible on screen.
In effect, the ListView is like a window that is moving up and down your internal list of data - the RetreiveVirtualItem method is what it calls to move items into that window as it moves along. It says, hey I just moved to row 15 - give me the item for that row. It will proceed to call RetreiveVirtualItem for each row index which would be visible. If the ListView was 5 rows in height on the screen, you would receive 5 calls to RetrieveVirtualItem - even if the actual data backing the listview had 3000 items. Each time the top row of the ListView changed (because of navigation), you would receive 5 calls to RetrieveVirtualItem (this is not always the case, but it is the right idea - for example, if you scroll down one row, it will simply ask you for the new last row - it will also simply discard the data that was used for the old top row that scrolled out of view).
I guess it might be even easier to explain if we assume the ListView was only one row high on the display (meaning only a single row is ever actually visible on the screen) - as you move the ListView up and down your list of data (i.e. the user navigates the ListView), it would call RetrieveVirtualItem exactly one time every time it moves to a new row.
Hope that helps...
Good Luck
Virtual listviews only deal with indices. So, if the 5th item is selected before the sort, the 5th item will still be selected after the sort. The control itself has no way of knowing that the data that used to be on the 5th row is now on the first row.
You will have to program that for yourself in your sorting method:
remember which items were selected (remember: you can't use SelectedItems property when in virtual mode)
do the sort
find the indices of the previously selected item now live
select those indices
You can see all this in action in ObjectListView -- a wrapper around a standard .NET ListView.
The RetrieveVirtualItem event is only used when the ListView is put into virtual mode. Rather than retaining ListViewItems in the Items collection (as when not in virtual mode), it creates the ListViewItems dynamically, when needed.
If you don't handle the RetrieveVirtualItem event then no ListViewItems will be added to your ListView. I've attached some sample code for a typical method written to handle the event:
//Dynamically returns a ListViewItem with the required properties; in this case, the square of the index.
void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
//Check if cache has been instantiated yet, and if so, whether the item corresponding to the index requested has been added to the cache already
if (myCache != null && e.ItemIndex >= firstItem && e.ItemIndex < firstItem + myCache.Length)
{
//Return cached item for index
e.Item = myCache[e.ItemIndex - firstItem];
}
else
{
//When item not in cache (or cache not available) return a new ListViewItem
int x = e.ItemIndex * e.ItemIndex;
e.Item = new ListViewItem(x.ToString());
}
}
This example is taken from MSDN (http://msdn.microsoft.com/en-us/library/system.windows.forms.listview.virtualmode(v=vs.90).aspx) where further details can be found.