Browse Source

Fix conflict

all.owing 6 years ago
parent
commit
54387d211e
70 changed files with 2093 additions and 214 deletions
  1. 55 3
      Avalonia.sln
  2. 8 0
      build/AndroidWorkarounds.props
  3. 1 0
      build/CoreLibraries.props
  4. 1 0
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  5. 9 4
      samples/ControlCatalog.NetCore/Program.cs
  6. 1 0
      samples/ControlCatalog/MainWindow.xaml.cs
  7. 170 59
      src/Avalonia.Base/Collections/AvaloniaList.cs
  8. 3 3
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  9. 14 12
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  10. 4 4
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  11. 9 2
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  12. 3 2
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  13. 2 2
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  14. 11 12
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  15. 4 4
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  16. 2 3
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  17. 1 2
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs
  18. 2 2
      src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs
  19. 20 8
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  20. 15 8
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  21. 15 6
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  22. 7 5
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  23. 9 4
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  24. 4 2
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  25. 16 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  26. 1 1
      src/Avalonia.Base/Data/Core/StreamNode.cs
  27. 11 0
      src/Avalonia.Controls/AppBuilderBase.cs
  28. 23 0
      src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs
  29. 24 0
      src/Avalonia.Controls/Platform/MountedDriveInfo.cs
  30. 13 3
      src/Avalonia.Controls/SystemDialog.cs
  31. 18 0
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  32. 40 0
      src/Avalonia.Dialogs/ByteSizeHelper.cs
  33. 21 0
      src/Avalonia.Dialogs/ChildFitter.cs
  34. 26 0
      src/Avalonia.Dialogs/FileSizeStringConverter.cs
  35. 31 0
      src/Avalonia.Dialogs/InternalViewModelBase.cs
  36. 144 0
      src/Avalonia.Dialogs/ManagedFileChooser.xaml
  37. 88 0
      src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
  38. 50 0
      src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs
  39. 9 0
      src/Avalonia.Dialogs/ManagedFileChooserItemType.cs
  40. 77 0
      src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs
  41. 9 0
      src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs
  42. 86 0
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  43. 368 0
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  44. 69 0
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  45. 21 0
      src/Avalonia.Dialogs/ResourceSelectorConverter.cs
  46. 11 0
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  47. 101 0
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs
  48. 16 0
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs
  49. 31 0
      src/Avalonia.FreeDesktop/NativeMethods.cs
  50. 6 6
      src/Avalonia.Input/Gestures.cs
  51. 2 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  52. 78 0
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  53. 1 0
      src/Avalonia.X11/Avalonia.X11.csproj
  54. 39 18
      src/Avalonia.X11/X11KeyTransform.cs
  55. 4 1
      src/Avalonia.X11/X11Platform.cs
  56. 2 0
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs
  57. 1 1
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  58. 1 1
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  59. 2 2
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  60. 18 8
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  61. 3 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  62. 71 0
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  63. 15 0
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs
  64. 17 0
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  65. 1 1
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  66. 7 7
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs
  67. 2 2
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs
  68. 4 4
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  69. 132 0
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  70. 13 7
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

+ 55 - 3
Avalonia.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27130.2027
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29102.190
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
 EndProject
@@ -197,7 +197,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
 EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
@@ -1842,6 +1846,54 @@ Global
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.Build.0 = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 8 - 0
build/AndroidWorkarounds.props

@@ -5,4 +5,12 @@
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
     <PackageReference Include="System.Buffers" Version="4.5.0" />
   </ItemGroup>
+  <Target Name="_RemoveNonExistingResgenFile" BeforeTargets="CoreCompile" Condition="'$(_SdkSetAndroidResgenFile)' == 'true' And '$(AndroidResgenFile)' != '' And !Exists('$(AndroidResgenFile)')">
+    <ItemGroup>
+      <Compile Remove="$(AndroidResgenFile)"/>
+    </ItemGroup>
+  </Target>
+  <PropertyGroup>
+    <DesignTimeBuild>false</DesignTimeBuild>
+  </PropertyGroup>
 </Project>

+ 1 - 0
build/CoreLibraries.props

@@ -13,6 +13,7 @@
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Styling/Avalonia.Styling.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.OpenGL/Avalonia.OpenGL.csproj" />
+      <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Dialogs/Avalonia.Dialogs.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup/Avalonia.Markup.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" />
       <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj" Condition="'$(TargetFramework)' != 'netstandard2.0'" />

+ 1 - 0
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@@ -7,6 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />

+ 9 - 4
samples/ControlCatalog.NetCore/Program.cs

@@ -8,6 +8,9 @@ using Avalonia.Controls;
 using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.Skia;
 using Avalonia.ReactiveUI;
+using Avalonia.Dialogs;
+using System.Collections.Generic;
+using System.Threading.Tasks;
 
 namespace ControlCatalog.NetCore
 {
@@ -51,21 +54,22 @@ namespace ControlCatalog.NetCore
             else
                 return builder.StartWithClassicDesktopLifetime(args);
         }
-        
+
         /// <summary>
         /// This method is needed for IDE previewer infrastructure
         /// </summary>
         public static AppBuilder BuildAvaloniaApp()
             => AppBuilder.Configure<App>()
                 .UsePlatformDetect()
-                .With(new X11PlatformOptions {EnableMultiTouch = true})
+                .With(new X11PlatformOptions { EnableMultiTouch = true })
                 .With(new Win32PlatformOptions
                 {
                     EnableMultitouch = true,
                     AllowEglInitialization = true
                 })
                 .UseSkia()
-                .UseReactiveUI();
+                .UseReactiveUI()
+                .UseManagedSystemDialogs();
 
         static void SilenceConsole()
         {
@@ -74,7 +78,8 @@ namespace ControlCatalog.NetCore
                 Console.CursorVisible = false;
                 while (true)
                     Console.ReadKey(true);
-            }) {IsBackground = true}.Start();
+            })
+            { IsBackground = true }.Start();
         }
     }
 }

+ 1 - 0
samples/ControlCatalog/MainWindow.xaml.cs

@@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml;
 using Avalonia.Threading;
 using ControlCatalog.ViewModels;
 using System;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 
 namespace ControlCatalog

+ 170 - 59
src/Avalonia.Base/Collections/AvaloniaList.cs

@@ -55,15 +55,15 @@ namespace Avalonia.Collections
     /// </remarks>
     public class AvaloniaList<T> : IAvaloniaList<T>, IList, INotifyCollectionChangedDebug
     {
-        private List<T> _inner;
+        private readonly List<T> _inner;
         private NotifyCollectionChangedEventHandler _collectionChanged;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaList{T}"/> class.
         /// </summary>
         public AvaloniaList()
-            : this(Enumerable.Empty<T>())
         {
+            _inner = new List<T>();
         }
 
         /// <summary>
@@ -89,8 +89,8 @@ namespace Avalonia.Collections
         /// </summary>
         public event NotifyCollectionChangedEventHandler CollectionChanged
         {
-            add { _collectionChanged += value; }
-            remove { _collectionChanged -= value; }
+            add => _collectionChanged += value;
+            remove => _collectionChanged -= value;
         }
 
         /// <summary>
@@ -150,7 +150,7 @@ namespace Avalonia.Collections
 
                 T old = _inner[index];
 
-                if (!object.Equals(old, value))
+                if (!EqualityComparer<T>.Default.Equals(old, value))
                 {
                     _inner[index] = value;
 
@@ -187,45 +187,38 @@ namespace Avalonia.Collections
             Validate?.Invoke(item);
             int index = _inner.Count;
             _inner.Add(item);
-            NotifyAdd(new[] { item }, index);
+            NotifyAdd(item, index);
         }
 
         /// <summary>
         /// Adds multiple items to the collection.
         /// </summary>
         /// <param name="items">The items.</param>
-        public virtual void AddRange(IEnumerable<T> items)
-        {
-            Contract.Requires<ArgumentNullException>(items != null);
-
-            var list = (items as IList) ?? items.ToList();
-
-            if (list.Count > 0)
-            {
-                if (Validate != null)
-                {
-                    foreach (var item in list)
-                    {
-                        Validate((T)item);
-                    }
-                }
-
-                int index = _inner.Count;
-                _inner.AddRange(items);
-                NotifyAdd(list, index);
-            }
-        }
+        public virtual void AddRange(IEnumerable<T> items) => InsertRange(_inner.Count, items);
 
         /// <summary>
         /// Removes all items from the collection.
         /// </summary>
         public virtual void Clear()
         {
-            if (this.Count > 0)
+            if (Count > 0)
             {
-                var old = _inner;
-                _inner = new List<T>();
-                NotifyReset(old);
+                if (_collectionChanged != null)
+                {
+                    var e = ResetBehavior == ResetBehavior.Reset ?
+                        EventArgsCache.ResetCollectionChanged :
+                        new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToList(), 0);
+
+                    _inner.Clear();
+
+                    _collectionChanged(this, e);
+                }
+                else
+                {
+                    _inner.Clear();
+                }
+
+                NotifyCountChanged();
             }
         }
 
@@ -253,9 +246,20 @@ namespace Avalonia.Collections
         /// Returns an enumerator that enumerates the items in the collection.
         /// </summary>
         /// <returns>An <see cref="IEnumerator{T}"/>.</returns>
-        public IEnumerator<T> GetEnumerator()
+        IEnumerator<T> IEnumerable<T>.GetEnumerator()
         {
-            return _inner.GetEnumerator();
+            return new Enumerator(_inner);
+        }
+
+        /// <inheritdoc/>
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return new Enumerator(_inner);
+        }
+
+        public Enumerator GetEnumerator()
+        {
+            return new Enumerator(_inner);
         }
 
         /// <summary>
@@ -289,7 +293,7 @@ namespace Avalonia.Collections
         {
             Validate?.Invoke(item);
             _inner.Insert(index, item);
-            NotifyAdd(new[] { item }, index);
+            NotifyAdd(item, index);
         }
 
         /// <summary>
@@ -301,20 +305,83 @@ namespace Avalonia.Collections
         {
             Contract.Requires<ArgumentNullException>(items != null);
 
-            var list = (items as IList) ?? items.ToList();
+            bool willRaiseCollectionChanged = _collectionChanged != null;
+            bool hasValidation = Validate != null;
 
-            if (list.Count > 0)
+            if (items is IList list)
             {
-                if (Validate != null)
+                if (list.Count > 0)
                 {
-                    foreach (var item in list)
+                    if (list is ICollection<T> collection)
                     {
-                        Validate((T)item);
+                        if (hasValidation)
+                        {
+                            foreach (T item in collection)
+                            {
+                                Validate(item);
+                            }
+                        }
+
+                        _inner.InsertRange(index, collection);
+                        NotifyAdd(list, index);
+                    }
+                    else
+                    {
+                        using (IEnumerator<T> en = items.GetEnumerator())
+                        {
+                            int insertIndex = index;
+
+                            while (en.MoveNext())
+                            {
+                                T item = en.Current;
+
+                                if (hasValidation)
+                                {
+                                    Validate(item);
+                                }
+
+                                _inner.Insert(insertIndex++, item);
+                            }
+                        }
+
+                        NotifyAdd(list, index);
                     }
                 }
+            }
+            else
+            {
+                using (IEnumerator<T> en = items.GetEnumerator())
+                {
+                    if (en.MoveNext())
+                    {
+                        // Avoid allocating list for collection notification if there is no event subscriptions.
+                        List<T> notificationItems = willRaiseCollectionChanged ?
+                            new List<T>() :
+                            null;
+
+                        int insertIndex = index;
+
+                        do
+                        {
+                            T item = en.Current;
+
+                            if (hasValidation)
+                            {
+                                Validate(item);
+                            }
 
-                _inner.InsertRange(index, items);
-                NotifyAdd((items as IList) ?? items.ToList(), index);
+                            _inner.Insert(insertIndex++, item);
+
+                            if (willRaiseCollectionChanged)
+                            {
+                                notificationItems.Add(item);
+                            }
+
+                        } while (en.MoveNext());
+
+                        NotifyAdd(notificationItems, index);
+                    }
+                }
             }
         }
 
