diff --git a/OpenRA.FileFormats/OpenRA.FileFormats.csproj b/OpenRA.FileFormats/OpenRA.FileFormats.csproj
index 02c58d7291..95e2f659fa 100644
--- a/OpenRA.FileFormats/OpenRA.FileFormats.csproj
+++ b/OpenRA.FileFormats/OpenRA.FileFormats.csproj
@@ -120,7 +120,10 @@
+
+
+
diff --git a/OpenRA.FileFormats/Primitives/IObservableCollection.cs b/OpenRA.FileFormats/Primitives/IObservableCollection.cs
new file mode 100644
index 0000000000..f7da4fe6c3
--- /dev/null
+++ b/OpenRA.FileFormats/Primitives/IObservableCollection.cs
@@ -0,0 +1,25 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2013 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections;
+
+namespace OpenRA.FileFormats.Primitives
+{
+ public interface IObservableCollection
+ {
+ event Action OnAdd;
+ event Action OnRemove;
+ event Action OnRemoveAt;
+ event Action OnSet;
+ event Action OnRefresh;
+ IEnumerable ObservedItems { get; }
+ }
+}
diff --git a/OpenRA.FileFormats/Primitives/ObservableCollection.cs b/OpenRA.FileFormats/Primitives/ObservableCollection.cs
new file mode 100644
index 0000000000..610cb28762
--- /dev/null
+++ b/OpenRA.FileFormats/Primitives/ObservableCollection.cs
@@ -0,0 +1,59 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2013 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace OpenRA.FileFormats.Primitives
+{
+ public class ObservableCollection : Collection, IObservableCollection
+ {
+ public event Action OnAdd = k => { };
+ public event Action OnRemove = k => { };
+ public event Action OnRemoveAt = i => { };
+ public event Action OnSet = (o, n) => { };
+ public event Action OnRefresh = () => { };
+
+ public ObservableCollection() : base() { }
+ public ObservableCollection(IList list) : base(list) { }
+
+ protected override void SetItem(int index, T item)
+ {
+ var old = this[index];
+ base.SetItem(index, item);
+ OnSet(old, item);
+ }
+
+ protected override void InsertItem(int index, T item)
+ {
+ base.InsertItem(index, item);
+ OnAdd(item);
+ }
+
+ protected override void ClearItems()
+ {
+ base.ClearItems();
+ OnRefresh();
+ }
+
+ protected override void RemoveItem(int index)
+ {
+ base.RemoveItem(index);
+ OnRemoveAt(index);
+ }
+
+ public IEnumerable ObservedItems
+ {
+ get { return base.Items; }
+ }
+ }
+}
diff --git a/OpenRA.FileFormats/Primitives/ObservableDictionary.cs b/OpenRA.FileFormats/Primitives/ObservableDictionary.cs
new file mode 100644
index 0000000000..2f078cb451
--- /dev/null
+++ b/OpenRA.FileFormats/Primitives/ObservableDictionary.cs
@@ -0,0 +1,137 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2013 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace OpenRA.FileFormats.Primitives
+{
+ public class ObservableSortedDictionary : ObservableDictionary
+ {
+ public ObservableSortedDictionary(IComparer comparer)
+ {
+ InnerDict = new SortedDictionary(comparer);
+ }
+
+ public override void Add(TKey key, TValue value)
+ {
+ InnerDict.Add(key, value);
+ FireOnRefresh();
+ }
+ }
+
+ public class ObservableDictionary : IDictionary, IObservableCollection
+ {
+ protected IDictionary InnerDict;
+
+ public event Action OnAdd = k => { };
+ public event Action OnRemove = k => { };
+ public event Action OnRemoveAt = i => { };
+ public event Action OnSet = (o, n) => { };
+ public event Action OnRefresh = () => { };
+
+ protected void FireOnRefresh()
+ {
+ OnRefresh();
+ }
+
+ protected ObservableDictionary() { }
+
+ public ObservableDictionary(IEqualityComparer comparer)
+ {
+ InnerDict = new Dictionary(comparer);
+ }
+
+ public virtual void Add(TKey key, TValue value)
+ {
+ InnerDict.Add(key, value);
+ OnAdd(key);
+ }
+
+ public bool Remove(TKey key)
+ {
+ var found = InnerDict.Remove(key);
+ if (found)
+ OnRemove(key);
+ return found;
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return InnerDict.ContainsKey(key);
+ }
+
+ public ICollection Keys { get { return InnerDict.Keys; } }
+ public ICollection Values { get { return InnerDict.Values; } }
+
+ public bool TryGetValue(TKey key, out TValue value)
+ {
+ return InnerDict.TryGetValue(key, out value);
+ }
+
+ public TValue this[TKey key]
+ {
+ get { return InnerDict[key]; }
+ set { InnerDict[key] = value; }
+ }
+
+ public void Clear()
+ {
+ InnerDict.Clear();
+ OnRefresh();
+ }
+
+ public int Count
+ {
+ get { return InnerDict.Count; }
+ }
+
+ public void Add(KeyValuePair item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ return InnerDict.Contains(item);
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ InnerDict.CopyTo(array, arrayIndex);
+ }
+
+ public bool IsReadOnly
+ {
+ get { return InnerDict.IsReadOnly; }
+ }
+
+ public bool Remove(KeyValuePair item)
+ {
+ return Remove(item.Key);
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return InnerDict.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return InnerDict.GetEnumerator();
+ }
+
+ public IEnumerable ObservedItems
+ {
+ get { return InnerDict.Keys; }
+ }
+ }
+}