C# – Creating Reliable Complex Dictionary Keys Using Generic Tuples

by Dean 8. May 2010 12:56

As .NET developers we have all implemented dictionaries. Often, its a small dictionary with strings or Guids as the key – which is simple and robust.

However, sometimes we need to do something a little more ‘serious’ and implement a dictionary that has a complex key consisting of a combination of values. In this case, it is extremely important that the type we use for the key has the following characteristics:

  1. Instances of the key’s type must be immutable, as changing their state once they are added to the dictionary will most likely mean that the value they represent cannot be found
  2. The key’s type must produce a wide range of hash codes during use, so you’ll need to override the GetHashCode() method, and implement your own algorithm
  3. You must override the Equals() method of the key’s type to implement the correct equality algorithm. Also the keys type must implement IEquatable<T> for easy lookups in generic dictionaries.
  4. The keys type should ideally be a struct rather than a class, so the key collection can be further optimised by the JIT compiler.

With all of these pre-requisites, it’s easy to see why developers steer clear of using complex keys in their dictionary, as this represents a lot of work, and must be done correctly.

Well, to solve this problem I have created a customised generic Tuple class that can be used for this situation

public struct Tuple<T, U, V> : IEquatable<Tuple<T,U, V>>
{
    public readonly T First;
    public readonly U Second;
    public readonly V Third;
 
    public Tuple(T first, U second, V third) 
    {
        First = first;
        Second = second;
        Third = third;
    }
 
    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;
        if (this.GetType() != obj.GetType())
            return false;            
        return AreEqual(this,(Tuple<T,U,V>)obj);
    }
 
    public bool Equals(Tuple<T, U, V> other)
    {
        return AreEqual(this,other);
    }
 
    private static bool AreEqual(Tuple<T, U, V> a, Tuple<T, U, V> b)
    {
        if (!a.First.Equals(b.First))
            return false;
        if (!a.Second.Equals(b.Second))
            return false;
        if (!a.Third.Equals(b.Third))
            return false;
        return true;
    }
 
    public static bool operator == (Tuple<T,U,V> a, Tuple<T,U,V> b)
    {
        return AreEqual(a, b);
    }
 
    public static bool operator !=(Tuple<T, U, V> a, Tuple<T, U, V> b)
    {
        return !AreEqual(a, b);
    }
 
    public override int GetHashCode()
    {
        return First.GetHashCode() ^ Second.GetHashCode() ^ Third.GetHashCode();
    }
}
 
public static class Tuple
{
    public static Tuple<T, U, V> CreateNew<T,U,V>(T first, U second, V third)
    {
        return new Tuple<T, U, V>(first, second, third);
    }
}

 

The Tuple struct above is a three-value Tuple, but you could easily use the same pattern to create a Tuple of any size from 2 values upwards

To test our Tuple dictionary key, lets first create a test class for demonstration purposes

public class TestData
{
    public string Name { get; set; }
    public int Items { get; set; }
    public DateTime Start { get; set; }
    public ComplexObjectGraph OtherData { get; set; }
}

Now lets see us use this key in our code that adds values to a dictionary

var dict = new Dictionary<Tuple<string, int, DateTime>, TestData>();
var test = new TestData() { Name = "Test1", Items = 23, Start = new DateTime(2010, 01, 01) };
dict.Add(Tuple.CreateNew(test.Name, test.Items, test.Start), test);

And finally, lets lookup our value from via a key

var key = Tuple.CreateNew("Test1",23,new DateTime(2010, 01, 01));
var data = dict[key];

 

Dean

Tags: , ,

C#

WPF MVVM – Simple ‘MessageBox.Show’ With Action & Func

by Dean 6. May 2010 08:08

In the MVVM world, things like message boxes (MessageBox.Show) and Dialogs (open file, save file etc), don't naturally fit.

These popups are closely tied to the ‘View’ part of MVVM, but they can only really be invoked from the ‘ViewModel’ which will break the clean separation in MVVM.

