Browse Source

Merge pull request #1008 from AvaloniaUI/fixes/1004-remove-devtools-reactiveui-depenency

Remove ReactiveUI dependency from DevTools.
Nikita Tsukanov 8 years ago
parent
commit
99393ab36d

+ 1 - 0
samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj

@@ -22,6 +22,7 @@
     <ItemGroup>
         <ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />
         <ProjectReference Include="..\..\..\src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj" />
+        <ProjectReference Include="..\..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
         <ProjectReference Include="..\..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
         <ProjectReference Include="..\..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
         <ProjectReference Include="..\..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />

+ 33 - 90
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
@@ -59,7 +60,8 @@ namespace Avalonia.Collections
         /// the index in the collection and the item.
         /// </param>
         /// <param name="reset">
-        /// An action called when the collection is reset.
+        /// An action called when the collection is reset. This will be followed by calls to 
+        /// <paramref name="added"/> for each item present in the collection after the reset.
         /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
@@ -68,112 +70,38 @@ namespace Avalonia.Collections
             Action<int, T> removed,
             Action reset)
         {
-            int index;
-
-            NotifyCollectionChangedEventHandler handler = (_, e) =>
+            void Add(int index, IList items)
             {
-                switch (e.Action)
+                foreach (T item in items)
                 {
-                    case NotifyCollectionChangedAction.Add:
-                        index = e.NewStartingIndex;
-
-                        foreach (T item in e.NewItems)
-                        {
-                            added(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Replace:
-                        index = e.OldStartingIndex;
-
-                        foreach (T item in e.OldItems)
-                        {
-                            removed(index++, item);
-                        }
-
-                        index = e.NewStartingIndex;
-
-                        foreach (T item in e.NewItems)
-                        {
-                            added(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Remove:
-                        index = e.OldStartingIndex;
-
-                        foreach (T item in e.OldItems)
-                        {
-                            removed(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Reset:
-                        if (reset == null)
-                        {
-                            throw new InvalidOperationException(
-                                "Reset called on collection without reset handler.");
-                        }
-
-                        reset();
-                        break;
+                    added(index++, item);
                 }
-            };
+            }
 
-            index = 0;
-            foreach (T i in collection)
+            void Remove(int index, IList items)
             {
-                added(index++, i);
+                for (var i = items.Count - 1; i >= 0; --i)
+                {
+                    removed(index + i, (T)items[i]);
+                }
             }
 
-            collection.CollectionChanged += handler;
-
-            return Disposable.Create(() => collection.CollectionChanged -= handler);
-        }
-
-        /// <summary>
-        /// Invokes an action for each item in a collection and subsequently each item added or
-        /// removed from the collection.
-        /// </summary>
-        /// <typeparam name="T">The type of the collection items.</typeparam>
-        /// <param name="collection">The collection.</param>
-        /// <param name="added">
-        /// An action called initially with all items in the collection and subsequently with a
-        /// list of items added to the collection. The parameters passed are the index of the
-        /// first item added to the collection and the items added.
-        /// </param>
-        /// <param name="removed">
-        /// An action called with all items removed from the collection. The parameters passed 
-        /// are the index of the first item removed from the collection and the items removed.
-        /// </param>
-        /// <param name="reset">
-        /// An action called when the collection is reset.
-        /// </param>
-        /// <returns>A disposable used to terminate the subscription.</returns>
-        public static IDisposable ForEachItem<T>(
-            this IAvaloniaReadOnlyList<T> collection,
-            Action<int, IEnumerable<T>> added,
-            Action<int, IEnumerable<T>> removed,
-            Action reset)
-        {
             NotifyCollectionChangedEventHandler handler = (_, e) =>
             {
                 switch (e.Action)
                 {
                     case NotifyCollectionChangedAction.Add:
-                        added(e.NewStartingIndex, e.NewItems.Cast<T>());
+                        Add(e.NewStartingIndex, e.NewItems);
                         break;
 
+                    case NotifyCollectionChangedAction.Move:
                     case NotifyCollectionChangedAction.Replace:
-                        removed(e.OldStartingIndex, e.OldItems.Cast<T>());
-                        added(e.NewStartingIndex, e.NewItems.Cast<T>());
+                        Remove(e.OldStartingIndex, e.OldItems);
+                        Add(e.NewStartingIndex, e.NewItems);
                         break;
 
                     case NotifyCollectionChangedAction.Remove:
-                        removed(e.OldStartingIndex, e.OldItems.Cast<T>());
+                        Remove(e.OldStartingIndex, e.OldItems);
                         break;
 
                     case NotifyCollectionChangedAction.Reset:
@@ -184,16 +112,31 @@ namespace Avalonia.Collections
                         }
 
                         reset();
+                        Add(0, (IList)collection);
                         break;
                 }
             };
 
-            added(0, collection);
+            Add(0, (IList)collection);
             collection.CollectionChanged += handler;
 
             return Disposable.Create(() => collection.CollectionChanged -= handler);
         }
 
+        public static IAvaloniaReadOnlyList<TDerived> CreateDerivedList<TSource, TDerived>(
+            this IAvaloniaReadOnlyList<TSource> collection,
+            Func<TSource, TDerived> select)
+        {
+            var result = new AvaloniaList<TDerived>();
+
+            collection.ForEachItem(
+                (i, item) => result.Insert(i, select(item)),
+                (i, item) => result.RemoveAt(i),
+                () => result.Clear());
+
+            return result;
+        }
+
         /// <summary>
         /// Listens for property changed events from all items in a collection.
         /// </summary>

+ 0 - 1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@@ -34,7 +34,6 @@
     <ProjectReference Include="..\Avalonia.Input\Avalonia.Input.csproj" />
     <ProjectReference Include="..\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
     <ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
-    <ProjectReference Include="..\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />

+ 1 - 2
src/Avalonia.Diagnostics/DevTools.xaml.cs

@@ -11,7 +11,6 @@ using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia
 {
@@ -74,7 +73,7 @@ namespace Avalonia.Diagnostics
                         Content = devTools,
                         DataTemplates = new DataTemplates
                         {
-                            new ViewLocator<ReactiveObject>(),
+                            new ViewLocator<ViewModelBase>(),
                         }
                     };
 

+ 1 - 2
src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -4,11 +4,10 @@
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class ControlDetailsViewModel : ReactiveObject
+    internal class ControlDetailsViewModel : ViewModelBase
     {
         public ControlDetailsViewModel(IVisual control)
         {

+ 43 - 31
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -5,32 +5,50 @@ using System;
 using System.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.Input;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class DevToolsViewModel : ReactiveObject
+    internal class DevToolsViewModel : ViewModelBase
     {
-        private ReactiveObject _content;
-
+        private ViewModelBase _content;
         private int _selectedTab;
-
         private TreePageViewModel _logicalTree;
-
         private TreePageViewModel _visualTree;
-
-        private readonly ObservableAsPropertyHelper<string> _focusedControl;
-
-        private readonly ObservableAsPropertyHelper<string> _pointerOverElement;
+        private string _focusedControl;
+        private string _pointerOverElement;
 
         public DevToolsViewModel(IControl root)
         {
             _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
             _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
 
-            this.WhenAnyValue(x => x.SelectedTab).Subscribe(index =>
+            UpdateFocusedControl();
+            KeyboardDevice.Instance.PropertyChanged += (s, e) =>
             {
-                switch (index)
+                if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
+                {
+                    UpdateFocusedControl();
+                }
+            };
+
+            root.GetObservable(TopLevel.PointerOverElementProperty)
+                .Subscribe(x => PointerOverElement = x?.GetType().Name);
+        }
+
+        public ViewModelBase Content
+        {
+            get { return _content; }
+            private set { RaiseAndSetIfChanged(ref _content, value); }
+        }
+
+        public int SelectedTab
+        {
+            get { return _selectedTab; }
+            set
+            {
+                _selectedTab = value;
+
+                switch (value)
                 {
                     case 0:
                         Content = _logicalTree;
@@ -39,34 +57,23 @@ namespace Avalonia.Diagnostics.ViewModels
                         Content = _visualTree;
                         break;
                 }
-            });
 
-            _focusedControl = KeyboardDevice.Instance
-                .WhenAnyValue(x => x.FocusedElement)
-                .Select(x => x?.GetType().Name)
-                .ToProperty(this, x => x.FocusedControl);
-
-            _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
-                .Select(x => x?.GetType().Name)
-                .ToProperty(this, x => x.PointerOverElement);
+                RaisePropertyChanged();
+            }
         }
 
-        public ReactiveObject Content
+        public string FocusedControl
         {
-            get { return _content; }
-            private set { this.RaiseAndSetIfChanged(ref _content, value); }
+            get { return _focusedControl; }
+            private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
         }
 
-        public int SelectedTab
+        public string PointerOverElement
         {
-            get { return _selectedTab; }
-            set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
+            get { return _pointerOverElement; }
+            private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
         }
 
-        public string FocusedControl => _focusedControl.Value;
-
-        public string PointerOverElement => _pointerOverElement.Value;
-
         public void SelectControl(IControl control)
         {
             var tree = Content as TreePageViewModel;
@@ -76,5 +83,10 @@ namespace Avalonia.Diagnostics.ViewModels
                 tree.SelectControl(control);
             }
         }
+
+        private void UpdateFocusedControl()
+        {
+            _focusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
+        }
     }
 }

+ 2 - 2
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -2,9 +2,9 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -13,7 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public LogicalTreeNode(ILogical logical, TreeNode parent)
             : base((Control)logical, parent)
         {
-            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this));
+            Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this));
         }
 
         public static LogicalTreeNode[] Create(object control)

+ 4 - 7
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@@ -3,16 +3,13 @@
 
 using System;
 using Avalonia.Data;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class PropertyDetails : ReactiveObject
+    internal class PropertyDetails : ViewModelBase
     {
         private object _value;
-
         private string _priority;
-
         private string _diagnostic;
 
         public PropertyDetails(AvaloniaObject o, AvaloniaProperty property)
@@ -41,19 +38,19 @@ namespace Avalonia.Diagnostics.ViewModels
         public string Priority
         {
             get { return _priority; }
-            private set { this.RaiseAndSetIfChanged(ref _priority, value); }
+            private set { RaiseAndSetIfChanged(ref _priority, value); }
         }
 
         public string Diagnostic
         {
             get { return _diagnostic; }
-            private set { this.RaiseAndSetIfChanged(ref _diagnostic, value); }
+            private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
         }
 
         public object Value
         {
             get { return _value; }
-            private set { this.RaiseAndSetIfChanged(ref _value, value); }
+            private set { RaiseAndSetIfChanged(ref _value, value); }
         }
     }
 }

+ 5 - 5
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs

@@ -5,13 +5,13 @@ using System;
 using System.Collections.Specialized;
 using System.Reactive;
 using System.Reactive.Linq;
+using Avalonia.Collections;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreeNode : ReactiveObject
+    internal class TreeNode : ViewModelBase
     {
         private string _classes;
         private bool _isExpanded;
@@ -47,7 +47,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public IReadOnlyReactiveList<TreeNode> Children
+        public IAvaloniaReadOnlyList<TreeNode> Children
         {
             get;
             protected set;
@@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public string Classes
         {
             get { return _classes; }
-            private set { this.RaiseAndSetIfChanged(ref _classes, value); }
+            private set { RaiseAndSetIfChanged(ref _classes, value); }
         }
 
         public IVisual Visual
@@ -67,7 +67,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public bool IsExpanded
         {
             get { return _isExpanded; }
-            set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
+            set { RaiseAndSetIfChanged(ref _isExpanded, value); }
         }
 
         public TreeNode Parent

+ 14 - 10
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs

@@ -1,25 +1,19 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreePageViewModel : ReactiveObject
+    internal class TreePageViewModel : ViewModelBase
     {
         private TreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
+        private ControlDetailsViewModel _details;
 
         public TreePageViewModel(TreeNode[] nodes)
         {
             Nodes = nodes;
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Visual) : null)
-                .ToProperty(this, x => x.Details);
         }
 
         public TreeNode[] Nodes { get; protected set; }
@@ -27,10 +21,20 @@ namespace Avalonia.Diagnostics.ViewModels
         public TreeNode SelectedNode
         {
             get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _selected, value))
+                {
+                    Details = value != null ? new ControlDetailsViewModel(value.Visual) : null;
+                }
+            }
         }
 
-        public ControlDetailsViewModel Details => _details.Value;
+        public ControlDetailsViewModel Details
+        {
+            get { return _details; }
+            private set { RaiseAndSetIfChanged(ref _details, value); }
+        }
 
         public TreeNode FindNode(IControl control)
         {

+ 32 - 0
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    public class ViewModelBase : INotifyPropertyChanged
+    {
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        [NotifyPropertyChangedInvocator]
+        protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (!EqualityComparer<T>.Default.Equals(field, value))
+            {
+                field = value;
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+                return true;
+            }
+
+            return false;
+        }
+
+        [NotifyPropertyChangedInvocator]
+        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+    }
+}

+ 3 - 4
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs

@@ -1,10 +1,9 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using Avalonia.Controls;
+using Avalonia.Collections;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -17,11 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels
 
             if (host?.Root == null)
             {
-                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this));
+                Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this));
             }
             else
             {
-                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
+                Children = new AvaloniaList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
             }
 
             if ((Visual is IStyleable styleable))

+ 10 - 10
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@@ -8,7 +8,6 @@ using Avalonia.Controls;
 using Avalonia.Diagnostics.ViewModels;
 using Avalonia.Media;
 using Avalonia.Styling;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.Views
 {
@@ -16,6 +15,7 @@ namespace Avalonia.Diagnostics.Views
     {
         private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
             AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>("ViewModel");
+        private SimpleGrid _grid;
 
         public ControlDetailsView()
         {
@@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views
         public ControlDetailsViewModel ViewModel
         {
             get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
+            private set
+            {
+                SetValue(ViewModelProperty, value);
+                _grid[GridRepeater.ItemsProperty] = value?.Properties;
+            }
         }
 
         private void InitializeComponent()
@@ -36,7 +40,7 @@ namespace Avalonia.Diagnostics.Views
 
             Content = new ScrollViewer
             {
-                Content = new SimpleGrid
+                Content = _grid = new SimpleGrid
                 {
                     Styles = new Styles
                     {
@@ -49,7 +53,6 @@ namespace Avalonia.Diagnostics.Views
                         },
                     },
                     [GridRepeater.TemplateProperty] = pt,
-                    [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(),
                 }
             };
         }
@@ -62,16 +65,13 @@ namespace Avalonia.Diagnostics.Views
             {
                 Text = property.Name,
                 TextWrapping = TextWrapping.NoWrap,
-                [!ToolTip.TipProperty] = property
-                    .WhenAnyValue(x => x.Diagnostic)
-                    .ToBinding(),
+                [!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
             };
 
             yield return new TextBlock
             {
                 TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property
-                    .WhenAnyValue(v => v.Value)
+                [!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
                     .Select(v => v?.ToString())
                     .ToBinding(),
             };
@@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.Views
             yield return new TextBlock
             {
                 TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).ToBinding(),
+                [!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
             };
         }
     }

+ 30 - 0
src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs

@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Reflection;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal static class PropertyChangedExtenions
+    {
+        public static IObservable<T> GetObservable<T>(this INotifyPropertyChanged source, string propertyName)
+        {
+            Contract.Requires<ArgumentNullException>(source != null);
+            Contract.Requires<ArgumentNullException>(propertyName != null);
+
+            var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName);
+
+            if (property == null)
+            {
+                throw new ArgumentException($"Property '{propertyName}' not found on '{source}.");
+            }
+
+            return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
+                e => source.PropertyChanged += e,
+                e => source.PropertyChanged -= e)
+                    .Where(e => e.EventArgs.PropertyName == propertyName)
+                    .Select(_ => (T)property.GetValue(source))
+                    .StartWith((T)property.GetValue(source));
+        }
+    }
+}

+ 2 - 1
src/Avalonia.Input/IKeyboardDevice.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
 
 namespace Avalonia.Input
 {
@@ -26,7 +27,7 @@ namespace Avalonia.Input
         Toggled = 2,
     }
 
-    public interface IKeyboardDevice : IInputDevice
+    public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
     {
         IInputElement FocusedElement { get; }
 

+ 134 - 0
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs

@@ -0,0 +1,134 @@
+using System;
+using System.Linq;
+using Avalonia.Collections;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Collections
+{
+    public class AvaloniaListExtenionsTests
+    {
+        [Fact]
+        public void CreateDerivedList_Creates_Initial_Items()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Add()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Add(4);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Insert()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Insert(1, 4);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Remove()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Remove(2);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_RemoveRange()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.RemoveRange(1, 2);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Move()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Move(2, 0);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_MoveRange()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.MoveRange(1, 2, 0);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Replace()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source[1] = 4;
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Clear()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Clear();
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        private class Wrapper
+        {
+            public Wrapper(int value)
+            {
+                Value = value;
+            }
+
+            public int Value { get; }
+        }
+    }
+}