none
DataGrid exception on validation failure (DeferRefresh is not allowed...)

    Question

  • In the same series as the DataGrid validation scheme issues , I get an exception when

    • a cell is invalid
    • the DataContext changes

    This happens in a DataGrid, which is bound to a CollectionViewSource. This CVS itself has a DataContext bound to the currently selected product of a ListBox. So we are editing the details of product, those details include a data list in a DataGrid (DataGridTextColumn in this case).

    Either by using a data type that implements IDataErrorInfo or by using a ValidationRule, if the validation fails the DataGrid reacts by keeping the TextBox and showing a red box around it (when the validation is OK, the cell is tranformed back to a TextBlock).

    Nothing prevents me from selecting another product. The DataGrid then shows the new data and everything is fine. But if I come back to the product which failed validation, I get this exception:

    InvalidOperationException, 'DeferRefresh' is not allowed during an AddNew or EditItem transaction .

    How can I prevent that? What is the correct way of cancelling the edit on that faulty cell when another product is selected, or when the DataContext changes?

    I can detect those events, but CancelEdit() doesn't work on the DataGrid (no result).

    I'm using .NET 4.

     

    The code can be viewed here: C# part , XAML part . The code can be downloaded from here .

    Sunday, August 29, 2010 8:21 PM

All replies

  • Problem reported to MS feedback (for anyone interested).
    Wednesday, September 01, 2010 9:19 AM
  • I'm using the WPF Toolkit's DataGrid in .NET 3.5SP1. I experienced the same issue, and solved the problem by canceling the pending changes on the DataGrid, just before those exceptions may occur, that is:
    - when the DataGrid loses focus (actually, keyboard focus), or
    - when its DataContext changes

    So I built an attached behaviour ("DataGridRollbackOnUnfocusedBehaviour") that simply does this job.

    It can be used easily in XAML in this way:

     

     

    <toolkit:DataGrid
    	local:DataGridRollbackOnUnfocusedBehaviour.DataGridRollbackOnUnfocused="True" />

     

    and the implementation of the behaviour is the following.

     

    public class DataGridRollbackOnUnfocusedBehaviour
    	{
    		#region DataGridRollbackOnUnfocusedBehaviour
    
    		public static bool GetDataGridRollbackOnUnfocused(DataGrid datagrid)
    		{
    			return (bool)datagrid.GetValue(DataGridRollbackOnUnfocusedProperty);
    		}
    
    		public static void SetDataGridRollbackOnUnfocused(
    		 DataGrid datagrid, bool value)
    		{
    			datagrid.SetValue(DataGridRollbackOnUnfocusedProperty, value);
    		}
    
    		public static readonly DependencyProperty DataGridRollbackOnUnfocusedProperty =
    			DependencyProperty.RegisterAttached(
    			"DataGridRollbackOnUnfocused",
    			typeof(bool),
    			typeof(DataGridRollbackOnUnfocusedBehaviour),
    			new UIPropertyMetadata(false, OnDataGridRollbackOnUnfocusedChanged));
    
    		static void OnDataGridRollbackOnUnfocusedChanged(
    		 DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    		{
    			DataGrid datagrid = depObj as DataGrid;
    			if (datagrid == null)
    				return;
    
    			if (e.NewValue is bool == false)
    				return;
    
    			if ((bool)e.NewValue)
    			{
    				datagrid.LostKeyboardFocus += RollbackDataGridOnLostFocus;
    				datagrid.DataContextChanged += RollbackDataGridOnDataContextChanged;
    			}
    			else
    			{
    				datagrid.LostKeyboardFocus -= RollbackDataGridOnLostFocus;
    				datagrid.DataContextChanged -= RollbackDataGridOnDataContextChanged;
    			}
    		}
    
    		static void RollbackDataGridOnLostFocus(object sender, KeyboardFocusChangedEventArgs e)
    		{
    			DataGrid senderDatagrid = sender as DataGrid;
    
    			if (senderDatagrid == null)
    				return;
    
    			UIElement focusedElement = Keyboard.FocusedElement as UIElement;
    			if (focusedElement == null)
    				return;
    
    			DataGrid focusedDatagrid = GetParentDatagrid(focusedElement); //let's see if the new focused element is inside a datagrid
    			if (focusedDatagrid == senderDatagrid) 
    			{
    				return;
    				//if the new focused element is inside the same datagrid, then we don't need to do anything;
    				//this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus, which passes to the selected DataGridCell child
    			}
    
    			//otherwise, the focus went outside the datagrid; in order to avoid exceptions like ("DeferRefresh' is not allowed during an AddNew or EditItem transaction")
    			//or ("CommitNew is not allowed for this view"), we undo the possible pending changes, if any
    			IEditableCollectionView collection = senderDatagrid.Items as IEditableCollectionView;
    
    			if (collection.IsEditingItem)
    			{
    				collection.CancelEdit();
    			}
    			else if (collection.IsAddingNew)
    			{
    				collection.CancelNew();
    			}
    		}
    
    		private static DataGrid GetParentDatagrid(UIElement element)
    		{
    			UIElement childElement; //element from which to start the tree navigation, looking for a Datagrid parent
    
    			if (element is ComboBoxItem) //since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
    			{
    				ItemsPresenter parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>((element as ComboBoxItem));
    				ComboBox combobox = parentItemsPresenter.TemplatedParent as ComboBox;
    				childElement = combobox;
    			}
    			else
    			{
    				childElement = element;
    			}
    
    			DataGrid parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement); //let's see if the new focused element is inside a datagrid
    			return parentDatagrid;
    		}
    
    		static void RollbackDataGridOnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    		{
    			DataGrid senderDatagrid = sender as DataGrid;
    
    			if (senderDatagrid == null)
    				return;
    
    			IEditableCollectionView collection = senderDatagrid.Items as IEditableCollectionView;
    
    			if (collection.IsEditingItem)
    			{
    				collection.CancelEdit();
    			}
    			else if (collection.IsAddingNew)
    			{
    				collection.CancelNew();
    			}
    		}
    
    		#endregion DataGridRollbackOnUnfocusedBehaviour
    	}

     

    It works like a charm for me, hope it helps for you too!

     

    • Proposed as answer by epanzavolta Wednesday, September 22, 2010 7:08 AM
    Wednesday, September 22, 2010 7:04 AM
  • https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception

    documents the limit of the proposed workaround : the model class whose rows are displayed in the DataGrid must implement 'IEditableObject' 

     

    • Edited by Rhodos Saturday, October 09, 2010 9:01 AM updateing answer completeness
    Wednesday, October 06, 2010 12:29 PM
  • VisualTreeFinder is apparently one of your own classes? There's no reference to it in MSDN other than one entry by a responder to one of Beth Massi's blogs.
    Saturday, October 09, 2010 5:04 PM
  • https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception

    documents the limit of the proposed workaround : the model class whose rows are displayed in the DataGrid must implement 'IEditableObject' 

     

    Hi, in the Microsoft Connect's page (https://connect.microsoft.com/VisualStudio/feedback/details/551128/wpf-datagrid-and-tabcontrol-deferrefresh-exception#) you say that:

    In my case, the IEditableObject constraint cannot be added to the view model class because of unwanted side effects on row and cell transactional commit/cancel aspects

    I don't get what you mean. As far as I know, implementing IEditableObject may allow you to get some transactional behaviour on editing, but only if you write specific code for it in the interface members. That is, a view-model, even if IEditableObject, can have no transactional semantics and push back the UI updates on a cell-by-cell basis. So the question is: do 1) a view-model implementing IEditableObject, with empty methods implementations, and 2) a view-model that does not implement that interface, behave differently one from the other, with regard to commit / cancel behaviour? In my opinion, the answer is NO. Did you test that the answer is yes instead?

    Monday, October 11, 2010 6:39 AM
  • VisualTreeFinder is apparently one of your own classes? There's no reference to it in MSDN other than one entry by a responder to one of Beth Massi's blogs.


    Ah, yes, it's a class of mine, but it doesn't do anything special, just go up the visual tree in order to find a parent of the specified class.

    class VisualTreeFinder
    	{
    
    		/// <summary>
    		/// Find a specific parent object type in the visual tree
    		/// </summary>
    		public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
    		{
    			DependencyObject dObj = VisualTreeHelper.GetParent(outerDepObj);
    			if (dObj == null)
    				return null;
    
    			if (dObj is T)
    				return dObj as T;
    
    			while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
    			{
    				if (dObj is T)
    					return dObj as T;
    			}
    
    			return null;
    		}
    
    	}
    

    • Proposed as answer by Brian_I Wednesday, January 05, 2011 6:23 PM
    Monday, October 11, 2010 6:43 AM
  • Thanks, This work fine.... Thanks you so much
    Thursday, October 28, 2010 4:25 PM
  • Wanted to thank epanzavolta - I spent a couple hours trying to figure out the problem/solution before running across this post.  I am using .NET 4.0, and the problem still exists in that version.  The solution epanzavolta posted worked for me -- my scenario:

    I am using a Datagrid as the detail in a master-detail view, and I am performing validation in that detail Datagrid.  If validation fails, then this error will occur if the user changes the master record and then returns (or closes the window and returns).  In fact, it appears that this error could occur anytime a user is performing validation in a datagrid that is used as the detail in a master-detail setup.

    The project I was working on was in VB, not C#.  So I converted epanzavolta's solution to a VB solution - hope this helps the VB programmers out there.

    Main Class:

    Public Class DataGridRollbackOnUnfocusedBehaviour
      Public Shared Function GetDataGridRollbackOnUnfocused(ByVal dg As DataGrid) As Boolean
        Return dg.GetValue(DataGridRollbackOnUnfocusedProperty)
      End Function
    
      Public Shared Sub SetDataGridRollbackOnUnfocused(ByVal dg As DataGrid, ByVal value As Boolean)
        dg.SetValue(DataGridRollbackOnUnfocusedProperty, value)
      End Sub
    
      Public Shared ReadOnly DataGridRollbackOnUnfocusedProperty As DependencyProperty = DependencyProperty.RegisterAttached("DataGridRollbackOnUnfocused", GetType(Boolean), GetType(DataGridRollbackOnUnfocusedBehaviour), New UIPropertyMetadata(False, AddressOf OnDataGridRollbackOnUnfocusedChanged))
    
      Shared Sub OnDataGridRollbackOnUnfocusedChanged(ByVal depObj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim dg = TryCast(depObj, DataGrid)
        If IsNothing(dg) Then
          Exit Sub
        End If
    
        If Not TypeOf e.NewValue Is Boolean Then
          Exit Sub
        End If
    
        If e.NewValue Then
          AddHandler dg.LostKeyboardFocus, AddressOf RollbackDataGridOnLostFocus
          AddHandler dg.DataContextChanged, AddressOf RollbackDataGridOnDataContextChanged
        Else
          RemoveHandler dg.LostKeyboardFocus, AddressOf RollbackDataGridOnLostFocus
          RemoveHandler dg.DataContextChanged, AddressOf RollbackDataGridOnDataContextChanged
        End If
      End Sub
    
      Shared Sub RollbackDataGridOnLostFocus(ByVal sender As Object, ByVal e As KeyboardFocusChangedEventArgs)
        Dim senderDatagrid = TryCast(sender, DataGrid)
        If IsNothing(senderDatagrid) Then
          Exit Sub
        End If
    
        Dim focusedElement = TryCast(Keyboard.FocusedElement, UIElement)
        If IsNothing(focusedElement) Then
          Exit Sub
        End If
    
        Dim focusedDatagrid = GetParentDatagrid(focusedElement)
        If focusedDatagrid Is senderDatagrid Then
          Exit Sub
        End If
    
        Dim collection = DirectCast(senderDatagrid.Items, IEditableCollectionView)
        If collection.IsEditingItem Then
          collection.CancelEdit()
        ElseIf collection.IsAddingNew Then
          collection.CancelNew()
        End If
      End Sub
    
      Private Shared Function GetParentDatagrid(ByVal element As UIElement) As DataGrid
        Dim childElement As UIElement
    
        If TypeOf element Is ComboBoxItem Then
          Dim parentItemsPresenter = VisualTreeFinder.FindParentControl(Of ItemsPresenter)(element)
          Dim cb = DirectCast(parentItemsPresenter.TemplatedParent, ComboBox)
          childElement = cb
        Else
          childElement = element
        End If
    
        Dim parentDatagrid = VisualTreeFinder.FindParentControl(Of DataGrid)(childElement)
        Return parentDatagrid
      End Function
    
      Shared Sub RollbackDataGridOnDataContextChanged(ByVal sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
        Dim senderDatagrid = TryCast(sender, DataGrid)
        If IsNothing(senderDatagrid) Then
          Exit Sub
        End If
    
        Dim collection = DirectCast(senderDatagrid.Items, IEditableCollectionView)
    
        If collection.IsEditingItem Then
          collection.CancelEdit()
        ElseIf collection.IsAddingNew Then
          collection.CancelNew()
        End If
      End Sub
    End Class
    
    

    Helper Class:

    Public Class VisualTreeFinder
      Public Shared Function FindParentControl(Of T As DependencyObject)(ByVal outerDepObj As DependencyObject) As T
        Dim dObj = VisualTreeHelper.GetParent(outerDepObj)
        If IsNothing(dObj) Then
          Return Nothing
        End If
    
        If TypeOf dObj Is T Then
          Return dObj
        End If
    
        While Not IsNothing(dObj)
          dObj = VisualTreeHelper.GetParent(dObj)
          If TypeOf dObj Is T Then
            Return dObj
          End If
        End While
    
        Return Nothing
      End Function
    End Class
    

    XAML:

    <DataGrid local:DataGridRollbackOnUnfocusedBehaviour.DataGridRollbackOnUnfocused="True">
    
    Wednesday, January 05, 2011 6:16 PM
  • Hi!

    I tried this solution, but I have a problem with it:

    After the call of

    collection.CancelNew();

    the row for adding new items is removed in my datagrid.

    Did anyone experience or even solve this problem?


    • Edited by Csaba84 Friday, April 29, 2011 2:52 PM English grammar
    Friday, April 29, 2011 10:03 AM
  • Mischiew,

    Recently I was working with WPF DataGrid and I got the following error during a Refresh tentative: 'Refresh' is not allowed during an AddNew or EditItem transaction.

    I have a typed List that is being used as ItemsSource of my DataGrid.

    This DataGrid is showing a product cart where user can select the quantity that he wants for each product. This is done by a numeric up and down logic.

    When user chooses 1 as quantity option and tries to decrease this quantity, something happens with the DataGrid. Because, if after this, user tries to increase the quantity, I’m receiving the error message above ('Refresh' is not allowed during an AddNew or EditItem transaction.).

    To me, this is happening because for some reason the DataGrid is entering in Edit mode during user UI experience. And my code is doing a Refresh (MyDataGrid.Items.Refresh()).

    So, to resolve this scenario, what I’m doing here is “Cancelling” the edit mode in case I got this exception. Like:

    try

    {               

        dgSale.Items.Refresh();

    }

    catch (Exception ex)

    {

        dgSale.CancelEdit(DataGridEditingUnit.Row);

        dgSale.Items.Refresh();

    }

     

    This is a workaround for the issue. I tried to reload the source, clear, begin edit, commit, etc… without success here. But this worked for me.

    I hope this can be useful.


    Creste
    Friday, May 06, 2011 8:57 PM

  • Hi,

    I know it's a little bit late answer, but I just ran to the same situation, so I thought I share what I have found.

    My scenario:

    WPF, .NET4.0, nested datagrids (I have a nested datagrid in the rowdetail of the 'parent' datagrid.)

    First I ran to the well known 'DeferRefresh' exception problem. I applied the RollBack functionality published by epanzavolta which solved most of my problems. However later on I ran to the next situation:

    1. selecting a row in the 'parent' datagrid which contains an empty 'nested' datagrid (empty but one NewItemPlaceholder line)
    2. starting to add a new line to the 'nested' datagrid, but dont't finish it (stay in edit mode)
    3. selecting a different row in the 'parent' datagrid
    4. going back to the previous row in the 'parent' datagrid
    => the unfinished new row in 'nested' datagrid is rolled back properly but the NewItemPlaceholder is missing also!

    To be honest I still don't understand this weird functionality, but I tried out the same scenario without using RollBack property, and it didn't cause 'DeferRefresh' exception. So I extended the RollBAck implementation to check this scenario and do not call CancelEdit() or CancelNew(). The modified RollbackDataGridOnLostFocus() function is:

    static void RollbackDataGridOnLostFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        DataGrid senderDatagrid = sender as DataGrid;
    
        if (senderDatagrid == null)
        {
            return;
        }
    
        UIElement focusedElement = Keyboard.FocusedElement as UIElement;
        if (focusedElement == null)
        {
            return;
        }
    
        //let's see if the new focused element is inside a datagrid
        DataGrid focusedDatagrid = GetParentDatagrid(focusedElement);
        if (focusedDatagrid == senderDatagrid)
        {
            //if the new focused element is inside the same datagrid, then we don't need to do anything;
            //this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus, which passes to the selected DataGridCell child
            return;
        }
    
        //let's see if the new focused element is our parent datagrid
        DataGrid parentDatagrid = GetParentDatagrid(senderDatagrid);
        if (focusedDatagrid == parentDatagrid)
        {
            //if the new element is within our parent datagrid, we don't do anything
            //this scenario doesn't cause 'DeferRefresh' exception but cancelling the edit removes the NewItemPlaceholder from the datagrid
            return;
        }
    
        //otherwise, the focus went outside the datagrid; in order to avoid exceptions like ("DeferRefresh' is not allowed during an AddNew or EditItem transaction")
        //or ("CommitNew is not allowed for this view"), we undo the possible pending changes, if any
        IEditableCollectionView collection = senderDatagrid.Items as IEditableCollectionView;
    
        if (collection.IsEditingItem)
        {
            collection.CancelEdit();
        }
        else if (collection.IsAddingNew)
        {
            collection.CancelNew();
        }
    }

    • Edited by MukiJames Thursday, June 07, 2012 9:17 AM
    Thursday, June 07, 2012 9:16 AM
  • This code block (which is a distillation of the suggestion above) worked for me.

    I commit the edit if I can, so at least valid data gets saved.

    Calling _dataGrid.CancelEdit() has the effect of fixing the NewItemPlaceHolder disappearing.

        Protected Sub OnPreviewLostKeyboardFocus(sender As Object, e As KeyboardFocusChangedEventArgs) Handles _dataGrid.PreviewLostKeyboardFocus
            If _dataGrid.IsAncestorOf(e.NewFocus) Then
                Return
            End If
    
            _dataGrid.CommitEdit(DataGridEditingUnit.Row, True)
            _dataGrid.CancelEdit()
    
            If _collectionView.IsAddingNew Then
                _collectionView.CancelNew()
            ElseIf _collectionView.IsEditingItem Then
                If _collectionView.CanCancelEdit Then
                    _collectionView.CancelEdit()
                End If
            End If
        End Sub
    

    _collectionView is the IEditableCollectionView (I already saved it in my helper class).

    Using the PreviewLostKeyboardFocus instead of LostKeyboardFocus makes it compatible with single-click editing of the datagrid.

    Tuesday, July 10, 2012 11:39 PM

  • Hi,

    I know it's a little bit late answer, but I just ran to the same situation, so I thought I share what I have found.

    My scenario:

    WPF, .NET4.0, nested datagrids (I have a nested datagrid in the rowdetail of the 'parent' datagrid.)

    First I ran to the well known 'DeferRefresh' exception problem. I applied the RollBack functionality published by epanzavolta which solved most of my problems. However later on I ran to the next situation:

    1. selecting a row in the 'parent' datagrid which contains an empty 'nested' datagrid (empty but one NewItemPlaceholder line)
    2. starting to add a new line to the 'nested' datagrid, but dont't finish it (stay in edit mode)
    3. selecting a different row in the 'parent' datagrid
    4. going back to the previous row in the 'parent' datagrid
    => the unfinished new row in 'nested' datagrid is rolled back properly but the NewItemPlaceholder is missing also!

    To be honest I still don't understand this weird functionality, but I tried out the same scenario without using RollBack property, and it didn't cause 'DeferRefresh' exception. So I extended the RollBAck implementation to check this scenario and do not call CancelEdit() or CancelNew(). The modified RollbackDataGridOnLostFocus() function is:

    static void RollbackDataGridOnLostFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        DataGrid senderDatagrid = sender as DataGrid;
    
        if (senderDatagrid == null)
        {
            return;
        }
    
        UIElement focusedElement = Keyboard.FocusedElement as UIElement;
        if (focusedElement == null)
        {
            return;
        }
    
        //let's see if the new focused element is inside a datagrid
        DataGrid focusedDatagrid = GetParentDatagrid(focusedElement);
        if (focusedDatagrid == senderDatagrid)
        {
            //if the new focused element is inside the same datagrid, then we don't need to do anything;
            //this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus, which passes to the selected DataGridCell child
            return;
        }
    
        //let's see if the new focused element is our parent datagrid
        DataGrid parentDatagrid = GetParentDatagrid(senderDatagrid);
        if (focusedDatagrid == parentDatagrid)
        {
            //if the new element is within our parent datagrid, we don't do anything
            //this scenario doesn't cause 'DeferRefresh' exception but cancelling the edit removes the NewItemPlaceholder from the datagrid
            return;
        }
    
        //otherwise, the focus went outside the datagrid; in order to avoid exceptions like ("DeferRefresh' is not allowed during an AddNew or EditItem transaction")
        //or ("CommitNew is not allowed for this view"), we undo the possible pending changes, if any
        IEditableCollectionView collection = senderDatagrid.Items as IEditableCollectionView;
    
        if (collection.IsEditingItem)
        {
            collection.CancelEdit();
        }
        else if (collection.IsAddingNew)
        {
            collection.CancelNew();
        }
    }

    You should use e.NewFocus instead of Keyboard.FocusedElement (otherwise some problem will stay)

    EO


    Eric Ouellet

    Monday, July 30, 2012 8:50 PM
  • @ epanzavolta, What would be the code license for this code you have posted here (e.g. Ms-Pl, BSD licence, etc)?  My understanding is that it is implicitly copyrighted unless you specify a code license. 
    Thursday, August 09, 2012 3:07 PM
  • Hi Epanzavolta,

    Thanks for your workaround. 

    I'm using wpf datagrid 4.0 and it seems that this issue still exists in 4.0. I added workaround that you suggested but after this I ran into new problem stating : "System.InvalidOperationException: CancelEdit is not supported for the current edit item."

    Stack Trace: 

    Unknown error occurred. Program will close. Restart program. The exception is: System.InvalidOperationException: CancelEdit is not supported for the current edit item.
       at System.Windows.Data.ListCollectionView.CancelEdit()
       at System.Windows.Controls.ItemCollection.System.ComponentModel.IEditableCollectionView.CancelEdit()

    .....

    Any help is appreciated. 

    Thanks

    Tuesday, September 24, 2013 5:44 AM