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.
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:
a BindingListCollectionView when the collection being bound is a BindingList<T>
. BindingListCollectionView
implements IEditableCollectionView but doesn't implement IEditableCollectionViewAddNewItem
.
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)
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.
I've found another solution to this problem. In my case, my objects need to be initialized using a factory, and there isn't really any way to get around that.
I couldn't use BindingList<T>
because my collection must support grouping, sorting, and filtering, which BindingList<T>
does not support.
I solved the problem by using DataGrid's AddingNewItem
event. This almost entirely undocumented event not only tells you a new item is being added, but also allows lets you choose which item is being added. AddingNewItem
fires before anything else; the NewItem
property of the EventArgs
is simply null
.
Even if you provide a handler for the event, DataGrid will refuse to allow the user to add rows if the class doesn't have a default constructor. However, bizarrely (but thankfully) if you do have one, and set the NewItem
property of the AddingNewItemEventArgs
, it will never be called.
If you choose to do this, you can make use of attributes such as [Obsolete("Error", true)]
and [EditorBrowsable(EditorBrowsableState.Never)]
in order to make sure no one ever invokes the constructor. You can also have the constructor body throw an exception
Decompiling the control lets us see what's happening in there.
private object AddNewItem()
{
this.UpdateNewItemPlaceholder(true);
object newItem1 = (object) null;
IEditableCollectionViewAddNewItem collectionViewAddNewItem = (IEditableCollectionViewAddNewItem) this.Items;
if (collectionViewAddNewItem.CanAddNewItem)
{
AddingNewItemEventArgs e = new AddingNewItemEventArgs();
this.OnAddingNewItem(e);
newItem1 = e.NewItem;
}
object newItem2 = newItem1 != null ? collectionViewAddNewItem.AddNewItem(newItem1) : this.EditableItems.AddNew();
if (newItem2 != null)
this.OnInitializingNewItem(new InitializingNewItemEventArgs(newItem2));
CommandManager.InvalidateRequerySuggested();
return newItem2;
}
As we can see, in version 4.5
, the DataGrid does indeed make use of AddNewItem
. The contents of CollectionListView.CanAddNewItem
are simply:
public bool CanAddNewItem
{
get
{
if (!this.IsEditingItem)
return !this.SourceList.IsFixedSize;
else
return false;
}
}
So this doesn't explain why we we still need to have a constructor (even if it is a dummy) in order for the add row option to appear. I believe the answer lies in some code that determines the visibility of the NewItemPlaceholder
row using CanAddNew
rather than CanAddNewItem
. This might be considered some sort of bug.
I had a look at IEditableCollectionViewAddNewItem and it seems to be adding this functionality.
From MSDN
The IEditableCollectionViewAddNewItem
interface enables application
developers to specify what type of
object to add to a collection. This
interface extends
IEditableCollectionView, so you can
add, edit, and remove items in a
collection.
IEditableCollectionViewAddNewItem adds
the AddNewItem method, which takes an
object that is added to the
collection. This method is useful when
the collection and objects that you
want to add have one or more of the
following characteristics:
- The objects in the CollectionView are different types.
- The objects do not have a default constructor.
- The object already exists.
- You want to add a null object to the collection.
Although at Bea Stollnitz blog, you can read the following
- The limitation of not being able to add a new item when the source has no
default constructor is very well
understood by the team. WPF 4.0 Beta 2
has a new feature that brings us a
step closer to having a solution: the
introduction of
IEditableCollectionViewAddNewItem
containing the AddNewItem method. You
can read the MSDN documentation about
this feature. The sample in MSDN shows
how to use it when creating your own
custom UI to add a new item (using a
ListBox to display the data and a
dialog box to enter the new item).
From what I can tell, DataGrid doesn’t
yet use this method though (although
it’s a bit hard to be 100% sure
because Reflector doesn’t decompile
4.0 Beta 2 bits).
That answer is from 2009 so maybe it's usable for the DataGrid now
The simplest way I could suggest to provide wrapper for your class without default constructor, in which constructor for source class will be called.
For example you have this class without default constructor:
/// <summary>
/// Complicate class without default constructor.
/// </summary>
public class ComplicateClass
{
public ComplicateClass(string name, string surname)
{
Name = name;
Surname = surname;
}
public string Name { get; set; }
public string Surname { get; set; }
}
Write a wrapper for it:
/// <summary>
/// Wrapper for complicated class.
/// </summary>
public class ComplicateClassWraper
{
public ComplicateClassWraper()
{
_item = new ComplicateClass("def_name", "def_surname");
}
public ComplicateClassWraper(ComplicateClass item)
{
_item = item;
}
public ComplicateClass GetItem() { return _item; }
public string Name
{
get { return _item.Name; }
set { _item.Name = value; }
}
public string Surname
{
get { return _item.Surname; }
set { _item.Surname = value; }
}
ComplicateClass _item;
}
Codebehind.
In your ViewModel you need to create wrapper collection for your source collection, which will handle item adding/removing in datagrid.
public MainWindow()
{
// Prepare collection with complicated objects.
_sourceCollection = new List<ComplicateClass>();
_sourceCollection.Add(new ComplicateClass("a1", "b1"));
_sourceCollection.Add(new ComplicateClass("a2", "b2"));
// Do wrapper collection.
WrappedSourceCollection = new ObservableCollection<ComplicateClassWraper>();
foreach (var item in _sourceCollection)
WrappedSourceCollection.Add(new ComplicateClassWraper(item));
// Each time new item was added to grid need add it to source collection.
// Same on delete.
WrappedSourceCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
InitializeComponent();
DataContext = this;
}
void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
foreach (ComplicateClassWraper wrapper in e.NewItems)
_sourceCollection.Add(wrapper.GetItem());
else if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (ComplicateClassWraper wrapper in e.OldItems)
_sourceCollection.Remove(wrapper.GetItem());
}
private List<ComplicateClass> _sourceCollection;
public ObservableCollection<ComplicateClassWraper> WrappedSourceCollection { get; set; }
}
And finally, XAML code:
<DataGrid CanUserAddRows="True" AutoGenerateColumns="False"
ItemsSource="{Binding Path=Items}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="SecondName" Binding="{Binding Path=Surname}"/>
</DataGrid.Columns>
</DataGrid>