Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use a factory for DataGrid.CanUserAddRows = true

I would like to use the DataGrid.CanUserAddRows = true feature. Unfortunately, it seems to work only with concrete classes which have a default constructor. My collection of business objects doesn't provide a default constructor.

I'm looking for a way to register a factory that knows how to create the objects for the DataGrid. I had a look at the DataGrid and the ListCollectionView but none of them seems to support my scenario.

like image 346
jbe Avatar asked Dec 19 '10 17:12

jbe


1 Answers

The problem:

"I'm looking for a way to register a factory that knows how to create the objects for the DataGrid". (Because my collection of business objects doesn't provide a default constructor.)

The symptoms:

If we set DataGrid.CanUserAddRows = true and then bind a collection of items to the DataGrid where the item doesn't have a default constructor, then the DataGrid doesn't show a 'new item row'.

The causes:

When a collection of items is bound to any WPF ItemControl, WPF wraps the collection in either:

  1. a BindingListCollectionView when the collection being bound is a BindingList<T>. BindingListCollectionView implements IEditableCollectionView but doesn't implement IEditableCollectionViewAddNewItem.

  2. a ListCollectionView when the collection being bound is any other collection. ListCollectionView implements IEditableCollectionViewAddNewItem (and hence IEditableCollectionView).

For option 2) the DataGrid delegates creation of new items to the ListCollectionView. ListCollectionView internally tests for the existence of a default constructor and disables AddNew if one doesn't exist. Here's the relevant code from ListCollectionView using DotPeek.

public bool CanAddNewItem (method from IEditableCollectionView)
{
  get
  {
    if (!this.IsEditingItem)
      return !this.SourceList.IsFixedSize;
    else
      return false;
  }
}

bool CanConstructItem
{
  private get
  {
    if (!this._isItemConstructorValid)
      this.EnsureItemConstructor();
    return this._itemConstructor != (ConstructorInfo) null;
  }
}

There doesn't seem to be an easy way to override this behaviour.

For option 1) the situation is a lot better. The DataGrid delegates creation of new items to the BindingListView, which in turn delegates to BindingList. BindingList<T> also checks for the existence of a default constructor, but fortunately BindingList<T> also allows the client to set the AllowNew property and attach an event handler for supplying a new item. See the solution later, but here's the relevant code in BindingList<T>

public bool AllowNew
{
  get
  {
    if (this.userSetAllowNew || this.allowNew)
      return this.allowNew;
    else
      return this.AddingNewHandled;
  }
  set
  {
    bool allowNew = this.AllowNew;
    this.userSetAllowNew = true;
    this.allowNew = value;
    if (allowNew == value)
      return;
    this.FireListChanged(ListChangedType.Reset, -1);
  }
}

Non-solutions:

  • Support by DataGrid (not available)

It would reasonable to expect the DataGrid to allow the client to attach a callback, through which the DataGrid would request a default new item, just like BindingList<T> above. This would give the client the first crack at creating a new item when one is required.

Unfortunately this isn't supported directly from the DataGrid, even in .NET 4.5.

.NET 4.5 does appear to have a new event 'AddingNewItem' that wasn't available previously, but this only lets you know a new item is being added.

Work arounds:

  • Business object created by a tool in the same assembly: use a partial class

This scenario seems very unlikely, but imagine that Entity Framework created its entity classes with no default constructor (not likely since they wouldn't be serializable), then we could simply create a partial class with a default constructor. Problem solved.

  • Business object is in another assembly, and isn't sealed: create a super-type of the business object.

Here we can inherit from the business object type and add a default constructor.

This initially seemed like a good idea, but on second thoughts this may require more work than is necessary because we need to copy data generated by the business layer into our super-type version of the business object.

We would need code like

class MyBusinessObject : BusinessObject
{
    public MyBusinessObject(BusinessObject bo){ ... copy properties of bo }
    public MyBusinessObject(){}
}

And then some LINQ to project between lists of these objects.

  • Business object is in another assembly, and is sealed (or not): encapsulate the business object.

This is much easier

class MyBusinessObject
{
    public BusinessObject{ get; private set; }

    public MyBusinessObject(BusinessObject bo){ BusinessObject = bo;  }
    public MyBusinessObject(){}
}

Now all we need to do is use some LINQ to project between lists of these objects, and then bind to MyBusinessObject.BusinessObject in the DataGrid. No messy wrapping of properties or copying of values required.

The solution: (hurray found one)

  • Use BindingList<T>

If we wrap our collection of business objects in a BindingList<BusinessObject> and then bind the DataGrid to this, with a few lines of code our problem is solved and the DataGrid will appropriately show a new item row.

public void BindData()
{
   var list = new BindingList<BusinessObject>( GetBusinessObjects() );
   list.AllowNew = true;
   list.AddingNew += (sender, e) => 
       {e.NewObject = new BusinessObject(... some default params ...);};
}

Other solutions

  • implement IEditableCollectionViewAddNewItem on top of an existing collection type. Probably a lot of work.
  • inherit from ListCollectionView and override functionality. I was partially successful trying this, probably can be done with more effort.
like image 100
Phil Avatar answered Nov 16 '22 01:11

Phil