Browse Source

Fix conflict

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

58
Avalonia.sln

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio Version 16
VisualStudioVersion = 15.0.27130.2027 VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject EndProject
@ -197,7 +197,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject 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 EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution 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|iPhone.Build.0 = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

8
build/AndroidWorkarounds.props

@ -5,4 +5,12 @@
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" /> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
<PackageReference Include="System.Buffers" Version="4.5.0" /> <PackageReference Include="System.Buffers" Version="4.5.0" />
</ItemGroup> </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> </Project>

1
build/CoreLibraries.props

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

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

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

13
samples/ControlCatalog.NetCore/Program.cs

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

1
samples/ControlCatalog/MainWindow.xaml.cs

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

229
src/Avalonia.Base/Collections/AvaloniaList.cs

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

6
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -24,7 +24,7 @@ namespace Avalonia.Data.Core
{ {
try try
{ {
if (Target.IsAlive && Target.Target is IAvaloniaObject obj) if (Target.TryGetTarget(out object target) && target is IAvaloniaObject obj)
{ {
obj.SetValue(_property, value, priority); obj.SetValue(_property, value, priority);
return true; 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); _subscription = new AvaloniaPropertyObservable<object>(obj, _property).Subscribe(ValueChanged);
} }

26
src/Avalonia.Base/Data/Core/ExpressionNode.cs

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

8
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

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

11
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

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

5
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -13,9 +13,10 @@ namespace Avalonia.Data.Core
{ {
private IDisposable _subscription; 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 incc = target as INotifyCollectionChanged;
var inpc = target as INotifyPropertyChanged; var inpc = target as INotifyPropertyChanged;
var inputs = new List<IObservable<object>>(); var inputs = new List<IObservable<object>>();

4
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 /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
public IPropertyAccessor Start(WeakReference reference, string propertyName) public IPropertyAccessor Start(WeakReference<object> reference, string propertyName)
{ {
Contract.Requires<ArgumentNullException>(reference != null); Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null); Contract.Requires<ArgumentNullException>(propertyName != null);
var instance = reference.Target; reference.TryGetTarget(out object instance);
var o = (AvaloniaObject)instance; var o = (AvaloniaObject)instance;
var p = LookupProperty(o, propertyName); var p = LookupProperty(o, propertyName);

23
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

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

8
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -12,17 +12,17 @@ namespace Avalonia.Data.Core.Plugins
public class ExceptionValidationPlugin : IDataValidationPlugin public class ExceptionValidationPlugin : IDataValidationPlugin
{ {
/// <inheritdoc/> /// <inheritdoc/>
public bool Match(WeakReference reference, string memberName) => true; public bool Match(WeakReference<object> reference, string memberName) => true;
/// <inheritdoc/> /// <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); 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) : base(inner)
{ {
} }

5
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="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param> /// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns> /// <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> /// <summary>
/// Starts monitoring the data validation state of a property on an object. /// 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 /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
IPropertyAccessor Start( IPropertyAccessor Start(WeakReference<object> reference,
WeakReference reference,
string propertyName, string propertyName,
IPropertyAccessor inner); IPropertyAccessor inner);
} }

3
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 /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
IPropertyAccessor Start( IPropertyAccessor Start(WeakReference<object> reference,
WeakReference reference,
string propertyName); string propertyName);
} }
} }

4
src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs

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

28
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

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

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

21
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -9,12 +9,12 @@ namespace Avalonia.Data.Core.Plugins
public bool Match(object obj, string methodName) public bool Match(object obj, string methodName)
=> obj.GetType().GetRuntimeMethods().Any(x => x.Name == 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>(reference != null);
Contract.Requires<ArgumentNullException>(methodName != 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); var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName);
if (method != null) 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>(reference != null);
Contract.Requires<ArgumentNullException>(method != null); Contract.Requires<ArgumentNullException>(method != null);
@ -61,8 +61,17 @@ namespace Avalonia.Data.Core.Plugins
var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray();
PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); 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; } public override Type PropertyType { get; }

