Maintain scroll position on updating the ItemSource of a silverlight datagrid - silverlight

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;

Related

DataGrid + ContextMenu: How to get the row under the right click

In Silverlight 5, I have a DataGrid with a ContextMenuService.ContextMenu. If you click a row to select, then right click, you just check the grid.selecteditem for context. However, if you right click a row without selecting it, you don't have that row's context when the menu opens. How do you get the DataContext row of the grid that was right clicked on when the context menu opens? The right click on the grid seems to be an option, but it is intercepted for the contextmenu and does not fire unless a contextmenu is already open/in focus.
I have found tons of examples of getting around the original issue with Silverlight 4 and detecting the rown on right click. However, the contextmenu now intercepts the rigth click of the grid, so those no logner work. I also found posts on 'bugs' with the initial relase of the ContextMenu. All these posts/blogs are making it hard to find a current answer or solution.
yeah it seems like Silverlight 5 has changed something that breaks the old tricks.
We've been doing this: add row enters on row load handler. EG:
private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.MouseEnter += new MouseEventHandler(Row_MouseEnter);
e.Row.MouseLeave += new MouseEventHandler(Row_MouseLeave);
}
void Row_MouseEnter(object sender, MouseEventArgs e)
{
DataGridRow dgr = sender as DataGridRow;
IncidentGrid.SelectedItem = dgr.DataContext;
}
pretty ugly i know, but it's working.
Besides Roger's concerns of accuracy, I didn't want to wire up those events on every row because my grid is quite large, and I have to keep it lean.
I found a solution here...
https://mutelight.org/silverlight-datagrid-make-right-click-select-a-row
Apply just one event handler to the grid's MouseRightButtonDown event. The event args has the mouse position and you can use VisualTreeHelper to find which DataGridRow the mouse is over. Then that row's DataContext will have your SelectedItem.

Reset scrollbar on ItemsSource change

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

SL4: need to register for a move (or redraw) event on an Item in an ItemsControl

Not finding a move event or redraw event in the FrameworkElement class. And Google not helping either. So...
I have a custom ItemsControl populated by an observable collection in the VM. The ItemsControl itself leverages the
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True"/>
</i:Interaction.Behaviors>
behavior so the user can drag around the whole assembly.
When the user moves the assembly, I want to be notified by each item as the item is repositioned as a result of the assembly moving. So far I have tried registering for
this.myItem.LayoutUpdated += this.OnSomethingNeedsToUpdate;
but it doesn't seem to fire as I drag the assembly around.
Also
this.myItem.MouseMove += this.OnSomethingNeedsToUpdate;
only works if I mouse into the item which is not good enough. Because I am moving the ItemsControl and then have to go mouse into the item to get the event to fire.
Any ideas? Can I look to some ancestor in the visual tree for help in the form of a OneOfMyDecendantsWasRedrawn event or similar? Again I am trying to be notified when an item moves not be notified when the assembly moves.
I would say your best bet would be to add the MouseDragElementBehavior to your custom ItemsControl in code instead of in the Xaml. Here is how this might look (using a Grid since that is easier to demo):
public class DraggableGrid : Grid
{
public DraggableGrid()
{
Loaded += new RoutedEventHandler(DraggableGrid_Loaded);
}
void DraggableGrid_Loaded(object sender, RoutedEventArgs e)
{
MouseDragElementBehavior dragable = new MouseDragElementBehavior();
Interaction.GetBehaviors(this).Add(dragable);
dragable.Dragging += new MouseEventHandler(dragable_Dragging);
}
void dragable_Dragging(object sender, MouseEventArgs e)
{
// Custom Code Here
}
}
In the section that says Custom Code Here you would loop through you Items and notify them that they are being dragged.
I ended up writting another behavior for the individual items I care about and then wrote a LINQ query to search up the visual tree looking for ancestors with the MouseDragElementBehavior attached to them. That query found the ItemsControl since it was an eventual parent of the Item. I was then able to register for the Dragging event as desried.
Thanks again to Bryant for providing the solution over here.

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

Resources