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

Was it helpful?

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; 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...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top