What exactly is ListView.RetrieveVirtualItem Event - C# - c#

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.

Related

WinForms ListView selection changes randomly when assigning selected index from code

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.

Set Row Count of Grid Control in WPF

I have a very large dataset(can be up to 3 million items) that I am retrieving on demand based on the scroll position of a grid. This means that I will never have all the items in one collection. But, I need the grid to be interactive and allow the user to scroll as if all the items were in memory.
So, I need a way to set the amount of rows (data items) so that the scroll bar will be the proper size for my database collection. I know the total number of items in the database, so I just need to set the total number of rows in the grid to match that number.
Is there a simple way to do this in WPF with a Datagrid or GridView?
Edit: The important thing is that the scroll bar is properly sized. That way the collection can be indexed based off of it.
Use event handlers for several button controls to make buttons: Next, Previous, First, Last. Parameterise the SQL called by these buttons with firstrow and lastrow inputs say 1 to 10. Each time Next is called, the SQL will spit out the next 10 rows. The rows on the grid per page will be equal to 10 in this case.

Prevent ListView vertically scrolling when oldest item is removed

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))

Set a value runtime to combobox

I have a combobox populated with four items already. My app is receiving any of these four items on run time (one at a time). How can I set my comboBox with the received item (which is one of the four) on run time.
This is to make app a bit user friendly. So that user wouldnt have to select by himself. I know there can be other ways to do it but I want it to work like the way I explained in the first paragraph.
Also I have added this to make it uneditable.
comboBox.DropDownStyle = ComboBoxStyle.DropDownList;
As far as I know, there are no events fired when items are added to ComboBoxes. You are in control of when items are added, so when your code adds an item, it should fire it's own event.
However, if you know when you add the item, you can set it to equal the most recently added item like so:
comboBox1.Items.Add(item);
comboBox1.SelectedIndex = comboBox1.Items.Count - 1;
or if you may be removing some items, like so:
comboBox1.Items.Add(item);
comboBox1.SelectedItem = item;

ListBox ScrollIntoView and SelectedItem unreliable

I currently have a list box set up as follows:
.xaml:
<ListBox Name="DetailsList" ItemsSource="{Binding}" LayoutUpdated="DetailsList_LayoutUpdated" />
.xaml.cs:
private ObservableCollection<string> details = new ObservableCollection<string>();
In the window constructor:
DetailsList.DataContext = details;
I had a button that did the following:
details.Add(System.DateTime.Now.ToString("HH:mm:ss ") + someString);
DetailsList.UpdateLayout();
var lastItem = DetailsList.Items[DetailsList.Items.Count - 1];
DetailsList.SelectedItem = lastItem;
DetailsList.ScrollIntoView(lastItem);
That should select the last item in the list and scroll to it, but it only does it around 75% of the time. Instead, it will often select the second last and scroll to that instead.
I tried moving the scroll and selection into a LayoutUpdated event handler, no change.
I tried two separate buttons - one to add, one to select and scroll. If I add one item at a time then scroll, it seems to be slightly more reliable - it works 90% of the time. If I add half a dozen items before scrolling, it almost never works. It will typically select one of the new items, but not the last one.
Am I doing something wrong, or is there a bug with System.Windows.Controls.ListBox?
The problem is that multiple items with the same string value have been added to the list. When setting the SelectedItem property on the ListView it will call the Equals method to find the correct item. It will select the first item for which Equals returns true which is why you're getting the observed behaviour.
You need to add unique strings (e.g. add milliseconds) or wrap them in another class to be able to uniquely identify them.

Categories