What is the best way to force the WPF DataGrid to add a specific new item?
-
28-05-2021 - |
Question
I have a DataGrid
in a WPF application which has for its ItemsSource
a custom collection that I wrote. The collection enforces that all its items satisfy a certain requirement (namely they must be between some minimum and maximum values).
The collection's class signature is:
public class CheckedObservableCollection<T> : IList<T>, ICollection<T>, IList, ICollection,
INotifyCollectionChanged
where T : IComparable<T>, IEditableObject, ICloneable, INotifyPropertyChanged
I want to be able to use the DataGrid
feature in which committing an edit on the last row in the DataGrid
results in a new item being added to the end of the ItemsSource
.
Unfortunately the DataGrid
simply adds a new item created using the default constructor. So, when adding a new item, DataGrid
indirectly (through its ItemCollection
which is a sealed class) declares:
ItemsSource.Add(new T())
where T is the type of elements in the CheckedObservableCollection. I would like for the grid to instead add a different T, one that satisfies the constraints imposed on the collection.
My questions are: Is there a built in way to do this? Has somebody done this already? What's the best (easiest, fastest to code; performance is not an issue) way to do this?
Currently I just derived DataGrid
to override the OnExecutedBeginEdit
function with my own as follows:
public class CheckedDataGrid<T> : DataGrid where T : IEditableObject, IComparable<T>, INotifyPropertyChanged, ICloneable
{
public CheckedDataGrid() : base() { }
private IEditableCollectionView EditableItems {
get { return (IEditableCollectionView)Items; }
}
protected override void OnExecutedBeginEdit(ExecutedRoutedEventArgs e) {
try {
base.OnExecutedBeginEdit(e);
} catch (ArgumentException) {
var source = ItemsSource as CheckedObservableCollection<T>;
source.Add((T)source.MinValue.Clone());
this.Focus();
}
}
}
Where MinValue
is the smallest allowable item in the collection.
I do not like this solution. If any of you have advice I would be very appreciative!
Thanks
Solution 2
For anybody interested, I ended up solving the problem by just deriving from BindingList<T>
instead of ObservableCollection<T>
, using my derived class as the ItemsSource
in a regular DataGrid
:
public class CheckedBindingList<T> : BindingList<T>, INotifyPropertyChanged where T : IEditableObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Predicate<T> _check;
private DefaultProvider<T> _defaultProvider;
public CheckedBindingList(Predicate<T> check, DefaultProvider<T> defaultProvider) {
if (check == null)
throw new ArgumentNullException("check cannot be null");
if (defaultProvider != null && !check(defaultProvider()))
throw new ArgumentException("defaultProvider does not pass the check");
_check = check;
_defaultProvider = defaultProvider;
}
/// <summary>
/// Predicate the check item in the list against.
/// All items in the list must satisfy Check(item) == true
/// </summary>
public Predicate<T> Check {
get { return _check; }
set {
if (value != _check) {
RaiseListChangedEvents = false;
int i = 0;
while (i < Items.Count)
if (!value(Items[i]))
++i;
else
RemoveAt(i);
RaiseListChangedEvents = true;
SetProperty(ref _check, value, "Check");
ResetBindings();
}
}
}
public DefaultProvider<T> DefaultProvider {
get { return _defaultProvider; }
set {
if (!_check(value()))
throw new ArgumentException("value does not pass the check");
}
}
protected override void OnAddingNew(AddingNewEventArgs e) {
if (e.NewObject != null)
if (!_check((T)e.NewObject)) {
if (_defaultProvider != null)
e.NewObject = _defaultProvider();
else
e.NewObject = default(T);
}
base.OnAddingNew(e);
}
protected override void OnListChanged(ListChangedEventArgs e) {
switch (e.ListChangedType) {
case (ListChangedType.ItemAdded):
if (!_check(Items[e.NewIndex])) {
RaiseListChangedEvents = false;
RemoveItem(e.NewIndex);
if (_defaultProvider != null)
InsertItem(e.NewIndex, _defaultProvider());
else
InsertItem(e.NewIndex, default(T));
RaiseListChangedEvents = true;
}
break;
case (ListChangedType.ItemChanged):
if (e.NewIndex >= 0 && e.NewIndex < Items.Count) {
if (!_check(Items[e.NewIndex])) {
Items[e.NewIndex].CancelEdit();
throw new ArgumentException("item did not pass the check");
}
}
break;
default:
break;
}
base.OnListChanged(e);
}
protected void SetProperty<K>(ref K field, K value, string name) {
if (!EqualityComparer<K>.Default.Equals(field, value)) {
field = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
This class is incomplete, but the implementation above is enough for validating lists of statically-typed (not built by reflection or with the DLR) objects or value types.
OTHER TIPS
This problem is now semi-solvable under 4.5
using the AddingNewItem
event of the DataGrid
. Here is my answer to a similar question.
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][2].AddingNewItem
fires before anything else; theNewItem
property of theEventArgs
is simplynull
.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 theAddingNewItemEventArgs
, 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 exceptionDecompiling the control lets us see what's happening in there...