Reset scrollbar on ItemsSource change - wpf

Whenever I change the ItemsSource (or it fires a collection reset), WPF doesn't reset the scrollbar position of ListView/DataGrid to the top. This is no big deal if there is a nice way of doing this manually, but it seems there isn't.
Currently I do:
ScrollViewer.ScrollToTop();
ScrollViewer.UpdateLayout();
Grid.ItemsSource = Data;
Which is causing some flicker because of UpdateLayout repainting the control first.
The other way is to do:
Grid.ItemsSource = Data;
ScrollViewer.ScrollToTop();
This doesn't seem to cause any flicker, but because the scroll position is changed AFTER ItemsSource is set, it causes WPF to first iterate over the wrong items in the ItemsSource, before it gets the right items (with offset 0). Because the collection is virtualized this causes needless SQL queries for items that will never be displayed.
Is there another way of doing this which doesn't flicker or retrieves the wrong items first?

Just attached this event on the control that will hold the collection:
Assuming that it is a listbox:
lstItems.SourceUpdated += new EventHandler<DataTransferEventArgs>(lstItems_SourceUpdated);
Then you will have a event that looks like this.
void lstItems_SourceUpdated(object sender, DataTransferEventArgs e)
{
ScrollViewerName.ScrollToTop();
}

Related

Checkbox in silverlight DataGrid behaving strangely

I am using checkbox in an itemtemplate column in a Silverlight 5 DataGrid.
I am facing a strange problem with it. When I select more than one checkbox and then scroll the grid up and down, the selection shifts to some other checkbox.
I fixed this problem in my code. I did handling within the LoadingRow and UnloadingRow events of the grid.
As soon as a row is loaded, we need to look for the condition on the basis of which we want to keep the check-box checked or unchecked. But as soon as you set the IsChecked property, Checked or UnChecked event of the check-box will get fired.
In this scenario we can unregister the Checked and UnChecked events of the check-box if we have any, set the IsChecked property. After setting this, again register the events.
Below is the code for your help.
Add LoadingRow and UnloadingRow events to your grid.
... LoadingRow="DGUserList_RowLoadUnload" UnloadingRow="DGUserList_RowLoadUnload">
In your code behind file:
private void DGUserList_RowLoadUnload(object sender, DataGridRowEventArgs e)
{
DataGridRow row = e.Row;
CheckBox cbox = (CheckBox)this.dgUserList.Columns[0].GetCellContent(row);
this.UpdateHookedEventsForCheckBox(cbox, false);
cbox.IsChecked = true; // Here put your condition for check/uncheck
this.UpdateHookedEventsForCheckBox(cbox, true);
}
private void UpdateHookedEventsForCheckBox(CheckBox chkBox, bool register)
{
if (register)
{
chkBox.Checked += this.CheckBox_Checked;
chkBox.Unchecked += this.CheckBox_Unchecked;
}
else
{
chkBox.Checked -= this.CheckBox_Checked;
chkBox.Unchecked -= this.CheckBox_Unchecked;
}
}
This way I need not bother about putting some hake code in my Checked and UnChecked events.
This is a known behaviour since Silverlight is re-using its graphical resources in the DataGrid. There's a discussion about it in this Silverlight thread.
It seems one way to fix it is to databind the IsSelected property:
My solution at that time was to add a new property in my data source:
IsSelected, and to bind the checkbox to that value.
You have more additional info in this thread, where Microsoft answers:
This is not a bug. What happens when you scroll around in the
DataGrid is the same checkboxes are being used for new data because
the DataGrid recycles the visuals. When your new data has different
values, the check will change through the Binding and you'll receive
the event. What you can do to get this scenario to work is to listen
to LoadingRow which is raised when a row comes into view. In there,
you can call column.GetCellContents to get the contents of the cell.
This will give you the CheckBox, and you can attach to CheckChanged at
this time. If you do this, you need to do something similar and
listen to UnloadingRow so you can detach the eventhandler when the
checkbox is scrolled out of view.

Maintain scroll position on updating the ItemSource of a silverlight datagrid

