I have a ListView which might contains a lot of items, so it is virtualized and recycling items. It does not use sort. I need to refresh some value display, but when there are too many items, it is too slow to update everything, so I would like to refresh only the visible items.
How could I get a list of all currently displayed items ? I tried to look into the ListView or in the ScrollViewer, but I still have no idea how to achieve this. The solution must NOT go through all items to test if they can be seen, because this would be too slow.
I'm not sure code or xaml would be useful, it is just a Virtualized/Recycling ListView with its ItemSource bound to an Array.
Edit :
Answer :
thanks to akjoshi, I found the way :
get the ScrollViewer of the ListView
(with a FindDescendant method, that you can do yourself with the VisualTreeHelper ).
read its ScrollViewer.VerticalOffset : it is the number of the first item shown
read its ScrollViewer.ViewportHeight : it is the count of items shown.
Rq : CanContentScroll must be true.
Have a look at this question on MSDN showing a technique to find out the visible ListView items -
How to find the rows (ListViewItem(s)) in a ListView that are actually visible?
Here's the relevant code from that post -
listView.ItemsSource = from i in Enumerable.Range(0, 100) select "Item" + i.ToString();
listView.Loaded += (sender, e) =>
{
ScrollViewer scrollViewer = listView.GetVisualChild<ScrollViewer>(); //Extension method
if (scrollViewer != null)
{
ScrollBar scrollBar = scrollViewer.Template.FindName("PART_VerticalScrollBar", scrollViewer) as ScrollBar;
if (scrollBar != null)
{
scrollBar.ValueChanged += delegate
{
//VerticalOffset and ViweportHeight is actually what you want if UI virtualization is turned on.
Console.WriteLine("Visible Item Start Index:{0}", scrollViewer.VerticalOffset);
Console.WriteLine("Visible Item Count:{0}", scrollViewer.ViewportHeight);
};
}
}
};
Another thing you should do is to use ObservableCollection as your ItemSource instead of an Array; that will definitely improve the performance.
Update:
Ya that might be true(array vs. ObservableCollection) but I would like to see some statistics related to this;
The real benefit of ObservableCollection is if you have a requirement to add/remove items from your ListView at run-time, in case of an Array you will have to reassign the ItemSource of ListView and the ListView first throws away its previous items and regenerates its entire list.
After trying to figure out something similar, I thought I would share my result here (as it seems easier than the other responses):
Simple visibility test I got from here.
private static bool IsUserVisible(FrameworkElement element, FrameworkElement container)
{
if (!element.IsVisible)
return false;
Rect bounds =
element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
var rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
return rect.Contains(bounds.TopLeft) || rect.Contains(bounds.BottomRight);
}
Afterwards you can loop through the listboxitems and use that test to determine which are visible. Since the listboxitems are always ordered the same the first visible one in this list would be the first visible one to the user.
private List<object> GetVisibleItemsFromListbox(ListBox listBox, FrameworkElement parentToTestVisibility)
{
var items = new List<object>();
foreach (var item in PhotosListBox.Items)
{
if (IsUserVisible((ListBoxItem)listBox.ItemContainerGenerator.ContainerFromItem(item), parentToTestVisibility))
{
items.Add(item);
}
else if (items.Any())
{
break;
}
}
return items;
}
How I see things :
on one side, you have your data. They must be up to date, because this is where your information is in memory. Iterating on your data list should be pretty fast, and most of all, can be done on another thread, in background
on the other side, you have the display. Your ListView already make the trick of refreshing only the datas displayed, since it's virtualizing ! You need no more tricks, it's already in place !
On last work, using a binding on an ObservableCollection is a good advice. If you intend to modify the ObservableCollection from an another thread, I would recommend this : http://blog.quantumbitdesigns.com/2008/07/22/wpf-cross-thread-collection-binding-part-1/
I spend a lot of time finding a better solution for this,
In my situation i have a scrollviewer, filled with items with custom heigths that can be set visible/invisible, i came up with this. It does the same as above solutions but with a fraction of the CPU. I hope it helps some one.
The first items of the listview / scrollpanel is TopVisibleItem
public int TopVisibleItem { get; private set; }
private double CurrentDistance;
private void TouchScroller_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (myItemControl.Items.Count > 0)
{
MoveDirection direction = (MoveDirection)Math.Sign(e.VerticalChange);
if (direction == MoveDirection.Positive)
while (CurrentDistance < e.VerticalOffset && TopVisibleItem < myItemControl.Items.Count)
{
CurrentDistance += ((FrameworkElement)myItemControl.Items[TopVisibleItem]).ActualHeight;
TopVisibleItem += 1;
}
else
while (CurrentDistance >= e.VerticalOffset && TopVisibleItem > 0)
{
CurrentDistance -= ((FrameworkElement)myItemControl.Items[TopVisibleItem]).ActualHeight;
TopVisibleItem -= 1;
}
}
}
public enum MoveDirection
{
Negative = -1,
Positive = 1,
}
If you have a virtualization enabled ListView, Then you can get all Current Visible items as below:
Get VirtualizingStackPanel
Get all ListViewItems in VirtualizingStackPanel
The code is shown below.
VirtualizingStackPanel virtualizingStackPanel = FindVisualChild<VirtualizingStackPanel>(requiredListView);
List<ListViewItem> items = GetVisualChildren<ListViewItem>(virtualizingStackPanel);
The Functions are shown below.
private childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
return (childItem)child;
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
private List<childItem> GetVisualChildren<childItem>(DependencyObject obj) where childItem : DependencyObject
{
List<childItem> childList = new List<childItem>();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
childList.Add(child as childItem);
}
if (childList.Count > 0)
return childList;
return null;
}
This will return you list of current ListViewItem loaded for displaying.
Hope it helps :).
Related
I have bookmarks list, when I tap on a bookmark, another page is loaded with a listA consists of several items.
Now suppose, I tap on a bookmark, which points to the item index 100 of the listA... the other listA opens, I manage to set the SelectedIndex of the listA to 100, which is somewhere down the list is not visible.
The problem is that, the SelectedIndex is set to 100, but the list still shows the top most item, on the top.
How can I set the item number 100 on the top, when it loads the contents?
Works perfectly with ScrollViewer.ScrollToVerticalOffset Method
Step I. Call Loaded event of the listA
<ListBox Name="ListA" Loaded="HookScrollViewer">
Step II. Define the "HookScrollViewer" method
private void HookScrollViewer(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
var scrollViewer = FindChildOfType<ScrollViewer>(element);
if (scrollViewer == null)
return;
scrollViewer.ScrollToVerticalOffset(lstA.SelectedIndex);
}
Step III. Define the "FindChildOfType" method
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
for (int i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
And it works with "ListA" (Replace ListA by the name of your ListBox)
Ref: ListBox offset in WP7
ListBox is really pain in the ass (especially with complex datatemplates and virtualizing). You should use this workaround:
listbox.SelectedIndex = 1000; //I mean index of the last item
listbox.UpdateLayout();
listbox.SelectedIndex = 100;
listbox.UpdateLayout();
Hope this helps
Try this:
listBox2.TopIndex = listBox2.SelectedIndex;
I'm not sure if I understand your question correctly. But If you just want to set an item on top of the listbox items then I would do..
listbox.Items.Insert(0, "something);
or
Use linq to order them
listbox.Items.OrderBy("something");
I have a difficult task to solve. When using WPF Datagrid the Paging works fine as long as you don't have any groupings.
However as soon as groupings come into the place, it becomes a bit tricky. In fact The pageup/down don't work correctly anymore.
One workaround I have found is to access the internal scroll viewer that is wrapper inside the dataGrid by using this:
helper class:
public static class VisualTreeUtilities
{
public static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
Actual code:
ctor:
_scrollViewer2 = VisualTreeUtilities.GetVisualChild<ScrollViewer>(this.dataGrid);
private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.PageUp:
_scrollViewer2.PageUp();
e.Handled = true;
break;
}
}
This way I can perfectly scroll fine despite using the groupings. The only problem that remains is that Datagrid doesn't take the actual selectedItem with it downwards when paging down, or upwards when paging up. The selected cell remains at the former position and out of sight when paging away. I wished it was like excel when the selected cell remains there. It means when the second cell of third row is selected and you page down, still the second cell of the third row would be selected. How could I do something like this in a WPF datagrid?
I need to understand how internally it works. How does the ListCollectionView know how many rows to page down? if I knew how it counts down the rows, I could use the same principle to count down the rows and set the selectedItem to that position and do a cell.focus() and problem solved.
Can anybody help me with understanding the missing bit?
Many Thanks,
I have a list of files in a ListView in WPF. Users can drag files onto the list view, and right now they are just appended to the end of the list. Is it possible to insert the file into the ListView right where the user dropped it?
WPF isn't really designed to be used that way. While you can brute force add ListViewItem's directly to the ListView, the way it's really supposed to work is that you have a collection of some kind (ObservableCollection<FileInfo> would work well) and bind the ListView's ItemsSource property to that collection.
Then the answer is simple. Instead of the Add method, you use the Insert method of the collection which takes an index.
As for finding which ListViewItem the mouse event occurred over, you could use the VisualTreeHelper.HitTest method.
From my point of view it is little tricky when I used the templated item. I have fight with it little bit. I am sharing my usecase which works with DraggableListBox. But I suppose the same solution works with ListBox control.
As the first I created the dependency object extension which is able to provide me ListItem element:
public static class WpfDomHelper
{
public static T FindParent<T>(this DependencyObject child) where T : DependencyObject
{
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
if (parentObject == null) return null;
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
}
Then I implemented Drop logic which inserts(adds) item according specific Drop Y position of destination ListBoxItems:
private void Grid_Drop(object sender, DragEventArgs e)
{
int dropIndex = -1; // default position directong to add() call
// checking drop destination position
Point pt = e.GetPosition((UIElement)sender);
HitTestResult result = VisualTreeHelper.HitTest(this, pt);
if (result != null && result.VisualHit != null)
{
// checking the object behin the drop position (Item type depend)
var theOne = result.VisualHit.FindParent<Microsoft.TeamFoundation.Controls.WPF.DraggableListBoxItem>();
// identifiing the position according bound view model (context of item)
if (theOne != null)
{
//identifing the position of drop within the item
var itemCenterPosY = theOne.ActualHeight / 2;
var dropPosInItemPos = e.GetPosition(theOne);
// geting the index
var itemIndex = tasksListBox.Items.IndexOf(theOne.Content);
// decission if insert before or below
if (dropPosInItemPos.Y > itemCenterPosY)
{ // when drag is gropped in second half the item is inserted bellow
itemIndex = itemIndex + 1;
}
dropIndex = itemIndex;
}
}
.... here create the item .....
if (dropIndex < 0)
ViewModel.Items.Add(item);
else
ViewModel.Items.Insert(dropIndex, item);
e.Handled = true;
}
So this solution works with my template DraggableListBoxView, I suppose the same solution must work with standard ListBoxView. Good Luck
You can do this. It takes a bit of work, but it can be done. There are a couple demos out there, here is one on CodeProject. This particular one is by the wpf master known as Josh Smith. It's probably not exactly what you are looking for, but it should be pretty darn close.
sorry about not making myself clear enough and not putting enough effort (wont happen again :)). i'm building a form App where users have to fill out the form. i have a TabControl with 3 TabItem one of the TabItem has TextBoxes the second has TextBoxes and RadioButton and third has only CheckBoxes. i have written a code to dictect error by on clicking submit using ValidationRule/ValidationResult and and using GroupBinding (got it from msdn samples). now the problem am having is code to search through the tabs, compare the controls (e.g. controlA,controlB) to know wich one comes before the other and return the tabindex. one of the use i want with this is letting the user jump to the uncompleted TextBox,RadioButton or CheckBoxes in order like starting from the first Tabitem in the TabControl
with this code i could work the tree to locate the controls(code from Philipp Sumi blog but i modified it a little)
private void Button_Click(
object sender,
RoutedEventArgs e)
{
IEnumerator enumerator = FindLogicalChildren(_parentStackPanel).GetEnumerator();
while (enumerator.MoveNext())
MessageBox.Show(enumerator.Current.ToString());
}
private IEnumerable FindLogicalChildren(
DependencyObject depObj)
{
if (depObj != null)
{
foreach (object childObj in LogicalTreeHelper.GetChildren(depObj))
{
DependencyObject child = childObj as DependencyObject;
if (child != null && child is Control)
{
yield return (Control)child;
}
foreach (Control childOfChild in FindLogicalChildren(child))
{
yield return childOfChild;
}
}
}
}
but i dodnt know how continue to get the tabindex of each control in order form as i work down the tree. can any one please help me on this? Thanks
im using this method:
private int Compare(
Control controlA,
Control controlB)
{
DependencyObject commonAncestor = controlA.FindCommonVisualAncestor(controlB);
for (int index = 0; index < VisualTreeHelper.GetChildrenCount(commonAncestor); index++)
{
Visual childVisual = (Visual)VisualTreeHelper.GetChild(commonAncestor, index);
Control control = (Control)childVisual;
control.TabIndex = index;
}
return controlA.TabIndex.CompareTo(controlB.TabIndex);
}
that returns 1,-1 or 0 to compare two controls to find out which one comes before the other. the question is, can anyone tell me a better way of doing this.
Something strange is going on with ObservableCollection.
I have the following code:
private readonly ObservableCollection<DisplayVerse> _display;
private readonly ListBox _box;
private void TransferToDisplay()
{
double elementsHeight = 0;
_display.Clear();
for (int i = 0; i < _source.Count; i++) {
DisplayVerse verse = _source[i];
_display.Add(verse);
elementsHeight += CalculateItemsHeight(i);
if (elementsHeight + Offset > _box.ActualHeight) {
_display.RemoveAt(_display.Count - 1);
break;
}
}
MessageBox.Show(elementsHeight.ToString());
}
private double CalculateItemsHeight(int index)
{
ListBoxItem lbi = _box.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
return lbi != null ? lbi.ActualHeight : 0;
}
What I am trying to do here is control how many items go into the ObservableCollection _display. Now, within this for loop you can see that elements are added until the total elements height (+offset) is greater than the listbox itself.
Now, this is strange, the elementsHeight equals 0 after this for loop. (CalculateItemsHeight returns 0 in all for loop iterations even though the lbi is not null) It seems that the UI elements defined in the datatemplate are not created...
Yet.
Now, if I put some MessageBoxes after the _display.Add(verse) you can see that the CalculateItemsHeight actually returns the height of an item.
for (int i = 0; i < _source.Count; i++) {
DisplayVerse verse = _source[i];
_display.Add(verse);
MessageBox.Show("pause"); // <----- PROBLEM?
elementsHeight += CalculateItemsHeight(i);
if (elementsHeight + Offset > _box.ActualHeight) {
_display.RemoveAt(_display.Count - 1);
break;
}
}
MessageBox.Show(elementsHeight.ToString());
After I modify the for loop as shown, the last MessageBox actually shows the actual height for all processed elements.
My question is - when are the UI elements actually created? It seems that it was done somewhere during the MessageBox display. This behaviour is pretty strange for me, maybe it has something to do with threading, not sure.
Adding to the _display ObservableCollection obviously creates an item immediately, but not its visual elements (they are however added afterwards, I just don't know exactly when). How can I do this same behaviour without having to pop the message box up?
Actually, I was trying to get this to work and I found the ".UpdateLayout()" function, which works perfectly for me. I realize that you're doing vertical and I'm doing horizontal, but here's my code, it's pretty simple:
for (int i = 0; i < listOfItems.ItemsIn.Count; ++i)
{
//CalculateItemsHeight(i);
ListBoxItem abc = (lb.ItemContainerGenerator.ContainerFromItem(lb.Items[i]) as ListBoxItem);
abc.UpdateLayout();
totalWidth += abc.ActualWidth;
}
Hopefully this helps!
The wpf layout engine won't have been through the layout and arrange pass so your listboxitems won't have been given a size yet. Sticking in the message box will allow the background threads that do this run. Try forcing a call to Measure() on your items before looking at their size.
SOLVED
This creates somewhat flickering effect for a fraction of second (as if loading items one by one), but actually suits my needs.
The point is to refresh the UI for an item before retrieving its height.
I have created an extension method:
public static void RefreshUI(this DependencyObject obj)
{
obj.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)delegate { });
}
And then before retrieving the height, I refresh the UI.
private double CalculateItemsHeight(int index)
{
ListBoxItem lbi = _box.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
if (lbi != null) {
lbi.RefreshUI();
return lbi.ActualHeight;
}
return 0;
}