Using IEditableCollectionView with dynamically generated GridViewColumns


I recently got a question on how to implement IEditableCollectionView with GridViewColumns that are dynamically created.  Creating the columns dynamically actually doesn’t really have a big impact on IEditableCollectionView implementation.  I have a sample below that dynamically adds columns but really does not differ all that much from the first sample. 


Side note:


There are some design issues to think about when you want to design with an IEditableCollectionView.  If you recall from the first sample I used, I relied on the template (TextBlock for non-editable, TextBox for editable) to control the user from being able to edit an item.  Since a TextBox has two-way binding by default, it was actually changing the data source when in edit mode.  Implementing IEditableObject and keeping a copy was my mechanism to revert changes if the user wanted to cancel the operation.  There is actually another (more optimal) solution that leverages another new feature, but I’m going to have to leave that for a separate post.  The new feature is actually briefly mentioned on Jossef’s post for new perf improvements in WPF 3.5 SP1 under Controls Improvements.  You can probably already guess what it is, but I won’t say anything for now.


Back to the task at hand:


I want to create columns dynamically and use IEditableCollectionView.  I decided to create the column templates in code and create columns for each public property of the data source.   The default template is a TextBlock that binds to the particular property of the data source.     



// automatically creates GridViewColumns based on the public properties


private void GenerateGridView(Type type)


{


  GridView gridView = new GridView();


  foreach (PropertyInfo pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))


  {


    GridViewColumn column = new GridViewColumn();


 


    // set the header


    column.Header = pi.Name;


 


    // the default cell template


    FrameworkElementFactory cellTemplateFactory = new FrameworkElementFactory(typeof(TextBlock));


    Binding binding = new Binding(pi.Name);


    cellTemplateFactory.SetBinding(TextBlock.TextProperty, binding);


    DataTemplate defaultCellTemplate = new DataTemplate();


    defaultCellTemplate.VisualTree = cellTemplateFactory;


    column.CellTemplate = defaultCellTemplate;


 


    gridView.Columns.Add(column);


  }


 


  this.itemsList.View = gridView;


}


 


Before when I wanted to make an item editable, I changed the ListBoxItem template to a TextBox.  Since I’m using columns now, there are some changes to the visual tree being used, and I cannot just change the whole ListBoxItem to a TextBox.  Oh, and now that I’m using ListView, they are ListViewItems now and not ListBoxItems. 


When using a GridView as the View, the ListViewItem uses a GridViewRowPresenter which will contain ContentPresenters for each column.  Now each time I have to update the item’s template, I’m going to drill down into the ListViewItem and update the cell template of each ContentPresenter to be a TextBox.



private void UpdateContentTemplate(bool isEditing, ListViewItem lvi)


{


  // get the content presenter of the particular column
  GridViewRowPresenter rowPresenter = this.GetVisualChild<GridViewRowPresenter>(lvi);


 


  // go through each column and update the content template


  int columnIndex = 0;


  foreach (GridViewColumn column in rowPresenter.Columns)


  {


    // get the content presenter of the particular column
    ContentPresenter cp = this.GetVisualChild<ContentPresenter>(rowPresenter, columnIndex);


 


    DataTemplate cellTemplate = new DataTemplate();


    FrameworkElementFactory cellTemplateFactory;


 


    // if editing, set the template to a textbox, else to a textblock


    if (isEditing)


    {
      cellTemplateFactory = new FrameworkElementFactory(typeof(TextBox));


      Binding binding = new Binding(column.Header.ToString());


      binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;


      cellTemplateFactory.SetBinding(TextBox.TextProperty, binding);
    }


    else


    {
      cellTemplateFactory = new FrameworkElementFactory(typeof(TextBlock));


      Binding binding = new Binding(column.Header.ToString());


      cellTemplateFactory.SetBinding(TextBlock.TextProperty, binding);
    }


 


    cellTemplate.VisualTree = cellTemplateFactory;


    cp.ContentTemplate = cellTemplate;


    columnIndex++;
  }
}


 


The rest of the code is basically the same as in the first sample (with some refactoring).  Adding/Removing/Editing all work the same and there are no changes to the data source.  For kicks I added commands in addition to the buttons since I was getting tired of having to mouse down to the buttons each time I wanted to edit and submit.   The commands are, F2 = Edit, Enter = Submit, Esc = Cancel.  Note that this isn’t the most efficient but hopefully it will give you some ideas to expand on.  Here is the project.

IEditableCollectionViewSample2.zip