@@ -382,7 +449,7 @@ namespace Avalonia.Collections
             if (index != -1)
             {
                 _inner.RemoveAt(index);
-                NotifyRemove(new[] { item }, index);
+                NotifyRemove(item , index);
                 return true;
             }
 
@@ -412,7 +479,7 @@ namespace Avalonia.Collections
         {
             T item = _inner[index];
             _inner.RemoveAt(index);
-            NotifyRemove(new[] { item }, index);
+            NotifyRemove(item , index);
         }
 
         /// <summary>
@@ -480,12 +547,6 @@ namespace Avalonia.Collections
             _inner.CopyTo((T[])array, index);
         }
 
-        /// <inheritdoc/>
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return _inner.GetEnumerator();
-        }
-
         /// <inheritdoc/>
         Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
 
@@ -505,13 +566,29 @@ namespace Avalonia.Collections
             NotifyCountChanged();
         }
 
+        /// <summary>
+        /// Raises the <see cref="CollectionChanged"/> event with a add action.
+        /// </summary>
+        /// <param name="item">The item that was added.</param>
+        /// <param name="index">The starting index.</param>
+        private void NotifyAdd(T item, int index)
+        {
+            if (_collectionChanged != null)
+            {
+                var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new[] { item }, index);
+                _collectionChanged(this, e);
+            }
+
+            NotifyCountChanged();
+        }
+
         /// <summary>
         /// Raises the <see cref="PropertyChanged"/> event when the <see cref="Count"/> property
         /// changes.
         /// </summary>
         private void NotifyCountChanged()
         {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
+            PropertyChanged?.Invoke(this, EventArgsCache.CountPropertyChanged);
         }
 
         /// <summary>
@@ -531,23 +608,57 @@ namespace Avalonia.Collections
         }
 
         /// <summary>
-        /// Raises the <see cref="CollectionChanged"/> event with a reset action.
+        /// Raises the <see cref="CollectionChanged"/> event with a remove action.
         /// </summary>
-        /// <param name="t">The items that were removed.</param>
-        private void NotifyReset(IList t)
+        /// <param name="item">The item that was removed.</param>
+        /// <param name="index">The starting index.</param>
+        private void NotifyRemove(T item, int index)
         {
             if (_collectionChanged != null)
             {
-                NotifyCollectionChangedEventArgs e;
-
-                e = ResetBehavior == ResetBehavior.Reset ? 
-                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) : 
-                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, t, 0);
-
+                var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index);
                 _collectionChanged(this, e);
             }
 
             NotifyCountChanged();
         }
+
+        /// <summary>
+        /// Enumerates the elements of a <see cref="AvaloniaList{T}"/>.
+        /// </summary>
+        public struct Enumerator : IEnumerator<T>
+        {
+            private List<T>.Enumerator _innerEnumerator;
+
+            public Enumerator(List<T> inner)
+            {
+                _innerEnumerator = inner.GetEnumerator();
+            }
+
+            public bool MoveNext()
+            {
+                return _innerEnumerator.MoveNext();
+            }
+
+            void IEnumerator.Reset()
+            {
+                ((IEnumerator)_innerEnumerator).Reset();
+            }
+
+            public T Current => _innerEnumerator.Current;
+
+            object IEnumerator.Current => Current;
+
+            public void Dispose()
+            {
+                _innerEnumerator.Dispose();
+            }
+        }
+    }
+
+    internal static class EventArgsCache
+    {
+        internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(nameof(AvaloniaList<object>.Count));
+        internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
     }
 }

+ 3 - 3
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Data.Core
         {
             try
             {
-                if (Target.IsAlive && Target.Target is IAvaloniaObject obj)
+                if (Target.TryGetTarget(out object target) && target is IAvaloniaObject obj)
                 {
                     obj.SetValue(_property, value, priority);
                     return true;
@@ -37,9 +37,9 @@ namespace Avalonia.Data.Core
             }
         }
 
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
-            if (reference.Target is IAvaloniaObject obj)
+            if (reference.TryGetTarget(out object target) && target is IAvaloniaObject obj)
             {
                 _subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
             }

+ 14 - 12
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@@ -8,27 +8,27 @@ namespace Avalonia.Data.Core
     public abstract class ExpressionNode
     {
         private static readonly object CacheInvalid = new object();
-        protected static readonly WeakReference UnsetReference = 
-            new WeakReference(AvaloniaProperty.UnsetValue);
+        protected static readonly WeakReference<object> UnsetReference = 
+            new WeakReference<object>(AvaloniaProperty.UnsetValue);
 
-        private WeakReference _target = UnsetReference;
+        private WeakReference<object> _target = UnsetReference;
         private Action<object> _subscriber;
         private bool _listening;
 
-        protected WeakReference LastValue { get; private set; }
+        protected WeakReference<object> LastValue { get; private set; }
 
         public abstract string Description { get; }
         public ExpressionNode Next { get; set; }
 
-        public WeakReference Target
+        public WeakReference<object> Target
         {
             get { return _target; }
             set
             {
                 Contract.Requires<ArgumentNullException>(value != null);
 
-                var oldTarget = _target?.Target;
-                var newTarget = value.Target;
+                _target.TryGetTarget(out var oldTarget);
+                value.TryGetTarget(out object newTarget);
 
                 if (!ReferenceEquals(oldTarget, newTarget))
                 {
@@ -72,9 +72,11 @@ namespace Avalonia.Data.Core
             _subscriber = null;
         }
 
-        protected virtual void StartListeningCore(WeakReference reference)
+        protected virtual void StartListeningCore(WeakReference<object> reference)
         {
-            ValueChanged(reference.Target);
+            reference.TryGetTarget(out object target);
+
+            ValueChanged(target);
         }
 
         protected virtual void StopListeningCore()
@@ -96,7 +98,7 @@ namespace Avalonia.Data.Core
 
             if (notification == null)
             {
-                LastValue = new WeakReference(value);
+                LastValue = new WeakReference<object>(value);
 
                 if (Next != null)
                 {
@@ -109,7 +111,7 @@ namespace Avalonia.Data.Core
             }
             else
             {
-                LastValue = new WeakReference(notification.Value);
+                LastValue = new WeakReference<object>(notification.Value);
 
                 if (Next != null)
                 {
@@ -125,7 +127,7 @@ namespace Avalonia.Data.Core
 
         private void StartListening()
         {
-            var target = _target.Target;
+            _target.TryGetTarget(out object target);
 
             if (target == null)
             {

+ 4 - 4
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -78,7 +78,7 @@ namespace Avalonia.Data.Core
 
             _node = node;
             Description = description;
-            _root = new WeakReference(root);
+            _root = new WeakReference<object>(root);
         }
 
         /// <summary>
@@ -120,7 +120,7 @@ namespace Avalonia.Data.Core
             Contract.Requires<ArgumentNullException>(update != null);
             Description = description;
             _node = node;
-            _node.Target = new WeakReference(rootGetter());
+            _node.Target = new WeakReference<object>(rootGetter());
             _root = update.Select(x => rootGetter());
         }
 
@@ -285,13 +285,13 @@ namespace Avalonia.Data.Core
             if (_root is IObservable<object> observable)
             {
                 _rootSubscription = observable.Subscribe(
-                    x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
+                    x => _node.Target = new WeakReference<object>(x != AvaloniaProperty.UnsetValue ? x : null),
                     x => PublishCompleted(),
                     () => PublishCompleted());
             }
             else
             {
-                _node.Target = (WeakReference)_root;
+                _node.Target = (WeakReference<object>)_root;
             }
         }
 

+ 9 - 2
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@@ -36,7 +36,9 @@ namespace Avalonia.Data.Core
         {
             try
             {
-                _setDelegate.DynamicInvoke(Target.Target, value);
+                Target.TryGetTarget(out object target);
+
+                _setDelegate.DynamicInvoke(target, value);
                 return true;
             }
             catch (Exception)
@@ -64,6 +66,11 @@ namespace Avalonia.Data.Core
             return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
         }
 
-        protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?;
+        protected override int? TryGetFirstArgumentAsInt()
+        {
+            Target.TryGetTarget(out object target);
+
+            return _firstArgumentDelegate.DynamicInvoke(target) as int?;
+        } 
     }
 }

+ 3 - 2
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@@ -13,9 +13,10 @@ namespace Avalonia.Data.Core
     {
         private IDisposable _subscription;
         
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
-            var target = reference.Target;
+            reference.TryGetTarget(out object target);
+
             var incc = target as INotifyCollectionChanged;
             var inpc = target as INotifyPropertyChanged;
             var inputs = new List<IObservable<object>>();

+ 2 - 2
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -31,12 +31,12 @@ namespace Avalonia.Data.Core.Plugins
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        public IPropertyAccessor Start(WeakReference reference, string propertyName)
+        public IPropertyAccessor Start(WeakReference<object> reference, string propertyName)
         {
             Contract.Requires<ArgumentNullException>(reference != null);
             Contract.Requires<ArgumentNullException>(propertyName != null);
 
-            var instance = reference.Target;
+            reference.TryGetTarget(out object instance);
             var o = (AvaloniaObject)instance;
             var p = LookupProperty(o, propertyName);
 

+ 11 - 12
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@@ -15,9 +15,11 @@ namespace Avalonia.Data.Core.Plugins
     public class DataAnnotationsValidationPlugin : IDataValidationPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference, string memberName)
+        public bool Match(WeakReference<object> reference, string memberName)
         {
-            return reference.Target?
+            reference.TryGetTarget(out object target);
+
+            return target?
                 .GetType()
                 .GetRuntimeProperty(memberName)?
                 .GetCustomAttributes<ValidationAttribute>()
@@ -25,25 +27,22 @@ namespace Avalonia.Data.Core.Plugins
         }
 
         /// <inheritdoc/>
-        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
+        public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor inner)
         {
             return new Accessor(reference, name, inner);
         }
 
-        private class Accessor : DataValidationBase
+        private sealed class Accessor : DataValidationBase
         {
-            private ValidationContext _context;
+            private readonly ValidationContext _context;
 
-            public Accessor(WeakReference reference, string name, IPropertyAccessor inner)
+            public Accessor(WeakReference<object> reference, string name, IPropertyAccessor inner)
                 : base(inner)
             {
-                _context = new ValidationContext(reference.Target);
-                _context.MemberName = name;
-            }
+                reference.TryGetTarget(out object target);
 
-            public override bool SetValue(object value, BindingPriority priority)
-            {
-                return base.SetValue(value, priority);
+                _context = new ValidationContext(target);
+                _context.MemberName = name;
             }
 
             protected override void InnerValueChanged(object value)

+ 4 - 4
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@@ -12,17 +12,17 @@ namespace Avalonia.Data.Core.Plugins
     public class ExceptionValidationPlugin : IDataValidationPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference, string memberName) => true;
+        public bool Match(WeakReference<object> reference, string memberName) => true;
 
         /// <inheritdoc/>
-        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
+        public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor inner)
         {
             return new Validator(reference, name, inner);
         }
 
-        private class Validator : DataValidationBase
+        private sealed class Validator : DataValidationBase
         {
-            public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+            public Validator(WeakReference<object> reference, string name, IPropertyAccessor inner)
                 : base(inner)
             {
             }

+ 2 - 3
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Data.Core.Plugins
         /// <param name="reference">A weak reference to the object.</param>
         /// <param name="memberName">The name of the member to validate.</param>
         /// <returns>True if the plugin can handle the object; otherwise false.</returns>
-        bool Match(WeakReference reference, string memberName);
+        bool Match(WeakReference<object> reference, string memberName);
 
         /// <summary>
         /// Starts monitoring the data validation state of a property on an object.
@@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        IPropertyAccessor Start(
-            WeakReference reference,
+        IPropertyAccessor Start(WeakReference<object> reference,
             string propertyName,
             IPropertyAccessor inner);
     }

+ 1 - 2
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs

@@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        IPropertyAccessor Start(
-            WeakReference reference, 
+        IPropertyAccessor Start(WeakReference<object> reference,
             string propertyName);
     }
 }

+ 2 - 2
src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Data.Core.Plugins
         /// </summary>
         /// <param name="reference">A weak reference to the value.</param>
         /// <returns>True if the plugin can handle the value; otherwise false.</returns>
-        bool Match(WeakReference reference);
+        bool Match(WeakReference<object> reference);
 
         /// <summary>
         /// Starts producing output based on the specified value.
@@ -24,6 +24,6 @@ namespace Avalonia.Data.Core.Plugins
         /// <returns>
         /// An observable that produces the output for the value.
         /// </returns>
-        IObservable<object> Start(WeakReference reference);
+        IObservable<object> Start(WeakReference<object> reference);
     }
 }

+ 20 - 8
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@@ -15,20 +15,25 @@ namespace Avalonia.Data.Core.Plugins
     public class IndeiValidationPlugin : IDataValidationPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo;
+        public bool Match(WeakReference<object> reference, string memberName)
+        {
+            reference.TryGetTarget(out object target);
+
+            return target is INotifyDataErrorInfo;
+        }
 
         /// <inheritdoc/>
-        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
+        public IPropertyAccessor Start(WeakReference<object> reference, string name, IPropertyAccessor accessor)
         {
             return new Validator(reference, name, accessor);
         }
 
         private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
         {
-            WeakReference _reference;
-            string _name;
+            private readonly WeakReference<object> _reference;
+            private readonly string _name;
 
-            public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+            public Validator(WeakReference<object> reference, string name, IPropertyAccessor inner)
                 : base(inner)
             {
                 _reference = reference;
@@ -45,7 +50,7 @@ namespace Avalonia.Data.Core.Plugins
 
             protected override void SubscribeCore()
             {
-                var target = _reference.Target as INotifyDataErrorInfo;
+                var target = GetReferenceTarget() as INotifyDataErrorInfo;
 
                 if (target != null)
                 {
@@ -60,7 +65,7 @@ namespace Avalonia.Data.Core.Plugins
 
             protected override void UnsubscribeCore()
             {
-                var target = _reference.Target as INotifyDataErrorInfo;
+                var target = GetReferenceTarget() as INotifyDataErrorInfo;
 
                 if (target != null)
                 {
@@ -80,7 +85,7 @@ namespace Avalonia.Data.Core.Plugins
 
             private BindingNotification CreateBindingNotification(object value)
             {
-                var target = (INotifyDataErrorInfo)_reference.Target;
+                var target = (INotifyDataErrorInfo)GetReferenceTarget();
 
                 if (target != null)
                 {
@@ -101,6 +106,13 @@ namespace Avalonia.Data.Core.Plugins
                 return new BindingNotification(value);
             }
 
+            private object GetReferenceTarget()
+            {
+                _reference.TryGetTarget(out object target);
+
+                return target;
+            }
+
             private Exception GenerateException(IList<object> errors)
             {
                 if (errors.Count == 1)

+ 15 - 8
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@@ -28,12 +28,12 @@ namespace Avalonia.Data.Core.Plugins
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        public IPropertyAccessor Start(WeakReference reference, string propertyName)
+        public IPropertyAccessor Start(WeakReference<object> reference, string propertyName)
         {
             Contract.Requires<ArgumentNullException>(reference != null);
             Contract.Requires<ArgumentNullException>(propertyName != null);
 
-            var instance = reference.Target;
+            reference.TryGetTarget(out object instance);
             var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName);
 
             if (p != null)
@@ -50,11 +50,11 @@ namespace Avalonia.Data.Core.Plugins
 
         private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
         {
-            private readonly WeakReference _reference;
+            private readonly WeakReference<object> _reference;
             private readonly PropertyInfo _property;
             private bool _eventRaised;
 
-            public Accessor(WeakReference reference,  PropertyInfo property)
+            public Accessor(WeakReference<object> reference,  PropertyInfo property)
             {
                 Contract.Requires<ArgumentNullException>(reference != null);
                 Contract.Requires<ArgumentNullException>(property != null);
@@ -69,7 +69,7 @@ namespace Avalonia.Data.Core.Plugins
             {
                 get
                 {
-                    var o = _reference.Target;
+                    var o = GetReferenceTarget();
                     return (o != null) ? _property.GetValue(o) : null;
                 }
             }
@@ -79,7 +79,7 @@ namespace Avalonia.Data.Core.Plugins
                 if (_property.CanWrite)
                 {
                     _eventRaised = false;
-                    _property.SetValue(_reference.Target, value);
+                    _property.SetValue(GetReferenceTarget(), value);
 
                     if (!_eventRaised)
                     {
@@ -109,7 +109,7 @@ namespace Avalonia.Data.Core.Plugins
 
             protected override void UnsubscribeCore()
             {
-                var inpc = _reference.Target as INotifyPropertyChanged;
+                var inpc = GetReferenceTarget() as INotifyPropertyChanged;
 
                 if (inpc != null)
                 {
@@ -120,6 +120,13 @@ namespace Avalonia.Data.Core.Plugins
                 }
             }
 
+            private object GetReferenceTarget()
+            {
+                _reference.TryGetTarget(out object target);
+
+                return target;
+            }
+
             private void SendCurrentValue()
             {
                 try
@@ -132,7 +139,7 @@ namespace Avalonia.Data.Core.Plugins
 
             private void SubscribeToChanges()
             {
-                var inpc = _reference.Target as INotifyPropertyChanged;
+                var inpc = GetReferenceTarget() as INotifyPropertyChanged;
 
                 if (inpc != null)
                 {

+ 15 - 6
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@@ -9,12 +9,12 @@ namespace Avalonia.Data.Core.Plugins
         public bool Match(object obj, string methodName)
             => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName);
 
-        public IPropertyAccessor Start(WeakReference reference, string methodName)
+        public IPropertyAccessor Start(WeakReference<object> reference, string methodName)
         {
             Contract.Requires<ArgumentNullException>(reference != null);
             Contract.Requires<ArgumentNullException>(methodName != null);
 
-            var instance = reference.Target;
+            reference.TryGetTarget(out object instance);
             var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName);
 
             if (method != null)
@@ -35,9 +35,9 @@ namespace Avalonia.Data.Core.Plugins
             }
         }
 
-        private class Accessor : PropertyAccessorBase
+        private sealed class Accessor : PropertyAccessorBase
         {
-            public Accessor(WeakReference reference, MethodInfo method)
+            public Accessor(WeakReference<object> reference, MethodInfo method)
             {
                 Contract.Requires<ArgumentNullException>(reference != null);
                 Contract.Requires<ArgumentNullException>(method != null);
@@ -61,8 +61,17 @@ namespace Avalonia.Data.Core.Plugins
                     var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray();
                     PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters);
                 }
-                
-                Value = method.IsStatic ? method.CreateDelegate(PropertyType) : method.CreateDelegate(PropertyType, reference.Target);
+
+                if (method.IsStatic)
+                {
+                    Value = method.CreateDelegate(PropertyType);
+                }
+                else
+                {
+                    reference.TryGetTarget(out object target);
+
+                    Value = method.CreateDelegate(PropertyType, target);
+                }
             }
 
             public override Type PropertyType { get; }

+ 7 - 5
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@@ -20,9 +20,11 @@ namespace Avalonia.Data.Core.Plugins
         /// </summary>
         /// <param name="reference">A weak reference to the value.</param>
         /// <returns>True if the plugin can handle the value; otherwise false.</returns>
-        public virtual bool Match(WeakReference reference)
+        public virtual bool Match(WeakReference<object> reference)
         {
-            return reference.Target.GetType().GetInterfaces().Any(x =>
+            reference.TryGetTarget(out object target);
+
+            return target != null && target.GetType().GetInterfaces().Any(x =>
               x.IsGenericType &&
               x.GetGenericTypeDefinition() == typeof(IObservable<>));
         }
@@ -34,9 +36,9 @@ namespace Avalonia.Data.Core.Plugins
         /// <returns>
         /// An observable that produces the output for the value.
         /// </returns>
-        public virtual IObservable<object> Start(WeakReference reference)
+        public virtual IObservable<object> Start(WeakReference<object> reference)
         {
-            var target = reference.Target;
+            reference.TryGetTarget(out object target);
 
             // If the observable returns a reference type then we can cast it.
             if (target is IObservable<object> result)
@@ -46,7 +48,7 @@ namespace Avalonia.Data.Core.Plugins
 
             // If the observable returns a value type then we need to call Observable.Select on it.
             // First get the type of T in `IObservable<T>`.
-            var sourceType = reference.Target.GetType().GetInterfaces().First(x =>
+            var sourceType = target.GetType().GetInterfaces().First(x =>
                   x.IsGenericType &&
                   x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0];
 

+ 9 - 4
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@@ -19,7 +19,12 @@ namespace Avalonia.Data.Core.Plugins
         /// </summary>
         /// <param name="reference">A weak reference to the value.</param>
         /// <returns>True if the plugin can handle the value; otherwise false.</returns>
-        public virtual bool Match(WeakReference reference) => reference.Target is Task;
+        public virtual bool Match(WeakReference<object> reference)
+        {
+            reference.TryGetTarget(out object target);
+
+            return target is Task;
+        } 
 
         /// <summary>
         /// Starts producing output based on the specified value.
@@ -28,11 +33,11 @@ namespace Avalonia.Data.Core.Plugins
         /// <returns>
         /// An observable that produces the output for the value.
         /// </returns>
-        public virtual IObservable<object> Start(WeakReference reference)
+        public virtual IObservable<object> Start(WeakReference<object> reference)
         {
-            var task = reference.Target as Task;
+            reference.TryGetTarget(out object target);
 
-            if (task != null)
+            if (target is Task task)
             {
                 var resultProperty = task.GetType().GetRuntimeProperty("Result");
 

+ 4 - 2
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@@ -37,9 +37,11 @@ namespace Avalonia.Data.Core
             return false;
         }
 
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
-            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
+            reference.TryGetTarget(out object target);
+
+            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
             var accessor = plugin?.Start(reference, PropertyName);
 
             if (_enableValidation && Next == null)

+ 16 - 2
src/Avalonia.Base/Data/Core/SettableNode.cs

@@ -19,11 +19,25 @@ namespace Avalonia.Data.Core
             {
                 return false;
             }
+
+            if (LastValue == null)
+            {
+                return false;
+            }
+
+            bool isLastValueAlive = LastValue.TryGetTarget(out object lastValue);
+
+            if (!isLastValueAlive)
+            {
+                return false;
+            }
+
             if (PropertyType.IsValueType)
             {
-                return LastValue?.Target != null && LastValue.Target.Equals(value);
+                return lastValue.Equals(value);
             }
-            return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value);
+
+            return ReferenceEquals(lastValue, value);
         }
 
         protected abstract bool SetTargetValueCore(object value, BindingPriority priority);

+ 1 - 1
src/Avalonia.Base/Data/Core/StreamNode.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Data.Core
 
         public override string Description => "^";
 
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
             foreach (var plugin in ExpressionObserver.StreamHandlers)
             {

+ 11 - 0
src/Avalonia.Controls/AppBuilderBase.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.Generic;
 using System.Reflection;
 using System.Linq;
 using Avalonia.Controls.ApplicationLifetimes;
@@ -59,6 +60,8 @@ namespace Avalonia.Controls
         public Action<TAppBuilder> AfterSetupCallback { get; private set; } = builder => { };
 
 
+        public Action<TAppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
+        
         protected AppBuilderBase(IRuntimePlatform platform, Action<TAppBuilder> platformServices)
         {
             RuntimePlatform = platform;
@@ -97,6 +100,13 @@ namespace Avalonia.Controls
             AfterSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
             return Self;
         }
+        
+        
+        public TAppBuilder AfterPlatformServicesSetup(Action<TAppBuilder> callback)
+        {
+            AfterPlatformServicesSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
+            return Self;
+        }
 
         /// <summary>
         /// Starts the application with an instance of <typeparamref name="TMainWindow"/>.
@@ -274,6 +284,7 @@ namespace Avalonia.Controls
             RuntimePlatformServicesInitializer();
             WindowingSubsystemInitializer();
             RenderingSubsystemInitializer();
+            AfterPlatformServicesSetupCallback(Self);
             Instance.RegisterServices();
             Instance.Initialize();
             AfterSetupCallback(Self);

+ 23 - 0
src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs

@@ -0,0 +1,23 @@
+// 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;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Platform
+{
+    /// <summary>
+    /// Defines a platform-specific mount volumes info provider implementation.
+    /// </summary>
+    public interface IMountedVolumeInfoProvider 
+    {
+        /// <summary>
+        /// Listens to any changes in volume mounts and
+        /// forwards updates to the referenced
+        /// <see cref="ObservableCollection{MountedDriveInfo}"/>.
+        /// </summary> 
+        IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives);
+    }
+}

+ 24 - 0
src/Avalonia.Controls/Platform/MountedDriveInfo.cs

@@ -0,0 +1,24 @@
+// 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;
+
+namespace Avalonia.Controls.Platform
+{
+    /// <summary>
+    /// Describes a Drive's properties.
+    /// </summary>
+    public class MountedVolumeInfo : IEquatable<MountedVolumeInfo>
+    {
+        public string VolumeLabel { get; set; }
+        public string VolumePath { get; set; }
+        public ulong VolumeSizeBytes { get; set; }
+
+        public bool Equals(MountedVolumeInfo other)
+        {
+            return this.VolumeSizeBytes.Equals(other.VolumeSizeBytes) &&
+                   this.VolumePath.Equals(other.VolumePath) &&
+                   (this.VolumeLabel ?? string.Empty).Equals(other.VolumeLabel ?? string.Empty);
+        }
+    }
+}

+ 13 - 3
src/Avalonia.Controls/SystemDialog.cs

@@ -14,7 +14,13 @@ namespace Avalonia.Controls
 
     public abstract class FileSystemDialog : SystemDialog
     {
-        public string InitialDirectory { get; set; }
+        [Obsolete("Use Directory")]
+        public string InitialDirectory
+        {
+            get => Directory;
+            set => Directory = value;
+        }
+        public string Directory { get; set; }
     }
 
     public class SaveFileDialog : FileDialog
@@ -45,8 +51,12 @@ namespace Avalonia.Controls
 
     public class OpenFolderDialog : FileSystemDialog
     {
-        public string DefaultDirectory { get; set; }
-
+        [Obsolete("Use Directory")]
+        public string DefaultDirectory
+        {
+            get => Directory;
+            set => Directory = value;
+        }
         public Task<string> ShowAsync(Window parent)
         {
             if(parent == null)

+ 18 - 0
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <AvaloniaResource Include="**\*.xaml">
+      <SubType>Designer</SubType>
+    </AvaloniaResource>
+  </ItemGroup>
+
+  <Import Project="..\..\build\BuildTargets.targets" />
+
+  <ItemGroup>
+    <ProjectReference Include="..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
+  </ItemGroup>
+</Project>

+ 40 - 0
src/Avalonia.Dialogs/ByteSizeHelper.cs

@@ -0,0 +1,40 @@
+using System;
+
+namespace Avalonia.Dialogs
+{
+    internal static class ByteSizeHelper
+    {
+        private const string formatTemplate = "{0}{1:0.#} {2}";
+
+        private static readonly string[] Prefixes =
+        {
+            "B",
+            "KB",
+            "MB",
+            "GB",
+            "TB",
+            "PB",
+            "EB",
+            "ZB",
+            "YB" 
+        };
+
+        public static string ToString(ulong bytes)
+        {
+            if (bytes == 0)
+            {
+                return string.Format(formatTemplate, null, 0, Prefixes[0]);
+            }
+
+            var absSize = Math.Abs((double)bytes);
+            var fpPower = Math.Log(absSize, 1000);
+            var intPower = (int)fpPower;
+            var iUnit = intPower >= Prefixes.Length
+                ? Prefixes.Length - 1
+                : intPower;
+            var normSize = absSize / Math.Pow(1000, iUnit);
+
+            return string.Format(formatTemplate,bytes < 0 ? "-" : null, normSize, Prefixes[iUnit]);
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Dialogs/ChildFitter.cs

@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace Avalonia.Dialogs
+{
+    internal class ChildFitter : Decorator
+    {
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            return new Size(0, 0);
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            Child.Measure(finalSize);
+            base.ArrangeOverride(finalSize);
+            return finalSize;
+        }
+    }
+}

+ 26 - 0
src/Avalonia.Dialogs/FileSizeStringConverter.cs

@@ -0,0 +1,26 @@
+using Avalonia.Data.Converters;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace Avalonia.Dialogs
+{
+    internal class FileSizeStringConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is long size && size > 0)
+            {
+                return ByteSizeHelper.ToString((ulong)size);
+            }
+
+            return "";
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 31 - 0
src/Avalonia.Dialogs/InternalViewModelBase.cs

@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
+
+namespace Avalonia.Dialogs
+{
+    internal class InternalViewModelBase : 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));
+        }
+    }
+}

+ 144 - 0
src/Avalonia.Dialogs/ManagedFileChooser.xaml

@@ -0,0 +1,144 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:dialogs="clr-namespace:Avalonia.Dialogs"
+             xmlns:internal="clr-namespace:Avalonia.Dialogs"
+             x:Class="Avalonia.Dialogs.ManagedFileChooser" Margin="10">
+    <UserControl.Resources>
+        <internal:FileSizeStringConverter x:Key="FileSizeConverter" />
+        <DrawingGroup x:Key="LevelUp">
+            <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
+            <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M14.5,0L6.39,0 5.39,2 2.504,2C1.677,2,1,2.673,1,3.5L1,10.582 1,10.586 1,15.414 3,13.414 3,16 7,16 7,13.414 9,15.414 9,13 14.5,13C15.327,13,16,12.327,16,11.5L16,1.5C16,0.673,15.327,0,14.5,0" />
+            <GeometryDrawing Brush="#FFDCB679" Geometry="F1M14,3L7.508,3 8.008,2 8.012,2 14,2z M14.5,1L7.008,1 6.008,3 2.504,3C2.227,3,2,3.224,2,3.5L2,9.582 4.998,6.586 9,10.586 9,12 14.5,12C14.775,12,15,11.776,15,11.5L15,1.5C15,1.224,14.775,1,14.5,1" />
+            <GeometryDrawing Brush="#FF00529C" Geometry="F1M8,11L5,8 2,11 2,13 4,11 4,15 6,15 6,11 8,13z" />
+            <GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8.0001,1.9996L7.5001,3.0006 14.0001,3.0006 14.0001,1.9996z" />
+        </DrawingGroup>
+        <dialogs:ResourceSelectorConverter x:Key="Icons">
+            <DrawingGroup x:Key="Icon_Folder">
+                <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M0,0L16,0 16,16 0,16z" />
+                <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M1.5,1L9.61,1 10.61,3 13.496,3C14.323,3,14.996,3.673,14.996,4.5L14.996,12.5C14.996,13.327,14.323,14,13.496,14L1.5,14C0.673,14,0,13.327,0,12.5L0,2.5C0,1.673,0.673,1,1.5,1" />
+                <GeometryDrawing Brush="#FFDCB67A" Geometry="F1M2,3L8.374,3 8.874,4 2,4z M13.496,4L10,4 9.992,4 8.992,2 1.5,2C1.225,2,1,2.224,1,2.5L1,12.5C1,12.776,1.225,13,1.5,13L13.496,13C13.773,13,13.996,12.776,13.996,12.5L13.996,4.5C13.996,4.224,13.773,4,13.496,4" />
+                <GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M2,3L8.374,3 8.874,4 2,4z" />
+            </DrawingGroup>
+            <DrawingGroup x:Key="Icon_File">
+                <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
+                <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4,15C3.03,15,2,14.299,2,13L2,3C2,1.701,3.03,1,4,1L10.061,1 14,4.556 14,13C14,13.97,13.299,15,12,15z" />
+                <GeometryDrawing Brush="#FF9B4E96" Geometry="F1M12,13L4,13 4,3 9,3 9,6 12,6z M9.641,2L3.964,2C3.964,2,3,2,3,3L3,13C3,14,3.964,14,3.964,14L11.965,14C12.965,14,13,13,13,13L13,5z" />
+                <GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M4,3L9,3 9,6 12,6 12,13 4,13z" />
+            </DrawingGroup>
+            <DrawingGroup x:Key="Icon_Volume">
+                <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
+                <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,12L0,6.5C0,5.122,1.122,4,2.5,4L13.5,4C14.879,4,16,5.122,16,6.5L16,12z" />
+                <GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M13,8L12,8 12,7 13,7z M11,8L10,8 10,7 11,7z M13.5,6L2.5,6C2.224,6,2,6.224,2,6.5L2,10 14,10 14,6.5C14,6.224,13.775,6,13.5,6" />
+                <GeometryDrawing Brush="#FF424242" Geometry="F1M13,7L12,7 12,8 13,8z M11,7L10,7 10,8 11,8z M2,10L14,10 14,6.5C14,6.224,13.775,6,13.5,6L2.5,6C2.224,6,2,6.224,2,6.5z M15,11L1,11 1,6.5C1,5.673,1.673,5,2.5,5L13.5,5C14.327,5,15,5.673,15,6.5z" />
+            </DrawingGroup>
+        </dialogs:ResourceSelectorConverter>
+    </UserControl.Resources>
+    <DockPanel>
+        <DockPanel DockPanel.Dock="Top" Margin="0 0 0 5">
+            <dialogs:ChildFitter DockPanel.Dock="Right" Width="{Binding ElementName=Location, Path=Bounds.Height}">
+                <Button Command="{Binding GoUp}" >
+
+                    <DrawingPresenter Drawing="{StaticResource LevelUp}" Stretch="Fill"/>
+                </Button>
+            </dialogs:ChildFitter>
+            <TextBox x:Name="Location" Text="{Binding Location}" Margin="0 0 5 0">
+                <TextBox.KeyBindings>
+                    <KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
+                </TextBox.KeyBindings>
+            </TextBox>
+        </DockPanel>
+        <DockPanel Margin="0 5 0 0"  DockPanel.Dock="Bottom">
+            <StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
+                <CheckBox IsChecked="{Binding ShowHiddenFiles}">
+                    <TextBlock>Show hidden files</TextBlock>
+                </CheckBox>
+            </StackPanel>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
+                <StackPanel.Styles>
+                    <Style Selector="Button">
+                        <Setter Property="Margin">4</Setter>
+                    </Style>
+                </StackPanel.Styles>
+                <Button Command="{Binding Ok}">OK</Button>
+                <Button Command="{Binding Cancel}">Cancel</Button>
+            </StackPanel>
+        </DockPanel>
+
+        <DropDown DockPanel.Dock="Bottom" 
+                  IsVisible="{Binding ShowFilters}" 
+                  Items="{Binding Filters}"
+                  SelectedItem="{Binding SelectedFilter}"
+                  Margin="0 5 0 0" />
+
+        <TextBox Text="{Binding FileName}" Watermark="File name" DockPanel.Dock="Bottom" IsVisible="{Binding !SelectingFolder}" />
+
+        <ListBox Margin="0 0 5 5" BorderBrush="Transparent" x:Name="QuickLinks" Items="{Binding QuickLinks}"
+                 SelectedIndex="{Binding QuickLinksSelectedIndex}"
+                 DockPanel.Dock="Left" Background="{DynamicResource ThemeControlMidBrush}" Focusable="False">
+            <ListBox.ItemTemplate>
+                <DataTemplate>
+                    <StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
+                        <DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
+                        <TextBlock Text="{Binding DisplayName}"/>
+                    </StackPanel>
+                </DataTemplate>
+            </ListBox.ItemTemplate>
+        </ListBox>
+        <DockPanel Grid.IsSharedSizeScope="True">
+            <Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="20" SharedSizeGroup="Icon" />
+                    <ColumnDefinition Width="16" SharedSizeGroup="Splitter"  />
+                    <ColumnDefinition Width="400" SharedSizeGroup="Name" />
+                    <ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
+                    <ColumnDefinition Width="200" SharedSizeGroup="Modified" />
+                    <ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
+                    <ColumnDefinition Width="150" SharedSizeGroup="Type" />
+                    <ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
+                    <ColumnDefinition Width="200" SharedSizeGroup="Size" />
+                </Grid.ColumnDefinitions>
+                <GridSplitter Grid.Column="1" />
+                <TextBlock Grid.Column="2" Text="Name" />
+                <GridSplitter Grid.Column="3" />
+                <TextBlock Grid.Column="4" Text="Date Modified" />
+                <GridSplitter Grid.Column="5" />
+                <TextBlock Grid.Column="6" Text="Type" />
+                <GridSplitter Grid.Column="7" />
+                <TextBlock Grid.Column="8" Text="Size" />
+            </Grid>
+            <ListBox x:Name="Files"
+                 VirtualizationMode="Simple"
+                 Items="{Binding Items}"
+                 Margin="0 5"
+                 SelectionMode="{Binding SelectionMode}"
+                 SelectedItems="{Binding SelectedItems}"
+                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+                <ListBox.ItemTemplate>
+                    <DataTemplate>
+                        <Grid Background="Transparent">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition SharedSizeGroup="Icon" />
+                                <ColumnDefinition SharedSizeGroup="Splitter"  />
+                                <ColumnDefinition SharedSizeGroup="Name" />
+                                <ColumnDefinition SharedSizeGroup="Splitter" />
+                                <ColumnDefinition SharedSizeGroup="Modified" />
+                                <ColumnDefinition SharedSizeGroup="Splitter" />
+                                <ColumnDefinition SharedSizeGroup="Type" />
+                                <ColumnDefinition SharedSizeGroup="Splitter" />
+                                <ColumnDefinition SharedSizeGroup="Size" />
+                            </Grid.ColumnDefinitions>
+                            <DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
+                            <TextBlock Grid.Column="2" Text="{Binding DisplayName}"/>
+                            <TextBlock Grid.Column="4" Text="{Binding Modified}" />
+                            <TextBlock Grid.Column="6" Text="{Binding Type}" />
+                            <TextBlock Grid.Column="8" Text="{Binding Size, Converter={StaticResource FileSizeConverter}}" />
+                        </Grid>
+                    </DataTemplate>
+                </ListBox.ItemTemplate>
+            </ListBox>
+        </DockPanel>
+    </DockPanel>
+
+</UserControl>

+ 88 - 0
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs

@@ -0,0 +1,88 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml;
+
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooser : UserControl
+    {
+        private Control _quickLinksRoot;
+        private ListBox _filesView;
+
+        public ManagedFileChooser()
+        {
+            AvaloniaXamlLoader.Load(this);
+            AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
+            _quickLinksRoot = this.FindControl<Control>("QuickLinks");
+            _filesView = this.FindControl<ListBox>("Files");
+        }
+
+        ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel;
+
+        private void OnPointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel;
+
+            if (model == null)
+            {
+                return;
+            }
+
+            var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control);
+            if (e.ClickCount == 2 || isQuickLink)
+            {
+                if (model.ItemType == ManagedFileChooserItemType.File)
+                {
+                    Model?.SelectSingleFile(model);
+                }
+                else
+                {
+                    Model?.Navigate(model.Path);
+                }
+
+                e.Handled = true;
+            }
+        }
+
+        protected override async void OnDataContextChanged(EventArgs e)
+        {
+            base.OnDataContextChanged(e);
+
+            var model = (DataContext as ManagedFileChooserViewModel);
+
+            if (model == null)
+            {
+                return;
+            }
+
+            var preselected = model.SelectedItems.FirstOrDefault();
+
+            if (preselected == null)
+            {
+                return;
+            }
+
+            //Let everything to settle down and scroll to selected item
+            await Task.Delay(100);
+
+            if (preselected != model.SelectedItems.FirstOrDefault())
+            {
+                return;
+            }
+
+            // Workaround for ListBox bug, scroll to the previous file
+            var indexOfPreselected = model.Items.IndexOf(preselected);
+
+            if (indexOfPreselected > 1)
+            {
+                _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]);
+            }
+        }
+    }
+}

+ 50 - 0
src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooserFilterViewModel : InternalViewModelBase
+    {
+        private readonly string[] _extensions;
+        public string Name { get; }
+
+        public ManagedFileChooserFilterViewModel(FileDialogFilter filter)
+        {
+            Name = filter.Name;
+
+            if (filter.Extensions.Contains("*"))
+            {
+                return;
+            }
+
+            _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray();
+        }
+
+        public ManagedFileChooserFilterViewModel()
+        {
+            Name = "All files";
+        }
+
+        public bool Match(string filename)
+        {
+            if (_extensions == null)
+            {
+                return true;
+            }
+
+            foreach (var ext in _extensions)
+            {
+                if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        public override string ToString() => Name;
+    }
+}

+ 9 - 0
src/Avalonia.Dialogs/ManagedFileChooserItemType.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Dialogs
+{
+    public enum ManagedFileChooserItemType
+    {
+        File,
+        Folder,
+        Volume
+    }
+}

+ 77 - 0
src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs

@@ -0,0 +1,77 @@
+using System;
+
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooserItemViewModel : InternalViewModelBase
+    {
+        private string _displayName;
+        private string _path;
+         private DateTime _modified;
+        private string _type;
+        private long _size;
+        private ManagedFileChooserItemType _itemType;
+
+        public string DisplayName
+        {
+            get => _displayName;
+            set => this.RaiseAndSetIfChanged(ref _displayName, value);
+        }
+
+        public string Path
+        {
+            get => _path;
+            set => this.RaiseAndSetIfChanged(ref _path, value);
+        }
+
+        public DateTime Modified
+        {
+            get => _modified;
+            set => this.RaiseAndSetIfChanged(ref _modified, value);
+        }
+
+        public string Type
+        {
+            get => _type;
+            set => this.RaiseAndSetIfChanged(ref _type, value);
+        }
+
+        public long Size
+        {
+            get => _size;
+            set => this.RaiseAndSetIfChanged(ref _size, value);
+        }
+
+        public ManagedFileChooserItemType ItemType
+        {
+            get => _itemType;
+            set => this.RaiseAndSetIfChanged(ref _itemType, value);
+        }
+
+        public string IconKey
+        {
+            get
+            {
+                switch (ItemType)
+                {
+                    case ManagedFileChooserItemType.Folder:
+                        return "Icon_Folder";
+                    case ManagedFileChooserItemType.Volume:
+                        return "Icon_Volume";
+                    default:
+                        return "Icon_File";
+                }
+            }
+        }
+ 
+        public ManagedFileChooserItemViewModel()
+        {
+        }
+
+        public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item)
+        {
+            ItemType = item.ItemType;
+            Path = item.Path;
+            DisplayName = item.DisplayName;
+        }
+    }
+}

+ 9 - 0
src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooserNavigationItem
+    {
+        public string DisplayName { get; set; }
+        public string Path { get; set; }
+        public ManagedFileChooserItemType ItemType { get; set; }
+    }
+}

+ 86 - 0
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Controls.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooserSources
+    {
+        public Func<ManagedFileChooserNavigationItem[]> GetUserDirectories { get; set; }
+            = DefaultGetUserDirectories;
+
+        public Func<ManagedFileChooserNavigationItem[]> GetFileSystemRoots { get; set; }
+            = DefaultGetFileSystemRoots;
+
+        public Func<ManagedFileChooserSources, ManagedFileChooserNavigationItem[]> GetAllItemsDelegate { get; set; }
+            = DefaultGetAllItems;
+
+        public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this);
+        public static readonly ObservableCollection<MountedVolumeInfo> MountedVolumes = new ObservableCollection<MountedVolumeInfo>();
+
+        public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources)
+        {
+            return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray();
+        }
+
+        private static Environment.SpecialFolder[] s_folders = new[]
+        {
+            Environment.SpecialFolder.Desktop,
+            Environment.SpecialFolder.UserProfile,
+            Environment.SpecialFolder.MyDocuments,
+            Environment.SpecialFolder.MyMusic,
+            Environment.SpecialFolder.MyPictures,
+            Environment.SpecialFolder.MyVideos
+        };
+
+        public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories()
+        {
+            return s_folders.Select(Environment.GetFolderPath).Distinct()
+                .Where(d => !string.IsNullOrWhiteSpace(d))
+                .Where(Directory.Exists)
+                .Select(d => new ManagedFileChooserNavigationItem
+                {
+                    ItemType = ManagedFileChooserItemType.Folder,
+                    Path = d,
+                    DisplayName = Path.GetFileName(d)
+                }).ToArray();
+        }
+
+        public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots()
+        {
+            return MountedVolumes
+                   .Select(x =>
+                   {
+                       var displayName = x.VolumeLabel;
+
+                       if (displayName == null & x.VolumeSizeBytes > 0)
+                       {
+                           displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume";
+                       };
+
+                       try
+                       {
+                           Directory.GetFiles(x.VolumePath);
+                       }
+                       catch (UnauthorizedAccessException _)
+                       {
+                           return null;
+                       }
+
+                       return new ManagedFileChooserNavigationItem
+                       {
+                           ItemType = ManagedFileChooserItemType.Volume,
+                           DisplayName = displayName,
+                           Path = x.VolumePath
+                       };
+                   })
+                   .Where(x => x != null)
+                   .ToArray();
+        }
+    }
+}

+ 368 - 0
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@@ -0,0 +1,368 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Dialogs
+{
+    internal class ManagedFileChooserViewModel : InternalViewModelBase
+    {
+        public event Action CancelRequested;
+        public event Action<string[]> CompleteRequested;
+
+        public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } =
+            new AvaloniaList<ManagedFileChooserItemViewModel>();
+
+        public AvaloniaList<ManagedFileChooserItemViewModel> Items { get; } =
+            new AvaloniaList<ManagedFileChooserItemViewModel>();
+
+        public AvaloniaList<ManagedFileChooserFilterViewModel> Filters { get; } =
+            new AvaloniaList<ManagedFileChooserFilterViewModel>();
+
+        public AvaloniaList<ManagedFileChooserItemViewModel> SelectedItems { get; } =
+            new AvaloniaList<ManagedFileChooserItemViewModel>();
+
+        string _location;
+        string _fileName;
+        private bool _showHiddenFiles;
+        private ManagedFileChooserFilterViewModel _selectedFilter;
+        private bool _selectingDirectory;
+        private bool _savingFile;
+        private bool _scheduledSelectionValidation;
+        private bool _alreadyCancelled = false;
+        private string _defaultExtension;
+        private CompositeDisposable _disposables;
+
+        public string Location
+        {
+            get => _location;
+            private set => this.RaiseAndSetIfChanged(ref _location, value);
+        }
+
+        public string FileName
+        {
+            get => _fileName;
+            private set => this.RaiseAndSetIfChanged(ref _fileName, value);
+        }
+
+        public bool SelectingFolder => _selectingDirectory;
+
+        public bool ShowFilters { get; }
+        public SelectionMode SelectionMode { get; }
+        public string Title { get; }
+
+        public int QuickLinksSelectedIndex
+        {
+            get
+            {
+                for (var index = 0; index < QuickLinks.Count; index++)
+                {
+                    var i = QuickLinks[index];
+
+                    if (i.Path == Location)
+                    {
+                        return index;
+                    }
+                }
+
+                return -1;
+            }
+            set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex));
+        }
+
+        public ManagedFileChooserFilterViewModel SelectedFilter
+        {
+            get => _selectedFilter;
+            set
+            {
+                this.RaiseAndSetIfChanged(ref _selectedFilter, value);
+                Refresh();
+            }
+        }
+
+        public bool ShowHiddenFiles
+        {
+            get => _showHiddenFiles;
+            set
+            {
+                this.RaiseAndSetIfChanged(ref _showHiddenFiles, value);
+                Refresh();
+            }
+        }
+
+        private void RefreshQuickLinks(ManagedFileChooserSources quickSources)
+        {
+            QuickLinks.Clear();
+            QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i)));
+        }
+
+        public ManagedFileChooserViewModel(FileSystemDialog dialog)
+        {
+            _disposables = new CompositeDisposable();
+
+            var quickSources = AvaloniaLocator.Current
+                                              .GetService<ManagedFileChooserSources>()
+                                              ?? new ManagedFileChooserSources();
+
+            var sub1 = AvaloniaLocator.Current
+                                      .GetService<IMountedVolumeInfoProvider>()
+                                      .Listen(ManagedFileChooserSources.MountedVolumes);
+
+            var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes,
+                                            nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged))
+                                 .ObserveOn(AvaloniaScheduler.Instance)
+                                 .Subscribe(x => RefreshQuickLinks(quickSources));
+
+            _disposables.Add(sub1);
+            _disposables.Add(sub2);
+
+            CompleteRequested += delegate { _disposables?.Dispose(); };
+            CancelRequested += delegate { _disposables?.Dispose(); };
+
+            RefreshQuickLinks(quickSources);
+
+            Title = dialog.Title ?? (
+                        dialog is OpenFileDialog ? "Open file"
+                        : dialog is SaveFileDialog ? "Save file"
+                        : dialog is OpenFolderDialog ? "Select directory"
+                        : throw new ArgumentException(nameof(dialog)));
+
+            var directory = dialog.InitialDirectory;
+
+            if (directory == null || !Directory.Exists(directory))
+            {
+                directory = Directory.GetCurrentDirectory();
+            }
+
+            if (dialog is FileDialog fd)
+            {
+                if (fd.Filters?.Count > 0)
+                {
+                    Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f)));
+                    _selectedFilter = Filters[0];
+                    ShowFilters = true;
+                }
+
+                if (dialog is OpenFileDialog ofd)
+                {
+                    if (ofd.AllowMultiple)
+                    {
+                        SelectionMode = SelectionMode.Multiple;
+                    }
+                }
+            }
+
+            _selectingDirectory = dialog is OpenFolderDialog;
+
+            if (dialog is SaveFileDialog sfd)
+            {
+                _savingFile = true;
+                _defaultExtension = sfd.DefaultExtension;
+                FileName = sfd.InitialFileName;
+            }
+
+            Navigate(directory, (dialog as FileDialog)?.InitialFileName);
+            SelectedItems.CollectionChanged += OnSelectionChangedAsync;
+        }
+
+        public void EnterPressed()
+        {
+            if (Directory.Exists(Location))
+            {
+                Navigate(Location);
+            }
+            else if (File.Exists(Location))
+            {
+                CompleteRequested?.Invoke(new[] { Location });
+            }
+        }
+
+        private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (_scheduledSelectionValidation)
+            {
+                return;
+            }
+
+            _scheduledSelectionValidation = true;
+            await Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                try
+                {
+                    if (_selectingDirectory)
+                    {
+                        SelectedItems.Clear();
+                    }
+                    else
+                    {
+                        var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList();
+                        foreach (var item in invalidItems)
+                        {
+                            SelectedItems.Remove(item);
+                        }
+
+                        if (!_selectingDirectory)
+                        {
+                            FileName = SelectedItems.FirstOrDefault()?.DisplayName;
+                        }
+                    }
+                }
+                finally
+                {
+                    _scheduledSelectionValidation = false;
+                }
+            });
+        }
+
+        void NavigateRoot(string initialSelectionName)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName);
+            }
+            else
+            {
+                Navigate("/", initialSelectionName);
+            }
+        }
+
+        public void Refresh() => Navigate(Location);
+
+        public void Navigate(string path, string initialSelectionName = null)
+        {
+            if (!Directory.Exists(path))
+            {
+                NavigateRoot(initialSelectionName);
+            }
+            else
+            {
+                Location = path;
+                Items.Clear();
+                SelectedItems.Clear();
+
+                try
+                {
+                    var infos = new DirectoryInfo(path).EnumerateFileSystemInfos();
+
+                    if (!ShowHiddenFiles)
+                    {
+                        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                        {
+                            infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) == 0);
+                        }
+                        else
+                        {
+                            infos = infos.Where(i => !i.Name.StartsWith("."));
+                        }
+                    }
+
+                    if (SelectedFilter != null)
+                    {
+                        infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name));
+                    }
+
+                    Items.AddRange(infos.Where(x =>
+                    {
+                        if (_selectingDirectory)
+                        {
+                            if (!(x is DirectoryInfo))
+                            {
+                                return false;
+                            }
+                        }
+
+                        return true;
+                    })
+                    .Where(x => x.Exists)
+                    .Select(info => new ManagedFileChooserItemViewModel
+                    {
+                        DisplayName = info.Name,
+                        Path = info.FullName,
+                        Type = info is FileInfo ? info.Extension : "File Folder",
+                        ItemType = info is FileInfo ? ManagedFileChooserItemType.File
+                                                     : ManagedFileChooserItemType.Folder,
+                        Size = info is FileInfo f ? f.Length : 0,
+                        Modified = info.LastWriteTime
+                    })
+                    .OrderByDescending(x => x.ItemType == ManagedFileChooserItemType.Folder)
+                    .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase));
+
+                    if (initialSelectionName != null)
+                    {
+                        var sel = Items.FirstOrDefault(i => i.ItemType == ManagedFileChooserItemType.File && i.DisplayName == initialSelectionName);
+
+                        if (sel != null)
+                        {
+                            SelectedItems.Add(sel);
+                        }
+                    }
+
+                    this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex));
+                }
+                catch (System.UnauthorizedAccessException)
+                {
+                }
+            }
+        }
+
+        public void GoUp()
+        {
+            var parent = Path.GetDirectoryName(Location);
+
+            if (string.IsNullOrWhiteSpace(parent))
+            {
+                return;
+            }
+
+            Navigate(parent);
+        }
+
+        public void Cancel()
+        {
+            if (!_alreadyCancelled)
+            {
+                // INFO: Don't misplace this check or it might cause
+                //       StackOverflowException because of recursive
+                //       event invokes.
+                _alreadyCancelled = true;
+                CancelRequested?.Invoke();
+            }
+        }
+
+        public void Ok()
+        {
+            if (_selectingDirectory)
+            {
+                CompleteRequested?.Invoke(new[] { Location });
+            }
+            else if (_savingFile)
+            {
+                if (!string.IsNullOrWhiteSpace(FileName))
+                {
+                    if (!Path.HasExtension(FileName) && !string.IsNullOrWhiteSpace(_defaultExtension))
+                    {
+                        FileName = Path.ChangeExtension(FileName, _defaultExtension);
+                    }
+
+                    CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) });
+                }
+            }
+            else
+            {
+                CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray());
+            }
+        }
+
+        public void SelectSingleFile(ManagedFileChooserItemViewModel item)
+        {
+            CompleteRequested?.Invoke(new[] { item.Path });
+        }
+    }
+}

+ 69 - 0
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Dialogs;
+using Avalonia.Platform;
+
+namespace Avalonia.Dialogs
+{
+    public static class ManagedFileDialogExtensions
+    {
+        class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
+        {
+            async Task<string[]> Show(SystemDialog d, IWindowImpl parent)
+            {
+                var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
+
+                var dialog = new T
+                {
+                    Content = new ManagedFileChooser(),
+                    DataContext = model
+                };
+
+                dialog.Closed += delegate { model.Cancel(); };
+
+                string[] result = null;
+                
+                model.CompleteRequested += items =>
+                {
+                    result = items;
+                    dialog.Close();
+                };
+
+                model.CancelRequested += dialog.Close;
+
+                await dialog.ShowDialog<object>(parent);
+                return result;
+            }
+
+            public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+            {
+                return await Show(dialog, parent);
+            }
+
+            public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+            {
+                return (await Show(dialog, parent))?.FirstOrDefault();
+            }
+        }
+
+        public static TAppBuilder UseManagedSystemDialogs<TAppBuilder>(this TAppBuilder builder)
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
+        {
+            builder.AfterSetup(_ =>
+                AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<Window>>());
+            return builder;
+        }
+
+        public static TAppBuilder UseManagedSystemDialogs<TAppBuilder, TWindow>(this TAppBuilder builder)
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TWindow : Window, new()
+        {
+            builder.AfterSetup(_ =>
+                AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<TWindow>>());
+            return builder;
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Dialogs/ResourceSelectorConverter.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Dialogs
+{
+    internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter
+    {
+        public object Convert(object key, Type targetType, object parameter, CultureInfo culture)
+        {
+            TryGetResource((string)key, out var value);
+            return value;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 11 - 0
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
+  </ItemGroup>
+
+</Project>

+ 101 - 0
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs

@@ -0,0 +1,101 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Avalonia.Controls.Platform;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Text.RegularExpressions;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Avalonia.FreeDesktop
+{
+    internal class LinuxMountedVolumeInfoListener : IDisposable
+    {
+        private const string DevByLabelDir = "/dev/disk/by-label/";
+        private const string ProcPartitionsDir = "/proc/partitions";
+        private const string ProcMountsDir = "/proc/mounts";
+        private CompositeDisposable _disposables;
+        private ObservableCollection<MountedVolumeInfo> _targetObs;
+        private bool _beenDisposed = false;
+
+        public LinuxMountedVolumeInfoListener(ref ObservableCollection<MountedVolumeInfo> target)
+        {
+            _disposables = new CompositeDisposable();
+            this._targetObs = target;
+
+            var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
+                                      .Subscribe(Poll);
+
+            _disposables.Add(pollTimer);
+
+            Poll(0);
+        }
+
+        private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x)));
+
+        private void Poll(long _)
+        {
+            var fProcPartitions = File.ReadAllLines(ProcPartitionsDir)
+                                      .Skip(1)
+                                      .Where(p => !string.IsNullOrEmpty(p))
+                                      .Select(p => Regex.Replace(p, @"\s{2,}", " ").Trim().Split(' '))
+                                      .Select(p => (p[2].Trim(), p[3].Trim()))
+                                      .Select(p => (Convert.ToUInt64(p.Item1) * 1024, "/dev/" + p.Item2));
+
+            var fProcMounts = File.ReadAllLines(ProcMountsDir)
+                                  .Select(x => x.Split(' '))
+                                  .Select(x => (x[0], x[1]));
+
+            var labelDirEnum = Directory.Exists(DevByLabelDir) ?
+                               new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty<FileInfo>();
+
+            var labelDevPathPairs = labelDirEnum
+                                    .Select(x => (GetSymlinkTarget(x.FullName), x.Name));
+
+            var q1 = from mount in fProcMounts
+                     join device in fProcPartitions on mount.Item1 equals device.Item2
+                     join label in labelDevPathPairs on device.Item2 equals label.Item1 into labelMatches
+                     from x in labelMatches.DefaultIfEmpty()
+                     select new MountedVolumeInfo()
+                     {
+                         VolumePath = mount.Item2,
+                         VolumeSizeBytes = device.Item1,
+                         VolumeLabel = x.Name
+                     };
+
+            var mountVolInfos = q1.ToArray();
+
+            if (_targetObs.SequenceEqual(mountVolInfos))
+                return;
+            else
+            {
+                _targetObs.Clear();
+
+                foreach (var i in mountVolInfos)
+                    _targetObs.Add(i);
+            }
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_beenDisposed)
+            {
+                if (disposing)
+                {
+                    _disposables.Dispose();
+                    _targetObs.Clear();
+                }
+
+                _beenDisposed = true;
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+    }
+}

+ 16 - 0
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.ObjectModel;
+
+using Avalonia.Controls.Platform;
+
+namespace Avalonia.FreeDesktop
+{
+    public class LinuxMountedVolumeInfoProvider : IMountedVolumeInfoProvider
+    {
+        public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        {
+            Contract.Requires<ArgumentNullException>(mountedDrives != null);
+            return new LinuxMountedVolumeInfoListener(ref mountedDrives);
+        }
+    }
+}

+ 31 - 0
src/Avalonia.FreeDesktop/NativeMethods.cs

@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Avalonia.Controls.Platform;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Text.RegularExpressions;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Avalonia.FreeDesktop
+{
+    internal static class NativeMethods
+    {
+        [DllImport("libc", SetLastError = true)]
+        private static extern long readlink([MarshalAs(UnmanagedType.LPArray)] byte[] filename,
+                                            [MarshalAs(UnmanagedType.LPArray)] byte[] buffer,
+                                            long len);
+
+        public static string ReadLink(string path)
+        {
+            var symlink = Encoding.UTF8.GetBytes(path);
+            var result = new byte[4095];
+            readlink(symlink, result, result.Length);
+            var rawstr = Encoding.UTF8.GetString(result);
+            return rawstr.Substring(0, rawstr.IndexOf('\0'));
+        }
+    }
+}

+ 6 - 6
src/Avalonia.Input/Gestures.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Input
             RoutedEvent.Register<ScrollGestureEventArgs>(
                 "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
 
-        private static WeakReference s_lastPress;
+        private static WeakReference<IInteractive> s_lastPress;
 
         static Gestures()
         {
@@ -47,11 +47,11 @@ namespace Avalonia.Input
 
                 if (e.ClickCount <= 1)
                 {
-                    s_lastPress = new WeakReference(e.Source);
+                    s_lastPress = new WeakReference<IInteractive>(e.Source);
                 }
-                else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source)
+                else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right)
                 {
-                    if (e.MouseButton != MouseButton.Right)
+                    if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
                     {
                         e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
                     }
@@ -65,10 +65,10 @@ namespace Avalonia.Input
             {
                 var e = (PointerReleasedEventArgs)ev;
 
-                if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source)
+                if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
                 {
                     var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
-                    ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et));
+                    e.Source.RaiseEvent(new RoutedEventArgs(et));
                 }
             }
         }

+ 2 - 2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -84,8 +84,8 @@ namespace Avalonia.Native
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
                 .Bind<IWindowingPlatformGlFeature>().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature()))
-                .Bind<PlatformHotkeyConfiguration>()
-                .ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows));
+                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows))
+                .Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider());
         }
 
         public IWindowImpl CreateWindow()

+ 78 - 0
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Controls.Platform;
+
+namespace Avalonia.Native
+{
+    internal class WindowsMountedVolumeInfoListener : IDisposable
+    {
+        private readonly CompositeDisposable _disposables;
+        private readonly ObservableCollection<MountedVolumeInfo> _targetObs;
+        private bool _beenDisposed = false;
+        private ObservableCollection<MountedVolumeInfo> mountedDrives;
+
+        public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        {
+            this.mountedDrives = mountedDrives;
+            _disposables = new CompositeDisposable();
+
+            var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
+                                      .Subscribe(Poll);
+
+            _disposables.Add(pollTimer);
+
+            Poll(0);
+        }
+
+        private void Poll(long _)
+        {
+            var mountVolInfos = Directory.GetDirectories("/Volumes")
+                                .Select(p => new MountedVolumeInfo()
+                                {
+                                    VolumeLabel = Path.GetFileName(p),
+                                    VolumePath = p,
+                                    VolumeSizeBytes = 0
+                                })
+                                .ToArray();
+
+            if (_targetObs.SequenceEqual(mountVolInfos))
+                return;
+            else
+            {
+                _targetObs.Clear();
+
+                foreach (var i in mountVolInfos)
+                    _targetObs.Add(i);
+            }
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_beenDisposed)
+            {
+                if (disposing)
+                {
+
+                }
+                _beenDisposed = true;
+            }
+        }
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+    }
+
+    public class MacOSMountedVolumeInfoProvider : IMountedVolumeInfoProvider
+    {
+        public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        {
+            Contract.Requires<ArgumentNullException>(mountedDrives != null);
+            return new WindowsMountedVolumeInfoListener(mountedDrives);
+        }
+    }
+}

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

@@ -8,6 +8,7 @@
     <ItemGroup>
         <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
         <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+        <ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
     </ItemGroup>
 
 </Project>

+ 39 - 18
src/Avalonia.X11/X11KeyTransform.cs

@@ -52,16 +52,6 @@ namespace Avalonia.X11
             {X11Key.Delete, Key.Delete},
             {X11Key.KP_Delete, Key.Delete},
             {X11Key.Help, Key.Help},
-            {X11Key.XK_0, Key.D0},
-            {X11Key.XK_1, Key.D1},
-            {X11Key.XK_2, Key.D2},
-            {X11Key.XK_3, Key.D3},
-            {X11Key.XK_4, Key.D4},
-            {X11Key.XK_5, Key.D5},
-            {X11Key.XK_6, Key.D6},
-            {X11Key.XK_7, Key.D7},
-            {X11Key.XK_8, Key.D8},
-            {X11Key.XK_9, Key.D9},
             {X11Key.A, Key.A},
             {X11Key.B, Key.B},
             {X11Key.C, Key.C},
@@ -114,8 +104,8 @@ namespace Avalonia.X11
             {X11Key.x, Key.X},
             {X11Key.y, Key.Y},
             {X11Key.z, Key.Z},
-            //{ X11Key.?, Key.LWin }
-            //{ X11Key.?, Key.RWin }
+            {X11Key.Meta_L, Key.LWin },
+            {X11Key.Meta_R, Key.RWin },
             {X11Key.Menu, Key.Apps},
             //{ X11Key.?, Key.Sleep }
             {X11Key.KP_0, Key.NumPad0},
