Overview of the editing features in the WPF DataGrid


Introduction


I’m going to talk a little on the editing features of the DataGrid.  I will dive deep into a couple things but overall I just want to give you a good idea of the APIs you can work with and how to tweak a couple things.  So basically I will be introducing a bit of the internal implementation that may be beneficial for you to understand, the editing commands and how to customize, and the editing events that are available.


Background on the data source and DataGrid working together


Some major updates were done in 3.5 SP1 to enable this editing scenario for the DataGrid.  In particular, the IEditableCollectionView interface was added and ListCollectionView and BindingListCollectionView were updated to support this interface.  Read more on IEditableCollectionView here.  As a refresher, ListCollectionView is the view created for an ItemsControl when your data source implements IList such as ObservableCollection<T>.   BindingListCollectionView is the view created for an ItemsControl when your data source implements IBindingList such as an ADO.net DataView.    


The DataGrid uses IEditableCollectionView underneath the covers to support transactional editing as well as adding and removing data items.  In the implementations of IEditableCollectionView in ListCollectionView and BindingListCollectionView, they both follow the IEditableObject pattern where the calls to IEditableCollectionView.EditItem, IEditableCollectionView.CancelEdit, and IEditableCollectionView.CommitEdit end up calling IEditableObject.BeginEdit, IEditableObject.CancelEdit, and IEditableObject.CommitEdit respectively.  It is in the implementation of IEditableObject where you can provide the functionality to commit or rollback changes to the data source.  For an ADO.net DataTable, you get this functionality for free as DataRowView already implements IEditableObject.  When using an ObservableCollection<T> you will have to provide your own implementation for IEditableObject for T.  See the MSDN documentation of IEditableObject for an example of how to implement it for a data item.


Things to keep in mind:


·         DataGrid checks for IEditableCollectionView’s CanAddNew, CanCancelEdit, and CanRemove properties before executing the EditItem, CancelEdit, or CommitEdit methods.  So if editing appears to not work for some reason, be sure to check that it is able to edit.




















 


ListCollectionView


BindingListCollectionView


CanAddNew


True if there is not an edit transaction occurring, if the collection is not a fixed size, and if it can create an object of the correct type.


True if there is not an edit transaction occurring, if the collection is not a fixed size, and if the collection is not read-only.


CanCancelEdit


True if the edited item implements the IEditableObject interface.


True if the edited item implements the IEditableObject interface.


CanRemove


True if the collection is not a fixed size and if an add or edit transaction is not occurring.


True if the collection is not a fixed size and if an add or edit transaction is not occurring and if the collection is not read-only.


 


For information on how the data binding is hooked to the UI, see this post on Stock and Template Columns and Dissecting the Visual Layout. 


Note about DataGrid properties related to editing


There are three properties on DataGrid to control editing/adding/deleting.  These properties are:


·         CanUserAddRows


·         CanUserDeleteRows


·         IsReadOnly (not in CTP)


They are basically self-documenting but beware of CanUserAddRows and CanUserDeleteRows as they can appear a little magical.  Their values are coerced based on other properties such as DataGrid.IsReadOnly, DataGrid.IsEnabled, IEditableCollectionView.CanAddNew, and IEditableCollectionView.CanRemove.  So this is another thing to watch out for when editing.  If you run into a situation where you set CanUserAddRows or CanUserDeleteRows to true but it is changed to false automatically, check that the conditions below are met.










CanUserAddRows


True if the DataGrid is not ReadOnly and IsEnabled, CanUserAddRows is set to true and IEditableCollectionView.CanAddNew is true


CanUserDeleteRows


True if the DataGid is not ReadOnly and IsEnabled, CanUserDeleteRows is set to true and IEditableCollectionView.CanRemove is true


 


Working with editing commands


Default commands have been added to the DataGrid to support editing.  These commands and their default input bindings are:


·         BeginEditCommand (F2)


·         CancelEditCommand (Esc)


·         CommitEditCommand (Enter)


·         DeleteCommand (Delete)