I'm using a DataGrid in my silverlight application to display some data that's refreshed on a timer. My problem is that when this happens the vertical scrollbar in the grid resets to the top, whereas I want it to stay in the same position. Does anyone know how I can make this happen?
I've tried overriding the ItemsSource property on the grid to store the vertical scroll position and then reset it, but this only affects the scrollbar and doesn't force the correct rows to be displayed. Is there a way to force this behaviour?
Here is a similar question about Setting the scroll bar position on a ListBox
After rebinding Silverlight Listbox control how do you get it listbox to scroll to back to the top?
Since the DataGrid also supports a ScrollIntoView method, you should be able to use a similar technique such as
theDataGrid.ItemsSource = data;
theDataGrid.UpdateLayout();
theDataGrid.ScrollIntoView(theDataGrid.SelectedItem, theDataGrid.Columns[0]);
I couldn't find a decent answer last time I looked. I wanted to keep the current element selected in the grid but that wouldn't work on an ICollectionView refresh (I use MVVM and get automatic updates from the server).
ScrollIntoView() was not an option for me because the currently selected item may NOT be in view. Having the CurrentChanged event firing out of control was also quite a bother.
In the end, I used the Infragistics grid and it does just that out of the box. Problem solved for me.
You may have a look at the DevExpress free grid. I think it had the same nice behaviour (I tested it but I can't remember the outcome).
You could try setting the SelectedItem thro the UI thread, so that the UI can refresh itself,
like so
private void Button_Click(object sender, RoutedEventArgs e)
{
Person p = new Person() { Name="sss",Age=11}; //datagird's itemsSource is Collection<person>
people.Add(p);
dg.SelectedItem = p; //dg is my datagrid name
Dispatcher.BeginInvoke(() => { dg.SelectedItem = p; });
}
Im assuming that new rows are loaded thro the ViewModel, so thats why it makes sense to place the BeginInvoke there. Since the ViewModel operations run on a different thread, and just setting the SelectedItem on its own might not work, this has worked for someone else
I've also had issues with this. I solved it by remembering the item I want to scroll to, then re-binding the DataGrid. I handle the LayoutUpdated event in order to implement the desired functionality:
void MyDataGrid_LayoutUpdated(object sender, EventArgs e)
{
// Reference the data item in the list you want to scroll to.
object dataItem = yourDataItem;
// Make sure the item is not null and didn't already scroll to the item.
if (dataItem != null && this.dataItemScrolledTo != dataItem)
{
// Remember the item scrolled to.
this.dataItemScrolledTo = dataItem;
// Scroll datagrid to the desired item.
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
}
}
I've modified CodeMaster's solution so that you don't need a class level variable. Put this code in the method that updates the ItemsSource. It will dynamically create the eventhandler, attach it, then detach it.
EventHandler MyDataGrid_LayoutUpdated = null;
MyDataGrid_LayoutUpdated = (s, e) =>
{
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
MyDataGrid.LayoutUpdated -= MyDataGrid_LayoutUpdated;
};
MyDataGrid.LayoutUpdated += MyDataGrid_LayoutUpdated;

WPF DataGrid how to get when ItemsSource updates

Which event fires when DataGrid's source is updating? I've tried DataContextChanged and SourceUpdated but it never worked out.
Actually I need a simple thing. I want, if there is a new row comes, scroll the GridView's scrollbar down to the bottom to see what it was.
I had the same problem and I manage it this way
DataGrid myGrid = new DataGrid();
CollectionView myCollectionView = (CollectionView)CollectionViewSource.GetDefaultView(myGrid.Items);
((INotifyCollectionChanged)myCollectionView).CollectionChanged += new NotifyCollectionChangedEventHandler(DataGrid_CollectionChanged);
You then need to implement the logic in the event handler DataGrid_CollectionChanged.
Set NotifyOnTargetUpdated = true for the ItemsSource binding and handle TargetUpdated event. If you've multiple bindings, then look for DataTransferEventArgs Property to find out if the target is ItemsSource or not.
If you are trying to have the grid refresh when something is added to the database itself, that's not going to happen. I'm more familiar with WinForms than WPF but I'm assuming there is no magical way to keep a grid in sync with the database without writing some background process that continuously checks for database changes.
If you are updating the actual data source of the grid (ex. Collection) then that will update the grid.
For my part i've used SelectionChange notification which raise each event Del/Add/Edit/Select
It's work very well
private void dataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Console.WriteLine("hi");
}

