1

This problem has been keeping me busy for half a day now and I start to lose my sanity:

I'm using Items for UI Logic stuff. There are "parent" Items, that can contain ObservableCollections of other Items. (Both inherit from the same ItemBase, picture nodes with nodes, sort of recursive) For not having to recreate Observer logic on each "parent" item class, I wanted to add the functionality to the common baseclass, called ItemBase. The idea is, that the parent can just register its ObservableCollections and the baseclass takes care of the event routing and all. The problem is, that I can't seem to find a way to save a reference to these ObservableCollections (of different types with the same baseclass) for the way that generics work.

Here's the code:

public abstract class ItemBase : ViewModelBase
    {
        private List<ObservableItemCollection<ItemBase>> _trackedChildItemsList = new List<ObservableItemCollection<ItemBase>>();

        public event EventHandler<ItemPropertyChangedEventArgs> ChildItemPropertyChanged;
        public event EventHandler<IsDirtyChangedEventArgs> ChildItemIsDirtyChanged;

        public override bool IsDirty
        {
            get { return base.IsDirty || AreAnyChildItemsDirty; }
            set { base.IsDirty = value; }
        }

        private bool AreAnyChildItemsDirty
        {
            get
            {
                return _trackedChildItemsList.Any(i => i.Any(l => l.IsDirty));
            }
        }

        protected void RegisterItemCollection<T>(ObservableItemCollection<T> collection)
            where T : ItemBase
        {
            _trackedChildItemsList.Add(collection); // intellisense underlines 'collection'; cannot convert from 'ObservableItemCollection<T>' to ObservableItemCollection<ItemBase>:

            collection.ItemPropertyChanged += Collection_ItemPropertyChanged;
            collection.ItemIsDirtyChanged += Collection_ItemIsDirtyChanged;
        }

        public override void Dispose()
        {
            foreach (ObservableItemCollection<ItemBase> collection in _trackedChildItemsList)
            {
                collection.ItemPropertyChanged -= Collection_ItemPropertyChanged;
                collection.ItemIsDirtyChanged -= Collection_ItemIsDirtyChanged;
            }

            base.Dispose();
        }

        private void Collection_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs e)
        {
            OnChildItemPropertyChanged(e);
        }

        protected virtual void OnChildItemPropertyChanged(ItemPropertyChangedEventArgs e)
        {
            ChildItemPropertyChanged?.Invoke(this, e);
        }

        private void Collection_ItemIsDirtyChanged(object sender, IsDirtyChangedEventArgs e)
        {
            OnItemIsDirtyChanged(e);
        }

        protected virtual void OnItemIsDirtyChanged(IsDirtyChangedEventArgs e)
        {
            ChildItemIsDirtyChanged?.Invoke(this, e);
        }
    }

As you can see, I'm using a derived, custom type of the ObservableCollection, namely ObservableItemCollection, which takes care of the ItemPropertyChanged and ItemIsDirtyChanged invokation for the collection itself. This allows one to catch those events from the outside. Now, instead of having that 'catching the events' logic in each parent item itself (duplicated), I wanted it to be in a centralized spot, namely the baseclass.

Now the main problem is, that upon registering the ObservableItemCollections, I cannot possibly keep a reference to them since there's no common base. ObservableItemCollection<CustomItem> does not inherit from ObservableItemCollection<ItemBase>, since its a collection. I tried solving the whole thing with generics, however, the above is as far as I got. It fails to compile where i wrote the 'cannot convert from 'ObservableItemCollection' to ObservableItemCollection' comment.

I understand why it fails to compile, however, I can't seem to find a workaround/working solution.

I absolutely need a direct reference to the collections (casted as my custom type ObservableItemCollection), else the whole thingy won't work. You can see in the code that I'm accessing both the events of the collection itself, as well as properties of the ItemBase.

Either way, I can't seem to find a common base for the collections. I tried using dynamics and reflection based casting, Interfaces, a Custom generic ParentItem type, neither worked (i might have overlooked something) and even if it did, it would be rather ugly.

Is it really not possible to achieve what I want with a limited amount of hacking things together? I can't believe that I didn't find a good solution after all the time I've invested in this.

Additional info:

In the parent item i have the following ObservableCollections:

public ObservableItemCollection<SomeItem1> Collection1 { get; set; } = new ObservableItemCollection<SomeItem1>();
public ObservableItemCollection<SomeItem2> Collection2 { get; set; } = new ObservableItemCollection<SomeItem2>();

Where both item types inherit from ItemBase. Then i call the base method RegisterItemCollection in the parent item constructor like so:

RegisterItemCollection(Collection1);
RegisterItemCollection(Collection2);
Dids
  • 137
  • 2
  • 2
  • 18
