ObservableCollection and ListBoxItem DataTemplate generation problem - c#

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

Related

UWP get the vertical offset of Datagrid's ScrollViewer

I'm using a datagrid from "Microsoft.Toolkit.Uwp.UI.Controls" and I'm trying to get the scrollViewer that is built into it to be able to get the vertical offset and implement certain behavior when scrolled all the way to the bottom or the middle. So far I've tried a number of methods I found in other different threads like the one below, that scans the visual tree and gets the scrollViewer. But the value of the verticalOffset property of the scrollViewer returned from this method is always zero and it's events like ViewChanged never gets fired, I've tried calling the updateLayout method but it changed nothing.
I've also tried wrapping the datagrid in a scrollviewer and used that instead. While that worked fine, it caused a huge performance issue due to virtualization. So is there any solution to this ?
private ScrollViewer GetScrollViewer(UIElement element)
{
if (element == null) return null;
ScrollViewer retour = null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element) && retour == null; i++)
{
if (VisualTreeHelper.GetChild(element, i) is ScrollViewer)
{
retour = (ScrollViewer)(VisualTreeHelper.GetChild(element, i));
}
else
{
retour = GetScrollViewer(VisualTreeHelper.GetChild(element, i) as UIElement);
}
}
return retour;
}
As Oleg Mikhailov mentioned the source code, it was not implemented with ScrollViewer, and we have tested with your code, we can't get the scrollviewer instance. for this scenario, you could detect vertical scrollbar's value change event to get vertical offset, please refer the following code.
var scrollbar = MyFindDataGridChildOfType<ScrollBar>(MyDataGrid);
scrollbar.ValueChanged += Scroll_ValueChanged;
private void Scroll_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
Debug.WriteLine(e.NewValue);
// here is vertical value.
}

WPF Listbox Virtualization creates DisconnectedItems

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."

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

How to set an Item of a ListBox to the top?

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

When PageUp/PageDown a Datagrid - how does the ICollectionView work?

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,

Categories