If you google this issue, you will find a wide range of elaborate solutions, many of which are significant engineering projects in their own right.

I am a huge fan of implementing simple solutions wherever possible, as verbose code is the number one culprit in un-maintainable projects, so I was keen to find a solution that is simple, robust, elegant and doesnt break the MVVM pattern

The solution I came up with, is to use generic Action and Func Delegates.

OK, to illustrate my solution, I have created a new project using the ‘WPF Model-View-ViewModel Toolkit’, (http://wpf.codeplex.com/wikipage?title=WPF%20Model-View-ViewModel%20Toolkit), which installs a project template in VS2008

Here is my altered ‘MainViewModel'.cs’ class

public class MainViewModel : ViewModelBase
{
    private DelegateCommand exitCommand;
    private Action<string> popup;
    private Func<string, string, bool> confirm;
 
    public MainViewModel(Action<string> popup, Func<string, string, bool> confirm)
    {
        this.popup = popup;
        this.confirm = confirm;
    }
 
    public ICommand ExitCommand
    {
        get
        {
            if (exitCommand == null)
                exitCommand = new DelegateCommand(Exit);
            return exitCommand;
        }
    }
 
    private void Exit()
    {
        if (confirm("Are you sure you want to exit", "confirm exit"))
            Application.Current.Shutdown();
    }
}

As you can see, the MainViewModel’s constructor takes 2 delegates, 1 for popup and 1 for confirm

Now take a look at App.xaml.cs, where the View and the ViewModel get instantiated

private void OnStartup(object sender, StartupEventArgs args)
{
    // messagebox
    var popup = (Action<string>)(msg => MessageBox.Show(msg));
 
    // confirm box
    var confirm = (Func<string, string, bool>)((msg, capt) => 
        MessageBox.Show(msg, capt, MessageBoxButton.YesNo) == MessageBoxResult.Yes);
 
    Views.MainView view = new Views.MainView();
    view.DataContext = new ViewModels.MainViewModel(popup,confirm);
    view.Show();
}

If you look closely, you’ll see that my delegates actually map to methods in the static class ‘MessageBox’, which will give us the popups we need. The popup delegate will instantiate a simple message popup, and the confirm delegate will instantiate a message popup with confirm buttons.

And this is what happens when we click on the Exit menu item (note: this menu is created by default when you create a new project using the toolkit)

popup

Now, when we want to run unit tests on our ViewModel, we can just pass in dummy delegates

[TestMethod()]
public void MainViewModelConstructorTest()
{
    var dummyPopup = (Action<string>)((a) => {return;});
    var dummyConfirm = (Func<string,string,bool>)((a,b) => {return true;});
    ViewModels.MainViewModel target = new ViewModels.MainViewModel(dummyPopup, dummyConfirm);
    Assert.Inconclusive("TODO: Implement code to verify target");
}

Dean

Tags: ,

MVVM | C# | WPF | Unit Tests

A C# AIML Chatterbot – Artificial Intelligence In 500 Lines Of Code

by Dean 20. March 2010 21:28

I recently stumbled across an area of artificial intelligence programming called AIML ‘chatterbots’. These programs are interpreters for an XML based AI language called AIML. AIML and the code that processes it are the basis of the first and most famous chatterbot called A.L.I.C.E, where the founders and followers are infamous for promoting and winning the Loebner Prize in Artificial Intelligence.

Among the available technologies, there are the usual suspects including implementations in PHP, Perl, Python, Java and even Pascal – but nothing in .NET

On Saturday my wife (who runs her own gourmet coffee roasting business here in rural Kent – www.coffeebeanshop.co.uk) was in Germany buying a new coffee roaster, so to fill time I decided to have a go at a C# implementation, which is detailed below.

Firstly, before you do anything, you need to download some AIML files, which describe how the interpreter should handle human conversation, and sets out the rules of engagement. AIML also includes it’s own XML based mini-language, which was tough to implement. To get enough AIML to make this worthwhile, I recommend that you get files from this location - http://www.alicebot.org/aiml/alice.zip . This is the AIML created by Richard Wallace – winner of the Loebner Prize on at least one occasion.

Once you have the AIML, just use the code below, and you can chat to your own Artificial Intelligence ‘Bot’ for as long as you like :)

 

