I'm having a bit of weird behavior that I can't seem to work out. When I iterate through the items in my ListBox.ItemsSource property, I can't seem to get the container? I'm expecting to see a ListBoxItem returned, but I only get null.
Any ideas?
Here's the bit of code I'm using:
this.lstResults.ItemsSource.ForEach(t =>
{
ListBoxItem lbi = this.lstResults.ItemContainerGenerator.ContainerFromItem(t) as ListBoxItem;
if (lbi != null)
{
this.AddToolTip(lbi);
}
});
The ItemsSource is currently set to a Dictionary and does contain a number of KVPs.
I found something that worked better for my case in this StackOverflow question:
Get row in datagrid
By putting in UpdateLayout and a ScrollIntoView calls before calling ContainerFromItem or ContainerFromIndex, you cause that part of the DataGrid to be realized which makes it possible for it return a value for ContainerFromItem/ContainerFromIndex:
dataGrid.UpdateLayout();
dataGrid.ScrollIntoView(dataGrid.Items[index]);
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);
If you don't want the current location in the DataGrid to change, this probably isn't a good solution for you but if that's OK, it works without having to turn off virtualizing.
Finally sorted out the problem... By adding VirtualizingStackPanel.IsVirtualizing="False" into my XAML, everything now works as expected.
On the downside, I miss out on all the performance benefitst of the virtualization, so I changed my load routing to async and added a "spinner" into my listbox while it loads...
object viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
if (viewItem == null)
{
list.UpdateLayout();
viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
Debug.Assert(viewItem != null, "list.ItemContainerGenerator.ContainerFromItem(item) is null, even after UpdateLayout");
}
Step through the code with the debugger and see if there is actually nothing retured or if the as-cast is just wrong and thus turns it to null (you could just use a normal cast to get a proper exception).
One problem that frequently occurs is that when an ItemsControl is virtualizing for most of the items no container will exist at any point in time.
Also i would not recommend dealing with the item containers directly but rather binding properties and subscribing to events (via the ItemsControl.ItemContainerStyle).
Use this subscription:
TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
TheListBox.Dispatcher.Invoke(() =>
{
var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
if (TheOne != null)
// Use The One
});
};
I'm a bit late for the party but here's another solution that's fail-proof in my case,
After trying many solutions suggesting to add IsExpanded and IsSelected to underlying object and binding to them in TreeViewItem style, while this mostly works in some case it still fails ...
Note: my objective was to write a mini/custom Explorer-like view where when I click a folder in the right pane it gets selected on the TreeView, just like in Explorer.
private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var item = sender as ListViewItem;
var node = item?.Content as DirectoryNode;
if (node == null) return;
var nodes = (IEnumerable<DirectoryNode>)TreeView.ItemsSource;
if (nodes == null) return;
var queue = new Stack<Node>();
queue.Push(node);
var parent = node.Parent;
while (parent != null)
{
queue.Push(parent);
parent = parent.Parent;
}
var generator = TreeView.ItemContainerGenerator;
while (queue.Count > 0)
{
var dequeue = queue.Pop();
TreeView.UpdateLayout();
var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
if (queue.Count > 0) treeViewItem.IsExpanded = true;
else treeViewItem.IsSelected = true;
generator = treeViewItem.ItemContainerGenerator;
}
}
Multiple tricks used in here:
a stack for expanding every item from top to bottom
ensure to use current level generator to find the item (really important)
the fact that generator for top-level items never return null
So far it works very well,
no need to pollute your types with new properties
no need to disable virtualization at all.
Although disabling virtualization from XAML works, I think it's better to disable it from the .cs file which uses ContainerFromItem
VirtualizingStackPanel.SetIsVirtualizing(listBox, false);
That way, you reduce the coupling between the XAML and the code; so you avoid the risk of someone breaking the code by touching the XAML.
Most probably this is a virtualization-related issue so ListBoxItem containers get generated only for currently visible items (see https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs.110).aspx#Anchor_9)
If you are using ListBox I'd suggest switching to ListView instead - it inherits from ListBoxand it supports ScrollIntoView() method which you can utilize to control virtualization;
targetListView.ScrollIntoView(itemVM);
DoEvents();
ListViewItem itemContainer = targetListView.ItemContainerGenerator.ContainerFromItem(itemVM) as ListViewItem;
(the example above also utilizes the DoEvents() static method explained in more detail here; WPF how to wait for binding update to occur before processing more code?)
There are a few other minor differences between the ListBox and ListView controls (What is The difference between ListBox and ListView) - which should not essentially affect your use case.
VirtualizingStackPanel.IsVirtualizing="False" Makes the control fuzzy . See the below implementation. Which helps me to avoid the same issue.
Set your application VirtualizingStackPanel.IsVirtualizing="True" always.
See the link for detailed info
/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container != null)
{
if (container.DataContext == item)
{
return container as TreeViewItem;
}
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
ItemsPresenter itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
}
else
{
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
}
}
Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
UIElementCollection children = itemsHostPanel.Children;
MyVirtualizingStackPanel virtualizingPanel =
itemsHostPanel as MyVirtualizingStackPanel;
for (int i = 0, count = container.Items.Count; i < count; i++)
{
TreeViewItem subContainer;
if (virtualizingPanel != null)
{
// Bring the item into view so
// that the container will be generated.
virtualizingPanel.BringIntoView(i);
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
}
else
{
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
// Bring the item into view to maintain the
// same behavior as with a virtualizing panel.
subContainer.BringIntoView();
}
if (subContainer != null)
{
// Search the next level for the object.
TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
{
return resultContainer;
}
else
{
// The object is not under this TreeViewItem
// so collapse it.
subContainer.IsExpanded = false;
}
}
}
}
return null;
}
For anyone still having issues with this, I was able to work around this issue by ignoring the first selection changed event and using a thread to basically repeat the call. Here's what I ended up doing:
private int _hackyfix = 0;
private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//HACKYFIX:Hacky workaround for an api issue
//Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason. Basically we ignore the
//first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
//with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need I ignore the event twice just in case but I think you can get away with ignoring only the first one.
if (_hackyfix == 0 || _hackyfix == 1)
{
_hackyfix++;
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnMediaSelectionChanged(sender, e);
});
}
//END OF HACKY FIX//Actual code you need to run goes here}
EDIT 10/29/2014: You actually don't even need the thread dispatcher code. You can set whatever you need to null to trigger the first selection changed event and then return out of the event so that future events work as expected.
private int _hackyfix = 0;
private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//HACKYFIX: Daniel note: Very hacky workaround for an api issue
//Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason. Basically we ignore the
//first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
//with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need
if (_hackyfix == 0)
{
_hackyfix++;
/*
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnMediaSelectionChanged(sender, e);
});*/
return;
}
//END OF HACKY FIX
//Your selection_changed code here
}
Related
I want to access the elements in the DataGrid.I am using following code.But I am unable to get the row of DataGrid.I am getting null value.I just want to know why I am getting null value and how to resolve this issue.
int itemscount = (dgMtHdr.Items.Count);
dgMtHdr.UpdateLayout();
for (int rowidx = 0; rowidx < itemscount; rowidx++)
{
Microsoft.Windows.Controls.DataGridRow dgrow = (Microsoft.Windows.Controls.DataGridRow)this.dgMtHdr.ItemContainerGenerator.ContainerFromIndex(rowidx);
if (dgrow != null)
{
DataRow dr = ((DataRowView)dgrow.Item).Row;
if (dr != null)
{
obj = new WPFDataGrid();
Microsoft.Windows.Controls.DataGridCell cells = obj.GetCell(dgMtHdr, rowidx, 7);
if (cells != null)
{
ContentPresenter panel = cells.Content as ContentPresenter;
if (panel != null)
{
ComboBox cmb = obj.GetVisualChild<ComboBox>(panel);
}
}
}
}
}
DataGrid internally hosts items in DataGridRowsPresenter which derives from VirtualizingStackPanel which means items rendered on UI by default support Virtualization i.e. ItemContainer won't be generated for items which are not rendered on UI yet.
That's why you getting null when you try to fetch rows which are not rendered on UI.
So, in case you are ready to trade off with Virtualization, you can turn off the Virtualization like this -
<DataGrid x:Name="dgMtHdr" VirtualizingStackPanel.IsVirtualizing="False"/>
Now, DataGridRow won't be null for any index value.
OR
You can get the row by manually calling UpdateLayout() and ScrollIntoView() for the index so that container gets generated for you. For details refer to this link here. From the link -
if (row == null)
{
// May be virtualized, bring into view and try again.
grid.UpdateLayout();
grid.ScrollIntoView(grid.Items[index]);
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
}
EDIT
Since your DataGrid is in second tab which is not rendered yet. That's why its ItemContainerGenerator haven't generated corresponding containers required for items. So, you need to do it once item container is generated by hooking to StausChanged event -
dgMtHdr.ItemContainerGenerator.StatusChanged += new
EventHandler(ItemContainerGenerator_StatusChanged);
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if ((sender as ItemContainerGenerator).Status ==
GeneratorStatus.ContainersGenerated)
{
// ---- Do your work here and you will get rows as you intended ----
// Make sure to unhook event here, otherwise it will be called
// unnecessarily on every status changed and moreover its good
// practise to unhook events if not in use to avoid any memory
// leak issue in future.
dgMtHdr.ItemContainerGenerator.StatusChanged -=
ItemContainerGenerator_StatusChanged;
}
}
I have implemented a virtualizing panel that works pretty well, the panel implements IScrollInfo. I am in the process of getting it to work using a keyboard. The Panel is used in a ListView as the ListView.ItemsPanel. When the user presses down the selected item correctly changes but no scrolling occurs and when you reach the last visible item you can not press down any longer. I am trying to figure out how to have the panel aware of the selected item and scroll appropriately. I have tried searching around but I cant seem to find anything related to what I want to do. A point in the right direction would be greatly appreciated.
Edit: Here is the code for the VirtualizingWrapPanel
So I found a solution that works fairly well, I am not sure if it is the best route to take but it seems to work fine.
In the ArrangeOverride method while looping over the children I do the following.
var listViewItem = child as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
listViewItem.Selected += ListViewItemSelected;
}
In MeasureOverride I make a call to CleanUpItems in here we will need to unsubscribe from the selected event.
var listViewItem = children[i] as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
}
I also added the following three functions.
private void ListViewItemSelected(object sender, RoutedEventArgs e)
{
var listViewItem = sender as ListViewItem;
if(listViewItem == null) return;
var content = listViewItem.Content as CollectionViewGroup;
if(content != null) return; //item is a group header dont click
var items = ItemContainerGenerator as ItemContainerGenerator;
if(items == null) return;
BringIndexIntoView(items.IndexFromContainer(listViewItem));
listViewItem.Focus();
}
protected override void BringIndexIntoView(int index)
{
var offset = GetOffsetForFirstVisibleIndex(index);
SetVerticalOffset(offset.Height);
}
private Size GetOffsetForFirstVisibleIndex(int index)
{
int childrenPerRow = CalculateChildrenPerRow(_extent);
var actualYOffset = ((index / childrenPerRow) * ChildDimension.Height) - ((ViewportHeight - ChildDimension.Height) / 2);
if (actualYOffset < 0)
{
actualYOffset = 0;
}
Size offset = new Size(_offset.X, actualYOffset);
return offset;
}
The GetOffsetForFirstVisibleIndex function will probably vary depending on your implementation but that should be enough info if anyone else is having trouble coming up with a solution.
I have Tab Control which has many tab items, I am checking Data Grid Items Count while closing tab items. For the first time it works fine(I mean in first iteration). After closing one tab item, in second iteration sellDtg is null. Does anyone know why it is happening? I am concerning that this is UI problem, layout is not being refreshed. Please help me or show direction.
while (tc.HasItems)
{
TabItem ti = tc.SelectedItem as TabItem;
if (ti.Header == "Продажа")
{
Microsoft.Windows.Controls.DataGrid sellDtg = FindChild<Microsoft.Windows.Controls.DataGrid>(tc, "SellDataGrid");
if (sellDtg.Items.Count > 0)
{
Sell sl = new Sell();
if (Sell.basketfromSellDateListBox == false)
{
sl.ClearBasket(sellDtg);
Sell.ClearFromSellBasket((int)sellDtg.Tag);
}
}
}
if (ti != null)
tc.Items.Remove(ti);
}
Thanks in advance!!!
I've written a simple FindChildLogical function in analogy for LogicalTreeHelper below:
public static T FindChildLogical<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
if (parent == null) return null;
var child = LogicalTreeHelper.FindLogicalNode(parent, childName);
return (T)child;
}
and you call it as:
Microsoft.Windows.Controls.DataGrid sellDtg = FindChildLogical<Microsoft.Windows.Controls.DataGrid>(ti, "SellDataGrid");
I hope it gets you where you intend to.
I am going to assume your FindChild method uses the VisualTreeHelper to find its children.
In the first iteration, the TabItem's Content has been through a layout pass, and is visible. This means that the TabItem's Content will be in the visual tree.
However, for the other tab items, their Content hasn't been through a layout pass (it is only added to the visual tree when it's parent gets selected, and this has to then go through a layout/render pass), and won't be in the visual tree.
There are a couple of ways to get the child content of a TabItem that hasn't been through a layout pass as the selected tab:
1) You can try using the LogicalTreeHelper to find the Grid you're looking for (and you will likely have to search the Content of the TabItem, not the TabControl).
2) You can take your code out of the while loop, and do a callback on the dispatcher at the Loaded priority:
void RemoveAllItems()
{
if (!tc.HasItems) return;
TabItem ti = tc.SelectedItem as TabItem;
if (ti.Header == "Продажа")
{
var sellDtg = FindChild<Microsoft.Windows.Controls.DataGrid>(tc, "SellDataGrid");
if (sellDtg.Items.Count > 0)
{
Sell sl = new Sell();
if (Sell.basketfromSellDateListBox == false)
{
sl.ClearBasket(sellDtg);
Sell.ClearFromSellBasket((int)sellDtg.Tag);
}
if (ti != null)
tc.Items.Remove(ti);
}
}
Dispatcher.BeginInvoke(new Action(RemoveAllItems), DispatcherPriority.Loaded);
}
If you use the second method, you will likely be able to see the tab items removed one at a time, which may be something you don't want to see.
I'm implementing simple drag-and-drop functionality in my app, and I would like to know if the user has dropped the item above the first item in the list (in the header row) so i can just insert it as the first item.
I'm using VisualTreeHelper.HitTest to get the item at the drop position, but this only works if there actually is an item there.
HitTestResult hitTestResults = VisualTreeHelper.HitTest(myListView, location);
When the mouse is on the headers i get one of many several items in hitTestResults.VisualHit. In just a few tests, I've gotten ListBoxChrome, TextBlock, and Border How can i know if any of these are part of the header row? I can't just test for them specifically since there could be other UI elements returned.
Can i get the coordinates of the header row of the listview to see if my point is inside it? Or is there a way that i can know if my Point is inside that header row?
I don't know how your current implementation look but you can walk up the Visual Tree until you either find a ListViewItem or a GridViewColumnHeader. If you find a GridViewColumnHeader you know that the item was dropped in this specific Header.
Uploaded a small sample project here demonstrating the effect with MessageBox's on drop: http://www.mediafire.com/?v3l8nl4rnewhz5s
It will look something like this
private void ListView_Drop(object sender, DragEventArgs e)
{
ListView parent = sender as ListView;
YourDataClass data = e.Data.GetData(typeof(YourDataClass)) as YourDataClass;
if (data != null)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(parent, e.GetPosition(parent));
ListViewItem hitItem = VisualTreeHelpers.GetVisualParent<ListViewItem>(hitTestResult.VisualHit);
GridViewColumnHeader columnHeader = VisualTreeHelpers.GetVisualParent<GridViewColumnHeader>(hitTestResult.VisualHit);
if (hitItem != null) // ListViewItem Drop
{
//..
}
else if (columnHeader != null) // Header Drop
{
//..
}
}
}
public static T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
How can I programmatically force a silverlight list box to scroll to the bottom so that the last item added is always visible.
I've tried simply selecting the item. It ends up as selected but still not visible unless you manually scroll to it.
Use the ListBox's ScrollIntoView method passing in the last item. You may need to call UpdateLayout immediately before it for it to work.
The ScrollIntoView() method will scroll the last item into view, however listBox.UpdateLayout() must be called just before ScrollIntoView(). Here is a complete method with code:
// note that I am programming Silverlight on Windows Phone 7
public void AddItemAndScrollToBottom(string message)
{
string timestamp = DateTime.Now.ToString("mm:ss");
var item = new ListBoxItem();
item.Content = string.Format("{0} {1}", timestamp, message);
// note that when I added a string directly to the listbox, and tried to call ScrollIntoView() it did not work, but when I add the string to a ListBoxItem first, that worked great
listBoxEvents.Items.Add(item);
if (listBoxEvents.Items.Count > 0)
{
listBoxEvents.UpdateLayout();
var itemLast = (ListBoxItem)listBoxEvents.Items[listBoxEvents.Items.Count - 1];
listBoxEvents.UpdateLayout();
listBoxEvents.ScrollIntoView(itemLast);
}
}
Slightly refactored to reduce the lines of code:
listBoxEvents.Add(item)
listBoxEvents.UpdateLayout()
listBoxEvents.ScrollIntoView(listBoxEvents.Items(listBoxEvents.Items.Count - 1))
Just went through this and none of the solutions above worked in a Silverlight 5 app. The solution turned out to be this:
public void ScrollSelectedItemIntoView(object item)
{
if (item != null)
{
FrameworkElement frameworkElement = listbox.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement != null)
{
var scrollHost = listbox.GetScrollHost();
scrollHost.ScrollIntoView(frameworkElement);
}
}
}