Observable LinkedList
Asked Answered
C

6

5

In my WPF app, I have an ItemsControl whose items values are dependant upon the previous item displayed.

The ViewModel is an audio file split into parts of variable length, and i need to display it in such manner, with a DateTime displayed on the right, and that's what i need to calculate (I only know each part's length, i need to calculate the actual time it starts and ends, and the position on the ItemsControl).

--
  ----
      ------------
                  --
                    --------------------

My first approach was to use an ObservableCollection<MyviewModel> but soon enough some horrors occured :

5-way multibinding in which's IMultiValueConverter I'd calculate the value to return and set a property of the DataContext to that value, because I only knew the previous element at runtime.

The previous element was sent using a binding on Relativesource.PreviousData.

Now my problem is that after setting a value from the Converter (which is obviously a bad thing), and actually getting it to work, a regular Collection doesn't have a notion of order in its elements, so when further down the road when i want to add an audio part in the middle of the rest, the display is messed up.

Furthermore, when I'll implement more business logic, I may need to access the audio parts's start and end that are calculated in this converter, and what if it's not displayed yet...?

So that approach was wrong on several levels.

That's where i started googling and found out about LinkedList. Now I'm trying to make a class that is basically an Observable LinkedList (I don't need it to be generic):

public class ObservableSegmentLinkedList : LinkedList<MyViewModel>, INotifyCollectionChanged
    {
        //Overrides ???

        #region INotifyCollectionChanged Members

        public event NotifyCollectionChangedEventHandler CollectionChanged;
        public void OnNotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
            {
                CollectionChanged(this, e);
            }
        }

        #endregion
    }

And the heart of the problem is that i can't override the methods that modify the collection (Addfirst, AddLast etc), so i can't call OnNotifyCollectionChanged properly...

So I'm thinking i could make overloads for each of these methods, but that sounds quite nasty...

In short: I need some kind of collection in which each item knows details of the previous one in order to calculate one of its own properties.

Any clues? is this even a good solution?

Thanks!

Appendix, the ViewModel looks like:

public class MyViewModel : INotifyPropertyChanged
    {
        private DateTime m_SegmentLength;
        public DateTime SegmentLength
        {
            get { return m_SegmentLength; }
            set
            {
                m_SegmentLength = value;
                NotifyPropertyChanged("SegmentLength");
            }
        }

        private DateTime m_SegmentAdvert;
        public DateTime SegmentAdvert
        {
            get { return m_SegmentAdvert; }
            set
            {
                m_SegmentAdvert = value;
                NotifyPropertyChanged("SegmentAdvert");
            }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String prop)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

        #endregion
    }

EDIT: i think i will try to combine Thomas and Will's answers: I'll use composition (i.e I keep an instance of LinkedList in my custom object instead of inheriting from it) and redefine methods that are meant to be used (AddAfter, AddFirst etc) in which i'll just call OnNotifyPropertychanged after calling the actual LinkedList method. It's a bit of work but i guess there won't be any elegant solution to my problem...

Claudeclaudel answered 9/8, 2011 at 12:49 Comment(0)
C
7

Ok now, I made a custom generic class that supports IEnumerable and is used as if it was a LinkedList<T>, with the only difference that WPF gets notified of the changes.

Please note that this solution only works for a reasonably small collection, I only have to manage around 30 elements max, so it's fine for me, but everytime you modify this collection, it is considered "Reset".