Firstly, the console program to run this thing

class Program
{
    static void Main(string[] args)
    {
        var ai = new AIMLProcessor();
        ai.Load();
        var chat = Console.ReadLine();
        AIMLProcessor.Thats.Add("*");
        while (chat != "")
        {
            AIMLProcessor.Inputs.Insert(0, chat);
            AIMLProcessor.Thats.Insert(0, ai.FindTemplate(chat, AIMLProcessor.Thats[0], "*"));
            Console.WriteLine(AIMLProcessor.Thats[0]);
            chat = Console.ReadLine();
        }
    }
}

Then you need the classes to do the work

public class AIMLProcessor
{
    public static readonly List<string> Thats = new List<string>();
    public static readonly List<string> Inputs = new List<string>();
    private readonly Dictionary<string, string> BotData = new Dictionary<string, string>();
    private readonly Dictionary<string, string> Predicates = new Dictionary<string, string>();
    private readonly AIMLData data = new AIMLData(null, string.Empty);
    private readonly Random rand = new Random(DateTime.Now.Millisecond);
 
    public void Load()
    {
        // download from http://www.alicebot.org/aiml/alice.zip and unzip locally
        // then use that location for loading, as below
        foreach (string file in Directory.GetFiles(
            @"D:\Users\Administrator\Downloads\alice", "*.aiml"))
        {
            var doc = XDocument.Load(file);
            var first = doc.Element("aiml");
            var topics = first.Elements("topic");
            var cats = first.Elements("category");
            if (topics != null && topics.Count() > 0)
            {
                foreach (var topic in topics)
                {
                    var topicName = topic.Attribute("name") == null ?
                        "*" : topic.Attribute("name").Value;
                    foreach (var cat in topic.Elements())
                        ProcessCategory(cat, topicName);
                }
            }
            if (cats == null || cats.Count() <= 0) 
                continue;
            foreach (var cat in cats)
                ProcessCategory(cat, "*");
        }
 
        LoadInitialPredicates();
    }
 
    private void ProcessCategory(XElement cat, string topic)
    {
        var patt = cat.Element("pattern");
        var temp = cat.Element("template");
        var that = cat.Element("that");
        if (patt == null || temp == null)
            return;
        var t = that != null ? that.InnerText() : "*";
        string pattern = string.Format("{0} <THAT> {1} <TOPIC> {2}", 
            patt.Nodes().First(), t.Trim(), topic.Trim());
        LoadWord(data, new List<string>(pattern.Split(' ')), temp.InnerText());
    }
 
    public string FindTemplate(string text, string that, string topic)
    {
        var pattern = string.Format("{0} <THAT> {1} <TOPIC> {2}", 
            Normalise(text).Trim(), "*", topic.Trim());
        var temp = FindTemplateRecursive(data, new List<string>(pattern.Split(' ')), 
            new PatternData(), 0);
        var result = InterpretTemplate(temp.Template, temp.WildTexts, text, that, topic);
        return result.Trim().Replace("  ", " ");
    }
 
    public string Normalise(string text)
    {
        return Regex.Replace(text, @"[^\w\ ]", "").Trim().Replace("  ", " ");
    }
 
