Pages

Friday, 13 September 2013

C# ObservableList

I've long since needed a observable list that can handle changes from multiple threads. There are quite a few variations that people have put together to combat this issue but none ever worked well for me.

The ObservableCollection class has pretty much everything I needed with the exception that the collection can only be modified from the thread that owns it. So I started with that and looked to see what made it tick and it was there that I found what I needed - INotifyCollectionChanged.

INotifyCollectionChanged, when implemented in a class, provides notifications that a collection has changed and allows to UI refresh accordingly. The trick I used to make this multithread friendly is to pass the UI Dispatcher to the ObserableList constructor when creating the object. This allows us to Invoke the CollectionChanged event from INotifyCollectionChanged on the UI thread when working on another thread. This is particularly handy, for example, for when a change to the collection is made in an asyncronous callback method.

For my solution I decided to go with a List as my collection base, implementing IList to provide the methods to work with the collection. And thus, the ObservableList is born.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;

namespace MotoCommander
{
    public class ObservableList<T> : IList<T>, INotifyCollectionChanged
    {
        #region INotifyCollectionChanged

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        private void NotifyCollectionChanged(NotifyCollectionChangedAction pAction, object pItem)
        {
            if (CollectionChanged != null)
            {
                if (Thread.CurrentThread == _Dispatcher.Thread)
                    CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pItem));
                else
                    _Dispatcher.Invoke(new Action(() => CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pItem))));
            }
        }

        private void NotifyCollectionChanged(NotifyCollectionChangedAction pAction, object pItem, int piIndex)
        {
            if (CollectionChanged != null)
                if (Thread.CurrentThread == _Dispatcher.Thread)
                    CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pItem, piIndex));
                else
                    _Dispatcher.Invoke(new Action(() => CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pItem, piIndex))));

        }

        private void NotifyCollectionChanged(NotifyCollectionChangedAction pAction, IList<T> pList)
        {
            if (CollectionChanged != null)
                if (Thread.CurrentThread == _Dispatcher.Thread)
                    CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pList));
                else
                    _Dispatcher.Invoke(new Action(() => CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction, pList))));

        }

        private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            if (CollectionChanged != null)
                if (Thread.CurrentThread == _Dispatcher.Thread)
                    CollectionChanged(this, args);
                else
                    _Dispatcher.Invoke(new Action(() => CollectionChanged(this, args)));

        }

        private void NotifyCollectionChanged(NotifyCollectionChangedAction pAction)
        {
            if (CollectionChanged != null)
                if (Thread.CurrentThread == _Dispatcher.Thread)
                    CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction));
                else
                    _Dispatcher.Invoke(new Action(() => CollectionChanged(this, new NotifyCollectionChangedEventArgs(pAction))));
        }

        #endregion

        #region IEnumerable

        public IEnumerator<T> GetEnumerator()
        {
            foreach (var item in _List)
            {
                if (item == null)
                    break;

                yield return item;
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion

        #region Class Declarations

        private List<T> _List = new List<T>();
        private Dispatcher _Dispatcher = null;

        #endregion

        #region Constructor

        public ObservableList(Dispatcher pDispatcher)
        {
            _Dispatcher = pDispatcher;
        }

        public ObservableList(Dispatcher pDispatcher, List<T> pList)
        {
            _Dispatcher = pDispatcher;

            foreach (var item in pList)
            {
                _List.Add(item);
            }

        }

        #endregion

        #region IList Members

        public int IndexOf(T item)
        {
            return _List.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            _List.Insert(index, item);
            NotifyCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
        }

        public void RemoveAt(int index)
        {
            if (index > -1)
            {
                _List.RemoveAt(index);
                NotifyCollectionChanged(NotifyCollectionChangedAction.Remove, _List[index], index);
            }
        }

        public T this[int index]
        {
            get
            {
                if (index > -1 && _List.Count > 0)
                    return _List.ElementAt(index);
                else
                    return default(T);
            }
            set
            {
                if (index > -1)
                {
                    if (index > _List.Count)
                    {
                        _List.Add(value);
                        NotifyCollectionChanged(NotifyCollectionChangedAction.Add, value);
                    }
                    else
                    {
                        _List[index] = value;
                        NotifyCollectionChanged(NotifyCollectionChangedAction.Replace, value, index);
                    }
                }
            }
        }

        public void Add(T item)
        {
            if (item != null)
            {
                _List.Add(item);
                NotifyCollectionChanged(NotifyCollectionChangedAction.Add, item);
            }
        }

        public void Clear()
        {
            _List.Clear();
            NotifyCollectionChanged(NotifyCollectionChangedAction.Reset);
        }

        public bool Contains(T item)
        {
            return _List.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            _List.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return _List.Count; }
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(T item)
        {
            bool bReturn = false;

            if (item != null)
            {
                int index = _List.IndexOf(item);

                if (index > -1)
                {
                    bReturn = _List.Remove(item);

                    if (bReturn)
                        NotifyCollectionChanged(NotifyCollectionChangedAction.Remove, item, index);
                }
            }

            return bReturn;
        }

        #endregion

        #region Conversions

        //public static implicit operator ObservableList<T>(List<T> list)
        //{
        //    ObservableList<T> lReturn = new ObservableList<T>();

        //    foreach (var item in list)
        //    {
        //        lReturn.Add(item);
        //    }

        //    return lReturn;
        //}

        #endregion
    }

    public static class ObservableListExtensions
    {
        public static bool Exists<T>(this ObservableList<T> list, Predicate<T> match)
        {
            bool bReturn = false;

            Parallel.ForEach(list, item =>
            {
                if (match(item))
                {
                    bReturn = true;
                    return;
                }
            });

            return bReturn;
        }

    }
}

Monday, 25 July 2011

WPF C# IPAddress Control

During my quest to create a customised IP Scanner to use at work, I struggled to find a decent control to handle an IP Address. Some controls on the internet made use of MaskedTextProvider, applying a mask to a TextBox to assist in validating the input. But as you can imaging, that's kinda ugly and crap. So, I decided to take the plunge and create my own IPAddress user control.

In short, the control simply consists of four TextBoxes separated by three Labels containing the decimal separator.

A stripped down example:


    <Border Name="borderMain" BorderThickness="1" BorderBrush="#FFA5ACB2">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="30" />
                <ColumnDefinition Width="3" />
                <ColumnDefinition Width="30" />
                <ColumnDefinition Width="3" />
                <ColumnDefinition Width="30" />
                <ColumnDefinition Width="3" />
                <ColumnDefinition Width="30" />
            </Grid.ColumnDefinitions>
           
            <TextBox Grid.Column="0" TabIndex="0" x:Name="TextOctet1" Text="{Binding Path=Octet1 />

            <TextBlock Grid.Column="1" Text="." Width="5" />

            <TextBox Grid.Column="2" TabIndex="1" x:Name="TextOctet2" Text="{Binding Path=Octet2  />

            <TextBlock Grid.Column="3" Text="." Width="5" />

            <TextBox Grid.Column="4" TabIndex="2" x:Name="TextOctet3" Text="{Binding Path=Octet3 />

            <TextBlock Grid.Column="5" Text="." Width="5" />

            <TextBox Grid.Column="6" TabIndex="3" x:Name="TextOctet4" Text="{Binding Path=Octet4 />

        </Grid>
    </Border>


Altogether, it's pretty simple to create. The code behind just needs to handle validation to keep your input in check and also navigation within the control.

Link to Source and binary: http://dl.dropbox.com/u/26136919/IPAddress.7z