WPF Listbox Virtualization creates DisconnectedItems - c#

I'm attempting to create a Graph control using a WPF ListBox. I created my own Canvas which derives from a VirtualizingPanel and I handle the realization and virtualization of items myself.
The listbox' item panel is then set to be my custom virtualized canvas.
The problem I am encountering occurs in the following scenario:
ListBox Item A is created first.
ListBox Item B is created to the right of Item A on the canvas.
ListBox Item A is virtualized first (by panning it out of view).
ListBox Item B is virtualized second (again by panning it out of view).
Bring ListBox Item A and B in view (i.e: realize them)
Using Snoop, I detect that the ListBox has now 3 items, one of them being a "DisconnectedItem" located directly underneath ListBox Item B.
What causes the creation of this "DisconnectedItem" ? If I were to virtualize B first, followed by A, this item would not be created. My theory is that virtualizing items that precedes other items in a ListBox causes children to be disconnected.
The problem is even more apparent using a graph with hundreds of nodes, as I end up with hundreds of disconnected items as I pan around.
Here is a portion of the code for the canvas:
/// <summary>
/// Arranges and virtualizes child element positionned explicitly.
/// </summary>
public class VirtualizingCanvas : VirtualizingPanel
{
(...)
protected override Size MeasureOverride(Size constraint)
{
ItemsControl itemsOwner = ItemsControl.GetItemsOwner(this);
// For some reason you have to "touch" the children collection in
// order for the ItemContainerGenerator to initialize properly.
var necessaryChidrenTouch = Children;
IItemContainerGenerator generator = ItemContainerGenerator;
IDisposable generationAction = null;
int index = 0;
Rect visibilityRect = new Rect(
-HorizontalOffset / ZoomFactor,
-VerticalOffset / ZoomFactor,
ActualWidth / ZoomFactor,
ActualHeight / ZoomFactor);
// Loop thru the list of items and generate their container
// if they are included in the current visible view.
foreach (object item in itemsOwner.Items)
{
var virtualizedItem = item as IVirtualizingCanvasItem;
if (virtualizedItem == null ||
visibilityRect.IntersectsWith(GetBounds(virtualizedItem)))
{
if (generationAction == null)
{
GeneratorPosition startPosition =
generator.GeneratorPositionFromIndex(index);
generationAction = generator.StartAt(startPosition,
GeneratorDirection.Forward, true);
}
GenerateItem(index);
}
else
{
GeneratorPosition itemPosition =
generator.GeneratorPositionFromIndex(index);
if (itemPosition.Index != -1 && itemPosition.Offset == 0)
{
RemoveInternalChildRange(index, 1);
generator.Remove(itemPosition, 1);
}
// The generator needs to be "reseted" when we skip some items
// in the sequence...
if (generationAction != null)
{
generationAction.Dispose();
generationAction = null;
}
}
++index;
}
if (generationAction != null)
{
generationAction.Dispose();
}
return default(Size);
}
(...)
private void GenerateItem(int index)
{
bool newlyRealized;
var element =
ItemContainerGenerator.GenerateNext(out newlyRealized) as UIElement;
if (newlyRealized)
{
if (index >= InternalChildren.Count)
{
AddInternalChild(element);
}
else
{
InsertInternalChild(index, element);
}
ItemContainerGenerator.PrepareItemContainer(element);
element.RenderTransform = _scaleTransform;
}
element.Measure(new Size(double.PositiveInfinity,
double.PositiveInfinity));
}

I'm 6 years late, but the problem is still not fixed in WPF. Here is the solution (workaround).
Make a self-binding to the DataContext, eg.:
<Image DataContext="{Binding}" />
This worked for me, even for a very complex xaml.

It's used whenever a container is removed from the visual tree, either because the corresponding item was deleted, or the collection was refreshed, or the container was scrolled off the screen and re-virtualized.
This is a known bug in WPF 4
See this link for known bug, it also has a workaround you may be able to apply.
"You can make your solution a little more robust by saving a reference
to the sentinel object {DisconnectedItem} the first time you see it,
then comparing against the saved value after that.
We should have made a public way to test for {DisconnectedItem}, but
it slipped through the cracks. We'll fix that in a future release, but
for now you can count on the fact that there's a unique
{DisconnectedItem} object."

Related

Finding the actual extents of a ListBoxItem with UserControl as content

I'm trying to find the extents of a UI element in a ListBox. I use WPF / c# for this.
The code is roughly as follows:
ObservableCollection<SomeUserControl> TheCollection = new ObservableCollection<SomeUserControl>();
// setup
ListBox lb = new ListBox();
// code omitted here that setup the listbox with 2 columns
lb.ItemsSource = TheCollection;
// code omitted here that populates TheCollection with UI elements
Then I register a mouse event and I would like to know when a specific point is bounded by a specific element in the listbox. For this I have the following means to do it:
UIElement GetObjectHit(Point P) {
if (!lb.IsVisible) return null;
foreach(var i in TheCollection) {
FrameworkElement item = i as FrameworkElement;
if (item == null || !item.IsVisible)
continue;
Point P00 = item.TranslatePoint(new Point(0,0), lb);
Point P11 = item.TranslatePoint(new Point(item.ActualWidth, item.ActualHeight), lb);
if (IsBounded(P, P00, P11)
return item;
}
return null;
}
UIElement GetObjectHit(MouseButtonEventArgs e) {
return GetObjectHit(e.GetPosition(lb));
}
This works great with one exception...
The different user controls I have have varying sizes and the ListBox will then extend each item to the maximum element size currently in list and then center the smaller UserControls that are in the list.
So what's the problem?
Well when the position is in the white area which is inside the ListBoxItem area but outside the UserControl area the code above fails to find the intersection.
So my question is as follows:
Do I have to do a separate loop first to get the maximum extends of each UI element or is there any way to get the current extent of the ListBoxItem in the ListBox (even though the UserControl extent is smaller)?
In the unlikely event if anyone needs the answer to this I managed to get this to work as follows:
UIElement GetObjectHit(Point P) {
if (!lb.IsVisible) return null;
foreach(var i in TheCollection) {
var item = i as SomeUserControl;
ListBoxItem lbi = lb.ContainerFromElement(item) as ListBoxItem;
if (lbi == null || !lbi.IsVisible)
continue;
Point P00 = lbi.TranslatePoint(new Point(0,0), lb);
Point P11 = lbi.TranslatePoint(new Point(lbi.ActualWidth, lbi.ActualHeight), lb);
if (IsBounded(P, P00, P11)
return i;
}
return null;
}

Scroll LongListSelector when new item added

I have a chat application that shows messages in a LongListSelector and adds new ones with the statement ObservableCollection.Insert(0, message).
My issue is that when a new message is added, the LongListSelector doesn't scroll down to the new message.
The best solution in my opinion would be to automatically scroll to new messages if the LongListSelector is currently scrolled to the top, but I cannot find a method to detect current scrolling position (I see only LongListSelector.ScrollTo(), which isn't helpful).
How can I automatically scroll to new messages when the LongListSelector is currently scrolled to the top?
If you need to find current scroll position than you need to get the scrollbar inside longlistselector using VisualTreeHelper.
The sample function:
public static class VisualChildExtractHelper
{
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject 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;
}
}
usage:
ScrollBar LongListSelectorScrollBar = VisualChildExtractHelper.FindChildOfType<ScrollBar>(yourLongListSelectorName);
After that you can access ScrollBar Value (or ValueChanged event) and check if it equals 0 (that means that longlistselector is scrolled to the top) or other condition. If it is you can ScrollTo method of LongListSelector to bring element into view.

Get ListView Visible items

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

Drop Item into Specific Index in ListView in WPF C#

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.

ObservableCollection and ListBoxItem DataTemplate generation problem

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;
}

Categories