Click here to Skip to main content
Click here to Skip to main content

Observing Changes to an Underlying Array

By , 28 Feb 2011
 

Introduction

Recently, I came across the problem of visualizing an array using a WPF ItemsControl. The problem was that the array was constantly being modified by factors outside the control of my own code, so I could not raise the appropriate collection notifications because there was no fixed flow of control where these notifications should be raised. Furthermore, the array consisted of a few hundred thousands of elements, so simply resetting the ItemsControl's ItemsSource at fixed time intervals was out of the question.

So I came up with a solution thanks to the helpful feedback of John Simmons, a fellow CodeProject user. The idea behind this solution is to subclass the ObservableCollection<T> class so as to provide an INotifyCollectionChanged interface to the consumer, while in the background running a worker thread that keeps watch of the underlying array and moves the changed items back into the ObservableCollection.

Background

The main problem that has to be tackled in some way is that simply wrapping the underlying array around an ObservableCollection will not suffice, because the ObservableCollection constructor copies the array elements and emits CollectionChanged events when somebody modifies the copy through the ObservableCollection's ICollection<T> (or ICollection, or IList) interface. But what happens when for some reason the generating array is modified in some way? What if our purpose is to observe the generating array itself, not some copy?

So our solution inherits the ObservableCollection class and keeps a reference to, not a copy of, the underlying array. At fixed time intervals (checkpoints), it checks the contents of the underlying array and updates its instance so that it becomes in sync again with the array.

In addition, our solution tackles the possibility that the array may contain many items. To overcome this, it makes use of the excellent parallel processing classes found in .NET v4. Assuming that the array may contain millions of elements, but not many of them may change from one checkpoint to the next, we can easily monitor this array with minimal overhead.

Finally, as a "bonus", our new observable collection class is capable of projecting its elements to another type, given a projection function in its constructor. For example, we may have an array of a thousand integers, wrap it around our collection class and give it a projection function like i => i * i so that an observer of our collection sees the squares of those integers.

Using the Code

We have provided extension methods so that we can wrap an array around a monitor like so:

// Create an array
int[] theArray = new int[1000000];
// Populate the array
for (var i = 0; i < theArray.Length; i++)
    theArray[i] = i;

// Create a monitor wrapper
var mon = theArray.AsMonitored();
// Create a monitor wrapper which presents
// to its observer the squares of the array's elements.
var monProj = theArray.AsMonitoredProjected(i => i * i);

The extension methods are as follows:

  • AsMonitored<T>() - Monitors the underlying array of T every 100 milliseconds
  • AsMonitored<T>(int period) - Monitors the underlying array of T every period milliseconds
  • AsMonitoredProjected<T, P>(Func<T, P> project) - Monitors the underlying array of T every 500 milliseconds, while at the same time projecting it to type P; and
  • AsMonitoredProjected<T, P>(Func<T, P> project, int period) - Monitors the underlying array of T every period milliseconds, while at the same time projecting it to type P.

Implementation Details

The main class which implements our collection is called MonitoredProjectedArray.

public class MonitoredProjectedArray<T, P>
    : ObservableCollection<P>, IDisposable
{
    // Fields
    protected T[] _monitoredArray;
    protected Func<T, P> _project;

    // Construction
    public MonitoredProjectedArray(T[] a, int period, Func<T, P> project)
        : base(a.AsParallel().AsOrdered().Select(project))
    {
        _monitoredArray = a;
        _project = project;

        /* Code that sets up changed items */

        /* Code that sets up a timer which runs every "period"
           milliseconds and calls method QueueChangedItems() */
    }

    public MonitoredProjectedArray(T[] a, Func<T, P> project)
        : this(a, 500, project)
    {
    }

    /* More helper methods */
}

It is important to note that class MonitoredProjectedArray keeps a reference to the array that is monitored. Secondly, note that the underlying array is an array of items of type T, however it inherits ObservableColletion<P>. This happens because every item in the array is projected to type P using the function _project. This happens during the instance creation (note the base call).

The core of the class is method QueueChangedItems(), which is called periodically by a timer. This method first compares the elements of the underlying array with the elements of the monitored array instance. If it finds differences, it creates ChangedItem instances and stores them in a queue. Then, for each item in the queue, it dequeues it and updates the monitored array at the corresponding indices.

A ChangedItem is defined as follows:

struct ChangedItem
{
    public int Index;     // The index in the array of the changed item
    public T NewValue;    // The new value at the specified index
}

To take advantage of the Framework's new parallel processing capabilities, we make use of the Parallel class in our QueueChangedItems() method:

ConcurrentQueue<ChangedItem> _changedItems;

