WPF VirtualizingWrapPanel Selected Item Change cause scroll - wpf

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.

Related

Get Row from Microsoft.Windows.Controls.DataGrid

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

Record items visible to user in ListBox

I have a ListBox or DataGrid filled with thousands of entries. I would like to know items that the user has looked at (scrolling, searching or otherwise). How can I tell what is visible to the user in the ListBox?
Bonus: Set a timer so that the item has to be shown for a minimum of N milliseconds (in the event the user is just pulling down the scrollbar).
Update: This is a near duplicate of Get items in view within a listbox - but the solution it gives, using "SelectedItems", is not sufficient. I need to know the items whether they are selected or not!
All you need to do is to get the underlying StackPanel that's inside the ListBox. It has enough information about which elements are showing. (It implements the interface IScrollInfo).
To get the underlying StackPanel (or actually VirtualizingStackPanel) from a given ListBox, we'll have to use VisualTreeHelper to go through the Visual Tree and look for the VirtualizingStackPanel, like so:
private VirtualizingStackPanel GetInnerStackPanel(FrameworkElement element)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i) as FrameworkElement;
if (child == null) continue;
Debug.WriteLine(child.ToString());
if (child is VirtualizingStackPanel) return child as VirtualizingStackPanel;
var panel = GetInnerStackPanel(child);
if (panel != null)
return panel;
}
return null;
}
Now that we have the StackPanel, we're very close. The StackPanel has the properties VerticalOffset and ViewportHeight (both coming from IScrollInfo) that can give us all the information we need.
private void button1_Click(object sender, RoutedEventArgs e)
{
var theStackPanel = GetInnerStackPanel(MyListBox);
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < theStackPanel.Children.Count; i++)
{
if (i >= theStackPanel.VerticalOffset && i <= theStackPanel.VerticalOffset + theStackPanel.ViewportHeight)
{
visibleElements.Add(theStackPanel.Children[i] as FrameworkElement);
}
}
MessageBox.Show(visibleElements.Count.ToString());
MessageBox.Show(theStackPanel.VerticalOffset.ToString());
MessageBox.Show((theStackPanel.VerticalOffset + theStackPanel.ViewportHeight).ToString());
}

How can I know if the mouse is over the header row in a WPF ListView?

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

WinForms ListBox with readonly/disabled items

Is there a way to make some of the items in a ListBox readonly/disabled so they can't be selected? Or are there any similar controls to ListBox to provide this functionality?
ListBox doesn't have support for that. You can bolt something on, you could deselect a selected item. Here's a silly example that prevents even-numbered items from being selected:
private void listBox1_SelectedIndexChanged(object sender, EventArgs e) {
for (int ix = listBox1.SelectedIndices.Count - 1; ix >= 0; ix--) {
if (listBox1.SelectedIndices[ix] % 2 != 0)
listBox1.SelectedIndices.Remove(listBox1.SelectedIndices[ix]);
}
}
But the flicker is quite noticeable and it messes up keyboard navigation. You can get better results by using CheckedListBox, you can prevent the user from checking the box for an item:
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e) {
if (e.Index % 2 != 0) e.NewValue = CheckState.Unchecked;
}
But now you cannot override drawing to make it look obvious to the user that the item isn't selectable. No great solutions here, it is far simpler to just not display items in the box that shouldn't be selectable.
#Hans solution causing that the item id selected for a short time and then selection disappearing. I don't like that - this can be confusing for the enduser.
I prefer to hide some edit option buttons for the item that should be disabled:
if (lbSystemUsers.Items.Count > 0 && lbSystemUsers.SelectedIndices.Count > 0)
if (((RemoteSystemUserListEntity)lbSystemUsers.SelectedItem).Value == appLogin)
{
bSystemUsersDelete.Visible = false;
bSystemUsersEdit.Visible = false;
}
else
{
bSystemUsersDelete.Visible = true;
bSystemUsersEdit.Visible = true;
}
Here is the list that lists the users and disallow to edit user that is actually logged in to the edit panel.
ListBox doesn't have a ReadOnly (or similar) property, but you can make a custom ListBox control. Here's a solution that worked pretty well for me:
https://ajeethtechnotes.blogspot.com/2009/02/readonly-listbox.html
public class ReadOnlyListBox : ListBox
{
private bool _readOnly = false;
public bool ReadOnly
{
get { return _readOnly; }
set { _readOnly = value; }
}
protected override void DefWndProc(ref Message m)
{
// If ReadOnly is set to true, then block any messages
// to the selection area from the mouse or keyboard.
// Let all other messages pass through to the
// Windows default implementation of DefWndProc.
if (!_readOnly || ((m.Msg <= 0x0200 || m.Msg >= 0x020E)
&& (m.Msg <= 0x0100 || m.Msg >= 0x0109)
&& m.Msg != 0x2111
&& m.Msg != 0x87))
{
base.DefWndProc(ref m);
}
}
}
I know this is old thread, but i'll post a workaround for other readers in future.
listBox.Enabled = false;
listBox.BackColor = Color.LightGray;
This will change background color of list box to Light Gray. So this is not builtin "native way" to do it, but at least gives user some feedback that he is not supposed to / can't edit that field.
To get read-only behaviour I have MyCBLLocked, a boolean associated with the MyCBL checkbox list control, and on the CheckItem event I do:
private void MyCBL_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (MyCBLLocked)
e.NewValue = e.CurrentValue;
}
So instead of
MyCBL.Enabled = false;
I use
MyCBLLocked = true;
and the user can scroll through the many selections but not mess things up with changes.

Automatic Scrolling in a Silverlight List Box

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

Resources