    private string InterpretTemplate(string template, List<WildData> wilds, 
        string text, string that, string topic)
    {
        var xmltemplate = string.Format("<process>{0}</process>", template);
        var doc = XElement.Parse(xmltemplate);
        if (doc.Elements().Count() == 0)
            return doc.Value;
        string response = "";
 
        foreach (var node in doc.Nodes())
        {
            if (node.NodeType != XmlNodeType.Element)
            {
                response += " " + node;
                continue;
            }
            var element = node as XElement;
            switch (element.Name.LocalName.ToUpper())
            {
                case "INPUT":
                    response += " " + ProcessInput(element).Trim();
                    break;
                case "THAT":
                    response += " " + ProcessThat(element).Trim();
                    break;
                case "THATSTAR":
                    response += " " + ProcessThatStar(element, wilds).Trim();
                    break;
                case "TOPICSTAR":
                    response += " " + ProcessTopicStar(element, wilds).Trim();
                    break;
                case "STAR":
                    response += " " + ProcessStar(element, wilds).Trim();
                    break;
                case "PERSON":
                    response += " " + ProcessPerson(wilds).Trim();
                    break;
                case "SET":
                    response += " " + ProcessSet(element, wilds, text, that, topic).Trim();
                    break;
                case "GET":
                    response += " " + ProcessGet(element).Trim();
                    break;
                case "THINK":
                    response += " " + ProcessThink(element, wilds, text, that, topic).Trim();
                    break;
                case "RANDOM":
                    response += " " + ProcessRandom(element, wilds, text, that, topic).Trim();
                    break;
                case "BOT":
                    response += " " + ProcessBot(element).Trim();
                    break;
                case "SR":
                    response += " " + ProcessSR(element, wilds, text, that, topic).Trim();
                    break;
                case "SRAI":
                    response += " " + ProcessSRAI(element, wilds, text, that, topic).Trim();
                    break;
                default:
                    return element.ToString().Trim();
            }
        }
        return response.Trim();
    }
 