protected void QueueChangedItems()
{
    // Collect the changed items
    Parallel.For(0, _monitoredArray.Count(), i =>
    {
        if (!_project(_monitoredArray[i]).Equals(this[i]))
        {
            var ci = new ChangedItem() { Index = i, NewValue = _monitoredArray[i] };
            if (!_changedItems.Contains(ci))
                _changedItems.Enqueue(ci);
        }
    });

    // The following action updates this instance with the changed items.
    // Use _project to project the new values to type P.
    Action updateAction = () =>
    {
        ChangedItem item;
        while (_changedItems.TryDequeue(out item))
            this[item.Index] = _project(item.NewValue);
    };

    // Start four concurrent updateActions to consume the _changedItems queue
    Parallel.Invoke(updateAction, updateAction, updateAction, updateAction);
}

Note that instead of using a simple Queue, we elect to use a ConcurrentQueue object to represent our queue of changed items. ConcurrentQueue resides in the System.Collections.Concurrent namespace and is thread-safe, which is a requirement in our case since we consume the queue from four different threads.

As a final point of interest, note the fact that in our class we have overridden the OnNotifyCollectionChanged() method in such a way as to make use of the Dispatcher. This was done simply because we modify the collection from a different thread (actually from four different threads, the ones started with the Parallel.Invoke() method). The Dispatcher is the only thread-safe way to change an ObservableCollection from a different thread:

// Override OnCollectionChanged so that we make use of the Dispatcher
protected override void OnCollectionChanged
	(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    using (BlockReentrancy())
    {
        // Get the CollectionChanged event handler
        System.Collections.Specialized.NotifyCollectionChangedEventHandler 
				eventHandler = CollectionChanged;
        if (eventHandler != null)
        {
            foreach (var handler in eventHandler.GetInvocationList())
            {
                DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
                if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
                    dispatcherObject.Dispatcher.Invoke
			(DispatcherPriority.DataBind, handler, this, e);
                else
                    (handler as System.Collections.Specialized.
		NotifyCollectionChangedEventHandler)(this, e);
            }
        }
    }
}

We acquire all the delegates attached to the CollectionChanged event, and if any of them is on a different thread, we use the Dispatcher to invoke it. Otherwise, we proceed as normal.

The Final Word

The MonitoredProjectedArray class, along with its extension methods, is definitely not production-quality code. Some of its shortcomings are:

  • It only monitors arrays, not general collections (i.e. implementors of ICollection<T>, IList, etc);
  • Users cannot configure the monitoring strategy: it's hardcoded into the QueueChangedItems() method;
  • It makes the assumption that not many array elements change between two consecutive checkpoints, which is a sensible assumption for most real-world applications, however in the general case, we should not take such things for granted.

Despite its shortcomings however, the class is quite usable, so I hope it will present itself as a solution in similar cases in your programs. Have fun!

History

  • February 28, 2011: First version published
  • March 1, 2011: A few minor changes in the presentation

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

