Dan
Dan

Reputation: 1243

How to handle updates to a WPF DataGrid manually?

The Items for my DataGrid is an IList. The only way the IList is updated is via methods on the class of which it is a property.

class SomeObject
{
    public ReadOnlyCollection<SomeType> Items { get; }

    public void AddItem(SomeType someType);

    public event Action<SomeType> ItemAdded;
}

The list itself is a read-only collection and cannot be updated directly - iow, it is definitely not an ObservableCollection. Is there a way I can bind to the DataGrid for display purposes, but handle the create, update, and delete to the items myself using binding/datagrid hooks of some kind?

Upvotes: 1

Views: 1612

Answers (2)

Dan
Dan

Reputation: 1243

Create a proxy for your collection that implements both IBindingList and ICancelAddNew. With these two you can "intercept" calls to add new items and remove existing ones. The method AddNew() is called when a new item is added, and Remove/RemoveAt is called when an item is deleted. Instead of directly modifying the collection, at these times you can call API methods designed for that purpose. Below is the minimal implementation required. Note a few things:

  1. New items are not immediately added to the collection - the are stored temporarily in a _newItem field

  2. If escape is hit while editing a new item, first CancelNew, then EndNew is called

  3. It is assumed 'Items' is an observable collection, the events from which trigger corresponding ListChanged events

  4. This technique doesn't provide a means to intercept edits made to properties of existing items

...

class SomeTypeBindingList : IBindingList, ICancelAddNew
{
    public SomeTypeBindingList(SomeObject someObject)
    {
        _someObject = someObject;

        var observableCollection = _someObject.Items as ObservableCollection<SomeType>;
        if (observableCollection != null)
            observableCollection.CollectionChanged += ObservableCollectionOnCollectionChanged;
    }

    public IEnumerator GetEnumerator()
    {
        return new SomeTypeEnumerator(this);
    }

    public int Count => _someObject.Items.Count + (_newItem == null ? 0 : 1);

    public object SyncRoot { get; } = new object();

    public bool IsSynchronized { get; } = false;

    public bool Contains(object value)
    {
        return IndexOf(value) != -1;
    }

    public int IndexOf(object value)
    {
        if (ReferenceEquals(value, _newItem))
            return _someObject.Items.Count;

        return _someObject.Items.IndexOf((SomeType)value);
    }

    public void Remove(object value)
    {
        var someType = (SomeType)value;
        _someObject.RemoveItem(someType);
    }

    public void RemoveAt(int index)
    {
        var someType = _someObject.Items[index];
        _someObject.RemoveItem(someType);
    }

    public object this[int index]
    {
        get
        {
            if (index >= _someObject.Items.Count)
            {
                if(_newItem == null)
                    throw new IndexOutOfRangeException();

                return _newItem;
            }

            return _someObject.Items[index];
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public object AddNew()
    {
        _newItem = new SomeType();

        ListChanged?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemAdded, _someObject.Items.Count));
        return _newItem;
    }

    public void CancelNew(int itemIndex)
    {
        _newItem = null;
        ListChanged?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemDeleted, itemIndex));
    }

    public void EndNew(int itemIndex)
    {
        if (_newItem != null)
        {
            var someType = _newItem;
            _newItem = null;
            _someObject.AddItem(someType);
        }
    }

    private void ObservableCollectionOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Remove)
            Enumerable.Range(e.OldStartingIndex, e.OldItems.Count).ForEach(i => ListChanged?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemDeleted, i)));
        else if(e.Action == NotifyCollectionChangedAction.Add)
            Enumerable.Range(e.NewStartingIndex, e.NewItems.Count).ForEach(i => ListChanged?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemAdded, i)));
    }

    private readonly SomeObject _someObject;
    private SomeType _newItem;

    class SomeTypeEnumerator : IEnumerator
    {
        public SomeTypeEnumerator(SomeObject someObject)
        {
            _someObject = someObject;
            Reset();
        }

        public void Dispose()
        {

        }

        public bool MoveNext()
        {
            _index++;
            return _someObject.Items.Count < _index;
        }

        public void Reset()
        {
            _index = -1;
        }

        public object Current => _someObject.Items[_index];

        object IEnumerator.Current
        {
            get { return Current; }
        }

        private readonly SomeObject _someObject;
        private int _index;
    }
}

Upvotes: 1

mm8
mm8

Reputation: 169190

You can bind the ItemsSource property of a DataGrid to any public property that returns an IEnumerable, inclucing an IList<T> property. You can not bind to fields though so you should make Items a property if you intend to bind to it:

public IList<SomeType> Items { get; private set; }

But for you to be able to add items to the source collection dynamically at runtime and have the new items automatically show up in the DataGrid, the source collection must implement the INotifyCollectionChanged interface. Only the ObservableCollection<T> class does this in the .NET Framework. A List<T> does not.

And an IList<T> is not a read-only collection since it has an Add method: https://msdn.microsoft.com/en-us/library/system.collections.ilist.add%28v=vs.110%29.aspx. So I guess you might as well use an ObservableCollection<T>.

Edit:

If you really want to refesh the DataGrid "manually" you could subscribe to the ItemAdded event of your object in the code-behind of the view and use the UpdateTarget() method of the BindingExpression:

someObject.ItemAdded += (se, ee) => 
{
    var be = BindingOperations.GetBindingExpression(theDataGrid, DataGrid.ItemsSourceProperty);
    if (be != null)
        be.UpdateTarget();

};

Or you could just reset the ItemsSource property:

someObject.ItemAdded += (se, ee) => 
{
    theDataGrid.ItemsSource = someObject.Items;

};

Edit 2:

My issue is that I need a reliable way to intercept the outbound binding mechanism of the grid so that I can call AddItem() when a new row is added, for example. I've experimented with IBindingList to see if I could use that, but as of yet it has not panned out.

If you bind the ItemsSource property of the DataGrid to an ObservableCollection<SomeType>, you could handle the CollectionChanged event of this collection:

observableCollection.CollectionChanged += (ss, ee) =>
{
    if(ee.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
    {
        SomeType newItem = ee.NewItems[0] as SomeType;
        someObject.AddItem(newItem);
    }
};

This event will be raised when the DataGrid adds a new item to the source collection.

Upvotes: 1

Related Questions