Here goes the solution:

    /// <summary>
    /// This class is a LinkedList that can be used in a WPF MVVM scenario. Composition was used instead of inheritance,
    /// because inheriting from LinkedList does not allow overriding its methods.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ObservableLinkedList<T> : INotifyCollectionChanged, IEnumerable
    {
        private LinkedList<T> m_UnderLyingLinkedList;

        #region Variables accessors
        public int Count
        {
            get { return m_UnderLyingLinkedList.Count; }
        }

        public LinkedListNode<T> First
        {
            get { return m_UnderLyingLinkedList.First; }
        }

        public LinkedListNode<T> Last
        {
            get { return m_UnderLyingLinkedList.Last; }
        }
        #endregion

        #region Constructors
        public ObservableLinkedList()
        {
            m_UnderLyingLinkedList = new LinkedList<T>();
        }

        public ObservableLinkedList(IEnumerable<T> collection)
        {
            m_UnderLyingLinkedList = new LinkedList<T>(collection);
        }
        #endregion

        #region LinkedList<T> Composition
        public LinkedListNode<T> AddAfter(LinkedListNode<T> prevNode, T value)
        {
            LinkedListNode<T> ret = m_UnderLyingLinkedList.AddAfter(prevNode, value);
            OnNotifyCollectionChanged();
            return ret;
        }

        public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode)
        {
            m_UnderLyingLinkedList.AddAfter(node, newNode);
            OnNotifyCollectionChanged();
        }

        public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value)
        {
            LinkedListNode<T> ret = m_UnderLyingLinkedList.AddBefore(node, value);
            OnNotifyCollectionChanged();
            return ret;
        }

        public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode)
        {
            m_UnderLyingLinkedList.AddBefore(node, newNode);
            OnNotifyCollectionChanged();
        }

        public LinkedListNode<T> AddFirst(T value)
        {
            LinkedListNode<T> ret = m_UnderLyingLinkedList.AddFirst(value);
            OnNotifyCollectionChanged();
            return ret;
        }

        public void AddFirst(LinkedListNode<T> node)
        {
            m_UnderLyingLinkedList.AddFirst(node);
            OnNotifyCollectionChanged();
        }

        public LinkedListNode<T> AddLast(T value)
        {
            LinkedListNode<T> ret = m_UnderLyingLinkedList.AddLast(value);
            OnNotifyCollectionChanged();
            return ret;
        }

        public void AddLast(LinkedListNode<T> node)
        {
            m_UnderLyingLinkedList.AddLast(node);
            OnNotifyCollectionChanged();
        }

        public void Clear()
        {
            m_UnderLyingLinkedList.Clear();
            OnNotifyCollectionChanged();
        }

        public bool Contains(T value)
        {
            return m_UnderLyingLinkedList.Contains(value);
        }

        public void CopyTo(T[] array, int index)
        {
            m_UnderLyingLinkedList.CopyTo(array, index);
        }

        public bool LinkedListEquals(object obj)
        {
            return m_UnderLyingLinkedList.Equals(obj);
        }

        public LinkedListNode<T> Find(T value)
        {
            return m_UnderLyingLinkedList.Find(value);
        }

        public LinkedListNode<T> FindLast(T value)
        {
            return m_UnderLyingLinkedList.FindLast(value);
        }

        public Type GetLinkedListType()
        {
            return m_UnderLyingLinkedList.GetType();
        }

        public bool Remove(T value)
        {
            bool ret = m_UnderLyingLinkedList.Remove(value);
            OnNotifyCollectionChanged();
            return ret;
        }

        public void Remove(LinkedListNode<T> node)
        {
            m_UnderLyingLinkedList.Remove(node);
            OnNotifyCollectionChanged();
        }

        public void RemoveFirst()
        {
            m_UnderLyingLinkedList.RemoveFirst();
            OnNotifyCollectionChanged();
        }

        public void RemoveLast()
        {
            m_UnderLyingLinkedList.RemoveLast();
            OnNotifyCollectionChanged();
        }
        #endregion

        #region INotifyCollectionChanged Members

        public event NotifyCollectionChangedEventHandler CollectionChanged;
        public void OnNotifyCollectionChanged()
        {
            if (CollectionChanged != null)
            {
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (m_UnderLyingLinkedList as IEnumerable).GetEnumerator();
        }

        #endregion
    }

As mentionned by @AndrewS in the comments, LinkedListNode should be replaced with a custom class that returns an ObservableLinkedList from its List property.

