WPF 3.5 SP1 feature: IEditableCollectionView


Motivation 

A CollectionView is your interface into manipulating a collection of data items in an ItemsControl.  Common tasks with this view often involve applying sorting, filtering, and grouping.  In lieu of supporting a DataGrid control, transactional adding/editing/removing is an essential feature for data manipulation and a new view has been added in WPF 3.5 SP1 to support this functionality. 

What is it?

IEditableCollectionView is a new collection view that you can use to supports adding and removing new items, as well as editing items in a transactional way.  It is implemented by ListCollectionView (the default view for ObservableCollection) and BindingListCollectionView (the default view for DataTable).  I will go through an example to further describe it and summarize afterwards.

Background on the example

I will be creating a ListBox with some data that you can add/remove/edit each item.  You also have the option to cancel in the middle of editing the item.  I will use buttons to trigger an item for editing, adding, removing, etc.  Here is the ListBox xaml:                               

<!–defined in resource section–>  

<Style x:Key=”listBoxDefaultStyle” TargetType=”ListBoxItem” >

    <Setter Property=”ContentTemplate” Value=”{StaticResource DefaultTemplate}” />

</Style>

 

<ListBox Name=”itemsList”

  ItemsSource=”{StaticResource products}”

  ItemContainerStyle=”{StaticResource listBoxDefaultStyle}”/>

 

When the ListBox is not in edit mode it will use a template of TextBlocks.                 

               

<DataTemplate x:Key=”DefaultTemplate”>

  <StackPanel Orientation=”Horizontal”>

    <TextBlock Text=”{Binding Path=Book, StringFormat=Title: {0};}”></TextBlock>

    <TextBlock Text=”{Binding Path=Author, StringFormat=Author: {0};}”></TextBlock>

    <TextBlock Text=”{Binding Path=Price, StringFormat=Price: {0:C}}”></TextBlock>

  </StackPanel>

</DataTemplate>

 

When it is in edit mode, it will use a template of TextBoxes (I will set the DataTemplate dynamically in code).

<DataTemplate x:Key=”EditingTemplate”>

  <StackPanel Orientation=”Horizontal”>

    <TextBox Text=”{Binding Path=Book}” />

    <TextBox Text=”{Binding Path=Author}” />

    <TextBox Text=”{Binding Path=Price}” />

  </StackPanel>

</DataTemplate>

 

How do I use it?

Just like the other collection views, you can obtain a reference to this collection using the CollectionViewSource.GetDefaultView,            

// retrieve a reference to the view

ICollectionView view = CollectionViewSource.GetDefaultView(itemsList.Items);

IEditableCollectionView iecv = (IEditableCollectionView)view;

 

Before I go any further, there is an important point to note about delegation of work. When the data source implements IEditableObject, the IEditableCollectionView will call BeginEdit() when a new item is added or an existing item is opened for edit. It will call CancelEdit() when the item is cancelled and EndEdit() when the item is committed. IEditableCollectionView lets the app author handle this part of the transaction.

In my data source, I will be creating a copy of the selected data item when it is opened for edit. If the procedure is cancelled, I will use the copy to reset the original state, otherwise the new data will update the data source. I’ve only included the relevant information of the data source here.

 

public class Product : INotifyPropertyChanged, IEditableObject

{

  private Product copy;       

 

  #region IEditableObject Members

  public void BeginEdit()

  {

    if (this.copy == null)

        this.copy = new Product();

 

    copy.Book = this.Book;

    copy.Author = this.Author;

    copy.Price = this.Price;

  }

 

  public void CancelEdit()

  {
    this
.Book = copy.Book;

    this.Author = copy.Author;

    this.Price = copy.Price;
  }

 

  public void EndEdit()

  {
    copy.book = null;

    copy.author = null;

    copy.price = 0;
  }

  #endregion IEditableObject Members

}

 

Let’s first focus on editing items. To initiate an item to be edited you call IEditableCollectionView.EditItem(). As I just discussed, this will call BeginEdit on my selected data item. Here is the code that is called when the edit button is clicked (Notice the template of the item container is updated here):          

private void edit_Click(object sender, RoutedEventArgs e)

{

  // edit the item

  iecv.EditItem(itemsList.SelectedItem);

 

  // update the template

  ListBoxItem lbItem = (ListBoxItem)itemsList.ItemContainerGenerator.ContainerFromItem(iecv.Current EditItem);

  lbItem.ContentTemplate = (DataTemplate)this.myGrid.FindResource(“EditingTemplate”);

}

 

So now that the current selected item is editable, the changes to it can either be submitted or cancelled. We shall look at the submitted scenario next. To commit changes, you call IEditableCollectionView.CommmitEdit(). This will then call EndEdit() on my data item where I reset my copy of the data as it is not needed anymore. Here is the code when the submit button is clicked:

private void submit_Click(object sender, RoutedEventArgs e)