Comments (21)

  1. Need Info says:

    where can i find demo on the new features of .net framework sp1?

  2. vinsibal says:

    Currently there is no official demo.  There are a lot of blogs out with samples and tutorials though.  I’m currently surveying many of the new features relating to controls and tree services.  Greg Schechter is doing an awesome blog series on the new effects in SP1, http://blogs.msdn.com/greg_schechter/archive/2008/05/12/a-series-on-gpu-based-effects-for-wpf.aspx.  Marlon Grech also has a nice sample out using new effects, http://marlongrech.wordpress.com/2008/05/15/effects-in-net-sp1-for-wpf/.  When SP1 RTM’s there will be a good deal of documentation and samples out on msdn.

  3. Jackie says:

    If there is no inital row in the grid, it won’t work. Because the iecv.CanAddNew is false. If there is more than 1 rows, it work well. How to resolve this issue?

  4. vinsibal says:

    This issue crops up when you are using a collection where the type cannot be known ahead of time and there are no items in the collection.  So lets say I have my collection like in the example,

    class Products : ObservableCollection<Product>

    When there are existing items in the collection, the underlying ListCollectionView can infer the type being used through one of the existing items.  However, when there are no items in the collection, it cannot do this.  One workaround around this is to use a generic collection explicitly.  So instead, if I use ObservableCollection<Product> directly instead of subclassing.  The ListCollectionView will be able to infer the type it is working with even if there are no members in the collection as it is a generic collection with the type set.  Note also that it expects the data item to have a default constructor.  

    So to recap, iecv.CanAddNew for a ListCollectionView will be true when:

    1.  collection is not currently being edited

    2.  the source list is not fixed (IList.IsFixedSize)

    3.  can construct the item

         a. can infer the type of data item to create

         b. the data item has a default constructor

  5. Julie says:

    "ListCollectionView will be able to infer the type …" obviously, only if that type stands for a class, not an interface.

    Doesn’t that imply, under the present assumptions, that when using ObservableCollection<ISomething> in the business domain, as it is customary to work with interfaces, such construction will not work with a DataGrid NewItemPlaceHolder in the presentation layer ? Also, ‘last minute’ casting that business collection to observableCollection<Something> in the view is invalid, as per the rules of generic collections.

    Kind regards, Julie

  6. vinsibal says:

    One workaround is to derive from ObservableCollection<ISomething> and start with a non-empty list. For example:

    public class Products : ObservableCollection<IProduct>

       {

           public Products()

           {

               Add((new Product("Pro WPF in C# 2008", "Matthew MacDonald", 49.99)));

               Add((new Product("Window Presentation Foundation Unleashed", "Adam Nathan", 49.99)));

               Add((new Product("Programming WPF", "Chris Sells and and Ian Griffiths", 49.99)));

               Add((new Product("WPF Programming", "Chris Andrade, Shawn Livermore, Mike Meyers, and Scott Van Vliet", 49.99)));

               Add((new Product("3D Programming For Windows", "Charles Petzold", 49.99)));

               Add((new Product("Applications = Code + Markup", "Charles Petzold", 49.99)));

               Add((new Product("Practical WPF Graphics Programming", "Jack Xu", 49.99)));

               Add((new Product("Essential Windows Presentation Foundation", "Chris Anderson", 49.99)));

           }

       }

    where Product implements IProduct.  You do have to have a non-empty collection to begin with.  Since you derive from ObservableCollection<T> the ListViewCollectionView code will not try to construct the new object with the generic argument (IProduct) and instead will try to get a representative type by actually querying for an item in the list.  In this case it will return a type of Product which as long as there is a default constructor will create the new item for you.

    With just ObservableCollection<T>, the framework has to query for the type it has and use reflection to construct it.  If it’s an interface it cannot just construct the item solely based on that information.

  7. Julie says:

    I agree with your conclusions. This leaves us, I think, with the following remarks:

    – The above  is indeed a work around that requires the application to provide a non-empty collection: not always feasible.

    This would imply that, for those ‘empty list with interface type’ situations, the DataGrid would have to provide (e.g.) a protected method to overwrite where we can provide a new instance of the desired class.

    – The decision to cast first according to the declared type, then by reflection, is a “non-documented” feature we have to live with: it could as well be the other way around.

    Thanks for your very nice introduction material. It helps considerably, Julie.

  8. John Bucci says:

    Having some difficulty using a NewItemPlaceholderPosition that is not None. For example, if I set the position to AtEnd, the row displays as expected at the end of the ListView yet not in an editable state where the user can actually enter data.  Is there some property setter or action that must be performed before the user can enter data in the placeholder row?

  9. vinsibal says:

    Assuming you are using my sample, here is one example of how you can enable this.  First of all, the entry points to edit an item or add a new item in the sample are through the Add button and the begin edit command (F2).  If I were to change the placeholder position to AtEnd, one thing I can do is update the BeginEdit logic to allow adding as well:

    private static void OnExecutedBeginEdit(object sender, ExecutedRoutedEventArgs e)

           {

               ListView lv = (sender as Window1).itemsList;

               if (lv.Items[lv.SelectedIndex] == CollectionView.NewItemPlaceholder)

               {

                   (sender as Window1).AddItem();

               }

               else

               {

                   (sender as Window1).EditItem();

               }

               e.Handled = true;

           }

    So if you press F2 on the NewItemPlaceHolder, it will update to an editable state.  So basically, it’s up to you to define the necessary commands or entry points and once you’ve done that you call iecv.AddNew() to actually populate your collection.

  10. Recap In a previous post I introduced the BindingGroups. Well now I want to get into some of the things

  11. Keoz says:

    Hi, could you explain me how can i mannually begin an edit on a DataGrid cell? i want to start editing a cell when i click a button, are you going to post an example using IEditableCollectionView and DataGrid? thanks

  12. keoz@live.com.mx says:

    Hi i have managed to manually open a cell for editing in a DataGrid from WPF Toolkit using your helper functions in one of your DataGrid examples (thanks a lot for those examples!) enabling it for editing with IsEditing property however i notice the BeginningEdit event is not fired using this technique only when i double click the cell, how can i fire this event when i open the cell for editing manually?, however i guess this is not a problem because im using custom logic for editing using an IEditableCollectionView, is it true? thanks

  13. vinsibal says:

    Keoz,

    If you want to get the same capabilities for editing as a double-click but through code, you can select the specific cell, then call dataGrid.BeginEdit().  This should fire the BeginningEdit event.  I would suggest sticking to this instead of settings IsEditing and using custom logic with the collection view unless you have a good understanding of the editing implementation in DataGrid (it really depends how much customzing you are doing though so it can be ok).

    For an example post of IECV and DataGrid, is there anything in particular you are looking for?  DataGrid already makes use of IECV in its implementation.

  14. keoz@live.com.mx says:

    Hi thanks for your answer, this is part of the code im using

    iecv.EditItem(ClientesGrid.SelectedItem);

    foreach (DataGridCellInfo info in ClientesGrid.SelectedCells)

               {

                   GetCell(ClientesGrid.Items.CurrentPosition, ClientesGrid.Columns.IndexOf(info.Column)).IsEditing = true;

                 }

    ClientesGrid.BeginEdit();

    this is handler of a button i want to use when the user wants to edit a record, however the BeginEdit() command does not fire the BeginingEdit event, im using kind of custom logic using an IECV and an observable collection filled with objects obtained from xml data so thats why im using another IECV because in other way I could not edit the grid could I?

    Here im using your GetCell method to open all the cells in the grid for editing, however i still need to find a way to let the cells be open for editing still if the user changes his selection

    thanks

  15. vinsibal says:

    keoz,

    I’m not sure you have to call iecv.EditItem or set each cell’s IsEditing property to true.  calling Grid.BeginEdit() on what is selected should be good enough.

  16. keoz@live.com.mx says:

    i think that a cell edition will change the contents of the cell but not change the collection behind it and since my iecv is a copy of the collection i need to do both editing the cell and when finished editing, copy the contents of the edited cell to the iecv item but im not sure if this is really a copy

    its like this:

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

               iecv = (IEditableCollectionView)view;

               iecv.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;

    thanks

    btw i had alrady post some code but server ate it im afraid 🙁 i explained how i managed to let the cells open until pressing the "accept changes" button

  17. marco.ragogna says:

    Thanks for sharing, I found your code very useful.

  18. vinsibal says:

    Marco,

    You are very welcome.

  19. Tobias says:

    Regarding Julie’s post: If you derive from ObservableCollection<T> like this

    PersonsList : ObservableCollection<Person>

    {

    }

    you will find out that if the initial collection is empty, there won’t be a NewItemPlaceHolder showing up on the view. That’s because PersonsList cannot resolve type T at design time. A workaround that works for me is to pass type T as a parameter into the class like this

    PersonsList<T> : ObservableCollection<T> where T : Person

    {

    }

    This approach will place the NewItemPlaceHolder even if the collection is empty.

  20. vinsibal says:

    Tobias,

    That’s right, nice work there.  With that, it answers your other question about the new item placeholder with no items present.

  21. Marcus says:

    Regarding:

    PersonsList<T> : ObservableCollection<T> where T : Person

    Can anyone think of a way to do this in VB?