    private static string ProcessInput(XElement element)
    {
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= Inputs.Count)
            return string.Empty;
        return Inputs[num];
    }
 
    private static string ProcessThat(XElement element)
    {
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= Thats.Count)
            return string.Empty;
        return Thats[num];
    }
 
    private static string ProcessThatStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.That).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessTopicStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.Topic).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.Pattern).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessPerson(IList<WildData> wilds)
    {
        if (wilds.Count == 0)
            return string.Empty;
        var words = wilds[0].WildText.Split(' ');
        for (int i = 0; i < words.Count(); i++)
        {
            if (words[i].Trim().ToUpper() == "I")
                words[i] = "you";
            if (words[i].Trim().ToUpper() == "MY")
                words[i] = "your";
        }
        var response = string.Join(" ", words);
        response = response.Replace("you am", "you are");
        return response;
    }
 
    private string ProcessSet(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        if (element.Attribute("name") == null)
            return string.Empty;
        var att = element.Attribute("name").Value;
        if (!Predicates.ContainsKey(att))
            Predicates.Add(att, "");
        Predicates[att] = InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return Predicates[att];
    }
 
    private string ProcessGet(XElement element)
    {
        if (element.Attribute("name") == null)
            return string.Empty;
        var att = element.Attribute("name").Value;
        if (!Predicates.ContainsKey(att))
            return string.Empty;
        return Predicates[att];
    }
 
 
    private string ProcessThink(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return string.Empty;
    }
 
    private string ProcessRandom(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var num = rand.Next(0, element.Elements().Count());
        var ret = element.Elements().ToList()[num].InnerText();
        return InterpretTemplate(ret, wilds, text, that, topic);
    }
 
    private string ProcessSRAI(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var t = InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return FindTemplate(t, that, topic);
    }
 
    private string ProcessSR(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var w = wilds.Where(a => a.WildType == StarType.Pattern).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value) - 1;
        if (num >= w.Count)
            return string.Empty;
        var t = InterpretTemplate(w[num].WildText, wilds, text, that, topic);
        return FindTemplate(t, that, topic);
    }
 
    private string ProcessBot(XElement element)
    {
        return BotData[element.Attribute("name").Value.ToUpper()];
    }
 
 
    private static PatternData FindTemplateRecursive(AIMLData ai, List<string> text, 
        PatternData data, int searchPos)
    {
        var key = text[searchPos];
        if (data.IsInWildcard && searchPos < text.Count - 1 && !ai.Data.ContainsKey("_") &&
            !ai.Data.ContainsKey(key.ToUpper()) && !ai.Data.ContainsKey("*"))
        {
            data.WildTexts[data.WildTexts.Count - 1].WildText += key + " ";
            return FindTemplateRecursive(ai, text, data, searchPos + 1);
        }
        if (ai.Data.ContainsKey("_"))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data["_"].Template;
                return data;
            }
            data.WildTexts.Add(new WildData {WildText = key + " ", WildType = data.WildType});
            data.IsInWildcard = true;
            data = FindTemplateRecursive(ai.Data["_"], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
            data.WildTexts.RemoveAt(data.WildTexts.Count - 1);
            data.WildType = data.WildTexts.Count == 0 ? StarType.Pattern : 
                data.WildTexts[data.WildTexts.Count - 1].WildType;
        }
        if (ai.Data.ContainsKey(key.ToUpper()))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data[key.ToUpper()].Template;
                return data;
            }
            if (key.ToUpper() == "<THAT>")
                data.WildType = StarType.That;
            if (key.ToUpper() == "<TOPIC>")
                data.WildType = StarType.Topic;
            data.IsInWildcard = false;
            data = FindTemplateRecursive(ai.Data[key.ToUpper()], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
        }
        if (!data.IsAnswer && ai.Data.ContainsKey("*"))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data["*"].Template;
                return data;
            }
            data.WildTexts.Add(new WildData {WildText = key + " ", WildType = data.WildType});
            data.IsInWildcard = true;
            data = FindTemplateRecursive(ai.Data["*"], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
            data.WildTexts.RemoveAt(data.WildTexts.Count - 1);
            data.WildType = data.WildTexts.Count == 0 ? StarType.Pattern : 
                data.WildTexts[data.WildTexts.Count - 1].WildType;
        }
        return data;
    }
 
    private static void LoadWord(AIMLData parent, List<string> pattern, string template)
    {
        var key = pattern[0].ToUpper().Trim();
        pattern.RemoveAt(0);
        if (!parent.Data.ContainsKey(key))
            parent.Data.Add(key, new AIMLData(parent, key));
        if (pattern.Count > 0)
            LoadWord(parent.Data[key], pattern, template);
        else
            parent.Data[key].Template = template;
    }
 
    private void LoadInitialPredicates()
    {
        BotData.Add("NAME", "Gunther");
        BotData.Add("RELIGION", "Buddha Bubba is my mesiah");
        BotData.Add("PARTY", "monster raving loony party");
        BotData.Add("GENDER", "male");
        BotData.Add("SIGN", "Capricorn");
        BotData.Add("ARCH", "Windows 7");
        BotData.Add("BIRTHDAY", "6/6/1666");
        BotData.Add("SPECIES", "chatterbot");
        BotData.Add("GENUS", "computer algorithm");
        BotData.Add("FOAVOURITEFOOD", "chicken");
        BotData.Add("BOTMASTER", "programmer");
        BotData.Add("MASTER", "The Evil Genius");
        BotData.Add("AGE", "32");
        BotData.Add("FRIEND", "Robocop");
        BotData.Add("LOCATION", "Inside a VAIO");
        BotData.Add("FAMILY", "intelligence");
        BotData.Add("KINGDOM", "In the UK");
        BotData.Add("ORDER", "program");
        BotData.Add("PHYLUM", "AI");
        BotData.Add("FORFUN", "I like fiddling with my switches");
        BotData.Add("FRIENDS", "Chico, Harpo, Groucho and Karl");
    }
}
 
public class AIMLData
{
    public AIMLData(AIMLData parent, string key)
    {
        Parent = parent;
        Key = key;
        Data = new Dictionary<string, AIMLData>();
    }
 
    public Dictionary<string, AIMLData> Data { get; private set; }
    public AIMLData Parent { get; private set; }
    public string Template { get; set; }
    public string Key { get; set; }
}
 
public class PatternData
{
    public PatternData()
    {
        WildTexts = new List<WildData>();
        WildType = StarType.Pattern;
    }
 
    public string Template { get; set; }
    public bool IsAnswer { get; set; }
    public bool IsInWildcard { get; set; }
    public StarType WildType { get; set; }
    public List<WildData> WildTexts { get; private set; }
}
 