What's the best way to auto-scroll a list view to the last added item?

I use a ListView to show a list of errors as they occur in my application. It behaves and looks exactly like the Error List in Visual Studio. I want to add auto-scrolling when the last error item is selected (like how Visual Studio's Log Window auto-scrolls when you place the caret at the end).
The list of errors is in an ObservableCollection, which is passed to the ListView.ItemsSource like this:
public ObservableCollection<ErrorListItem> Items;
...
MyListView.ItemsSource = _Items;
I tried performing the auto-scroll in the _Items_CollectionChanged event handler, but because this is the event on the ItemsSource and not on the actual ListViewItems, it's a pain to figure out if the last item is selected, select the new row, etc. It's especially hard since it seems the ListViewItems are not created instantly. I managed to make it auto-scroll by delaying the call to set the last item selected like this:
void _Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// determine the last item to select from 'e'
...
_ItemPendingToBeScrolled = newItemToSelect;
ListView.SelectedItem = newItemToSelect;
Dispatcher.BeginInvoke(DispatcherPriority.Background,
(ThreadStart)delegate
{
if (_ItemPendingToBeScrolled != null)
{
ListView.ScrollIntoView(_ItemPendingToBeScrolled);
ItemPendingToBeScrolled = null;
}
})
}
But that's obviously not the right way to do it. Also, I want things to keep working if the list is filtered (not checking the last item in my source, but the last ListViewItem in the ListView).
Is there a way to listen to events when a ListViewItem gets added to the ListView following an addition to the bound collection? That would be the ideal event to capture in order to properly do my auto-scrolling. Or is there another technique I could use?
I have a lot of issues with listboxes/listviews and their scrolling, however, you mentioned hooking to the listview's changed event, is it because you can't listen to the observable collection's CollectionChanged event? ObservableCollection is way more stable than List controls, and you'll get the same notifications.
You can also bubble these events up if it's not working in the UI and you don't have access, this way you treat your scrolling in the UI without having access to the actual collection, just keep a reference to the Selected Item in your custom EventArgs class

Scrolling a listbox with page up/down

I had an ItemsControl, which you could use page up/down buttons to scroll expectedly. I switched it with a ListBox, to apply (thru triggers) a new DataTemplate when an item is selected.
Everything works fine, until using pageup or pagedown buttons. Instead of scrolling one page, it scrolls till the end or the beginning.
My previous attemps:
Not changing the item height when DataTemplate changes
Removed IsSelected=True trigger completely
All SelectionMode values
Do I miss something trivial?
private void RaiseKeyDownUpEventsOnEntitiesBox(Key key)
{
KeyEventArgs keyEventArgs = new KeyEventArgs(
InputManager.Current.PrimaryKeyboardDevice,
Keyboard.PrimaryDevice.ActiveSource,
System.Environment.ProcessorCount, key);
keyEventArgs.RoutedEvent = UIElement.KeyDownEvent;
entitiesBox.RaiseEvent(keyEventArgs);
keyEventArgs.RoutedEvent = UIElement.KeyUpEvent;
entitiesBox.RaiseEvent(keyEventArgs);
}
Page Down
RaiseKeyDownUpEventsOnEntitiesBox(Key.Next);
Page Up
RaiseKeyDownUpEventsOnEntitiesBox(Key.Prior);
Indeed, I missed something trivial. I forgot to remove the ScrollViewer outside the ItemsControl. I guess, this creates a confusion, since ListBox has its own ScrollViewer.
This raised another question. Previous ScrollViewer were automaticcally scrolled down from code behind. Now, that I can't reach the ListBox's ScrollViewer, I can't call its LineDown method. Here is my workaround:
// Responses is the ItemsSource
Responses.Add(e);
// xResponses is the ListBox
var item = xResponses.ItemContainerGenerator.ContainerFromIndex(0);
ScrollBar.LineDownCommand.Execute(null, item as IInputElement);
In the beginning, item might evaluate to null, however this does not create a problem. After adding a few items, luckily before we need to scroll, a container is returned successfully. Note that index is not important here, all we need is an IInputElement inside the ScrollViewer.

Resources