Stefan R.
  • 21
  • 2
  • Would changing the type of the items in the collection to an interface work for you? So the base is a collection of IBaseItem and your derived items are IDerivedItem that implement IBaseItem. I may have read a bit too fast as you mention that interfaces don't work, but this was the approach that came to my mind immediately. – Paul Palmpje Aug 07 '19 at 14:12
  • Interfaces dont change the problematic of the missing inheritance between `ObservableCollection` and `ObservableCollection` / `ObservableCollection`. It works within the ItemBase class itself, but then i cant invoke the `RegisterItemCollection()` with my custom collections. And i cant change the type of those collections, because they're used for binding etc. – Stefan R. Aug 07 '19 at 14:38
  • A) All the "item collection" methods and properties need to be in an `IItemCollection` interface. The events, whatever the "register" code needs to deal with. Then have a collection of that interface. Or B) `IDisposable`. – 15ee8f99-57ff-4f92-890c-b56153 Aug 07 '19 at 15:48
  • Hmmm … ObserveableCollection … and … interface ICustomItem : IItemBase … would do the trick? – Paul Palmpje Aug 07 '19 at 16:12
  • No it wouldnt do the trick, i've tried that too. See Ed's answer as to why that doesnt work and how to make it work. Thanks anyways! – Stefan R. Aug 08 '19 at 11:43

2 Answers2

1

WPF collection controls have the same problem: How do you define a property which can hold a reference to any kind of generic collection? Answer: Make the property a reference to a non-generic interface that all the collections implement. This is a very general question, and it's the reason why non-generic System.Collections.IEnumerable and System.Collections.IList are still in heavy use throughout the .NET framework, all these years after generics were introduced.

Nothing you're doing in RegisterItemCollection(), IsDirty, or Dispose() needs to care about the type of item in the collection. So take whatever methods and properties you need that code to interact with, and put it all in a non-generic interface or base class. Your base class is already generic (ObservableCollection<T>, I presume), so use an interface.

public interface IObservableItemCollection
{
    event EventHandler<ItemPropertyChangedEventArgs> ItemPropertyChanged;
    event EventHandler<IsDirtyChangedEventArgs> ItemIsDirtyChanged;
    bool IsDirty { get; }
}

public interface IDirtyable
{
    //  I'm pretty sure you'll want this event here, and I think you'll want your collection to 
    //  implement IDirtyable too.
    //event EventHandler<IsDirtyChangedEventArgs> IsDirtyChanged;
    bool IsDirty { get; }
}

public class ObservableItemCollection<T>
    : ObservableCollection<T>, IObservableItemCollection
    where T : IDirtyable
{
    public bool IsDirty => this.Any(item => item.IsDirty);

    public event EventHandler<ItemPropertyChangedEventArgs> ItemPropertyChanged;
    public event EventHandler<IsDirtyChangedEventArgs> ItemIsDirtyChanged;
}

public class ViewModelBase : IDisposable, IDirtyable
{
    public virtual bool IsDirty => true;

    public virtual void Dispose()
    {
    }
}

public class ItemBase : ViewModelBase
{
    private List<IObservableItemCollection> _trackedChildItemsList = new List<IObservableItemCollection>();

    public override bool IsDirty
    {
        get
        {
            return base.IsDirty || _trackedChildItemsList.Any(coll => coll.IsDirty);
        }
    }

    protected void RegisterItemCollection<T>(ObservableItemCollection<T> collection)
                where T : ItemBase
    {
        _trackedChildItemsList.Add(collection);

        collection.ItemPropertyChanged += Collection_ItemPropertyChanged;
        collection.ItemIsDirtyChanged += Collection_ItemIsDirtyChanged;
    }

    public override void Dispose()
    {
        foreach (IObservableItemCollection collection in _trackedChildItemsList)
        {
            collection.ItemPropertyChanged -= Collection_ItemPropertyChanged;
            collection.ItemIsDirtyChanged -= Collection_ItemIsDirtyChanged;
        }

        base.Dispose();
    }

    private void Collection_ItemIsDirtyChanged(object sender, IsDirtyChangedEventArgs e)
    {
    }

    private void Collection_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs e)
    {
    }
}

public class ItemPropertyChangedEventArgs : EventArgs
{
}

public class IsDirtyChangedEventArgs : EventArgs
{
}

You could also do this by making _trackedChildItemsList a collection of IDisposable, and have the collections clear their own event handlers, but a class clearing its own event handlers is pretty gruesome. Shun reflection when conventional OOP can be used to do the job in a readable and maintainable way. And you'd still have to think of something for IsDirty.

  • 1
    Hey Ed, appreciate your elaborate response, it makes a lot of sense to use a non-generic interface for this use case. I adapted a similar solution to your code above and it works great. Gonna mark this as answer. Thanks a lot! – Stefan R. Aug 08 '19 at 11:41
-1

You can not do this since if you could you could do something like

class A {}
class B : A { }
class C : A { }

var list = new List<List<A>>();
var sublist_b = new List<B>();
sublist_b.Add(new B());
list.Add(sublist_b);
var sublist = list.Single();
sublist.Add(new C()); // <- now a List<B> contains an object that ist not if type B or derived B

I would suggest that you only use ObservableItemCollection<ItemBase> to hold your objects.

Ackdari
  • 3,222
  • 1
  • 16
  • 33