{

  // update the template

  ListBoxItem lbItem = (ListBoxItem)itemsList.ItemContainerGenerator.ContainerFromItem(iecv.Current EditItem);

 

  iecv.CommitEdit();

 

  lbItem.ContentTemplate = (DataTemplate)this.myGrid.FindResource(“DefaultTemplate”);

}

 

The cancel scenario is very similar to the submit code, but instead CancelEdit() is called on my data item where I reset it’s values to the copy that I stored from BeginEdit():

private void cancel_Click(object sender, RoutedEventArgs e)

{

  // update the template

  ListBoxItem lbItem = (ListBoxItem)itemsList.ItemContainerGenerator.ContainerFromItem(iecv.Current EditItem);

 

  iecv.CancelEdit();

 

  lbItem.ContentTemplate = (DataTemplate)this.myGrid.FindResource(“DefaultTemplate”);

}

 

Adding new items and removing items follow a similar pattern where the view will call BeginEdit, CancelEdit, and/or EndEdit on the data item. One important difference however is how it is managed for you. While I was managed part of the editing transaction, the IEditableCollectionView will managed the addition and removal of an item. When IEditableCollectionView.AddNew() is called, a new data item is actually added to the data source by the collection view. In BeginEdit, you have the option to initialize the new item to default data. Same goes when CancelNew() or Remove() is called. The item that was added or selected is actually removed from the data source by the collection view. There is no additional code that you need to write to create the new data item and manually add it to the data source. You can check out the full project attached to view the source for adding and removing as well as the editing that I talk about above.  It works with the WPF 3.5 SP1 bits.

For completeness, here is the full list of APIs from IEditableCollection. I try to make use of most of them in my project.

      

public interface IEditableCollectionView

{

    bool CanAddNew { get; }

    bool CanCancelEdit { get; }

    bool CanRemove { get; }

    object CurrentAddItem { get; }

    object CurrentEditItem { get; }

    bool IsAddingNew { get; }

    bool IsEditingItem { get; }

    NewItemPlaceholderPosition NewItemPlaceholderPosition { get; set; }

 

    object AddNew();

    void CancelEdit();

    void CancelNew();

    void CommitEdit();

    void CommitNew();

    void EditItem(object item);

    void Remove(object item);

    void RemoveAt(int index);

}

IEditableCollectionViewSample.zip