George Tryfonas
Engineer
Greece Greece
Member
I am a software developer (mainly in C# and T-SQL) for a project management company in Athens, Greece. I have been working with computers since early 1987. I am adept at Pascal, C, C++, Java (my MSc was sponsored by Sun Microsystems), Lisp, Scheme, F#, C# VB.Net, Perl and some others that are too obscure to mention. When I want a quick and dirty solution to a programming problem I use a functional language, such as Haskell, Scheme or, more recently, F#.
 
I also play the keyboards and compose music.
 
---------------------------------------------------------
 
MSc Distributed Systems and Networks - University of Kent at Canterbury
BEng Computer Systems Engineering - University of Kent at Canterbury

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralIt would be nice if you opened and closed by mentioning that INPC is the preferred way to do this.memberJay R. Wren12 Mar '11 - 9:30 
But I voted you a 5 anyway because this is just too much fun, despite SledgeHammer01's concerns.   I can think of many more alternatives: * extending the type and wrapping it with INPC properties. * create a new type which composes the original type and wraps the mutating...
GeneralQuestions...memberSledgeHammer011 Mar '11 - 14:21 
Maybe I misunderstood your article, but it seems to me that your goal was to have an ObservableCollection<T> where you get collection change notifications (add item and remove item) AND notifications when some property on T itself changes?   If that is your goal, why not just use...
GeneralRe: Questions...memberGeorge Tryfonas1 Mar '11 - 18:48 
Hello,   these are all good questions. Thanks for asking, I'll try to clarify as best as I can.   First of all, BindingList<T> is a Windows Forms thing. The WPF version is ObservableCollection<T>, the difference being that the latter implements INotifyCollectionChanged,...
GeneralRe: Questions...memberSledgeHammer011 Mar '11 - 20:12 
Thanks for the response, allow me to respond to your response .   1) You said:   "First of all, BindingList<T> is a Windows Forms thing. The WPF version is ObservableCollection<T>, the difference being that the latter implements INotifyCollectionChanged, which is needed...
AnswerRe: Questions...memberGeorge Tryfonas1 Mar '11 - 21:32 
Dear SledgeHammer,   let me try to respond to your response to my response (this is getting awkward ).   You said:   1) This is just completely false. BindingList<T> is NOT a "Windows Forms thing", its a .Net thing. ObservableCollection<T> is simply a "lighter...
GeneralRe: Questions... [modified]memberSledgeHammer012 Mar '11 - 4:38 
Ok, well, implementing INPC and INCC (or BindingList) are a requirement of data binding .   Now, you asked a valid question: "what if T doesn't support INPC?"   If T is yours and you own it, you should obviously implement INPC because that is a requirement for data binding any way you...
RantRe: Questions...memberGeorge Tryfonas2 Mar '11 - 5:47 
You said:   If your "legacy" code is the one setting the data and it has no method of notifying you data has changed, I suppose this might be a way to use that in data binding. If it had *some* method of notifying you, I would probably try to use that and fake INPC notifications. ----...
GeneralRe: Questions...memberSledgeHammer012 Mar '11 - 6:56 
Ah, so the console app just dumped out the array every 100ms or so? or didn't use it at all?   Because it sounded like "somebody" creates the array once (the legacy library?) and then updates items in it and somehow the console application knew when to do something. So what I meant in my...
GeneralMy vote of 4memberJohn Adams1 Mar '11 - 9:07 
Looks like an intelligent solution to the problem. I believe I can see the usefulness and I really only have two issues with it.   First, the projection functionality feels non-cohesive. I think WPF Converters would be the better way to handle that.   Second, correct me if...
GeneralRe: My vote of 4memberGeorge Tryfonas1 Mar '11 - 9:44 
John, you're absolutely correct regarding the collection being read-only. That's a severe limitation which should have been noted in the article. I have created the read-write solution to the problem, and the article will be updated shortly.   About the first point, however. I'm not sure...
GeneralMy vote of 5memberManfred R. Bihy28 Feb '11 - 23:40 
I like your article. As you already mentioned there is room for improvement, but anybody with some knowledge in programming should be able to ingtroduce the nescessary modifications.
GeneralI fought a similar issuemembervegeta4ss28 Feb '11 - 5:51 
A few months back I fought a similar issue and if I had known about this I wouldn't have had to make a hacky workaround.   Time to review that code and use this. Thanks for sharing.
GeneralRe: I fought a similar issuememberGeorge Tryfonas28 Feb '11 - 19:05 
Thanks very much. But you should know that this is far from being production code. In particular, if I were to polish it up, I would generalize it so that it monitors an ICollection. Moreover, perhaps I would make it more customizable, letting the user choose a Watcher strategy, or provide their...
GeneralLike it, like it, have 5mvpSacha Barber28 Feb '11 - 2:31 
5 from me, good work, got to love the TPL man, I am writing series on that in case you are interested :   Task Parallel Library : 1 of n[^] Task Parallel Library : 2 of n[^] Task Parallel Library : 3 of n[^]   Just for interest, though I think you have is sussed already right!!...
GeneralRe: Like it, like it, have 5memberGeorge Tryfonas28 Feb '11 - 2:48 
Sacha, thanks, I'll make sure to check out your series! Anything about the TPL should make interesting reading.
GeneralRe: Like it, like it, have 5memberSledgeHammer011 Mar '11 - 20:21 
Did you give him a 5 just because he used TPL? Because this is a blatant wheel reinvention of BindingList<T> with a much worse design IMHO. Searching through a million items every 100ms? I really don't get why people are so anti-BindingList<T>. The WPF data binding engine supports...
GeneralRe: Like it, like it, have 5 [modified]mvpSacha Barber1 Mar '11 - 20:41 
SledgeHammer01 wrote:Did you give him a 5 just because he used TPL?   In part, I suppose I did, got a soft spot for TPL.   As for you question on BindingList, that is a good way to go, the only thing is to implement BindingList normally takes a bit more work than people are willing...
GeneralRe: Like it, like it, have 5memberGeorge Tryfonas1 Mar '11 - 22:28 
As I stated in my reply to SledgeHammer, BindingList will solve the issue provided that T implements INPC. I created this class because in my case I had to watch an array of elements that did not.   George.
GeneralRe: Like it, like it, have 5mvpSacha Barber1 Mar '11 - 22:36 
Fair enough Sacha Barber Microsoft Visual C# MVP 2008-2011Codeproject MVP 2008-2011Your best friend is you. I'm my best friend too. We share the same views, and hardly ever argue   My Blog : sachabarber.net
GeneralIt's TruemvpJohn Simmons / outlaw programmer28 Feb '11 - 2:03 
I really do inspire greatness. ".45 ACP - because shooting twice is just silly" - JSOP, 2010 ----- You can never have too much ammo - unless you're swimming, or on fire. - JSOP, 2010 ----- "Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your...

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 1 Mar 2011
Article Copyright 2011 by George Tryfonas
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid