We use cookies to give you the best experience on our website. If you continue to browse, then you agree to our privacy policy and cookie policy. Image for the cookie policy date
close icon

Re-arranging rows of datagrid while another thread modified the underlying collection

Hello,

The use case is that the user starts to drag a row in the datagrid (to sort it) with the mouse, and before the user drops the row - the underlying collection had an update with rows removal or addition. This produces the following exception:

An unhandled exception of type 'System.ArgumentOutOfRangeException' occurred in PresentationCore.dll
Index was out of range. Must be non-negative and less than the size of the collection.

Could you please advise is there a way to suspend any updates or throttle updates until the grid row re-order is processed, or a way to make this serial to the data update with some blocking. 

Thank you in advance, Alex

7 Replies

SS Susmitha Sundar Syncfusion Team August 5, 2019 05:06 PM UTC

Hi Alexandre, 
 
Thank you for using Syncfusion controls. 
 
Based on the provided information, we have checked the mentioned issue “System.ArgumentOutOfRangeException throws while drag and drop in DataGrid” and we unable to replicate the issue from end. We have prepared sample for the same, 
 
 
Please check the sample and let us know if you still facing the same issue? If not, please modify the sample based on your scenario. 
It will be helpful for us to check on it and provide you the solution at the earliest.   
 
Regards, 
Susmitha S 



AL Alexandre August 6, 2019 01:13 PM UTC

I've attached the modified project source code.

Main changes are as follows:

- using reactive observables streams through DynamicData nugget
- sfdatagrid is still bound to EmployeeDetails, a dynamically generated ReadOnlyObservableCollection

            //dynamicdata binding
            _SubscriptionToModelChanges = _readonlyEmployeeDetailsCacheChangeSet
                .Sort(SortExpressionComparer.Ascending(i => i.EmployeeId)) //some operation
                .ObserveOnDispatcher() //make sure binding is on GUI thread
                .Bind(out _EmployeeDetails) //bind to ReadOnlyObservableCollection bound to UI
                //.DisposeMany() //necessary if Model is disposable
                .Subscribe();

- updates moved from the dispatcher timer to an observable stream inside the view model

            //actual updates timer
            _SubscriptionToUpdatesPollingTimer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(5))
                        .SubscribeOn(NewThreadScheduler.Default)
                        .ObserveOn(NewThreadScheduler.Default)  // default test case
                        //.ObserveOnDispatcher()   // <--------------------has no effect if the subscribe() is executed on UI thread
                        .Subscribe(_ =>
                        {
                            //executes every five seconds - fetch 20 items with max delay before update cancelled of 3 seconds
                            var updates = _RemoteService.FetchRemoteSimple(20)
                                //.ObserveOn(NewThreadScheduler.Default)  //this can be observed on a separate thread or
                                //.ObserveOnDispatcher()                  // on GUI thread (if the parent observable is observed on dispatcher too
                                // the simpest case is no new thread
                                .Subscribe( newSnapshot =>
                                {
                                    lock (_EmployeeDetailsCache)
                                    {
                                        var expiredItems = _EmployeeDetailsCache.Items.Where(old => !newSnapshot.Any(upd => upd.EmployeeId==old.EmployeeId));
                                        _EmployeeDetailsCache.Edit(f =>
                                        {
                                            //remove expired and update items
                                            f.Remove(expiredItems);
                                            f.AddOrUpdate(newSnapshot);
                                        });

                                        /*
                                        //tried the following one by one and all together
                                        //1)
                                        _EmployeeDetailsCache.Refresh();
                                        //2)
                                        Application.Current.Dispatcher.BeginInvoke(
                                            DispatcherPriority.Render,
                                                new Action(() =>
                                                {
                                                    OnPropertyChanged("EmployeeDetails");
                                                })
                                            );
                                        */
                                       }
                                });

                        });

which works with the underlying data source.

To see the exception, start dragging the row and dont let go (dont drop) until the grid has a row added or removed. 

Running updates on dispatcher, including raising onpropertychanged event for the bound readonlycollection does not resolve this problem.

Thank you very much for looking into this issue.

Kind regards, AM




PS: This is the stack trace to show that the error happens in syncfusion and not in the dynamicdata:

> mscorlib.dll!System.ThrowHelper.ThrowArgumentOutOfRangeException(System.ExceptionArgument argument, System.ExceptionResource resource) Line 93 C#
  mscorlib.dll!System.Collections.Generic.List.this[int].get(int index) Line 178 C#
  Syncfusion.Data.WPF.dll!Syncfusion.Data.RecordsListBase.GetRecord(int index) Unknown
  Syncfusion.Data.WPF.dll!Syncfusion.Data.RecordsListBase.this[int].get(int index) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridRowDragDropController.DroppingItemsInDataGrid(Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex rowColumnIndex, Syncfusion.UI.Xaml.Grid.SfDataGrid sourceDataGrid, System.Windows.DragEventArgs args) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridRowDragDropController.ProcessOnDrop(System.Windows.DragEventArgs args, Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex rowColumnIndex) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridCell.GridCell_Drop(object sender, System.Windows.DragEventArgs e) Unknown
  PresentationCore.dll!System.Windows.DragEventArgs.InvokeEventHandler(System.Delegate genericHandler, object genericTarget) Unknown
  PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target) Unknown
  PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs) Unknown
  PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source, System.Windows.RoutedEventArgs args, bool reRaised) Unknown
  PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender, System.Windows.RoutedEventArgs args) Unknown
  PresentationCore.dll!System.Windows.UIElement.RaiseEvent(System.Windows.RoutedEventArgs e) Unknown
  PresentationCore.dll!System.Windows.OleDropTarget.RaiseDragEvent(System.Windows.RoutedEvent dragEvent, int dragDropKeyStates, ref int effects, System.Windows.DependencyObject target, System.Windows.Point targetPoint) Unknown
  PresentationCore.dll!System.Windows.OleDropTarget.MS.Win32.UnsafeNativeMethods.IOleDropTarget.OleDrop(object data, int dragDropKeyStates, long point, ref int effects) Unknown
  [Native to Managed Transition] 
  [Managed to Native Transition] 
...





AL Alexandre August 6, 2019 03:58 PM UTC

I tried a quick "fix" - to fire an observable within the ViewModel just before handling and binding the update. 

Meanwhile, in the view, mouse grid row ondrag event handler starts watching for update signal and mouse grid row drop is cancelled if update signalled inbetween.

This works if the drag and drop is within the grid and not into another control or system window (which throws OLE exception).

If the drop happens off the application, the mouse grid OnDrop event handler is never reached and various exceptions happen. 

Would appreciate your feedback how to resolve this issue in the most portable way (WPF and Xamarin Forms syncfusion controls).

          //the ViewModel has this Subject as a signal mechanism
          public Subject updatesObservable;

           //actual updates timer
            _SubscriptionToUpdatesPollingTimer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(5))
                        .SubscribeOn(NewThreadScheduler.Default)
                        .ObserveOn(NewThreadScheduler.Default)  // default test case
                        .Subscribe(_ =>
                        {
                            var updates = _RemoteService.FetchRemoteSimple(20)
                                .Do(i =>
                                {
                                    //signal pending update
                                    updatesObservable.OnNext(true);
                                })
                                .Subscribe( newSnapshot =>
                                {

                                    var expiredItems = _EmployeeDetailsCache.Items.Where(old => !newSnapshot.Any(upd => upd.EmployeeId==old.EmployeeId));
                                        _EmployeeDetailsCache.Edit(f =>
                                        {
                                            //remove expired and update items
                                            f.Remove(expiredItems);
                                            f.AddOrUpdate(newSnapshot);
                                        });
                                });
                        });

and within view

            ViewModel vm = new ViewModel();
                ...
            readonly object _lock = new object();
            bool updateSignaled = false;
            IDisposable updateStreamSubscription;
               ...
            this.sfdatagrid.RowDragDropController.DragStart += RowDragDropController_DragStart;
            this.sfdatagrid.RowDragDropController.Drop += RowDragDropController_Drop;
            this.sfdatagrid.RowDragDropController.DragOver += RowDragDropController_DragOver;
          ...

        //subscribe to the observable when mouse drag starts
        private void RowDragDropController_DragStart(object sender, GridRowDragStartEventArgs e)
        {
            updateStreamSubscription = vm.updatesObservable
                .Subscribe(flag =>
                    {
                        lock(_lock)
                        {
                            updateSignaled = true;
                        }
                    }
                );
        }

        //disable drop if update was signaled
        private void RowDragDropController_Drop(object sender, GridRowDropEventArgs e)
        {
            lock (_lock)
            {
                if (updateSignaled)
                {
                    e.Handled = true;
                    updateSignaled = false;
                }
                updateStreamSubscription?.Dispose();
            }
        }