Comments (22)

  1. Sharepoint SharePoint Development Conference (FireStarter) at Microsoft on June 11th [Via: Steve Fox…

  2. So far for the new WPF 3.5 SP1 features, I’ve surveyed Item Container Recycling , Data Formatting , and

  3. Need Info says:

    Can u give an example of IEditableCollectionView with GridViewColumn created dynamically with celltemplate.

    thx

  4. I recently got a question on how to implement IEditableCollectionView with GridViewColumns that are dynamically

  5. vinsibal says:

    Need Info, I just wrote a post on this: http://blogs.msdn.com/vinsibal/archive/2008/05/27/using-ieditablecollectionview-with-dynamically-generated-gridviewcolumns.aspx.  Please let me know if you have any other questions.

  6. Need Info says:

    i have a derived class of Listview how can i use NewItemPlaceholderPosition.

    in fact i had xamlparsing exception

  7. vinsibal says:

    NewItemPlaceholderPosition is basically the position in the collectionview where a new item will be added.  You can specify where you would like to add the new item through the enum.  

    Can you give me a little more details on the problem that you are having?

  8. Need Info says:

    i have a derived class of Listview and i’m adding dynamically the columns into the gridview.

    i’m trying to add the editing capabilities of .net framework 3.5 sp1.

    when i’m trying to use NewItemPlaceholderPosition it gives me an xamlparser exception

  9. vinsibal says:

    You’re still going to have to be a little more specific on the xamlparser exception and how you are using NewItemPlaceholderPosition.

    IEditableCollectionView is implemented by ListCollectionView and BindingListCollectionView.  You can set/get this from your derived class by doing something like this,

    iecv = (IEditableCollectionView)Items;

    iecv.NewItemPlaceholderPosition…

    It is not a DP so you cannot use it in xaml like most of the other properties.  Maybe you can send me a code snippet and I can be a little more helpful.

  10. Introduction I’m going to talk a little on the editing features of the DataGrid. I will dive deep into

  11. Moi says:

    I have a question regarding the IEditableCollectionView as it’s implemented for a ListCollectionView. If I have created a ListCollectionView for a collection with a filter, and I call EditItem() on an item that is filtered out, so that it’s not part of the collectionview, then calling CommitEdit() will cause an exception.

    I would have thought it would be disirable to be able to call EditItem() on any item in the underlying collection, for example if the item is currently filtered out, but we change some data on it, so that it should now be included in the filtered collectionview, currently we have to first check if the item is in the filtered collection view, if it is, we go the EditItem()/CommitEdit() route, if it’s not we have to remove it from the collection, modify it and re-add it to the collection to properly refilter this item, it would have been great if we only had to do it in one fashion, ie beeing able to call EditItem()/CommitEdit() on any item in the collection even if it’s currently filtered out.

    Disired behaviour or just an overlook of the fact that collection can be filtered when creating the current IEditableCollectionView implementation?

  12. vinsibal says:

    Moi,

    When you are using a filter on a collectionview, that collectionview will represent only that filtered view now.  So what is presented in the presentation layer is the same as the underlying data which makes it more transparent, easier to debug, and more intuitive actually.  So this is all by design.

  13. Moshe says:

    I have a question like Moi’s…

    How would you force the CollectionView to rerun the filter after you’ve changed the underlying data. The only way I could think of is calling .Refresh()…but doesn’t this contradict the idea of IEditableCollectionView?

  14. Tomi Junnila says:

    Could you describe the exact contract that IEditableCollectionView.AddNew has? Is anything other than creating the new object and adding it to the collection needed?

    The reason I’m asking is that I’d need to override the default AddNew in ListCollectionView to create objects of a different type than it otherwise seems to be creating (the types do share an abstract base class). I tried implementing AddNew like described above, and the WPF Toolkit DataGrid I’m using now only allows adding one new item, to the initial NewItemPlaceholder.

  15. vinsibal says:

    Tomi,

    Take a look at this blog post, http://blogs.msdn.com/vinsibal/archive/2008/10/01/overview-of-the-editing-features-in-the-wpf-datagrid.aspx.  There is a table showing the contract for IECV.CanAddNew for a ListCollectionView and a BindingListCollectionView.

  16. Rune says:

    vinsibal, I do not think Tomi was interested in CanAddNew.

    I have a datasource where adding and deleting rows are expensive operations.

    My hope was that I could implement a ListCollectionView descendant and implement the IEditableCollectionView interface myself. No go, it all falls apart rather quickly. (ListCollectionView simply shuts down; http://wpf.codeplex.com/Thread/View.aspx?ThreadId=60656)

    What I want is to virtualize the new row until it is committed. I want to create the new row, but keep it as far away from my physical dataset as possible.

    At the very least, I need to know when the new item gets committed, so I can update my remote dataset.

    PS: The documentation for IEditableCollectionView does not mention IEditableObject with a single word. I am greatful that you spend time educating us, but it would be nice if someone influential could influence the doc-team. 😉

  17. Rune says:

    Is there a reason why it is not implemented like this:

     public void EndEdit()

     {

       copy = null;

     }

    or just no code at all… (if you don’t want the overhead of creating another instance of copy later)

    Seeing as many classes will have to define a blanking method, there should be a good reason why they require every field to be reset rather than just set the instance to null (or do nothing at all).

    As I understand it, with LINQ we’re supposed to attribute each member with the [Column] attribute. Is there then not enough information for the framework to handle IEditableObject automatically?

  18. vinsibal says:

    Rune,

    It could have been implemented by just setting the copy to null.  Here is an official example from the doc team, http://msdn.microsoft.com/en-us/library/system.componentmodel.ieditableobject.aspx.

    As far as linq question, it may have enough information to handle IEO there but IEO is meant to work for POCOs (plain old CLR objects).

  19. zeekial says:

    When I remove all of the items "Books". Then attempt to add a book the "CanAddnew" flag becomes false and prevents the item from being inserted. These seems like a bug. Do you know of a workaround?

  20. saltug says:

    Zeekial,

    If your collection is empty you need to use this code when adding a new item.

    if (!itemsView.CanAddNew)

    {

       ConstructorInfo ci = typeof(Item).GetConstructor(new Type[] { });

       FieldInfo fi = itemsView.GetType().GetField("_itemConstructor", BindingFlags.Instance | BindingFlags.NonPublic);

       fi.SetValue(itemsView, ci);

    }

    itemsView.AddNew();

    Details:

    http://social.msdn.microsoft.com/Forums/en/wpf/thread/381dfbbd-1e9a-4a79-8a40-87b4d8767264

  21. Sergiu says:

    Hi Vincent,

    I confront with the following situation:

    I have WPF Datagrid bound to a CollectionViewSource.

    This CollectionViewSource has GroupDescriptions and the source collection is an ObservableCollection.

    At some point I do this:

    IEditableCollectionView iecv = CollectionViewSource.GetDefaultView((this as DataGrid).ItemsSource) as IEditableCollectionView;

    and manually add one item like this:

    object newItem = iecv.AddNew();

    iecv.CommitNew();

    Here a I get the following exception:"Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index"

    if the iecv has only NewItemPlaceholder as item.

    If I remove the GroupDescriptions from CollectionViewSource it works without any problem.

    Also, if I have at least one item (besides NewItemPlaceholder) in the collection, works fine.

    What I noticed is that in this case, after AddNew runs, the ListCollectionView’s Count remains the same (meaning 1), although it should be incremented by 1, that’s why the exception raises.

    So, what is happening? Is this a bug?

    Thanks in advance!

  22. Yeo says:

    Vincent, is it possible to include a cancelRemove() function to restore those removed items?