@@ -185,20 +175,51 @@ namespace Avalonia.X11
             //{ X11Key.?, Key.SelectMedia }
             //{ X11Key.?, Key.LaunchApplication1 }
             //{ X11Key.?, Key.LaunchApplication2 }
-            {X11Key.semicolon, Key.OemSemicolon},
+            {X11Key.minus, Key.OemMinus},
+            {X11Key.underscore, Key.OemMinus},
             {X11Key.plus, Key.OemPlus},
             {X11Key.equal, Key.OemPlus},
+            {X11Key.bracketleft, Key.OemOpenBrackets},
+            {X11Key.braceleft, Key.OemOpenBrackets},
+            {X11Key.bracketright, Key.OemCloseBrackets},
+            {X11Key.braceright, Key.OemCloseBrackets},
+            {X11Key.backslash, Key.OemPipe},
+            {X11Key.bar, Key.OemPipe},
+            {X11Key.semicolon, Key.OemSemicolon},
+            {X11Key.colon, Key.OemSemicolon},
+            {X11Key.apostrophe, Key.OemQuotes},
+            {X11Key.quotedbl, Key.OemQuotes},
             {X11Key.comma, Key.OemComma},
-            {X11Key.minus, Key.OemMinus},
+            {X11Key.less, Key.OemComma},
             {X11Key.period, Key.OemPeriod},
+            {X11Key.greater, Key.OemPeriod},
             {X11Key.slash, Key.Oem2},
+            {X11Key.question, Key.Oem2},
             {X11Key.grave, Key.OemTilde},
+            {X11Key.asciitilde, Key.OemTilde},
+            {X11Key.XK_1, Key.D1},
+            {X11Key.exclam, Key.D1},
+            {X11Key.XK_2, Key.D2},
+            {X11Key.at, Key.D2},
+            {X11Key.XK_3, Key.D3},
+            {X11Key.numbersign, Key.D3},
+            {X11Key.XK_4, Key.D4},
+            {X11Key.dollar, Key.D4},
+            {X11Key.XK_5, Key.D5},
+            {X11Key.percent, Key.D5},
+            {X11Key.XK_6, Key.D6},
+            {X11Key.asciicircum, Key.D6},
+            {X11Key.XK_7, Key.D7},
+            {X11Key.ampersand, Key.D7},
+            {X11Key.XK_8, Key.D8},
+            {X11Key.asterisk, Key.D8},
+            {X11Key.XK_9, Key.D9},
+            {X11Key.parenleft, Key.D9},
+            {X11Key.XK_0, Key.D0},
+            {X11Key.parenright, Key.D0},
+
             //{ X11Key.?, Key.AbntC1 }
             //{ X11Key.?, Key.AbntC2 }
-            {X11Key.bracketleft, Key.OemOpenBrackets},
-            {X11Key.backslash, Key.OemPipe},
-            {X11Key.bracketright, Key.OemCloseBrackets},
-            {X11Key.apostrophe, Key.OemQuotes},
             //{ X11Key.?, Key.Oem8 }
             //{ X11Key.?, Key.Oem102 }
             //{ X11Key.?, Key.ImeProcessed }

+ 4 - 1
src/Avalonia.X11/X11Platform.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Reflection;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
+using Avalonia.FreeDesktop;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.OpenGL;
@@ -12,6 +13,7 @@ using Avalonia.X11;
 using Avalonia.X11.Glx;
 using Avalonia.X11.NativeDialogs;
 using static Avalonia.X11.XLib;
+
 namespace Avalonia.X11
 {
     class AvaloniaX11Platform : IWindowingPlatform
@@ -48,7 +50,8 @@ namespace Avalonia.X11
                 .Bind<IClipboard>().ToConstant(new X11Clipboard(this))
                 .Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
                 .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
-                .Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog());
+                .Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog())
+                .Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider());
             
             X11Screens = Avalonia.X11.X11Screens.Init(this);
             Screens = new X11Screens(X11Screens);

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs

@@ -164,6 +164,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                 public bool IsStatic => true;
                 public string Name { get; protected set; }
                 public IXamlIlType DeclaringType { get; }
+                public IXamlIlMethod MakeGenericMethod(IReadOnlyList<IXamlIlType> typeArguments) 
+                    => throw new System.NotSupportedException();
 
 
                 public bool Equals(IXamlIlMethod other) =>

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

@@ -1 +1 @@
-Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022
+Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf

+ 1 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Markup.Parsers.Nodes
 
         public override string Description => $"#{_name}";
 
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
             if (_nameScope.TryGetTarget(out var scope))
                 _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged);

+ 2 - 2
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs

@@ -31,9 +31,9 @@ namespace Avalonia.Markup.Parsers.Nodes
             }
         }
 