public class WildData
{
    public string WildText { get; set; }
    public StarType WildType { get; set; }
}
 
public enum StarType
{
    Pattern,
    That,
    Topic
}
 
public static class ExtensionMethods
{
    public static string InnerText(this XElement element)
    {
        return element.Nodes().Aggregate("", 
            (current, node) => current + node.ToString());
    }
}

And there it is – its all you need.

I don’t doubt there are many refinements that could included (this WAS done in about 4 hours), and extra features that may be interesting.

Here is a screenshot of one of our first conversations:

aimlbot

As you can see, Gunther (the unfortunate name I gave it – did I mention my wife was in Germany ?), is quite a conversationalist. Not all conversations were quite as convincing, but you could extend this application and include a ‘learning mode’ where over time you can increase the Bots ability to say the right thing at the right time :)

If anyone has any suggestions or comments, the please email me (see contact details) or add a comment on this blog post

Happy Chatterbotting :)

 

Worthwhile Links:

http://alicebot.blogspot.com/
http://www.alicebot.org/aiml.html
http://www.loebner.net/Prizef/loebner-prize.html
http://en.wikipedia.org/wiki/AIML

Tags:

C# | AIML

Thread-Safe & Dispatcher-Safe Observable Collection for WPF

by Dean 1. February 2010 12:22

A common problem in WPF (& Silverlight) development is when you are working with multiple threads that need to change a collection that is a binding source and implements INotifyCollectionChanged.

Basically, the standard ObservableCollection<T> will only allow updates from the dispatcher thread, which means you need to write a lot of code for the worker threads to marshal changes onto the main message pump via the dispatcher. This can be a bit tedious, so I recently wrote a collection that performs all of the necessary marshalling internally, so users of this type do not have to be concerned about thread affinity issues.

Also, I decided to use a ReaderWriterLock to provide thread-safety during updates to the collection.

Here is my collection class:

 
public class SafeObservable<T> : IList<T>, INotifyCollectionChanged
{
    private IList<T> collection = new List<T>();
    private Dispatcher dispatcher;
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private ReaderWriterLock sync = new ReaderWriterLock();
 
    public SafeObservable()
    {
        dispatcher = Dispatcher.CurrentDispatcher;
    }
 
    public void Add(T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoAdd(item);
        else
            dispatcher.BeginInvoke((Action)(() => { DoAdd(item); }));
    }
 
    private void DoAdd(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Add(item);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
        sync.ReleaseWriterLock();
    }
 
    public void Clear()
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoClear();
        else
            dispatcher.BeginInvoke((Action)(() => { DoClear(); }));
    }
 
    private void DoClear()
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Clear();
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
    }
 
    public bool Contains(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        var result = collection.Contains(item);
        sync.ReleaseReaderLock();
        return result;
    }
 
    public void CopyTo(T[] array, int arrayIndex)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.CopyTo(array, arrayIndex);
        sync.ReleaseWriterLock();
    }
 
    public int Count
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            var result = collection.Count;
            sync.ReleaseReaderLock();
            return result;
        }
    }
 
    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }
 
    public bool Remove(T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            return DoRemove(item);
        else
        {
            var op = dispatcher.BeginInvoke(new Func<T,bool>(DoRemove), item);
            if (op == null || op.Result == null)
                return false;
            return (bool)op.Result;
        }
    }
 
    private bool DoRemove(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        var index = collection.IndexOf(item);
        if (index == -1)
        {
            sync.ReleaseWriterLock();
            return false;
        }
        var result = collection.Remove(item);
        if (result && CollectionChanged != null)
            CollectionChanged(this, new
                NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
        return result;
    }
 
    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }
 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }
 
    public int IndexOf(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        var result = collection.IndexOf(item);
        sync.ReleaseReaderLock();
        return result;
    }
 
    public void Insert(int index, T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoInsert(index, item);
        else
            dispatcher.BeginInvoke((Action)(() => { DoInsert(index, item); }));
    }
 
    private void DoInsert(int index, T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Insert(index, item);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        sync.ReleaseWriterLock();
    }
 
    public void RemoveAt(int index)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoRemoveAt(index);
        else
            dispatcher.BeginInvoke((Action)(() => { DoRemoveAt(index); }));
    }
 
    private void DoRemoveAt(int index)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        if (collection.Count == 0 || collection.Count <= index)
        {
            sync.ReleaseWriterLock();
            return;
        }
        collection.RemoveAt(index);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
 
    }
 
    public T this[int index]
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            var result = collection[index];
            sync.ReleaseReaderLock();
            return result;
        }
        set
        {
            sync.AcquireWriterLock(Timeout.Infinite);
            if (collection.Count == 0 || collection.Count <= index)
            {
                sync.ReleaseWriterLock();
                return;
            }
            collection[index] = value;
            sync.ReleaseWriterLock();
        }
 
    }
}
 