12
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -20,9 +20,11 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the value.</param> /// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns> /// <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.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>)); x.GetGenericTypeDefinition() == typeof(IObservable<>));
} }
@ -34,9 +36,9 @@ namespace Avalonia.Data.Core.Plugins
/// <returns> /// <returns>
/// An observable that produces the output for the value. /// An observable that produces the output for the value.
/// </returns> /// </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 the observable returns a reference type then we can cast it.
if (target is IObservable<object> result) 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. // 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>`. // 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.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0];

13
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -19,7 +19,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the value.</param> /// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns> /// <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> /// <summary>
/// Starts producing output based on the specified value. /// Starts producing output based on the specified value.
@ -28,11 +33,11 @@ namespace Avalonia.Data.Core.Plugins
/// <returns> /// <returns>
/// An observable that produces the output for the value. /// An observable that produces the output for the value.
/// </returns> /// </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"); var resultProperty = task.GetType().GetRuntimeProperty("Result");

6
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -37,9 +37,11 @@ namespace Avalonia.Data.Core
return false; 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); var accessor = plugin?.Start(reference, PropertyName);
if (_enableValidation && Next == null) if (_enableValidation && Next == null)

18
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -19,11 +19,25 @@ namespace Avalonia.Data.Core
{ {
return false; return false;
} }
if (LastValue == null)
{
return false;
}
bool isLastValueAlive = LastValue.TryGetTarget(out object lastValue);
if (!isLastValueAlive)
{
return false;
}
if (PropertyType.IsValueType) 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); protected abstract bool SetTargetValueCore(object value, BindingPriority priority);

2
src/Avalonia.Base/Data/Core/StreamNode.cs

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

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

23
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
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);
}
}
}

16
src/Avalonia.Controls/SystemDialog.cs

@ -14,7 +14,13 @@ namespace Avalonia.Controls
public abstract class FileSystemDialog : SystemDialog 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 public class SaveFileDialog : FileDialog
@ -45,8 +51,12 @@ namespace Avalonia.Controls
public class OpenFolderDialog : FileSystemDialog 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) public Task<string> ShowAsync(Window parent)
{ {
if(parent == null) if(parent == null)

18
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
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
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
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
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
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
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
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
src/Avalonia.Dialogs/ManagedFileChooserItemType.cs

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

77
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
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
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
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
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
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
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
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
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
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'));
}
}
}

12
src/Avalonia.Input/Gestures.cs

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

4
src/Avalonia.Native/AvaloniaNativePlatform.cs

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

78
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
src/Avalonia.X11/Avalonia.X11.csproj

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

57
src/Avalonia.X11/X11KeyTransform.cs