-        protected override void StartListeningCore(WeakReference reference)
+        protected override void StartListeningCore(WeakReference<object> reference)
         {
-            if (reference.Target is ILogical logical)
+            if (reference.TryGetTarget(out object target) && target is ILogical logical)
             {
                 _subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
             }

+ 18 - 8
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@@ -26,9 +26,11 @@ namespace Avalonia.Markup.Parsers.Nodes
 
         protected override bool SetTargetValueCore(object value, BindingPriority priority)
         {
-            var typeInfo = Target.Target.GetType().GetTypeInfo();
-            var list = Target.Target as IList;
-            var dictionary = Target.Target as IDictionary;
+            Target.TryGetTarget(out object target);
+
+            var typeInfo = target.GetType().GetTypeInfo();
+            var list = target as IList;
+            var dictionary = target as IDictionary;
             var indexerProperty = GetIndexer(typeInfo);
             var indexerParameters = indexerProperty?.GetIndexParameters();
 
@@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers.Nodes
                 // Try special cases where we can validate indices
                 if (typeInfo.IsArray)
                 {
-                    return SetValueInArray((Array)Target.Target, intArgs, value);
+                    return SetValueInArray((Array)target, intArgs, value);
                 }
                 else if (Arguments.Count == 1)
                 {
@@ -83,14 +85,14 @@ namespace Avalonia.Markup.Parsers.Nodes
                     else
                     {
                         // Fallback to unchecked access
-                        indexerProperty.SetValue(Target.Target, value, convertedObjectArray);
+                        indexerProperty.SetValue(target, value, convertedObjectArray);
                         return true;
                     }
                 }
                 else
                 {
                     // Fallback to unchecked access
-                    indexerProperty.SetValue(Target.Target, value, convertedObjectArray);
+                    indexerProperty.SetValue(target, value, convertedObjectArray);
                     return true;
                 }
             }
@@ -98,7 +100,7 @@ namespace Avalonia.Markup.Parsers.Nodes
             // multidimensional indexer, which doesn't take the same number of arguments
             else if (typeInfo.IsArray)
             {
-                SetValueInArray((Array)Target.Target, value);
+                SetValueInArray((Array)target, value);
                 return true;
             }
             return false;
@@ -126,7 +128,15 @@ namespace Avalonia.Markup.Parsers.Nodes
 
         public IList<string> Arguments { get; }
 
-        public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType;
+        public override Type PropertyType
+        {
+            get
+            {
+                Target.TryGetTarget(out object target);
+
+                return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType;
+            }
+        }
 
         protected override object GetValue(object target)
         {

+ 3 - 1
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -90,7 +90,9 @@ namespace Avalonia.Win32
                 .Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
                 .Bind<IWindowingPlatform>().ToConstant(s_instance)
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
-                .Bind<IPlatformIconLoader>().ToConstant(s_instance);
+                .Bind<IPlatformIconLoader>().ToConstant(s_instance)
+                .Bind<IMountedVolumeInfoProvider>().ToConstant(new WindowsMountedVolumeInfoProvider());
+
             if (options.AllowEglInitialization)
                 Win32GlManager.Initialize();
             

+ 71 - 0
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Controls.Platform;
+
+namespace Avalonia.Win32
+{
+    internal class WindowsMountedVolumeInfoListener : IDisposable
+    {
+        private readonly CompositeDisposable _disposables;
+        private readonly ObservableCollection<MountedVolumeInfo> _targetObs = new ObservableCollection<MountedVolumeInfo>();
+        private bool _beenDisposed = false;
+        private ObservableCollection<MountedVolumeInfo> mountedDrives;
+
+        public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        {
+            this.mountedDrives = mountedDrives;
+            _disposables = new CompositeDisposable();
+
+            var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
+                                      .Subscribe(Poll);
+
+            _disposables.Add(pollTimer);
+
+            Poll(0);
+        }
+
+        private void Poll(long _)
+        {
+            var allDrives = DriveInfo.GetDrives();
+
+            var mountVolInfos = allDrives
+                                .Select(p => new MountedVolumeInfo()
+                                {
+                                    VolumeLabel = p.VolumeLabel,
+                                    VolumePath = p.RootDirectory.FullName,
+                                    VolumeSizeBytes = (ulong)p.TotalSize
+                                })
+                                .ToArray();
+
+            if (_targetObs.SequenceEqual(mountVolInfos))
+                return;
+            else
+            {
+                _targetObs.Clear();
+
+                foreach (var i in mountVolInfos)
+                    _targetObs.Add(i);
+            }
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_beenDisposed)
+            {
+                if (disposing)
+                {
+
+                }
+                _beenDisposed = true;
+            }
+        }
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.ObjectModel;
+using Avalonia.Controls.Platform;
+
+namespace Avalonia.Win32
+{
+    public class WindowsMountedVolumeInfoProvider : IMountedVolumeInfoProvider
+    {
+        public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        {
+            Contract.Requires<ArgumentNullException>(mountedDrives != null);
+            return new WindowsMountedVolumeInfoListener(mountedDrives);
+        }
+    }
+}

+ 17 - 0
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections
             Assert.True(raised);
         }
 
+        [Fact]
+        public void AddRange_Items_Should_Raise_Correct_CollectionChanged()
+        {
+            var target = new AvaloniaList<object>();
+
+            var eventItems = new List<object>();
+
+            target.CollectionChanged += (sender, args) =>
+            {
+                eventItems.AddRange(args.NewItems.Cast<object>());
+            };
+            
+            target.AddRange(Enumerable.Range(0,10).Select(i => new object()));
+
+            Assert.Equal(eventItems, target);
+        }
+
         [Fact]
         public void Replacing_Item_Should_Raise_CollectionChanged()
         {

+ 1 - 1
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs

@@ -574,7 +574,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
             var source = new Class1 { Foo = "foo" };
             var target = new PropertyAccessorNode("Foo", false);
             Assert.NotNull(target);
-            target.Target = new WeakReference(source);
+            target.Target = new WeakReference<object>(source);
             target.Subscribe(_ => { });
             target.Unsubscribe();
             target.Unsubscribe();

+ 7 - 7
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
             var target = new DataAnnotationsValidationPlugin();
             var data = new Data();
 
-            Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10)));
+            Assert.True(target.Match(new WeakReference<object>(data), nameof(Data.Between5And10)));
         }
 
         [Fact]
@@ -29,7 +29,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
             var target = new DataAnnotationsValidationPlugin();
             var data = new Data();
 
-            Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber)));
+            Assert.True(target.Match(new WeakReference<object>(data), nameof(Data.PhoneNumber)));
         }
 
         [Fact]
@@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
             var target = new DataAnnotationsValidationPlugin();
             var data = new Data();
 
-            Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated)));
+            Assert.False(target.Match(new WeakReference<object>(data), nameof(Data.Unvalidated)));
         }
 
         [Fact]
@@ -47,8 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
             var validatorPlugin = new DataAnnotationsValidationPlugin();
             var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10));
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor);
+            var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Between5And10));
+            var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Between5And10), accessor);
             var result = new List<object>();
             
             var errmsg = new RangeAttribute(5, 10).FormatErrorMessage(nameof(Data.Between5And10));
@@ -79,8 +79,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
             var validatorPlugin = new DataAnnotationsValidationPlugin();
             var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber));
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor);
+            var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.PhoneNumber));
+            var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.PhoneNumber), accessor);
             var result = new List<object>();
 
             validator.Subscribe(x => result.Add(x));

+ 2 - 2
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs

@@ -19,8 +19,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
             var validatorPlugin = new ExceptionValidationPlugin();
             var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+            var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive));
+            var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive), accessor);
             var result = new List<object>();
 
             validator.Subscribe(x => result.Add(x));

+ 4 - 4
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

@@ -18,8 +18,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
             var validatorPlugin = new IndeiValidationPlugin();
             var data = new Data { Maximum = 5 };
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+            var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Value));
+            var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Value), accessor);
             var result = new List<object>();
 
             validator.Subscribe(x => result.Add(x));
@@ -53,8 +53,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
             var validatorPlugin = new IndeiValidationPlugin();
             var data = new Data { Maximum = 5 };
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+            var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.Value));
+            var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.Value), accessor);
 
             Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
             validator.Subscribe(_ => { });

+ 132 - 0
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal("baz", source.Foo);
         }
 
+        [Fact]
+        public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect()
+        {
+            var source = new WeakRefSource { Foo = null };
+            var target = new TestControl { DataContext = source };
+
+            var binding = new Binding
+            {
+                Path = "Foo",
+                Mode = BindingMode.TwoWay
+            };
+
+            target.Bind(TestControl.ValueProperty, binding);
+
+            var ref1 = AssignValue(target, "ref1");
+
+            Assert.Equal(ref1.Target, source.Foo);
+
+            GC.Collect();
+            GC.WaitForPendingFinalizers();
+
+            var ref2 = AssignValue(target, "ref2");
+
+            GC.Collect();
+            GC.WaitForPendingFinalizers();
+
+            target.Value = null;
+
+            Assert.Null(source.Foo);
+        }
+
+        private class DummyObject : ICloneable
+        {
+            private readonly string _val;
+
+            public DummyObject(string val)
+            {
+                _val = val;
+            }
+
+            public object Clone()
+            {
+                return new DummyObject(_val);
+            }
+
+            protected bool Equals(DummyObject other)
+            {
+                return string.Equals(_val, other._val);
+            }
+
+            public override bool Equals(object obj)
+            {
+                if (ReferenceEquals(null, obj)) return false;
+                if (ReferenceEquals(this, obj)) return true;
+                if (obj.GetType() != this.GetType()) return false;
+                return Equals((DummyObject) obj);
+            }
+
+            public override int GetHashCode()
+            {
+                return (_val != null ? _val.GetHashCode() : 0);
+            }
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private WeakReference AssignValue(TestControl source, string val)
+        {
+            var obj = new DummyObject(val);
+
+            source.Value = obj;
+
+            return new WeakReference(obj);
+        }
+
         [Fact]
         public void OneTime_Binding_Should_Be_Set_Up()
         {
@@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data
             }
         }
 
+        public class WeakRefSource : INotifyPropertyChanged
+        {
+            private WeakReference<object> _foo;
+
+            public object Foo
+            {
+                get
+                {
+                    if (_foo == null)
+                    {
+                        return null;
+                    }
+
+                    if (_foo.TryGetTarget(out object target))
+                    {
+                        if (target is ICloneable cloneable)
+                        {
+                            return cloneable.Clone();
+                        }
+
+                        return target;
+                    }
+
+                    return null;
+                }
+                set
+                {
+                    _foo = new WeakReference<object>(value);
+
+                    RaisePropertyChanged();
+                }
+            }
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            private void RaisePropertyChanged([CallerMemberName] string prop = "")
+            {
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
+            }
+        }
+
         private class OldDataContextViewModel
         {
             public int Foo { get; set; } = 1;
             public int Bar { get; set; } = 2;
         }
 
+        private class TestControl : Control
+        {
+            public static readonly DirectProperty<TestControl, object> ValueProperty =
+                AvaloniaProperty.RegisterDirect<TestControl, object>(
+                    nameof(Value),
+                    o => o.Value,
+                    (o, v) => o.Value = v);
+
+            private object _value;
+
+            public object Value
+            {
+                get => _value;
+                set => SetAndRaise(ValueProperty, ref _value, value);
+            }
+        }
+
         private class OldDataContextTest : Control
         {
             public static readonly StyledProperty<int> FooProperty =

+ 13 - 7
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@@ -309,8 +309,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
-        [Fact]
-        public void Binding_To_TextBlock_Text_With_StringConverter_Works()
+        [Theory,
+            InlineData(@"Hello \{0\}"),
+            InlineData(@"'Hello {0}'"),
+            InlineData(@"Hello {0}")]
+        
+        public void Binding_To_TextBlock_Text_With_StringConverter_Works(string fmt)
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
@@ -318,8 +322,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
         xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
-    <TextBlock Name='textBlock' Text='{Binding Foo, StringFormat=Hello \{0\}}'/> 
-</Window>"; 
+    <TextBlock Name='textBlock' Text=""{Binding Foo, StringFormat=" + fmt + @"}""/> 
+</Window>";
                 var loader = new AvaloniaXamlLoader();
                 var window = (Window)loader.Load(xaml); 
                 var textBlock = window.FindControl<TextBlock>("textBlock"); 
@@ -331,8 +335,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
-        [Fact(Skip="Issue #2592")]
-        public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works()
+        [Theory,
+            InlineData("{}{0} {1}!"),
+            InlineData(@"\{0\} \{1\}!")]
+        public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works(string fmt)
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
@@ -342,7 +348,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
         xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
     <TextBlock Name='textBlock'>
         <TextBlock.Text>
-            <MultiBinding StringFormat='\{0\} \{1\}!'>
+            <MultiBinding StringFormat='" + fmt + @"'>
                 <Binding Path='Greeting1'/>
                 <Binding Path='Greeting2'/>
             </MultiBinding>