Call stack on drag&drop outside the application window after update notification received:

System.Runtime.Serialization.SerializationException: 'Type 'SfDataGridDemo.Models.Model' in Assembly … is not marked as serializable
mscorlib.dll!System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers()
mscorlib.dll!System.Collections.Concurrent.ConcurrentDictionary<>.GetOrAdd()
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
PresentationCore.dll!System.Windows.DataObject.System.Runtime.InteropServices.ComTypes.IDataObject.GetData()




AL Alexandre August 6, 2019 04:43 PM UTC

Alternative I tried was to expose dynamic data binding through simple IObservableCollection<Model>  

The View datagrid has binding to the ViewModel EmployeeDetails. However, EmployeeDetails is now IObservableCollection.

Changes as follows:

        //underlying datasource stays the same
        private readonly SourceCache<Model, int> _EmployeeDetailsCache = new SourceCache<Model, int>(r => r.EmployeeId);

        //datagrid now bound to this
        public IObservableCollection<Model> EmployeeDetails { get; } = new ObservableCollectionExtended<Model>();

and in the ViewModel constructor:

            //dynamicdata binding
            _SubscriptionToModelChanges = _EmployeeDetailsCache
                    .Connect()
                    .Sort(SortExpressionComparer<Model>.Ascending(i => i.EmployeeId)) //some operation
                    .ObserveOnDispatcher()
                    .Bind(EmployeeDetails)
                    //.DisposeMany() //necessary if Model is disposable
                    .Subscribe();

While the timer thread stays the same:

            //actual updates timer
            _SubscriptionToUpdatesPollingTimer = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(5))
                        .SubscribeOn(NewThreadScheduler.Default)
                        .ObserveOn(NewThreadScheduler.Default)  // default test case
                        .Subscribe(_ =>
                        {
                            var updates = _RemoteService.FetchRemoteSimple(20)
                                  .Subscribe(newSnapshot =>
                                  {
                                      var expiredItems = _EmployeeDetailsCache.Items.Where(old => !newSnapshot.Any(upd => upd.EmployeeId == old.EmployeeId));
                                      _EmployeeDetailsCache.Edit(f =>
                                      {
                                          //remove expired and update items
                                          f.Remove(expiredItems);
                                          f.AddOrUpdate(newSnapshot);
                                      });
                                  });
                        });

Call Stack Trace:

> mscorlib.dll!System.ThrowHelper.ThrowArgumentOutOfRangeException(System.ExceptionArgument argument, System.ExceptionResource resource) Line 93 C#
  mscorlib.dll!System.Collections.Generic.List<System.__Canon>.this[int].get(int index) Line 178 C#
  Syncfusion.Data.WPF.dll!Syncfusion.Data.RecordsListBase.GetRecord(int index) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridRowDragDropController.DroppingItemsInDataGrid(Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex rowColumnIndex, Syncfusion.UI.Xaml.Grid.SfDataGrid sourceDataGrid, System.Windows.DragEventArgs args) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridRowDragDropController.ProcessOnDrop(System.Windows.DragEventArgs args, Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex rowColumnIndex) Unknown
  PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target) Unknown
  PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs) Unknown
  PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source, System.Windows.RoutedEventArgs args, bool reRaised) Unknown
  PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender, System.Windows.RoutedEventArgs args) Unknown
  PresentationCore.dll!System.Windows.OleDropTarget.RaiseDragEvent(System.Windows.RoutedEvent dragEvent, int dragDropKeyStates, ref int effects, System.Windows.DependencyObject target, System.Windows.Point targetPoint) Unknown
  PresentationCore.dll!System.Windows.OleDropTarget.MS.Win32.UnsafeNativeMethods.IOleDropTarget.OleDrop(object data, int dragDropKeyStates, long point, ref int effects) Unknown
  [Native to Managed Transition] 
  [Managed to Native Transition] 
  PresentationCore.dll!System.Windows.OleServicesContext.OleDoDragDrop(System.Runtime.InteropServices.ComTypes.IDataObject dataObject, MS.Win32.UnsafeNativeMethods.IOleDropSource dropSource, int allowedEffects, int[] finalEffect) Unknown
  PresentationCore.dll!System.Windows.DragDrop.OleDoDragDrop(System.Windows.DependencyObject dragSource, System.Windows.DataObject dataObject, System.Windows.DragDropEffects allowedEffects) Unknown
  PresentationCore.dll!System.Windows.DragDrop.DoDragDrop(System.Windows.DependencyObject dragSource, object data, System.Windows.DragDropEffects allowedEffects) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridRowDragDropController.ProcessOnDragStarting(System.Windows.Input.MouseEventArgs args, Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex rowColumnIndex) Unknown
  Syncfusion.SfGrid.WPF.dll!Syncfusion.UI.Xaml.Grid.GridCell.GridCell_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) Unknown
  PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target) Unknown
  PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs) Unknown
  PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source, System.Windows.RoutedEventArgs args, bool reRaised) Unknown
  PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender, System.Windows.RoutedEventArgs args) Unknown
  PresentationCore.dll!System.Windows.UIElement.RaiseTrustedEvent(System.Windows.RoutedEventArgs args) Unknown
  PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea() Unknown
  PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input) Unknown
  PresentationCore.dll!System.Windows.Input.InputProviderSite.ReportInput(System.Windows.Input.InputReport inputReport) Unknown
  PresentationCore.dll!System.Windows.Interop.HwndMouseInputProvider.ReportInput(System.IntPtr hwnd, System.Windows.Input.InputMode mode, int timestamp, System.Windows.Input.RawMouseActions actions, int x, int y, int wheel) Unknown
  PresentationCore.dll!System.Windows.Interop.HwndMouseInputProvider.FilterMessage(System.IntPtr hwnd, MS.Internal.Interop.WindowMessage msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) Unknown
  PresentationCore.dll!System.Windows.Interop.HwndSource.InputFilterMessage(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) Unknown
  WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) Unknown

This version of source code attached too.





Attachment: WPF_SfDataGridDemo_AMA2_95626947.zip


FP Farjana Parveen Ayubb Syncfusion Team August 7, 2019 01:00 PM UTC

Hi Alexandre, 
  
Thank you for your update. 
  
We could able to reproduce your reported issue from our end. You are added and removed the records from an underlying collection of DataGrid while drag and drop. Exception throws only if an underlying collection does not contain that dragging record. So Can you please confirm whether you need to add that record as a new record or just handle that exception? 
  
Please confirm on this, it will be helpful for us to provide you solution at earliest. 
 
Regards, 
Farjana Parveen A  



AL Alexandre August 7, 2019 02:08 PM UTC

Hello!

>Exception throws only if an underlying collection does not contain that dragging record.

This is what my example illustrates. I can not confirm the exception is only raised if the item was removed from bound collection while the grid is doing drag and drop. 

I can not exclude a more general case of drag and drop failing while collection is updated. 

Are you confirming that the bug is limited only to the use case of removed source record?


>Can you please confirm whether you need to add that record as a new record or just handle that exception?

It is my understanding that sfdatagrid caches and maintains it's own copy of the data source bound to the control for all things like filtering, ordering etc.. 

I do not understand what logic, if any would, requires the cached record "to be added back". 

The data source update clearly invalidates any cached copy sfdatagrid maintains. 

What would be the use case not to invalidate any GUI control operations and calculations in progress - such as totals/sums/groups and drag-and-drop?


In terms of my specific use case (always reflect the latest data in the grid, and disallow layout changes to interfere with updates), I can identify 2 potential solutions:


Solution 1

- The SfDataGrid drag and drop operation should be cancelled if the control detects underlying collection update, while in drag and drop, unless it can handle the resulting updates conflict


- Conflict cases are the drag-drop target OR source records had been removed from the bound data source. 

- Drag-and-drop is allowed to be happen if the conflict can be resolved and after the drag and drop the grid reflects the latest changes in the data source. 

- Otherwise the drag and drop should be cancelled. Drag and drop cancellation should be reflected visually by the "dragging" popup disappearing - ie as if Drop was denied or Dragging cancelled