@ -52,16 +52,6 @@ namespace Avalonia.X11
{X11Key.Delete, Key.Delete}, {X11Key.Delete, Key.Delete},
{X11Key.KP_Delete, Key.Delete}, {X11Key.KP_Delete, Key.Delete},
{X11Key.Help, Key.Help}, {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.A, Key.A},
{X11Key.B, Key.B}, {X11Key.B, Key.B},
{X11Key.C, Key.C}, {X11Key.C, Key.C},
@ -114,8 +104,8 @@ namespace Avalonia.X11
{X11Key.x, Key.X}, {X11Key.x, Key.X},
{X11Key.y, Key.Y}, {X11Key.y, Key.Y},
{X11Key.z, Key.Z}, {X11Key.z, Key.Z},
//{ X11Key.?, Key.LWin } {X11Key.Meta_L, Key.LWin },
//{ X11Key.?, Key.RWin } {X11Key.Meta_R, Key.RWin },
{X11Key.Menu, Key.Apps}, {X11Key.Menu, Key.Apps},
//{ X11Key.?, Key.Sleep } //{ X11Key.?, Key.Sleep }
{X11Key.KP_0, Key.NumPad0}, {X11Key.KP_0, Key.NumPad0},
@ -185,20 +175,51 @@ namespace Avalonia.X11
//{ X11Key.?, Key.SelectMedia } //{ X11Key.?, Key.SelectMedia }
//{ X11Key.?, Key.LaunchApplication1 } //{ X11Key.?, Key.LaunchApplication1 }
//{ X11Key.?, Key.LaunchApplication2 } //{ X11Key.?, Key.LaunchApplication2 }
{X11Key.semicolon, Key.OemSemicolon}, {X11Key.minus, Key.OemMinus},
{X11Key.underscore, Key.OemMinus},
{X11Key.plus, Key.OemPlus}, {X11Key.plus, Key.OemPlus},
{X11Key.equal, 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.comma, Key.OemComma},
{X11Key.minus, Key.OemMinus}, {X11Key.less, Key.OemComma},
{X11Key.period, Key.OemPeriod}, {X11Key.period, Key.OemPeriod},
{X11Key.greater, Key.OemPeriod},
{X11Key.slash, Key.Oem2}, {X11Key.slash, Key.Oem2},
{X11Key.question, Key.Oem2},
{X11Key.grave, Key.OemTilde}, {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.AbntC1 }
//{ X11Key.?, Key.AbntC2 } //{ X11Key.?, Key.AbntC2 }
{X11Key.bracketleft, Key.OemOpenBrackets},
{X11Key.backslash, Key.OemPipe},
{X11Key.bracketright, Key.OemCloseBrackets},
{X11Key.apostrophe, Key.OemQuotes},
//{ X11Key.?, Key.Oem8 } //{ X11Key.?, Key.Oem8 }
//{ X11Key.?, Key.Oem102 } //{ X11Key.?, Key.Oem102 }
//{ X11Key.?, Key.ImeProcessed } //{ X11Key.?, Key.ImeProcessed }

5
src/Avalonia.X11/X11Platform.cs

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

2
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 bool IsStatic => true;
public string Name { get; protected set; } public string Name { get; protected set; }
public IXamlIlType DeclaringType { get; } public IXamlIlType DeclaringType { get; }
public IXamlIlMethod MakeGenericMethod(IReadOnlyList<IXamlIlType> typeArguments)
=> throw new System.NotSupportedException();
public bool Equals(IXamlIlMethod other) => public bool Equals(IXamlIlMethod other) =>

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

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

2
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

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

4
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); _subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
} }

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

4
src/Windows/Avalonia.Win32/Win32Platform.cs

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

71
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
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
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.True(raised); 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] [Fact]
public void Replacing_Item_Should_Raise_CollectionChanged() public void Replacing_Item_Should_Raise_CollectionChanged()
{ {

2
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 source = new Class1 { Foo = "foo" };
var target = new PropertyAccessorNode("Foo", false); var target = new PropertyAccessorNode("Foo", false);
Assert.NotNull(target); Assert.NotNull(target);
target.Target = new WeakReference(source); target.Target = new WeakReference<object>(source);
target.Subscribe(_ => { }); target.Subscribe(_ => { });
target.Unsubscribe(); target.Unsubscribe();
target.Unsubscribe(); target.Unsubscribe();

14
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs

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

4
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 inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationPlugin(); var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data(); var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); var accessor = inpcAccessorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); var validator = validatorPlugin.Start(new WeakReference<object>(data), nameof(data.MustBePositive), accessor);
var result = new List<object>(); var result = new List<object>();
validator.Subscribe(x => result.Add(x)); validator.Subscribe(x => result.Add(x));

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

132
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("baz", source.Foo); 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] [Fact]
public void OneTime_Binding_Should_Be_Set_Up() 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 private class OldDataContextViewModel
{ {
public int Foo { get; set; } = 1; public int Foo { get; set; } = 1;
public int Bar { get; set; } = 2; 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 private class OldDataContextTest : Control
{ {
public static readonly StyledProperty<int> FooProperty = public static readonly StyledProperty<int> FooProperty =

20
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

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

Loading…
Cancel
Save