To test the effectiveness of this collection class, I wrote a simple WPF app, that bound to the new collection class and updated it via multiple threads:

 

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <StackPanel Orientation="Vertical" VerticalAlignment="Top">
        <Button Content="Start" Click="Button_Click" />
        <ListView Name="list" ItemsSource="{Binding}" DisplayMemberPath="Text" />        
    </StackPanel>
</Window>

And the code behind is below:

 

public partial class Window1 : Window
{
    class TestData
    {
        public string Text { get; set; }
    }
 
    private Random rand = new Random(DateTime.Now.Millisecond);
    private SafeObservable<TestData> data = new SafeObservable<TestData>();
    public Window1()
    {
        InitializeComponent();
    }
 
    void Button_Click(object sender, RoutedEventArgs e)
    {
        list.DataContext = data;
        List<Action> work = new List<Action>();
        for (int i = 0; i < 100; i++)
        {
            work.Add(new Action(DoWorkAdd));
            work.Add(new Action(DoWorkClear));
            work.Add(new Action(DoWorkRemove));
            work.Add(new Action(DoWorkRemoveAt));
            work.Add(new Action(DoWorkInsert));
            work.Add(new Action(DoWorkReplace));
        }
        for (int i = 0; i < 1000; i++)
            work[rand.Next(0, work.Count)].BeginInvoke(null, null);
 
    }
 
    void DoWorkAdd()
    {
        Thread.Sleep(rand.Next(500, 30000));
        data.Add(new TestData() { Text = string.Format("Thread {0} Added", Thread.CurrentThread.ManagedThreadId) });
    }
 
    void DoWorkClear()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data.Clear();
        Debug.WriteLine((string.Format("Thread {0} Clear", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkRemove()
    {
        Thread.Sleep(rand.Next(500, 10000));
        if (data.Count == 0)
            return;
        var item = data[0];
        data.Remove(item);
        Debug.WriteLine((string.Format("Thread {0} Remove", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkRemoveAt()
    {
        Thread.Sleep(rand.Next(500, 10000));
        if (data.Count == 0)
            return;
        data.RemoveAt(0);
        Debug.WriteLine((string.Format("Thread {0} RemoveAt", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkInsert()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data.Insert(rand.Next(0, data.Count), new TestData() 
            { Text = string.Format("Thread {0} Insert", Thread.CurrentThread.ManagedThreadId) });
    }
 
    void DoWorkReplace()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data[rand.Next(0, data.Count)] = new TestData() 
            { Text = string.Format("Thread {0} Replace", Thread.CurrentThread.ManagedThreadId) };
    }
 
}

All my WPF app does is run a number of random actions against the collection from a variety of threads.

NOTE:  When removing items from the collection I used the Refresh action of NotifyCollectionChangedAction instead of Remove. This is because the remove action doesnt work correctly in a multi-threaded scenario when used as a binding source for a list control in WPF.

If anyone has any siggestions or enhancements, please let me know

Dean

Tags:

DataBinding | C# | Threading

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010 Dean Chalk's Blog