mercoledì 21 settembre 2011

WPF - DataGrid (DeferRefresh exception)

C# 4.0
In una view con all’interno una DataGrid mi capitava a volte, aggiungendo o modificando una riga, la seguente exception:
"’DeferRefresh’ is not allowed during an AddNew or EditItem transaction"

Girando quindi per la rete ho trovato un behaviour che ho poi adattato alle mie esigenze, in pratica al lost focus della grid viene forzato un CommitNew o un CommitEdit a seconda del caso.

A seguire il behaviour e la relativa modifica allo XAML.

using System.ComponentModel;
using System.Windows.Input;
using System.Windows;
using System.Windows.Controls;

namespace Admin.Console.Behaviour
{
    class DataGridCommitOnUnfocusedBehaviour
    {
        #region DataGridCommitOnUnfocusedBehaviour

        public static bool GetDataGridCommitOnUnfocused(DataGrid datagrid)
        {
            return (bool)datagrid.GetValue(DataGridCommitOnUnfocusedProperty);
        }

        public static void SetDataGridCommitOnUnfocused(
         DataGrid datagrid, bool value)
        {
            datagrid.SetValue(DataGridCommitOnUnfocusedProperty, value);
        }

        public static readonly DependencyProperty DataGridCommitOnUnfocusedProperty =
            DependencyProperty.RegisterAttached(
            "DataGridCommitOnUnfocused",
            typeof(bool),
            typeof(DataGridCommitOnUnfocusedBehaviour),
            new UIPropertyMetadata(false, OnDataGridCommitOnUnfocusedChanged));

        static void OnDataGridCommitOnUnfocusedChanged(
         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 += CommitDataGridOnLostFocus;
                datagrid.DataContextChanged += CommitDataGridOnDataContextChanged;
            }
            else
            {
                datagrid.LostKeyboardFocus -= CommitDataGridOnLostFocus;
                datagrid.DataContextChanged -= CommitDataGridOnDataContextChanged;
            }
        }

        static void CommitDataGridOnLostFocus(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;
            if (collection.IsEditingItem)
            {
                collection.CommitEdit();
            }
            else if (collection.IsAddingNew)
            {
                collection.CommitNew();
            }
        }

        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 CommitDataGridOnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            DataGrid senderDatagrid = sender as DataGrid;

            if (senderDatagrid == null)
                return;

            IEditableCollectionView collection = senderDatagrid.Items;

            if (collection.IsEditingItem)
            {
                collection.CommitEdit();
            }
            else if (collection.IsAddingNew)
            {
                collection.CommitNew();
            }
        }

        #endregion DataGridCommitOnUnfocusedBehaviour
    }
}

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

Nello XAML ho introdotto il seguente attributo evidenziato in giallo:

<DataGrid AutoGenerateColumns="False" Height="200" HorizontalAlignment="Left" Margin="132,43,0,0" VerticalAlignment="Top" Width="400"
                                            ItemsSource="{Binding Threshold.Hierarchies, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
behaviour:DataGridCommitOnUnfocusedBehaviour.DataGridCommitOnUnfocused="True">
                                            <DataGrid.RowValidationRules>
                                                <Common:RowDataInfoValidationRule ValidationStep="UpdatedValue" />
                                            </DataGrid.RowValidationRules>
                                            <DataGrid.Columns>
                                                <DataGridTextColumn Width="190" Binding="{Binding Path=Code, UpdateSourceTrigger=PropertyChanged}"  EditingElementStyle="{StaticResource CellEditStyle}"
                                                 Header="{Binding Source={StaticResource CustomResources}, Path=LocalizedStrings.grdThresholdHiearchyCode}"/>
                                                <DataGridCheckBoxColumn Width="190" Binding="{Binding Path=Included, UpdateSourceTrigger=PropertyChanged}"
                                                 Header="{Binding Source={StaticResource CustomResources}, Path=LocalizedStrings.grdThresholdHiearchyIncluded}"/>
                                            </DataGrid.Columns>
                                        </DataGrid>




3 commenti:

  1. C'è qualcosa che mi sfugge. Ho provato sia la tua che la soluzione originale nell'msdn ma l'errore in compilazione è sempre
    Property 'DataGridCommitOnUnfocused' is not attachable to elements of type 'DataGrid'
    Puoi allegare lo xaml completo?
    F.

    RispondiElimina
  2. Ciao Francesco,forse nella view ti manca il riferimento al behaviour, che nel mio caso è:

    xmlns:behaviour="clr-namespace:Admin.Console.Behaviour"

    Vorrei inviarti lo XAML completo della view ma c'è un limite ai caratteri, quindi se vuoi mandami una mail a: diego_parolin "chiocciola" hotmail.com

    Diego.

    RispondiElimina
  3. Ti ringrazio per la risposta, il behaviour viene riconosciuto ma non mi consente di inserirlo nel DataGrid. Inizialmente pensavo anch'io ad un problema di namespaces.
    Ho risolto ugualmente attaccandomi all'evento LostFocus del DataGrid, e chiamando il metodo _datagrid.CancelEdit() nel code behind.
    Il mio caso è il seguente, ho 2 Page: A e B.
    Premendo un bottone all'interno della cella del datagrid in A, passo a B. A volte se da B torno in A (bottone "indietro") ricevevo la famosa eccezione; questo nonostante che il datagrid di A non sia abilitato ad aggiungere/rimuovere/editare le proprie righe.
    Comunque il baco è segnalato come risolto nella versione 4.5 del WPF.

    la mia mail è francesco.umani gmail com

    RispondiElimina