Solution 2

- Suspend the SfGridData update (including the cached copy it uses behind) while the grid layout is changing. Resume data source bind after grid layout changed.


Summary

Whichever solution you can help me and other users with, please make it portable across SfDataGrid implementations across all supported platforms. 

The solution should be universal across all SfDataGrid implementations - including WPF and Xamarin Forms/UWP, and should rely on a mechanism (ie. events or properties/methods) exposed across these targets



As a side note, I have tried to implement 1) and could not do it with my knowledge of the SfDataGrid. 

Implementing 2) is easier - the work-around is to disallow any drag-and-drop by default on the grid. Allow the user to toggle into the "Layout" mode which re-binds the SfDataGrid to the last good copy of the update stream and allows the drag-and-drop. The user then can toggle back into "Live" mode where the control is re-binded to the dynamic data source.



ViewModel changes:

private IObservableCollection<Model> _EmployeeDetailsSnapshot;
public IObservableCollection<Model> EmployeeDetailsSnapshot { get { return _EmployeeDetailsSnapshot; } }

        public void TakeSnapshot()
        {
            lock(_locker)
            {
                _EmployeeDetailsSnapshot = new ObservableCollectionExtended<Model>(EmployeeDetails.ToList());
            }
        }

View Changes:
            <ToggleButton  x:Name="tb" Content="Switch To Layout Mode" Width="150" Height="50"  Grid.Row="1"
                    Checked ="ChangeLayout_Mode"
                    Unchecked = "Live_Mode"/>

        private void ChangeLayout_Mode(object sender, RoutedEventArgs e)
        {
            vm.TakeSnapshot();
            sfdatagrid.ItemsSource = vm.EmployeeDetailsSnapshot;
            sfdatagrid.AllowDraggingRows = true;
            sfdatagrid.AllowDrop = true;
            tb.Content = "Switch to Live Update";
        }
        private void Live_Mode(object sender, RoutedEventArgs e)
        {
            sfdatagrid.AllowDraggingRows = false;
            sfdatagrid.AllowDrop = false;
            tb.Content = "Switch To Layout Mode";
            sfdatagrid.ItemsSource = vm.EmployeeDetails;
        }

I attach the updated source code.



  


Attachment: WPF_SfDataGridDemo_AMA3_3cf952e2.zip


FP Farjana Parveen Ayubb Syncfusion Team August 8, 2019 12:03 PM UTC

Hi Alexandre, 
 
Thank you for your update. 
 
Please find the details for your queries, 
 
Query1: It is my understanding that sfdatagrid caches and maintains it's own copy of the data source bound to the control for all things like filtering, ordering etc 
 
Based on our architecture, we did not maintained any copy of our bounded collection of DataGrid. DataGrid UI updates instantly while performing Grouping, Sorting etc. 
 
Query2: What would be the use case not to invalidate any GUI control operations and calculations in progress - such as totals/sums/groups and drag-and-drop? 
 
You can use the following code for Suspend and resume the DataGrid UI. BeginInit() method suspend the all updates including drag and drop. When calling the EndInit(), all the actions updates on DataGrid UI. 
 
C#: 
//Suspend the Grid UI 
this.sfdatagrid.View.BeginInit(); 
 
//Resume the Grid UI 
this.sfdatagrid.View.EndInit(); 
 
Query 3: The SfDataGrid drag and drop operation should be cancelled if the control detects underlying collection update, while in drag and drop, unless it can handle the resulting updates conflict 
- Conflict cases are the drag-drop target OR source records had been removed from the bound data source 
 
We have logged issue report for this query. We will fix this issue on our Volume 3, 2019 release which is expected on end of the September. Can you please confirm whether you will wait for our official release or need patch for this issue? 
 
You can track the status of this report through the following feedback link,   
 
Note: The provided feedback link is private and you need to login to view this feedback. 
 
Query 4: Suspend the SfDataGrid update (including the cached copy it uses behind) while the grid layout is changing. Resume data source bind after grid layout changed. 

We recommend to use your own work around (Layout Mode and Live mode) for this query. 
 
Please contact us for further assistance. 
 
Regards, 
Farjana Parveen A  


Loader.
Live Chat Icon For mobile
Up arrow icon