Claudeclaudel answered 9/8, 2011 at 15:40 Comment(4)
You probably want to exclude the methods that return a LinkedListNode<T> since that publicly exposes the internal LinkedList<T> you're wrapping (via its List property). Or create your own class that you can return instead.Sjambok
@Sjambok Why would I want to do that? What is to be done when accessing nodes?Claudeclaudel
Because it exposes the internal LinkedList publicly to any user of the observablelinkedlist so someone could manipulate that directly and that would not result in a collection change notification.Sjambok
@Sjambok You're right, it should be adressed. I don't use that property myself but it could lead to subtle bugs. I'll edit.Claudeclaudel
E
3

LinkedList<T> isn't designed for inheritance: most of its methods are not virtual, so there is nothing to override. If you want to reuse its implementation and implement INotifyCollectionChanged, use composition, not inheritance.

But anyway, it wouldn't make sense to implement an observable linked list, because a linked list doesn't support random access by index, and CollectionChanged notifications are only useful if you specify an index (unless you only raise NotifyCollectionChangedAction.Reset notifications, but then it's not very efficient)

Ellen answered 9/8, 2011 at 12:58 Comment(2)
NotifyCollectionChangedAction.Reset does do the deed, and anyway it's highly unlikely that I get more than 20 segments. Any other downside? I'm actually thinking of just calling OnNotifyCollectionChanged(NotifyCollectionChangedAction.Reset) on the CollectionChanged event that I'd subscribe to.Claudeclaudel
I can't think of any other downside... as long as the collection is small, it shouldn't have a major impact of performanceEllen
M
1

It is a good solution, you'll just have to create your own implementation of a LinkedList.

LinkedList<T> does not implement any linky listy interface, so you are on your own as to the methods/properties. I suppose a good guide would be to duplicate the public methods and properties of LinkedList<T>. This would allow you to use an actual instance of the collection as your backing store.

Masaccio answered 9/8, 2011 at 12:59 Comment(2)
I'd just like not to have to manually call OnNotifyCollectionChanged manually each time i use a method of LinkedList, but it's bad design in my opinion to leave methods that shouldn't be used...Claudeclaudel
@Baboon: Simple. Create a custom attribute that details the kind of updates a method causes, then use a T4 template to generate partial classes for types whose members are decorated with those attributes. Well, not so simple I guess (although I've done this for creating custom TypeDescriptors for classes, which are a drudgery to make). I don't think there's any easy way around this, unfortunately. But once you've written it you can use it over and over again!Masaccio
F
0

I think that eventually the simpler solution would be to calculate the start and end time beforehand and add them as properties on the ViewModel. Especially since you say that you may need this value in your business logic.

Flotage answered 9/8, 2011 at 13:20 Comment(2)
The start time and the end time depends on where the part is placed, it is highly variable.Claudeclaudel
When you move parts around, you can recalculate it. I mean...what do you think happens when you use converters and bindings, especially if you're going to use the reset event? :) I'd say one method that iterates through your list and sets the start and end time is much simpler than adding a new collection class and using several converters.Flotage
S
0

It sounds to me like you have two different issues. One is managing a list of items for display, and the other is allowing an item to access its preceding and following items.

That's how I'd approach it: add Previous and Next properties to the item class, set them when initially populating the collection, and then updating them when I insert and remove items from the list.

If you really want to go nuts and make a generic solution, you could implement an ILinkedListNode interface, and then subclass ObservableCollection<T> where T : ILinkedListNode, overriding the various insert and remove methods to update the items' Previous and Next properties. That's what I'd do if I needed the solution to be reusable.

But if not, I'd just make a view model class that wrapped the collection, exposing it as an Items property, and then implemented insert and remove commands that the UI can bind to.

Salinasalinas answered 9/8, 2011 at 13:25 Comment(0)
B
0

If you had made your class like this:

public class ObservableSegmentLinkedList<T> : LinkedList<T>, INotifyCollectionChanged 
{
...
public new void AddFirst(T value)
{
.. do something to make it your own - you can still call the base method so in effect you override it with the new keyword.
}
}

You should not have any problem overriding the methods with new keyword.
Notice the Class itself has a TYPE Specifier that matches your Linked List Type.

I made it generic here but you can put whatever you want between those < MyType > . Generic just means you can use this thing in a lot more places than 1 including a future project.

Burka answered 27/8, 2015 at 15:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.