diff --git a/Perspex.Base/Collections/PerspexReadOnlyListView.cs b/Perspex.Base/Collections/PerspexReadOnlyListView.cs
new file mode 100644
index 0000000000..dd5f8af61b
--- /dev/null
+++ b/Perspex.Base/Collections/PerspexReadOnlyListView.cs
@@ -0,0 +1,93 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Collections
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Linq;
+
+ public class PerspexReadOnlyListView : IReadOnlyPerspexList, IDisposable
+ {
+ private IReadOnlyPerspexList inner;
+
+ private Func convert;
+
+ public PerspexReadOnlyListView(
+ IReadOnlyPerspexList inner,
+ Func convert)
+ {
+ this.inner = inner;
+ this.convert = convert;
+ this.inner.CollectionChanged += this.InnerCollectionChanged;
+ }
+
+ public TOut this[int index]
+ {
+ get { return this.convert(this.inner[index]); }
+ }
+
+ public int Count
+ {
+ get { return this.inner.Count; }
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void Dispose()
+ {
+ this.inner.CollectionChanged -= this.InnerCollectionChanged;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return this.inner.Select(convert).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ private IList ConvertList(IList list)
+ {
+ return list.Cast().Select(this.convert).ToList();
+ }
+
+ private void InnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (this.CollectionChanged != null)
+ {
+ NotifyCollectionChangedEventArgs ev;
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ ev = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Add,
+ this.ConvertList(e.NewItems),
+ e.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ ev = new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Remove,
+ this.ConvertList(e.OldItems),
+ e.NewStartingIndex);
+ break;
+ default:
+ throw new NotSupportedException("Action not yet implemented.");
+ }
+
+ this.CollectionChanged(this, ev);
+ }
+ }
+ }
+}
diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj
index 1ddb527ce9..6959c65d9d 100644
--- a/Perspex.Base/Perspex.Base.csproj
+++ b/Perspex.Base/Perspex.Base.csproj
@@ -36,6 +36,7 @@
+
diff --git a/Perspex.Controls.UnitTests/ItemsControlTests.cs b/Perspex.Controls.UnitTests/ItemsControlTests.cs
new file mode 100644
index 0000000000..72a0f98a33
--- /dev/null
+++ b/Perspex.Controls.UnitTests/ItemsControlTests.cs
@@ -0,0 +1,294 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Controls.UnitTests
+{
+ using System;
+ using System.Collections.Specialized;
+ using System.Linq;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using Moq;
+ using Perspex.Collections;
+ using Perspex.Controls;
+ using Perspex.Controls.Presenters;
+ using Perspex.Layout;
+ using Perspex.Platform;
+ using Perspex.Styling;
+ using Perspex.VisualTree;
+ using Ploeh.AutoFixture;
+ using Ploeh.AutoFixture.AutoMoq;
+ using Splat;
+
+ [TestClass]
+ public class ItemsControlTests
+ {
+ [TestMethod]
+ public void Template_Should_Be_Instantiated()
+ {
+ using (var ctx = this.RegisterServices())
+ {
+ var target = new ItemsControl();
+ target.Items = new[] { "Foo" };
+ target.Template = this.GetTemplate();
+
+ target.Measure(new Size(100, 100));
+
+ var child = ((IVisual)target).VisualChildren.Single();
+ Assert.IsInstanceOfType(child, typeof(Border));
+ child = child.VisualChildren.Single();
+ Assert.IsInstanceOfType(child, typeof(ItemsPresenter));
+ child = child.VisualChildren.Single();
+ Assert.IsInstanceOfType(child, typeof(StackPanel));
+ child = child.VisualChildren.Single();
+ Assert.IsInstanceOfType(child, typeof(TextBlock));
+ }
+ }
+
+ [TestMethod]
+ public void Templated_Children_Should_Be_Styled()
+ {
+ using (var ctx = this.RegisterServices())
+ {
+ var root = new TestRoot();
+ var target = new ItemsControl();
+ var styler = new Mock();
+
+ Locator.CurrentMutable.Register(() => styler.Object, typeof(IStyler));
+ target.Items = new[] { "Foo" };
+ target.Template = this.GetTemplate();
+ root.Content = target;
+
+ target.ApplyTemplate();
+
+ styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once());
+ styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once());
+ styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once());
+ styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once());
+ styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once());
+ }
+ }
+
+ [TestMethod]
+ public void ItemsPresenter_And_Panel_Should_Have_TemplatedParent_Set()
+ {
+ var target = new ItemsControl();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { "Foo" };
+ target.ApplyTemplate();
+
+ var presenter = target.GetTemplateControls().OfType().Single();
+ var panel = target.GetTemplateControls().OfType().Single();
+
+ Assert.AreEqual(target, presenter.TemplatedParent);
+ Assert.AreEqual(target, panel.TemplatedParent);
+ }
+
+ [TestMethod]
+ public void Item_Should_Have_TemplatedParent_Set_To_Null()
+ {
+ var target = new ItemsControl();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { "Foo" };
+ target.ApplyTemplate();
+
+ var panel = target.GetTemplateControls().OfType().Single();
+ var item = (TextBlock)panel.GetVisualChildren().First();
+
+ Assert.IsNull(item.TemplatedParent);
+ }
+
+ [TestMethod]
+ public void Control_Item_Should_Set_Control_Parent()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { child };
+ target.ApplyTemplate();
+
+ Assert.AreEqual(target, child.Parent);
+ Assert.AreEqual(target, ((ILogical)child).LogicalParent);
+ }
+
+ [TestMethod]
+ public void Clearing_Control_Item_Should_Clear_Child_Controls_Parent()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { child };
+ target.ApplyTemplate();
+ target.Items = null;
+
+ Assert.IsNull(child.Parent);
+ Assert.IsNull(((ILogical)child).LogicalParent);
+ }
+
+ [TestMethod]
+ public void Control_Item_Should_Make_Control_Appear_In_LogicalChildren()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { child };
+ target.ApplyTemplate();
+
+ CollectionAssert.AreEqual(new[] { child }, ((ILogical)target).LogicalChildren.ToList());
+ }
+
+ [TestMethod]
+ public void String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { "Foo" };
+ target.ApplyTemplate();
+
+ var logical = (ILogical)target;
+ Assert.AreEqual(1, logical.LogicalChildren.Count);
+ Assert.IsInstanceOfType(logical.LogicalChildren[0], typeof(TextBlock));
+ }
+
+ [TestMethod]
+ public void Setting_Items_To_Null_Should_Remove_LogicalChildren()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { "Foo" };
+ target.ApplyTemplate();
+ target.Items = null;
+
+ CollectionAssert.AreEqual(new ILogical[0], ((ILogical)target).LogicalChildren.ToList());
+ }
+
+ [TestMethod]
+ public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+ var called = false;
+
+ target.Template = this.GetTemplate();
+ target.ApplyTemplate();
+
+ ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
+ called = e.Action == NotifyCollectionChangedAction.Add;
+
+ target.Items = new[] { child };
+
+ Assert.IsTrue(called);
+ }
+
+ [TestMethod]
+ public void Setting_Items_To_Null_Should_Fire_LogicalChildren_CollectionChanged()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+ var called = false;
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { child };
+ target.ApplyTemplate();
+
+ ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
+ called = e.Action == NotifyCollectionChangedAction.Remove;
+
+ target.Items = null;
+
+ Assert.IsTrue(called);
+ }
+
+ [TestMethod]
+ public void Changing_Items_Should_Fire_LogicalChildren_CollectionChanged()
+ {
+ var target = new ItemsControl();
+ var child = new Control();
+ var called = false;
+
+ target.Template = this.GetTemplate();
+ target.Items = new[] { child };
+ target.ApplyTemplate();
+
+ ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
+
+ target.Items = new[] { "Foo" };
+
+ Assert.IsTrue(called);
+ }
+
+ [TestMethod]
+ public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged()
+ {
+ var target = new ItemsControl();
+ var items = new PerspexList { "Foo" };
+ var called = false;
+
+ target.Template = this.GetTemplate();
+ target.Items = items;
+ target.ApplyTemplate();
+
+ ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
+ called = e.Action == NotifyCollectionChangedAction.Add;
+
+ items.Add("Bar");
+
+ Assert.IsTrue(called);
+ }
+
+ [TestMethod]
+ public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged()
+ {
+ var target = new ItemsControl();
+ var items = new PerspexList { "Foo", "Bar" };
+ var called = false;
+
+ target.Template = this.GetTemplate();
+ target.Items = items;
+ target.ApplyTemplate();
+
+ ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
+ called = e.Action == NotifyCollectionChangedAction.Remove;
+
+ items.Remove("Bar");
+
+ Assert.IsTrue(called);
+ }
+
+ private ControlTemplate GetTemplate()
+ {
+ return ControlTemplate.Create(parent =>
+ {
+ return new Border
+ {
+ Background = new Perspex.Media.SolidColorBrush(0xffffffff),
+ Content = new ItemsPresenter
+ {
+ Id = "presenter",
+ [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
+ }
+ };
+ });
+ }
+
+ private IDisposable RegisterServices()
+ {
+ var result = Locator.CurrentMutable.WithResolver();
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ var renderInterface = fixture.Create();
+ Locator.CurrentMutable.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface));
+ return result;
+ }
+ }
+}
diff --git a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
index c16687597e..35214ff6b9 100644
--- a/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
+++ b/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
@@ -64,6 +64,7 @@
+