When each command is executed it will do some internal housekeeping and at some point it will call into its IEditableCollectionView counterpart.  For example, BeginEditCommand calls into IEditableCollectionView.EditItem and CancelEditCommand calls into IEditableCollectionView.CancelItem.


DataGrid also has APIs where you can call editing commands programmatically.  Not surprisingly, the APIs are BeginEdit, CancelEdit, and CommitEdit.


Adding new input gestures


Adding new input gestures is similar to any other control in WPF.  The DataGrid commands are added through the CommandManager so one possible solution would be to register a new InputBinding with the CommandManager:               



CommandManager.RegisterClassInputBinding(


                      typeof(DataGrid),


                new InputBinding(DataGrid.BeginEditCommand, new KeyGesture(Key.<new key>)));


 


Disabling commands


You can disable any of the editing commands by attaching to the CommandManager.CanExecuteEvent, looking for the command to disable, then setting e.CanExecute accordingly.  Here is an example:



_handler = new CanExecuteRoutedEventHandler(OnCanExecuteRoutedEventHandler);


EventManager.RegisterClassHandler(typeof(DataGrid), CommandManager.CanExecuteEvent, _handler);


 


 


void OnCanExecuteRoutedEventHandler(object sender, CanExecuteRoutedEventArgs e)


{


  RoutedCommand routedCommand = (e.Command as RoutedCommand);


  if (routedCommand != null)


  {


    if (routedCommand.Name == “<command name>”)


    {


      e.CanExecute = <some condition>;


      if(!e.CanExecute)


        e.Handled = true;
    }
  }


}


 


This is a relatively cumbersome way of disabling an editing command from being executed.  Fortunately events were added to the DataGrid so that they can be canceled in a more direct fashion (although no direct event exists for the DeleteCommand).


Editing events on the DataGrid


These are the editing events that you can listen to and cancel the operation or modify data:


·         RowEditEnding


·         CellEditEnding


·         BeginningEdit


·         PreparingCellForEdit


·         InitializingNewItem


In the first three events; RowEditEnding, CellEditEnding, and BeginningEdit, you have access to the DataGridRow and DataGridColumn that is being committed, cancelled, or edited.  These events are called right before the actual operation will occur.  You have the ability to cancel the operation completely by setting e.Cancel to true.  RowEditEnding and CellEditEnding both have a parameter EditAction which lets you know if it is a commit or cancel action.  From CellEditEnding, you also have access to the editing FrameworkElement.  This gives you the ability to set or get properties on the visual itself before a cell commit or cancel.


PreparingCellForEdit is fired right after the cell has changed from a non-editing state to an editing state.  In this event you have the ability to modify the contents of the cell.  InitializingNewItem is called when a new item is added and in this event you have the option to set any properties on the newly created item.  This event is good when you want to set initial default values on a new item. 


Summary


So hopefully this will give you an idea on what APIs you have available for editing scenarios as well as some gotchas on how particular editing features work.  If there is other editing questions/topics that you would like to read about please let me know.  Another item that I plan to discuss in the future is how row validation will be tied into the DataGrid, so stay tuned!

Comments (35)

  1. As you might have heard, .NET Framework 3.5 SP1 and Visual Studio 2008 SP1 are out today! There are a

  2. There have been several questions on the WPF CodePlex discussion list relating to styling rows and columns

  3. orsino says:

    If you derive from ObservableCollection you should make the derived type generic:

    public class SomeCollection<T> :  ObservableCollection<T>

    otherwise IEditableCollectionView.CanAddNew will return false if the collection is empty.

  4. Tim Nguyen says:

    Can you post a source code and demo application for implementing editting feature in wpf datagrid using Entity Framework?

    Thanks,

    Xan

  5. Lee says:

    Is there a way to get the CanAddNew property to return true when ObservableCollection contains an interface object? Some magic factory attribute pointer or something?

    Regards

    Lee

  6. Lee,

    Unfortunately there isn’t a direct way to get an Interface object to create itself with IECV.  

  7. Vinit Sankhe says:

    Hi There,

    I use latest WPF Toolkit DataGrid. But setting up CanUserAddRows to true doesnt work. CanUserDeleteRows and IsReadOnly works though (to whatver value is set for them). My grid hosts a text column, a combobox column and a template column.

    I have checked that IsReadonly is False and IsEnabled is True.

    What am I missing here?

    Thx in Advance,

    Vinit.

  8. Vinit,

    Is IECV.CanAddNew true?  Can you describe your data source a bit more.

  9. Anschütz says:

    I try to use the Datagrid with an ObservableCollection. I try to persist my data in RowEndEdit event but if I get persist errors a cannot handle them. I also cannot see any CommittingEdit

    event as described above. How can catch persist errors correctly?

    Thx Anschütz

  10. Anschütz says:

    Here again,

    my actual Problem is: When I try to persist data in RowEditEnding and Errors occure I want the user to be able to react with a Dialog. A MessageBox or Window whatever. But any call like MessageBox.Show() or Window win = new Window(); win.Show() causes the DataGrid to call the RowEditEnding() Method again.

    Where can I put such I Dialog call?

    Thx for answer.

  11. Anschutz,

    Maybe you can make a special case that when an error occurs you cancel the commit in RowEditEnding.  Then when the user fixes it in the dialog you programmatically commit the data.  Also, CommittingEdit has been replaced by RowEditEnding.  I need to update two of the APIs in this post.

  12. wpf.wanna.be says:

    Hi Vincent,

    We are playing with the DataGrid and we run into a wall. I have noticed our problem has already been covered in the previous comments but unfortunately, the solution presented gives us big headaches elsewhere.

    In order to provide extended functionality we derived a class from ObservableCollection. This was also a MUST for us because we are using XamlReader/XamlWriter for serialization and they don’t work with generic classes.

    So in order to resolve IECV.CanAddNew returning false for empty collection (why??????), we must make the derived class generic, but making it generic totally screws up XamlWriter.

    So we obiviously need to resort to trickery to get the best of both worlds. And some trick I have already tried are very UGLY!

    Is there an easy way to make ListCollectionView behave correctly? Is this considered to be a bug in LCV? I have tried a naive approach by inheriting from LCV and forcing true on CanAddRow, but this failed miserably.

  13. wpf.wanna.be,

    I don’t know about all scenarios but some have been fixed in dev10.  As far as a workaround, there aren’t any easy and straightforward ones.  Maybe if you could provide some code we can take a look.

  14. wpf.wanna.be says:

    Vincent, thanks for a really quick response. As for the code, consider the following.

    class MyCollection<T> : ObservableCollection<T> {…}

    This works correctly in DataGrid with respect to ListCollectionView.CanAddRow. But serialization support is close to none for XamlReader/XamlWriter when it comes to generic classes. I have been trying all day to get a MarkupExtension to work but I have had limited success in reading a generic class from XAML. Writing still does not work.

    On the other hand, the following non-generic class:

    class MyCollection : ObservableCollection<T> {…}

    …works perfectly with both XamlReader/XamlWriter but there is a MAJOR problem in that ListCollectionView.CanAddRow (wrongly) returns FALSE if a collection is empty.

    As soon as I programmatically add at least one item to this collection, CanAddRow starts returning TRUE.

    So, there you go. If you have any ideas, I would be grateful.

  15. Trey says:

    Vincent-

    Sorry if this is a dumb question, but I am having a little trouble committing changes to my db with the DataGrid.

    I have a simple WPF application with a LINQ to Entities data model pulling from my database.

    In the DataGrid I have explicitly set the columns’ binding modes to TwoWay, and its ItemSource = myEntities.Customers.

    From here I can edit on the grid, add new items, etc. and all the changes are showing in the grid, but none of the changes are shoveled back to the db.

    What am I missing here to make the magic happen? It was a little clearer to me using a ListView, but the grid is definitely the tool I want to use for CRUD and less programming.

    twhite @ fire . ca . gov

    thank you,

    -Trey

  16. Trey,

    Are you doing the save changes part with your entities, myEntities.SaveChanges?

  17. wpf.wanna.be,

    The scenario that you are describing is fixed in dev10.  Unfortunately, there are no magic workarounds for it that haven’t already been described.  

  18. Mahender says:

    Hi ,

    Im having requirement like this,

    Im having n row and user will have save and reset button out side of datagrid.when i click on reset button ,is there any possiblity to reset all rows in datagrid which r modified.

  19. Mahender,

    Currently you can only reset one row at a time.  You will have to create this custom editing functionality yourself.  

  20. Andreas says:

    wpf.wanna.be, Vinsibal,

    I found that if you access the CanAddRow of ListCollectionView once before you use the collection, magically the CanUserAddRows of the DataGrid becomes true. Strange!

    IEditableCollectionView ecv = new ListCollectionView(myRecordCache);

    bool b = ecv.CanAddNew;   // dummy access

    MyGrid.DataContext = ecv;

  21. 1. DataGridColumn.SortDirection does not actually sort the column. DataGridColumn.SortDirection is used

  22. Chris says:

    Vincent,

    In my datagrid, I’ve got an event wired up on the CellEditEnding event that executes some code to lookup if the value they entered is valid or not (comparing against Zip codes).

    If the data they enter is invalid, I show a messagebox.

    The problem I’m having is that the messagebox continually fires if it’s invalid (I think the focus is switching between the cell and the messagebox) and gets caught in a bad loop. Any ideas with this?

    Thanks,

    Chris

  23. Chris,

    You could try keeping track of the state like this:

    <toolkit:DataGrid Grid.Row="1"

                             x:Name="MyDataGrid"

                             ItemsSource="{Binding Items}"                                                    AutoGenerateColumns="False"

                             Validation.Error="MyDataGrid_Error">

               <toolkit:DataGrid.Columns>

                   <toolkit:DataGridTextColumn Header="First Name" Binding="{Binding Path=Item.FirstName, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />                                            </toolkit:DataGrid.Columns>

           </toolkit:DataGrid>

    private bool _isInErrorState = false;

           private void OnDataGridValidationError(object sender, ValidationErrorEventArgs e)

           {

               if (e.Action == ValidationErrorEventAction.Added && !_isInErrorState)

               {

                   _isInErrorState = true;

                   MessageBox.Show(e.Error.ErrorContent.ToString());

                   _isInErrorState = false;

               }

           }

  24. Chris,

    MyDataGrid_Error and OnDataGridValidationError are the same thing.  Forgot to update it when I was copy/pasting.

  25. Tomi Junnila says:

    Thanks for the link to this post from the IEditableCollectionView one.

    Sadly, this page doesn’t cover IECV.AddNew which was the topic of my question.

    I did notice that I hadn’t implemented CanAddNew, but implementing it didn’t change my problem, i.e. that I can only add a new item on the initial empty line, as a new empty line doesn’t appear after committing that one.

    I have also noticed that CommitNew doesn’t get called after committing a new item in that single new empty line, CommitEdit is called instead.

    Currently, I implement the following:

    public class DerivedView : ListCollectionView, IEditableCollectionView

    {

       public DerivedView(System.Collections.IList list)

           : base(list)

       {

       }

       private bool isAddingNew = false;

       private Derived newDerived = null;

       Object IEditableCollectionView.AddNew()

       {

           newDerived = new Derived();

           // Initialize some non-user-modifiable properties here

           InternalList.Add(newDerived);

           isAddingNew = true;

           return newDerived;

       }

       bool IEditableCollectionView.CanAddNew

       {

           get { return (!isAddingNew); }

       }

       bool IEditableCollectionView.IsAddingNew

       {

           get { return isAddingNew; }

       }

       void IEditableCollectionView.CommitNew()

       {

           if (!isAddingNew) return;

           isAddingNew = false;

           base.CommitNew();

       }

    }

  26. Trey says:

    Vincent-

    I have a WPF application using the DataGrid from the toolkit. I have written it using an MVVM pattern architecture. Behind it all sits a DAL that persists changes to an Oracle DB.

    In particular I have a window that allows users to edit a form we process for business. It has two parts – a OTM relationship between the header material and totals (the parent record), and specific incident material (child records).

    This window is bound to a ViewModel of the parent record, who’s child records are exposed as an ObservableCollection<ChildRecordViewModels>.

    The DataGrid is then bound to this observable collection of children.

    When a user updates the record, persistence is happening for the entire form, and works flawless – it iterates through the collection and does well.

    However, I am stuck trying to figure out how to ADD and DELETE records from this grid bound to an OC within a view model.

    Any suggestions?

  27. Trey,

    This particular article has some good info on inserts and deletes, http://www.codeproject.com/KB/WPF/WPFDataGridExamples.aspx.  Let me know if that helps.

  28. Trey says:

    Vincent-

    Thanks for the comment back. I’ll check that out very soon. I got side-tracked (wisely for once) by a more immediate issue with my editing process.

    Same context as above, here’s the simplest way to describe it:

    1. Both ParentViewModel and ChildViewModel implement EditableViewModel (my base class with IEO, IDEI, and INPC).

    2. The view we are looking at is a modal window bound to the ParentViewModel. I manually call BeginEdit() on it as the parent record should be considered in an edit state for the life-time of the modal window.

    3. BeginEdit() in my abstract class calls a SaveProperties() method, that creates a dictionary of all the property values called _savedState. So that Cancel may restore it, and End may nullify it.

    4. The top portion of the view is text box controls… simple value type properties of the parent view model (strings, integers, etc.). The bottom half is a Toolkit DataGrid bound to a property that is an  ObservableCollection<ChildViewModels>.

    5. Lastly there are two buttons OK and CANCEL. OK calls end it, marks the records clean, and closes the modal. CANCEL calls cancel edit and restores the _savedState dictionary values for the parent view model.

    You can probably see what might be happening now… here’s the problem.

    A. When a user edits a row in the grid… the same process happens. the grid inherent calls Begin, we change it, the value changes and if we do not cancel the change then… the child view model is changed and marked dirty.

    B. Now when a user clicks the CANCEL button, it says "hey, either your parent or your child records are dirty, do you want to persist to db?" – and if i say no, CancelEdit is called… as it should be… restoring the parent record values, including the OC that the grid is bound to…

    C. The issue is that the OC is a reference type and therefore its values within the _savedState dictionary are getting updated each time a row is edited rather than kept pristine…

    Any thoughts or suggestions on how to make implement a Cancel button that will restore changes to the grid as well as the changes to its simple controls?

    Sorry for the long winded explanation:

    -trey

  29. Trey says:

    Vincent-

    More on the last post… I am trying simply to ensure that the "edit state" of both the parent and child view models is for the life of the window.

    My current implementation allows for the parent to function so… but the inherent functionality of the DataGrid calling End/CancelEdit() methods on a lost focus means their initial state is lost then.

    Is there a way to prevent a DataGrid from calling EndEdit() or CancelEdit() ?

    If so, I would simply modify my Ok and Cancel button commands to not only handle the parent’s state, but loop through the OC and an either End or Cancel each of them.

    -T

  30. Trey,

    There is a way to prevent the DataGrid from calling CancelEdit().  Listen to DataGrid.RowEditEnding and you can cancel it there based on criteria you are looking for.  Hope that helps.

  31. Ryan says:

    Vincent:

    I have a datagrid bound to an ObservableCollection of class X, and has CanUserAddRows set to True.  Is it possible to modify a property of class X when the user adds a new row (before it is displayed to the user)?

    Thank you.

  32. Matt says:

    This tripped me up for hours today, but the when using a collection of T, T must have a parameterless constructor or you won’t get the blank row.

  33. Robert Engzell says:

    Problem: after I removed a row from the ObservableCollection, my datagrid became uneditable. All settings above were correct.

    Answer found: Before removing row, validation was performed and I got an error (which I didn’t see since the row is removed). Since row was removed, I had no indication that I had a validation error.