Browse Source

Merge branch 'master' into local-assembly-for-xaml

 Conflicts:
	samples/BindingTest/MainWindow.xaml
	src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs
Steven Kirk 7 years ago
parent
commit
92cd84a353
100 changed files with 8658 additions and 685 deletions
  1. 17 0
      .github/PULL_REQUEST_TEMPLATE.md
  2. 4 51
      Avalonia.sln
  3. 0 1
      build/Rx.props
  4. 5 0
      build/System.Drawing.Common.props
  5. 3 4
      packages.cake
  6. 1 1
      parameters.cake
  7. 38 8
      readme.md
  8. 8 0
      samples/BindingTest/MainWindow.xaml
  9. 1 0
      samples/ControlCatalog.Desktop/Program.cs
  10. 2 0
      samples/ControlCatalog.NetCore/Program.cs
  11. 2 3
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  12. 7 181
      samples/ControlCatalog/ControlCatalog.csproj
  13. 6 1
      samples/ControlCatalog/MainView.xaml
  14. 59 0
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  15. 143 0
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  16. 24 0
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
  17. 54 0
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
  18. 46 0
      samples/ControlCatalog/Pages/DatePickerPage.xaml
  19. 36 0
      samples/ControlCatalog/Pages/DatePickerPage.xaml.cs
  20. 19 0
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  21. 71 0
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  22. 80 0
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  23. 94 0
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  24. 0 36
      samples/ControlCatalog/Properties/AssemblyInfo.cs
  25. 3 1
      samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
  26. 5 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  27. 4 5
      src/Avalonia.Base/AttachedProperty.cs
  28. 19 31
      src/Avalonia.Base/AvaloniaObject.cs
  29. 6 2
      src/Avalonia.Base/AvaloniaProperty.cs
  30. 133 150
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  31. 2 2
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  32. 21 5
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  33. 127 0
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  34. 3 0
      src/Avalonia.Base/DirectProperty.cs
  35. 7 0
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  36. 2 0
      src/Avalonia.Base/Threading/Dispatcher.cs
  37. 205 0
      src/Avalonia.Base/Utilities/StringTokenizer.cs
  38. 6 3
      src/Avalonia.Controls/Application.cs
  39. 2726 0
      src/Avalonia.Controls/AutoCompleteBox.cs
  40. 21 30
      src/Avalonia.Controls/Border.cs
  41. 1 1
      src/Avalonia.Controls/Button.cs
  42. 263 0
      src/Avalonia.Controls/ButtonSpinner.cs
  43. 1188 0
      src/Avalonia.Controls/Calendar/DatePicker.cs
  44. 1 1
      src/Avalonia.Controls/ColumnDefinitions.cs
  45. 4 3
      src/Avalonia.Controls/ContextMenu.cs
  46. 1 0
      src/Avalonia.Controls/DropDown.cs
  47. 12 7
      src/Avalonia.Controls/GridLength.cs
  48. 5 7
      src/Avalonia.Controls/ItemsControl.cs
  49. 6 0
      src/Avalonia.Controls/LayoutTransformControl.cs
  50. 35 0
      src/Avalonia.Controls/MenuItem.cs
  51. 998 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  52. 16 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
  53. 8 2
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  54. 11 0
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  55. 210 0
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  56. 71 82
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  57. 1 1
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  58. 62 4
      src/Avalonia.Controls/Primitives/Popup.cs
  59. 26 0
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  60. 4 4
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  61. 3 23
      src/Avalonia.Controls/ProgressBar.cs
  62. 2 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  63. 27 2
      src/Avalonia.Controls/RepeatButton.cs
  64. 1 1
      src/Avalonia.Controls/RowDefinitions.cs
  65. 1 1
      src/Avalonia.Controls/Screens.cs
  66. 174 0
      src/Avalonia.Controls/Spinner.cs
  67. 2 2
      src/Avalonia.Controls/TextBlock.cs
  68. 32 5
      src/Avalonia.Controls/TextBox.cs
  69. 4 0
      src/Avalonia.Controls/UserControl.cs
  70. 279 0
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  71. 64 0
      src/Avalonia.Controls/Utils/ISelectionAdapter.cs
  72. 342 0
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  73. 7 1
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  74. 93 6
      src/Avalonia.Controls/Window.cs
  75. 24 0
      src/Avalonia.Controls/WindowBase.cs
  76. 23 0
      src/Avalonia.Controls/WindowStartupLocation.cs
  77. 2 2
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  78. 9 0
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  79. 9 0
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  80. 1 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  81. 3 0
      src/Avalonia.DotNetCoreRuntime/AppBuilder.cs
  82. 1 1
      src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
  83. 4 1
      src/Avalonia.Input/Cursors.cs
  84. 15 0
      src/Avalonia.Input/DataFormats.cs
  85. 43 0
      src/Avalonia.Input/DataObject.cs
  86. 54 0
      src/Avalonia.Input/DragDrop.cs
  87. 111 0
      src/Avalonia.Input/DragDropDevice.cs
  88. 13 0
      src/Avalonia.Input/DragDropEffects.cs
  89. 18 0
      src/Avalonia.Input/DragEventArgs.cs
  90. 39 0
      src/Avalonia.Input/IDataObject.cs
  91. 1 1
      src/Avalonia.Input/KeyboardDevice.cs
  92. 10 7
      src/Avalonia.Input/Navigation/TabNavigation.cs
  93. 14 0
      src/Avalonia.Input/Platform/IPlatformDragSource.cs
  94. 8 0
      src/Avalonia.Input/Raw/IDragDropDevice.cs
  95. 26 0
      src/Avalonia.Input/Raw/RawDragEvent.cs
  96. 10 0
      src/Avalonia.Input/Raw/RawDragEventType.cs
  97. 1 1
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  98. 43 0
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  99. 86 0
      src/Avalonia.Themes.Default/ButtonSpinner.xaml
  100. 126 0
      src/Avalonia.Themes.Default/DatePicker.xaml

+ 17 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,17 @@
+This template is not intended to be prescriptive, but to help us review pull requests it would be useful if you included as much of the following information as possible:
+
+- What does the pull request do?
+- What is the current behavior?
+- What is the updated/expected behavior with this PR?
+- How was the solution implemented (if it's not obvious)?
+
+Checklist:
+
+- [ ] Added unit tests (if possible)?
+- [ ] Added XML documentation to any related classes?
+- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
+
+If the pull request fixes issue(s) list them like this:
+
+Fixes #123
+Fixes #456

+ 4 - 51
Avalonia.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 15
-VisualStudioVersion = 15.0.27130.2024
+VisualStudioVersion = 15.0.27130.2027
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
 EndProject
@@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout", "src\Aval
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}"
 EndProject
@@ -126,10 +126,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenderTest", "samples\Rende
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}"
 EndProject
-Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.Win32.Shared", "src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.shproj", "{9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.NetStandard", "src\Windows\Avalonia.Win32.NetStandard\Avalonia.Win32.NetStandard.csproj", "{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetCoreRuntime", "src\Avalonia.DotNetCoreRuntime\Avalonia.DotNetCoreRuntime.csproj", "{7863EA94-F0FB-4386-BF8C-E5BFA761560A}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}"
@@ -196,14 +192,11 @@ Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4
-		src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{40759a76-d0f2-464e-8000-6ff0f5c4bd7c}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{4a1abb09-9047-4bd5-a4ad-a055e52c5ee0}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{7863ea94-f0fb-4386-bf8c-e5bfa761560a}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 4
-		src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4
-		src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{9defc6b7-845b-4d8f-afc0-d32bf0032b8c}*SharedItemsImports = 13
 		tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
 	EndGlobalSection
@@ -369,6 +362,7 @@ Global
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
+		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|x86.Build.0 = Debug|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -378,6 +372,7 @@ Global
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
+		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|NetCoreOnly.Build.0 = Release|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|x86.ActiveCfg = Release|Any CPU
 		{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|x86.Build.0 = Release|Any CPU
 		{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
@@ -1994,46 +1989,6 @@ Global
 		{29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.ActiveCfg = Release|Any CPU
 		{29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Build.0 = Release|Any CPU
 		{29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Deploy.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.Build.0 = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.Build.0 = Debug|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.Build.0 = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.ActiveCfg = Release|Any CPU
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.Build.0 = Release|Any CPU
 		{7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
 		{7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
 		{7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@@ -2623,8 +2578,6 @@ Global
 		{C7A69145-60B6-4882-97D6-A3921DD43978} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
 		{F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098}
-		{9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
-		{40759A76-D0F2-464E-8000-6FF0F5C4BD7C} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
 		{7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E}
 		{BB1F7BB5-6AD4-4776-94D9-C09D0A972658} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
 		{39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098}

+ 0 - 1
build/Rx.props

@@ -5,6 +5,5 @@
     <PackageReference Include="System.Reactive.Interfaces" Version="3.1.1" />
     <PackageReference Include="System.Reactive.Linq" Version="3.1.1" />
     <PackageReference Include="System.Reactive.PlatformServices" Version="3.1.1" />
-    <PackageReference Condition="$(TargetFramework.StartsWith('net4'))" Include="System.Reactive.Windows.Threading" Version="3.1.1" />
   </ItemGroup>
 </Project>

+ 5 - 0
build/System.Drawing.Common.props

@@ -0,0 +1,5 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <PackageReference Include="System.Drawing.Common" Version="4.5.0-preview1-25914-04" />
+  </ItemGroup>
+</Project>

+ 3 - 4
packages.cake

@@ -370,14 +370,13 @@ public class Packages
             new NuGetPackSettings()
             {
                 Id = "Avalonia.Win32",
-                Dependencies = new []
+                Dependencies = new DependencyBuilder(this)
                 {
                     new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version }
-                },
+                }.Deps(new string[]{null}, "System.Drawing.Common"),
                 Files = new []
                 {
-                    new NuSpecContent { Source = "Avalonia.Win32/bin/" + parameters.DirSuffix + "/Avalonia.Win32.dll", Target = "lib/net45" },
-                    new NuSpecContent { Source = "Avalonia.Win32.NetStandard/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" }
+                    new NuSpecContent { Source = "Avalonia.Win32/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" }
                 },
                 BasePath = context.Directory("./src/Windows"),
                 OutputDirectory = parameters.NugetRoot

+ 1 - 1
parameters.cake

@@ -97,7 +97,7 @@ public class Parameters
             else
             {
                 // Use AssemblyVersion with Build as version
-                Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-alpha";
+                Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-beta";
             }
         }
 

+ 38 - 8
readme.md

@@ -2,15 +2,15 @@
 
 # Avalonia
 
-| Gitter Chat | Windows Build Status | Linux/Mac Build Status |
-|---|---|---|
-| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) |
+| Gitter Chat | Windows Build Status | Linux/Mac Build Status | Open Collective |
+|---|---|---|---|
+|  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) |
 
 ## About
 
 Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (GTK), MacOS, Android and iOS.
 
-<b>Avalonia is now in alpha.</b> This means that framework is now at a stage where you can have a play and hopefully create simple applications. There's still a lot missing, and you *will* find bugs, and the API *will* change, but this represents the first time where we've made it somewhat easy to have a play and experiment with the framework.
+**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development.
 
 | Control catalog | Desktop platforms | Mobile platforms |
 |---|---|---|
@@ -35,16 +35,46 @@ https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
 
 ## Documentation
 
-As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/tutorial/gettingstarted) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
+As mentioned above, Avalonia is still in beta and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
 
-There's also a high-level [architecture document](http://avaloniaui.net/spec/architecture) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
+There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
 
 Contributions are always welcome!
 
 ## Building and Using
 
-See the [build instructions here](http://avaloniaui.net/guidelines/build).
+See the [build instructions here](http://avaloniaui.net/contributing/build).
 
 ## Contributing
 
-Please read the [contribution guidelines](http://avaloniaui.net/guidelines/contributing) before submitting a pull request.
+Please read the [contribution guidelines](http://avaloniaui.net/contributing/contributing) before submitting a pull request.
+
+### Contributors
+
+This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)].
+<a href="graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
+
+
+### Backers
+
+Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
+
+<a href="https://opencollective.com/Avalonia#backers" target="_blank"><img src="https://opencollective.com/Avalonia/backers.svg?width=890"></a>
+
+
+### Sponsors
+
+Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)]
+
+<a href="https://opencollective.com/Avalonia/sponsor/0/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/0/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/1/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/1/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/2/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/2/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/3/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/3/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/4/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/4/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/5/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/5/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/6/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/6/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/7/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/7/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/8/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/8/avatar.svg"></a>
+<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
+
+

+ 8 - 0
samples/BindingTest/MainWindow.xaml

@@ -1,4 +1,5 @@
 <Window xmlns="https://github.com/avaloniaui"
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
         xmlns:vm="clr-namespace:BindingTest.ViewModels"
         xmlns:local="clr-namespace:BindingTest">
   <Window.Styles>
@@ -6,6 +7,9 @@
       <Setter Property="FontSize" Value="18"/>
     </Style>
   </Window.Styles>
+  <Window.Resources>
+    <vm:TestItem x:Key="SharedItem" StringValue="shared" />
+  </Window.Resources>
   
   <TabControl>
     <TabItem Header="Basic">
@@ -40,6 +44,10 @@
             <TextBlock FontSize="16" Text="Binding Sources"/>
             <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" 
                      Text="{Binding #first.Text, Mode=TwoWay}"/>
+            <TextBox Watermark="Value of SharedItem.StringValue" UseFloatingWatermark="True"
+                     Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/>
+            <TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
+                     Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/>
           </StackPanel>
           <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
             <TextBlock FontSize="16" Text="Scheduler"/>

+ 1 - 0
samples/ControlCatalog.Desktop/Program.cs

@@ -10,6 +10,7 @@ namespace ControlCatalog
 {
     internal class Program
     {
+        [STAThread]
         static void Main(string[] args)
         {
             // TODO: Make this work with GTK/Skia/Cairo depending on command-line args

+ 2 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -9,8 +9,10 @@ namespace ControlCatalog.NetCore
 {
     static class Program
     {
+        
         static void Main(string[] args)
         {
+            Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA);
             if (args.Contains("--wait-for-attach"))
             {
                 Console.WriteLine("Attach debugger and use 'Set next statement'");

+ 2 - 3
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@@ -170,15 +170,14 @@
       <Name>Avalonia.Themes.Default</Name>
     </ProjectReference>
     <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj">
-      <Project>{D2D3083-71DD-4CC9-8907-39A0D86FB322}</Project>
+      <Project>{7d2d3083-71dd-4cc9-8907-39a0d86fb322}</Project>
       <Name>Avalonia.Skia</Name>
-      <IsAppExtension>false</IsAppExtension>
-      <IsWatchApp>false</IsWatchApp>
     </ProjectReference>
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj">
       <Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
       <Name>ControlCatalog</Name>
     </ProjectReference>
   </ItemGroup>
+  <Import Project="..\..\build\Sprache.props" />
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
 </Project>

+ 7 - 181
samples/ControlCatalog/ControlCatalog.csproj

@@ -1,177 +1,17 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
-    <EnableDefaultCompileItems>False</EnableDefaultCompileItems>
-    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <TargetFramework>netstandard2.0</TargetFramework>    
   </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>full</DebugType>
-    <Optimize>false</Optimize>
-    <OutputPath>bin\Debug\</OutputPath>
-    <DefineConstants>DEBUG;TRACE</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <DebugType>pdbonly</DebugType>
-    <Optimize>true</Optimize>
-    <OutputPath>bin\Release\</OutputPath>
-    <DefineConstants>TRACE</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-  </PropertyGroup>
-  <ItemGroup>
-    <None Remove="Pages\ContextMenuPage.xaml" />
-  </ItemGroup>
   <ItemGroup>
-    <!-- A reference to the entire .NET Framework is automatically included -->
-    <EmbeddedResource Include="App.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="MainView.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="DecoratedWindow.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\DialogsPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\BorderPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ButtonPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\CalendarPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\CanvasPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\CarouselPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\CheckBoxPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ContextMenuPage.xaml" />
-    <EmbeddedResource Include="Pages\DropDownPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ExpanderPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ImagePage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\LayoutTransformControlPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\MenuPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ProgressBarPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\RadioButtonPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\SliderPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\TextBoxPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-    <EmbeddedResource Include="Pages\ToolTipPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-  </ItemGroup>
-  <ItemGroup>
-    <Compile Include="App.xaml.cs">
-      <DependentUpon>App.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="MainView.xaml.cs">
-      <DependentUpon>MainView.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="DecoratedWindow.xaml.cs">
-      <DependentUpon>DecoratedWindow.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="MainWindow.xaml.cs">
-      <DependentUpon>MainWindow.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\DialogsPage.xaml.cs">
-      <DependentUpon>DialogsPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\BorderPage.xaml.cs">
-      <DependentUpon>BorderPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ButtonPage.xaml.cs">
-      <DependentUpon>ButtonPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\CalendarPage.xaml.cs">
-      <DependentUpon>CalendarPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\CanvasPage.xaml.cs">
-      <DependentUpon>CanvasPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\CarouselPage.xaml.cs">
-      <DependentUpon>CarouselPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ContextMenuPage.xaml.cs">
-      <DependentUpon>ContextMenuPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\CheckBoxPage.xaml.cs">
-      <DependentUpon>CheckBoxPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\DropDownPage.xaml.cs">
-      <DependentUpon>DropDownPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ExpanderPage.xaml.cs">
-      <DependentUpon>ExpanderPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ImagePage.xaml.cs">
-      <DependentUpon>ImagePage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\LayoutTransformControlPage.xaml.cs">
-      <DependentUpon>LayoutTransformControlPage.xaml</DependentUpon>
+    <Compile Update="**\*.xaml.cs">
+      <DependentUpon>%(Filename)</DependentUpon>
     </Compile>
-    <Compile Include="Pages\MenuPage.xaml.cs">
-      <DependentUpon>MenuPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ProgressBarPage.xaml.cs">
-      <DependentUpon>ProgressBarPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\RadioButtonPage.xaml.cs">
-      <DependentUpon>RadioButtonPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\SliderPage.xaml.cs">
-      <DependentUpon>SliderPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\TreeViewPage.xaml.cs">
-      <DependentUpon>TreeViewPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\TextBoxPage.xaml.cs">
-      <DependentUpon>TextBoxPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ToolTipPage.xaml.cs">
-      <DependentUpon>ToolTipPage.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\ScreenPage.cs" />
-    <Compile Include="Properties\AssemblyInfo.cs" />
-  </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Include="Assets\delicate-arch-896885_640.jpg" />
-    <EmbeddedResource Include="Assets\github_icon.png" />
-    <EmbeddedResource Include="Assets\hirsch-899118_640.jpg" />
-    <EmbeddedResource Include="Assets\maple-leaf-888807_640.jpg" />
-  </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Include="SideBar.xaml">
+    <EmbeddedResource Include="**\*.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Assets\*" />
   </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
@@ -188,20 +28,6 @@
     <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
   </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Include="Assets\test_icon.ico" />
-  </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Include="Pages\TreeViewPage.xaml">
-      <SubType>Designer</SubType>
-    </EmbeddedResource>
-  </ItemGroup>
-  <ItemGroup>
-    <EmbeddedResource Include="MainWindow.xaml">
-      <Generator>MSBuild:Compile</Generator>
-      <SubType>
-      </SubType>
-    </EmbeddedResource>
-  </ItemGroup>
+  
   <Import Project="..\..\build\Serilog.props" />
 </Project>

+ 6 - 1
samples/ControlCatalog/MainView.xaml

@@ -5,18 +5,23 @@
     <TabControl.Transition>
       <CrossFade Duration="0.25"/>
     </TabControl.Transition>
+    <TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
     <TabItem Header="Border"><pages:BorderPage/></TabItem>
     <TabItem Header="Button"><pages:ButtonPage/></TabItem>
+    <TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem>
     <TabItem Header="Calendar"><pages:CalendarPage/></TabItem>
     <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
     <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
     <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
     <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
+    <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
+    <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
     <TabItem Header="DropDown"><pages:DropDownPage/></TabItem>
     <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
     <TabItem Header="Image"><pages:ImagePage/></TabItem>
     <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
     <TabItem Header="Menu"><pages:MenuPage/></TabItem>
+	  <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
     <TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
     <TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
     <TabItem Header="Slider"><pages:SliderPage/></TabItem>
@@ -24,4 +29,4 @@
     <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
     <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
   </TabControl>
-</UserControl>
+</UserControl>

+ 59 - 0
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@@ -0,0 +1,59 @@
+<UserControl xmlns="https://github.com/avaloniaui">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">AutoCompleteBox</TextBlock>
+    <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
+
+    <StackPanel Orientation="Horizontal"
+              Margin="0,16,0,0"
+              HorizontalAlignment="Center"
+              Gap="8">
+      <StackPanel Orientation="Vertical">
+        <TextBlock Text="MinimumPrefixLength: 1"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPrefixLength="1"/>
+        <TextBlock Text="MinimumPrefixLength: 3"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPrefixLength="3"/>
+        <TextBlock Text="MinimumPopulateDelay: 1 Second"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MinimumPopulateDelay="1"/>
+        <TextBlock Text="MaxDropDownHeight: 60"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         MaxDropDownHeight="60"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         Watermark="Watermark"/>
+        <TextBlock Text="Disabled"/>
+        <AutoCompleteBox Width="200"
+                         IsEnabled="False"/>
+      </StackPanel>
+      
+
+      <StackPanel Orientation="Vertical">
+        
+        <TextBlock Text="ValueMemeberSelector"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         ValueMemberSelector="Capital"/>
+        <TextBlock Text="ValueMemberBinding"/>
+        <AutoCompleteBox Width="200"
+                         Margin="0,0,0,8"
+                         ValueMemberBinding="{Binding Capital}"/>
+        <TextBlock Text="Multi-Binding"/>
+        <AutoCompleteBox Name="MultiBindingBox"
+                         Width="200"
+                         Margin="0,0,0,8"
+                         FilterMode="Contains"/>
+        <TextBlock Text="Async Populate"/>
+        <AutoCompleteBox Name="AsyncBox"
+                         Width="200"
+                         Margin="0,0,0,8"
+                         FilterMode="None"/>
+      </StackPanel>
+    </StackPanel>
+  </StackPanel>
+</UserControl>

+ 143 - 0
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@@ -0,0 +1,143 @@
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Markup;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ControlCatalog.Pages
+{
+    public class AutoCompleteBoxPage : UserControl
+    {
+        public class StateData
+        {
+            public string Name { get; private set; }
+            public string Abbreviation { get; private set; }
+            public string Capital { get; private set; }
+
+            public StateData(string name, string abbreviatoin, string capital)
+            {
+                Name = name;
+                Abbreviation = abbreviatoin;
+                Capital = capital;
+            }
+
+            public override string ToString()
+            {
+                return Name;
+            }
+        }
+
+        private StateData[] BuildAllStates()
+        {
+            return new StateData[]
+            {
+                new StateData("Alabama","AL","Montgomery"),
+                new StateData("Alaska","AK","Juneau"),
+                new StateData("Arizona","AZ","Phoenix"),
+                new StateData("Arkansas","AR","Little Rock"),
+                new StateData("California","CA","Sacramento"),
+                new StateData("Colorado","CO","Denver"),
+                new StateData("Connecticut","CT","Hartford"),
+                new StateData("Delaware","DE","Dover"),
+                new StateData("Florida","FL","Tallahassee"),
+                new StateData("Georgia","GA","Atlanta"),
+                new StateData("Hawaii","HI","Honolulu"),
+                new StateData("Idaho","ID","Boise"),
+                new StateData("Illinois","IL","Springfield"),
+                new StateData("Indiana","IN","Indianapolis"),
+                new StateData("Iowa","IA","Des Moines"),
+                new StateData("Kansas","KS","Topeka"),
+                new StateData("Kentucky","KY","Frankfort"),
+                new StateData("Louisiana","LA","Baton Rouge"),
+                new StateData("Maine","ME","Augusta"),
+                new StateData("Maryland","MD","Annapolis"),
+                new StateData("Massachusetts","MA","Boston"),
+                new StateData("Michigan","MI","Lansing"),
+                new StateData("Minnesota","MN","St. Paul"),
+                new StateData("Mississippi","MS","Jackson"),
+                new StateData("Missouri","MO","Jefferson City"),
+                new StateData("Montana","MT","Helena"),
+                new StateData("Nebraska","NE","Lincoln"),
+                new StateData("Nevada","NV","Carson City"),
+                new StateData("New Hampshire","NH","Concord"),
+                new StateData("New Jersey","NJ","Trenton"),
+                new StateData("New Mexico","NM","Santa Fe"),
+                new StateData("New York","NY","Albany"),
+                new StateData("North Carolina","NC","Raleigh"),
+                new StateData("North Dakota","ND","Bismarck"),
+                new StateData("Ohio","OH","Columbus"),
+                new StateData("Oklahoma","OK","Oklahoma City"),
+                new StateData("Oregon","OR","Salem"),
+                new StateData("Pennsylvania","PA","Harrisburg"),
+                new StateData("Rhode Island","RI","Providence"),
+                new StateData("South Carolina","SC","Columbia"),
+                new StateData("South Dakota","SD","Pierre"),
+                new StateData("Tennessee","TN","Nashville"),
+                new StateData("Texas","TX","Austin"),
+                new StateData("Utah","UT","Salt Lake City"),
+                new StateData("Vermont","VT","Montpelier"),
+                new StateData("Virginia","VA","Richmond"),
+                new StateData("Washington","WA","Olympia"),
+                new StateData("West Virginia","WV","Charleston"),
+                new StateData("Wisconsin","WI","Madison"),
+                new StateData("Wyoming","WY","Cheyenne"),
+            };
+        }
+        public StateData[] States { get; private set; }
+        
+        public AutoCompleteBoxPage()
+        {
+            this.InitializeComponent();
+
+            States = BuildAllStates();
+
+            foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
+            {
+                box.Items = States;
+            }
+
+            var converter = new FuncMultiValueConverter<string, string>(parts =>
+            {
+                return String.Format("{0} ({1})", parts.ToArray());
+            });
+            var binding = new MultiBinding { Converter = converter };
+            binding.Bindings.Add(new Binding("Name"));
+            binding.Bindings.Add(new Binding("Abbreviation"));
+
+            var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox");
+            multibindingBox.ValueMemberBinding = binding;
+
+            var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
+            asyncBox.AsyncPopulator = PopulateAsync;
+        }
+        private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox()
+        {
+            return
+                this.GetLogicalDescendants()
+                    .OfType<AutoCompleteBox>();
+        }
+
+        private bool StringContains(string str, string query)
+        {
+            return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
+        }
+        private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken)
+        {
+            await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken);
+
+            return
+                States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText))
+                      .ToList();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 24 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@@ -0,0 +1,24 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">ButtonSpinner</TextBlock>
+    <TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
+
+    <StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0">
+      <CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
+      <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
+      <ButtonSpinner Spin="OnSpin" Height="30"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+      <ButtonSpinner Spin="OnSpin" Height="30" ButtonSpinnerLocation="Left"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+    </StackPanel>
+  </StackPanel>
+
+</UserControl>

+ 54 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs

@@ -0,0 +1,54 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class ButtonSpinnerPage : UserControl
+    {
+        public ButtonSpinnerPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void OnSpin(object sender, SpinEventArgs e)
+        {
+            var spinner = (ButtonSpinner)sender;
+            var txtBox = (TextBlock)spinner.Content;
+
+            int value = Array.IndexOf(_mountains, txtBox.Text);
+            if (e.Direction == SpinDirection.Increase)
+                value++;
+            else
+                value--;
+
+            if (value < 0)
+                value = _mountains.Length - 1;
+            else if (value >= _mountains.Length)
+                value = 0;
+
+            txtBox.Text = _mountains[value];
+        }
+
+        private readonly string[] _mountains = new[]
+        {
+            "Everest",
+            "K2 (Mount Godwin Austen)",
+            "Kangchenjunga",
+            "Lhotse",
+            "Makalu",
+            "Cho Oyu",
+            "Dhaulagiri",
+            "Manaslu",
+            "Nanga Parbat",
+            "Annapurna"
+        };
+    }
+}

+ 46 - 0
samples/ControlCatalog/Pages/DatePickerPage.xaml

@@ -0,0 +1,46 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">DatePicker</TextBlock>
+    <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
+        
+    <StackPanel Orientation="Horizontal"
+                Margin="0,16,0,0"
+                HorizontalAlignment="Center"
+                Gap="16">
+      <StackPanel Orientation="Vertical"
+                  Width="200">
+        <TextBlock Text="SelectedDateFormat: Short"/>
+        <DatePicker Name="DatePicker1"
+                    SelectedDateFormat="Short"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="SelectedDateFormat: Long"/>
+        <DatePicker Name="DatePicker2"
+                    SelectedDateFormat="Long"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="SelectedDateFormat: Custom"/>
+        <DatePicker Name="DatePicker3"
+                    SelectedDateFormat="Custom"
+                    CustomDateFormatString="ddd, MMM d"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="Blackout Dates"/>
+        <DatePicker Name="DatePicker4"
+                    Margin="0,0,0,8"/>
+
+        <DatePicker Margin="0,0,0,8"
+                    Watermark="Watermark"/>
+        <DatePicker Margin="0,0,0,8"
+                    Name="DatePicker5"
+                    Watermark="Floating Watermark"
+                    UseFloatingWatermark="True"/>
+                
+        <TextBlock Text="Disabled"/>
+        <DatePicker IsEnabled="False"/>
+      </StackPanel>
+
+    </StackPanel> 
+  </StackPanel>
+</UserControl>

+ 36 - 0
samples/ControlCatalog/Pages/DatePickerPage.xaml.cs

@@ -0,0 +1,36 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using System;
+
+namespace ControlCatalog.Pages
+{
+    public class DatePickerPage : UserControl
+    {
+        public DatePickerPage()
+        {
+            InitializeComponent();
+            
+            var dp1 = this.FindControl<DatePicker>("DatePicker1");
+            var dp2 = this.FindControl<DatePicker>("DatePicker2");
+            var dp3 = this.FindControl<DatePicker>("DatePicker3");
+            var dp4 = this.FindControl<DatePicker>("DatePicker4");
+            var dp5 = this.FindControl<DatePicker>("DatePicker5");
+
+            dp1.SelectedDate = DateTime.Today;
+            dp2.SelectedDate = DateTime.Today.AddDays(10);
+            dp3.SelectedDate = DateTime.Today.AddDays(20);
+            dp5.SelectedDate = DateTime.Today;
+
+            dp4.TemplateApplied += (s, e) =>
+            {
+                dp4.BlackoutDates.AddDatesInPast();
+            };
+            
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 19 - 0
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@@ -0,0 +1,19 @@
+<UserControl xmlns="https://github.com/avaloniaui">
+    <StackPanel Orientation="Vertical" Gap="4">
+        <TextBlock Classes="h1">Drag+Drop</TextBlock>
+        <TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock>
+
+        <StackPanel Orientation="Horizontal"
+                Margin="0,16,0,0"
+                HorizontalAlignment="Center"
+                Gap="16">
+            <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
+                <TextBlock Name="DragState">Drag Me</TextBlock>
+            </Border>
+            <Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16" 
+                    DragDrop.AllowDrop="True">
+                <TextBlock Name="DropState">Drop some text or files here</TextBlock>
+            </Border>
+        </StackPanel>
+    </StackPanel>
+</UserControl>

+ 71 - 0
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@@ -0,0 +1,71 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace ControlCatalog.Pages
+{
+    public class DragAndDropPage : UserControl
+    {
+        private TextBlock _DropState;
+        private TextBlock _DragState;
+        private Border _DragMe;
+        private int DragCount = 0;
+
+        public DragAndDropPage()
+        {
+            this.InitializeComponent();
+
+            _DragMe.PointerPressed += DoDrag;
+
+            AddHandler(DragDrop.DropEvent, Drop);
+            AddHandler(DragDrop.DragOverEvent, DragOver);
+        }
+
+        private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
+        {
+            DataObject dragData = new DataObject();
+            dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
+
+            var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
+            switch(result)
+            {
+                case DragDropEffects.Copy:
+                    _DragState.Text = "The text was copied"; break;
+                case DragDropEffects.Link:
+                    _DragState.Text = "The text was linked"; break;
+                case DragDropEffects.None:
+                    _DragState.Text = "The drag operation was canceled"; break;
+            }
+        }
+
+        private void DragOver(object sender, DragEventArgs e)
+        {
+            // Only allow Copy or Link as Drop Operations.
+            e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
+
+            // Only allow if the dragged data contains text or filenames.
+            if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
+                e.DragEffects = DragDropEffects.None; 
+        }
+
+        private void Drop(object sender, DragEventArgs e)
+        {
+            if (e.Data.Contains(DataFormats.Text))
+                _DropState.Text = e.Data.GetText();
+            else if (e.Data.Contains(DataFormats.FileNames))
+                _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+
+            _DropState = this.Find<TextBlock>("DropState");
+            _DragState = this.Find<TextBlock>("DragState");
+            _DragMe = this.Find<Border>("DragMe");
+        }
+    }
+}

+ 80 - 0
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -0,0 +1,80 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
+    <TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
+
+    <TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
+    <Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
+      <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
+        <CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock>
+        <CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock>
+        <CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock>
+        <CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
+
+      </Grid>
+      <Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
+        <DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
+                  VerticalAlignment="Center" Margin="2">
+          <DropDown.ItemTemplate>
+            <DataTemplate>
+              <StackPanel Orientation="Horizontal" Gap="2">
+                <TextBlock Text="{Binding Name}"/>
+                <TextBlock Text="-"/>
+                <TextBlock Text="{Binding Value}"/>
+              </StackPanel>
+            </DataTemplate>
+          </DropDown.ItemTemplate>
+        </DropDown>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
+        <DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
+                  VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
+        <DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
+                  VerticalAlignment="Center" Margin="2"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
+        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" />
+
+        <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
+        <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
+      </Grid>
+      <Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120">
+        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
+        <NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
+                       CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
+        <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
+                       CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
+        <NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
+                       Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+        <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
+        <NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
+                       Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
+
+      </Grid>
+    </Grid>
+
+    <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
+      <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
+      <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
+                     CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"
+                     Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
+    </StackPanel>
+
+  </StackPanel>
+</UserControl>

+ 94 - 0
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+using ReactiveUI;
+
+namespace ControlCatalog.Pages
+{
+    public class NumericUpDownPage : UserControl
+    {
+        public NumericUpDownPage()
+        {
+            this.InitializeComponent();
+            var viewModel = new NumbersPageViewModel();
+            DataContext = viewModel;
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+    }
+
+    public class NumbersPageViewModel : ReactiveObject
+    {
+        private IList<FormatObject> _formats;
+        private FormatObject _selectedFormat;
+        private IList<Location> _spinnerLocations;
+
+        public NumbersPageViewModel()
+        {
+            SelectedFormat = Formats.FirstOrDefault();
+        }
+
+        public IList<FormatObject> Formats
+        {
+            get
+            {
+                return _formats ?? (_formats = new List<FormatObject>()
+                {
+                    new FormatObject() {Name = "Currency", Value = "C2"},
+                    new FormatObject() {Name = "Fixed point", Value = "F2"},
+                    new FormatObject() {Name = "General", Value = "G"},
+                    new FormatObject() {Name = "Number", Value = "N"},
+                    new FormatObject() {Name = "Percent", Value = "P"},
+                    new FormatObject() {Name = "Degrees", Value = "{0:N2} °"},
+                });
+            }
+        }
+
+        public IList<Location> SpinnerLocations
+        {
+            get
+            {
+                if (_spinnerLocations == null)
+                {
+                    _spinnerLocations = new List<Location>();
+                    foreach (Location value in Enum.GetValues(typeof(Location)))
+                    {
+                        _spinnerLocations.Add(value);
+                    }
+                }
+                return _spinnerLocations ;
+            }
+        }
+
+        public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
+        {
+            new CultureInfo("en-US"),
+            new CultureInfo("en-GB"),
+            new CultureInfo("fr-FR"),
+            new CultureInfo("ar-DZ"),
+            new CultureInfo("zh-CN"),
+            new CultureInfo("cs-CZ")
+        };
+
+        public FormatObject SelectedFormat
+        {
+            get { return _selectedFormat; }
+            set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); }
+        }
+    }
+
+    public class FormatObject
+    {
+        public string Value { get; set; }
+        public string Name { get; set; }
+    }
+}

+ 0 - 36
samples/ControlCatalog/Properties/AssemblyInfo.cs

@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following 
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("ControlCatalog")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("ControlCatalog")]
-[assembly: AssemblyCopyright("Copyright ©  2015")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible 
-// to COM components.  If you need to access a type in this assembly from 
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("61bec86c-f307-4295-b5b8-9428610d7d55")]
-
-// Version information for an assembly consists of the following four values:
-//
-//      Major Version
-//      Minor Version 
-//      Build Number
-//      Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers 
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]

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

@@ -17,7 +17,9 @@
       <None Remove="MiniCube.fx" />
     </ItemGroup>
     <ItemGroup>
-      <EmbeddedResource Include="MiniCube.fx" />
+      <EmbeddedResource Include="MiniCube.fx">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </EmbeddedResource>
     </ItemGroup>
     <ItemGroup>
         <ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />

+ 5 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs

@@ -36,7 +36,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _clientSize = value;
             UpdateParams();
         }
-        
+
+        public void SetMinMaxSize(Size minSize, Size maxSize)
+        {
+        }
+
         public IScreenImpl Screen { get; }
 
         public Point Position

+ 4 - 5
src/Avalonia.Base/AttachedProperty.cs

@@ -9,7 +9,7 @@ namespace Avalonia
     /// An attached avalonia property.
     /// </summary>
     /// <typeparam name="TValue">The type of the property's value.</typeparam>
-    public class AttachedProperty<TValue> : StyledPropertyBase<TValue>
+    public class AttachedProperty<TValue> : StyledProperty<TValue>
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="AttachedProperty{TValue}"/> class.
@@ -35,11 +35,10 @@ namespace Avalonia
         /// </summary>
         /// <typeparam name="TOwner">The owner type.</typeparam>
         /// <returns>The property.</returns>
-        public StyledProperty<TValue> AddOwner<TOwner>() where TOwner : IAvaloniaObject
+        public new AttachedProperty<TValue> AddOwner<TOwner>() where TOwner : IAvaloniaObject
         {
-            var result = new StyledProperty<TValue>(this, typeof(TOwner));
-            AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
-            return result;
+            AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
+            return this;
         }
     }
 }

+ 19 - 31
src/Avalonia.Base/AvaloniaObject.cs

@@ -12,7 +12,6 @@ using Avalonia.Diagnostics;
 using Avalonia.Logging;
 using Avalonia.Threading;
 using Avalonia.Utilities;
-using System.Reactive.Concurrency;
 
 namespace Avalonia
 {
@@ -218,11 +217,6 @@ namespace Avalonia
             }
             else
             {
-                if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
-                {
-                    ThrowNotRegistered(property);
-                }
-
                 return GetValueInternal(property);
             }
         }
@@ -377,11 +371,6 @@ namespace Avalonia
             {
                 PriorityValue v;
 
-                if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
-                {
-                    ThrowNotRegistered(property);
-                }
-
                 if (!_values.TryGetValue(property, out v))
                 {
                     v = CreatePriorityValue(property);
@@ -804,11 +793,6 @@ namespace Avalonia
 
             var originalValue = value;
 
-            if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
-            {
-                ThrowNotRegistered(property);
-            }
-
             if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
             {
                 throw new ArgumentException(string.Format(
@@ -836,18 +820,32 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Given a <see cref="AvaloniaProperty"/> returns a registered avalonia property that is
-        /// equal or throws if not found.
+        /// Given a direct property, returns a registered avalonia property that is equivalent or
+        /// throws if not found.
         /// </summary>
         /// <param name="property">The property.</param>
         /// <returns>The registered property.</returns>
-        public AvaloniaProperty GetRegistered(AvaloniaProperty property)
+        private AvaloniaProperty GetRegistered(AvaloniaProperty property)
         {
-            var result = AvaloniaPropertyRegistry.Instance.FindRegistered(this, property);
+            var direct = property as IDirectPropertyAccessor;
+
+            if (direct == null)
+            {
+                throw new AvaloniaInternalException(
+                    "AvaloniaObject.GetRegistered should only be called for direct properties");
+            }
+
+            if (property.OwnerType.IsAssignableFrom(GetType()))
+            {
+                return property;
+            }
+
+            var result =  AvaloniaPropertyRegistry.Instance.GetRegistered(this)
+                .FirstOrDefault(x => x == property);
 
             if (result == null)
             {
-                ThrowNotRegistered(property);
+                throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
             }
 
             return result;
@@ -898,15 +896,5 @@ namespace Avalonia
                 value,
                 priority);
         }
-
-        /// <summary>
-        /// Throws an exception indicating that the specified property is not registered on this
-        /// object.
-        /// </summary>
-        /// <param name="p">The property</param>
-        private void ThrowNotRegistered(AvaloniaProperty p)
-        {
-            throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}");
-        }
     }
 }

+ 6 - 2
src/Avalonia.Base/AvaloniaProperty.cs

@@ -311,7 +311,9 @@ namespace Avalonia
                 defaultBindingMode: defaultBindingMode);
 
             var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
-            AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result);
+            var registry = AvaloniaPropertyRegistry.Instance;
+            registry.Register(typeof(TOwner), result);
+            registry.RegisterAttached(typeof(THost), result);
             return result;
         }
 
@@ -344,7 +346,9 @@ namespace Avalonia
                 defaultBindingMode: defaultBindingMode);
 
             var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
-            AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result);
+            var registry = AvaloniaPropertyRegistry.Instance;
+            registry.Register(ownerType, result);
+            registry.RegisterAttached(typeof(THost), result);
             return result;
         }
 

+ 133 - 150
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Reflection;
 using System.Runtime.CompilerServices;
 
 namespace Avalonia
@@ -14,23 +13,14 @@ namespace Avalonia
     /// </summary>
     public class AvaloniaPropertyRegistry
     {
-        /// <summary>
-        /// The registered properties by type.
-        /// </summary>
         private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered =
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
-
-        /// <summary>
-        /// The registered properties by type cached values to increase performance.
-        /// </summary>
-        private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registeredCache =
-            new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
-
-        /// <summary>
-        /// The registered attached properties by owner type.
-        /// </summary>
         private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
             new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
+        private readonly Dictionary<Type, List<AvaloniaProperty>> _registeredCache =
+            new Dictionary<Type, List<AvaloniaProperty>>();
+        private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
+            new Dictionary<Type, List<AvaloniaProperty>>();
 
         /// <summary>
         /// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@@ -39,51 +29,68 @@ namespace Avalonia
             = new AvaloniaPropertyRegistry();
 
         /// <summary>
-        /// Gets all attached <see cref="AvaloniaProperty"/>s registered by an owner.
+        /// Gets all non-attached <see cref="AvaloniaProperty"/>s registered on a type.
         /// </summary>
-        /// <param name="ownerType">The owner type.</param>
+        /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetAttached(Type ownerType)
+        public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
         {
-            Dictionary<int, AvaloniaProperty> inner;
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            if (_registeredCache.TryGetValue(type, out var result))
+            {
+                return result;
+            }
 
-            // Ensure the type's static ctor has been run.
-            RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle);
+            var t = type;
+            result = new List<AvaloniaProperty>();
 
-            if (_attached.TryGetValue(ownerType, out inner))
+            while (t != null)
             {
-                return inner.Values;
+                // Ensure the type's static ctor has been run.
+                RuntimeHelpers.RunClassConstructor(t.TypeHandle);
+
+                if (_registered.TryGetValue(t, out var registered))
+                {
+                    result.AddRange(registered.Values);
+                }
+
+                t = t.BaseType;
             }
 
-            return Enumerable.Empty<AvaloniaProperty>();
+            _registeredCache.Add(type, result);
+            return result;
         }
 
         /// <summary>
-        /// Gets all <see cref="AvaloniaProperty"/>s registered on a type.
+        /// Gets all attached <see cref="AvaloniaProperty"/>s registered on a type.
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
+        public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            while (type != null)
+            if (_attachedCache.TryGetValue(type, out var result))
             {
-                // Ensure the type's static ctor has been run.
-                RuntimeHelpers.RunClassConstructor(type.TypeHandle);
+                return result;
+            }
 
-                Dictionary<int, AvaloniaProperty> inner;
+            var t = type;
+            result = new List<AvaloniaProperty>();
 
-                if (_registered.TryGetValue(type, out inner))
+            while (t != null)
+            {
+                if (_attached.TryGetValue(t, out var attached))
                 {
-                    foreach (var p in inner)
-                    {
-                        yield return p.Value;
-                    }
+                    result.AddRange(attached.Values);
                 }
 
-                type = type.GetTypeInfo().BaseType;
+                t = t.BaseType;
             }
+
+            _attachedCache.Add(type, result);
+            return result;
         }
 
         /// <summary>
@@ -99,142 +106,92 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Finds a <see cref="AvaloniaProperty"/> registered on a type.
+        /// Finds a registered non-attached property on a type by name.
         /// </summary>
         /// <param name="type">The type.</param>
-        /// <param name="property">The property.</param>
-        /// <returns>The registered property or null if not found.</returns>
-        /// <remarks>
-        /// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a 
-        /// different object but is equal according to <see cref="object.Equals(object)"/>.
-        /// </remarks>
-        public AvaloniaProperty FindRegistered(Type type, AvaloniaProperty property)
+        /// <param name="name">The property name.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        /// The property name contains a '.'.
+        /// </exception>
+        public AvaloniaProperty FindRegistered(Type type, string name)
         {
-            Type currentType = type;
-            Dictionary<int, AvaloniaProperty> cache;
-            AvaloniaProperty result;
+            Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(name != null);
 
-            if (_registeredCache.TryGetValue(type, out cache))
+            if (name.Contains('.'))
             {
-                if (cache.TryGetValue(property.Id, out result))
-                {
-                    return result;
-                }
+                throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            while (currentType != null)
-            {
-                Dictionary<int, AvaloniaProperty> inner;
-
-                if (_registered.TryGetValue(currentType, out inner))
-                {
-                    if (inner.TryGetValue(property.Id, out result))
-                    {
-                        if (cache == null)
-                        {
-                            _registeredCache[type] = cache = new Dictionary<int, AvaloniaProperty>();
-                        }
-
-                        cache[property.Id] = result;
-
-                        return result;
-                    }
-                }
-
-                currentType = currentType.GetTypeInfo().BaseType;
-            }
-
-            return null;
+            return GetRegistered(type).FirstOrDefault(x => x.Name == name);
         }
 
         /// <summary>
-        /// Finds <see cref="AvaloniaProperty"/> registered on an object.
+        /// Finds a registered non-attached property on a type by name.
         /// </summary>
         /// <param name="o">The object.</param>
-        /// <param name="property">The property.</param>
-        /// <returns>The registered property or null if not found.</returns>
-        /// <remarks>
-        /// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a
-        /// different object but is equal according to <see cref="object.Equals(object)"/>.
-        /// </remarks>
-        public AvaloniaProperty FindRegistered(object o, AvaloniaProperty property)
+        /// <param name="name">The property name.</param>
+        /// <returns>
+        /// The registered property or null if no matching property found.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        /// The property name contains a '.'.
+        /// </exception>
+        public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
         {
-            return FindRegistered(o.GetType(), property);
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(name != null);
+
+            return FindRegistered(o.GetType(), name);
         }
 
         /// <summary>
-        /// Finds a registered property on a type by name.
+        /// Finds a registered attached property on a type by name.
         /// </summary>
         /// <param name="type">The type.</param>
-        /// <param name="name">
-        /// The property name. If an attached property it should be in the form
-        /// "OwnerType.PropertyName".
-        /// </param>
+        /// <param name="ownerType">The owner type.</param>
+        /// <param name="name">The property name.</param>
         /// <returns>
         /// The registered property or null if no matching property found.
         /// </returns>
-        public AvaloniaProperty FindRegistered(Type type, string name)
+        /// <exception cref="InvalidOperationException">
+        /// The property name contains a '.'.
+        /// </exception>
+        public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
         {
             Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(ownerType != null);
             Contract.Requires<ArgumentNullException>(name != null);
 
-            var parts = name.Split('.');
-            var types = GetImplementedTypes(type).ToList();
-
-            if (parts.Length < 1 || parts.Length > 2)
+            if (name.Contains('.'))
             {
-                throw new ArgumentException("Invalid property name.");
+                throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            string propertyName;
-            var results = GetRegistered(type);
-
-            if (parts.Length == 1)
-            {
-                propertyName = parts[0];
-                results = results.Where(x => !x.IsAttached || types.Contains(x.OwnerType.Name));
-            }
-            else
-            {
-                if (!types.Contains(parts[0]))
-                {
-                    results = results.Where(x => x.OwnerType.Name == parts[0]);
-                }
-
-                propertyName = parts[1];
-            }
-
-            return results.FirstOrDefault(x => x.Name == propertyName);
+            return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
         }
 
         /// <summary>
-        /// Finds a registered property on an object by name.
+        /// Finds a registered non-attached property on a type by name.
         /// </summary>
         /// <param name="o">The object.</param>
-        /// <param name="name">
-        /// The property name. If an attached property it should be in the form
-        /// "OwnerType.PropertyName".
-        /// </param>
+        /// <param name="ownerType">The owner type.</param>
+        /// <param name="name">The property name.</param>
         /// <returns>
         /// The registered property or null if no matching property found.
         /// </returns>
-        public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
+        /// <exception cref="InvalidOperationException">
+        /// The property name contains a '.'.
+        /// </exception>
+        public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
         {
-            return FindRegistered(o.GetType(), name);
-        }
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(name != null);
 
-        /// <summary>
-        /// Returns a type and all its base types.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>The type and all its base types.</returns>
-        private IEnumerable<string> GetImplementedTypes(Type type)
-        {
-            while (type != null)
-            {
-                yield return type.Name;
-                type = type.GetTypeInfo().BaseType;
-            }
+            return FindRegisteredAttached(o.GetType(), ownerType, name);
         }
 
         /// <summary>
@@ -245,7 +202,11 @@ namespace Avalonia
         /// <returns>True if the property is registered, otherwise false.</returns>
         public bool IsRegistered(Type type, AvaloniaProperty property)
         {
-            return FindRegistered(type, property) != null;
+            Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            return Instance.GetRegistered(type).Any(x => x == property) ||
+                Instance.GetRegisteredAttached(type).Any(x => x == property);
         }
 
         /// <summary>
@@ -256,6 +217,9 @@ namespace Avalonia
         /// <returns>True if the property is registered, otherwise false.</returns>
         public bool IsRegistered(object o, AvaloniaProperty property)
         {
+            Contract.Requires<ArgumentNullException>(o != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
             return IsRegistered(o.GetType(), property);
         }
 
@@ -274,34 +238,53 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            Dictionary<int, AvaloniaProperty> inner;
-
-            if (!_registered.TryGetValue(type, out inner))
+            if (!_registered.TryGetValue(type, out var inner))
             {
                 inner = new Dictionary<int, AvaloniaProperty>();
+                inner.Add(property.Id, property);
                 _registered.Add(type, inner);
             }
-
-            if (!inner.ContainsKey(property.Id))
+            else if (!inner.ContainsKey(property.Id))
             {
                 inner.Add(property.Id, property);
             }
+ 
+            _registeredCache.Clear();
+        }
 
-            if (property.IsAttached)
+        /// <summary>
+        /// Registers an attached <see cref="AvaloniaProperty"/> on a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="property">The property.</param>
+        /// <remarks>
+        /// You won't usually want to call this method directly, instead use the
+        /// <see cref="AvaloniaProperty.RegisterAttached{THost, TValue}(string, Type, TValue, bool, Data.BindingMode, Func{THost, TValue, TValue})"/>
+        /// method.
+        /// </remarks>
+        public void RegisterAttached(Type type, AvaloniaProperty property)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+
+            if (!property.IsAttached)
             {
-                if (!_attached.TryGetValue(property.OwnerType, out inner))
-                {
-                    inner = new Dictionary<int, AvaloniaProperty>();
-                    _attached.Add(property.OwnerType, inner);
-                }
+                throw new InvalidOperationException(
+                    "Cannot register a non-attached property as attached.");
+            }
 
-                if (!inner.ContainsKey(property.Id))
-                {
-                    inner.Add(property.Id, property);
-                }
+            if (!_attached.TryGetValue(type, out var inner))
+            {
+                inner = new Dictionary<int, AvaloniaProperty>();
+                inner.Add(property.Id, property);
+                _attached.Add(type, inner);
+            }
+            else
+            {
+                inner.Add(property.Id, property);
             }
 
-            _registeredCache.Clear();
+            _attachedCache.Clear();
         }
     }
 }

+ 2 - 2
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@@ -117,7 +117,7 @@ namespace Avalonia.Collections
             _inner = new Dictionary<TKey, TValue>();
 
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]"));
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
             
 
             if (CollectionChanged != null)
@@ -222,4 +222,4 @@ namespace Avalonia.Collections
             }
         }
     }
-}
+}

+ 21 - 5
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@@ -34,14 +34,18 @@ namespace Avalonia.Collections
         /// <param name="reset">
         /// An action called when the collection is reset.
         /// </param>
+        /// <param name="weakSubscription">
+        /// Indicates if a weak subscription should be used to track changes to the collection.
+        /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
             this IAvaloniaReadOnlyList<T> collection,
             Action<T> added,
             Action<T> removed,
-            Action reset)
+            Action reset,
+            bool weakSubscription = false)
         {
-            return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset);
+            return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset, weakSubscription);
         }
 
         /// <summary>
@@ -63,12 +67,16 @@ namespace Avalonia.Collections
         /// An action called when the collection is reset. This will be followed by calls to 
         /// <paramref name="added"/> for each item present in the collection after the reset.
         /// </param>
+        /// <param name="weakSubscription">
+        /// Indicates if a weak subscription should be used to track changes to the collection.
+        /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
             this IAvaloniaReadOnlyList<T> collection,
             Action<int, T> added,
             Action<int, T> removed,
-            Action reset)
+            Action reset,
+            bool weakSubscription = false)
         {
             void Add(int index, IList items)
             {
@@ -118,9 +126,17 @@ namespace Avalonia.Collections
             };
 
             Add(0, (IList)collection);
-            collection.CollectionChanged += handler;
 
-            return Disposable.Create(() => collection.CollectionChanged -= handler);
+            if (weakSubscription)
+            {
+                return collection.WeakSubscribe(handler);
+            }
+            else
+            {
+                collection.CollectionChanged += handler;
+
+                return Disposable.Create(() => collection.CollectionChanged -= handler);
+            }
         }
 
         public static IAvaloniaReadOnlyList<TDerived> CreateDerivedList<TSource, TDerived>(

+ 127 - 0
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@@ -0,0 +1,127 @@
+// 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;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Utilities;
+
+namespace Avalonia.Collections
+{
+    public static class NotifyCollectionChangedExtensions
+    {
+        /// <summary>
+        /// Gets a weak observable for the CollectionChanged event.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <returns>An observable.</returns>
+        public static IObservable<NotifyCollectionChangedEventArgs> GetWeakCollectionChangedObservable(
+            this INotifyCollectionChanged collection)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+
+            return new WeakCollectionChangedObservable(new WeakReference<INotifyCollectionChanged>(collection));
+        }
+
+        /// <summary>
+        /// Subcribes to the CollectionChanged event using a weak subscription.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <param name="handler">
+        /// An action called when the collection event is raised.
+        /// </param>
+        /// <returns>A disposable used to terminate the subscription.</returns>
+        public static IDisposable WeakSubscribe(
+            this INotifyCollectionChanged collection, 
+            NotifyCollectionChangedEventHandler handler)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+            Contract.Requires<ArgumentNullException>(handler != null);
+
+            return
+                collection.GetWeakCollectionChangedObservable()
+                          .Subscribe(e => handler.Invoke(collection, e));
+        }
+
+        /// <summary>
+        /// Subcribes to the CollectionChanged event using a weak subscription.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <param name="handler">
+        /// An action called when the collection event is raised.
+        /// </param>
+        /// <returns>A disposable used to terminate the subscription.</returns>
+        public static IDisposable WeakSubscribe(
+            this INotifyCollectionChanged collection,
+            Action<NotifyCollectionChangedEventArgs> handler)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+            Contract.Requires<ArgumentNullException>(handler != null);
+
+            return
+                collection.GetWeakCollectionChangedObservable()
+                          .Subscribe(handler);
+        }
+
+        private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>,
+            IWeakSubscriber<NotifyCollectionChangedEventArgs>
+        {
+            private WeakReference<INotifyCollectionChanged> _sourceReference;
+            private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
+
+            private int _count;
+
+            public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
+            {
+                _sourceReference = source;
+            }
+
+            public void OnEvent(object sender, NotifyCollectionChangedEventArgs e)
+            {
+                _changed.OnNext(e);
+            }
+
+            protected override IDisposable SubscribeCore(IObserver<NotifyCollectionChangedEventArgs> observer)
+            {
+                if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
+                {
+                    if (_count++ == 0)
+                    {
+                        WeakSubscriptionManager.Subscribe(
+                            instance,
+                            nameof(instance.CollectionChanged),
+                            this);
+                    }
+
+                    return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
+                        .Subscribe(observer);
+                }
+                else
+                {
+                    _changed.OnCompleted();
+                    observer.OnCompleted();
+                    return Disposable.Empty;
+                }
+            }
+
+            private void DecrementCount()
+            {
+                if (--_count == 0)
+                {
+                    if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
+                    {
+                        WeakSubscriptionManager.Unsubscribe(
+                            instance,
+                            nameof(instance.CollectionChanged),
+                            this);
+                    }
+                }
+            }
+        }
+    }
+}

+ 3 - 0
src/Avalonia.Base/DirectProperty.cs

@@ -75,6 +75,9 @@ namespace Avalonia
         /// </summary>
         public Action<TOwner, TValue> Setter { get; }
 
+        /// <inheritdoc/>
+        Type IDirectPropertyAccessor.Owner => typeof(TOwner);
+
         /// <summary>
         /// Registers the direct property on another type.
         /// </summary>

+ 7 - 0
src/Avalonia.Base/IDirectPropertyAccessor.cs

@@ -1,6 +1,8 @@
 // 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
 {
     /// <summary>
@@ -14,6 +16,11 @@ namespace Avalonia
         /// </summary>
         bool IsReadOnly { get; }
 
+        /// <summary>
+        /// Gets the class that registered the property.
+        /// </summary>
+        Type Owner { get; }
+
         /// <summary>
         /// Gets the value of the property on the instance.
         /// </summary>

+ 2 - 0
src/Avalonia.Base/Threading/Dispatcher.cs

@@ -81,12 +81,14 @@ namespace Avalonia.Threading
         /// <inheritdoc/>
         public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
         {
+            Contract.Requires<ArgumentNullException>(action != null);
             return _jobRunner?.InvokeAsync(action, priority);
         }
 
         /// <inheritdoc/>
         public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
         {
+            Contract.Requires<ArgumentNullException>(action != null);
             _jobRunner?.Post(action, priority);
         }
 

+ 205 - 0
src/Avalonia.Base/Utilities/StringTokenizer.cs

@@ -0,0 +1,205 @@
+using System;
+using System.Globalization;
+using static System.Char;
+
+namespace Avalonia.Utilities
+{
+    public struct StringTokenizer : IDisposable
+    {
+        private const char DefaultSeparatorChar = ',';
+
+        private readonly string _s;
+        private readonly int _length;
+        private readonly char _separator;
+        private readonly string _exceptionMessage;
+        private readonly IFormatProvider _formatProvider;
+        private int _index;
+        private int _tokenIndex;
+        private int _tokenLength;
+
+        public StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null)
+            : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
+        {
+            _formatProvider = formatProvider;
+        }
+
+        public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null)
+        {
+            _s = s ?? throw new ArgumentNullException(nameof(s));
+            _length = s?.Length ?? 0;
+            _separator = separator;
+            _exceptionMessage = exceptionMessage;
+            _formatProvider = CultureInfo.InvariantCulture;
+            _index = 0;
+            _tokenIndex = -1;
+            _tokenLength = 0;
+
+            while (_index < _length && IsWhiteSpace(_s, _index))
+            {
+                _index++;
+            }
+        }
+
+        public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength);
+
+        public void Dispose()
+        {
+            if (_index != _length)
+            {
+                throw GetFormatException();
+            }
+        }
+
+        public bool TryReadInt32(out Int32 result, char? separator = null)
+        {
+            var success = TryReadString(out var stringResult, separator);
+            result = success ? int.Parse(stringResult, _formatProvider) : 0;
+            return success;
+        }
+
+        public int ReadInt32(char? separator = null)
+        {
+            if (!TryReadInt32(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        public bool TryReadDouble(out double result, char? separator = null)
+        {
+            var success = TryReadString(out var stringResult, separator);
+            result = success ? double.Parse(stringResult, _formatProvider) : 0;
+            return success;
+        }
+
+        public double ReadDouble(char? separator = null)
+        {
+            if (!TryReadDouble(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        public bool TryReadString(out string result, char? separator = null)
+        {
+            var success = TryReadToken(separator ?? _separator);
+            result = CurrentToken;
+            return success;
+        }
+
+        public string ReadString(char? separator = null)
+        {
+            if (!TryReadString(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        private bool TryReadToken(char separator)
+        {
+            _tokenIndex = -1;
+
+            if (_index >= _length)
+            {
+                return false;
+            }
+
+            var c = _s[_index];
+
+            var index = _index;
+            var length = 0;
+
+            while (_index < _length)
+            {
+                c = _s[_index];
+
+                if (IsWhiteSpace(c) || c == separator)
+                {
+                    break;
+                }
+
+                _index++;
+                length++;
+            }
+
+            SkipToNextToken(separator);
+
+            _tokenIndex = index;
+            _tokenLength = length;
+
+            if (_tokenLength < 1)
+            {
+                throw GetFormatException();
+            }
+
+            return true;
+        }
+
+        private void SkipToNextToken(char separator)
+        {
+            if (_index < _length)
+            {
+                var c = _s[_index];
+
+                if (c != separator && !IsWhiteSpace(c))
+                {
+                    throw GetFormatException();
+                }
+
+                var length = 0;
+
+                while (_index < _length)
+                {
+                    c = _s[_index];
+
+                    if (c == separator)
+                    {
+                        length++;
+                        _index++;
+
+                        if (length > 1)
+                        {
+                            throw GetFormatException();
+                        }
+                    }
+                    else
+                    {
+                        if (!IsWhiteSpace(c))
+                        {
+                            break;
+                        }
+
+                        _index++;
+                    }
+                }
+
+                if (length > 0 && _index >= _length)
+                {
+                    throw GetFormatException();
+                }
+            }
+        }
+
+        private FormatException GetFormatException() =>
+            _exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException();
+
+        private static char GetSeparatorFromFormatProvider(IFormatProvider provider)
+        {
+            var c = DefaultSeparatorChar;
+
+            var formatInfo = NumberFormatInfo.GetInstance(provider);
+            if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0])
+            {
+                c = ';';
+            }
+
+            return c;
+        }
+    }
+}

+ 6 - 3
src/Avalonia.Controls/Application.cs

@@ -2,16 +2,17 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Concurrency;
 using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
 using Avalonia.Layout;
-using Avalonia.Rendering;
+using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.Threading;
-using System.Reactive.Concurrency;
 
 namespace Avalonia
 {
@@ -234,7 +235,9 @@ namespace Avalonia
                 .Bind<IStyler>().ToConstant(_styler)
                 .Bind<ILayoutManager>().ToSingleton<LayoutManager>()
                 .Bind<IApplicationLifecycle>().ToConstant(this)
-                .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
+                .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
+                .Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
+                .Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
         }
     }
 }

+ 2726 - 0
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -0,0 +1,2726 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
+    /// event.
+    /// </summary>
+    public class PopulatedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets the list of possible matches added to the drop-down portion of
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The list of possible matches added to the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />.</value>
+        public IEnumerable Data { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.PopulatedEventArgs" />.
+        /// </summary>
+        /// <param name="data">The list of possible matches added to the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</param>
+        public PopulatedEventArgs(IEnumerable data)
+        {
+            Data = data;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populating" />
+    /// event.
+    /// </summary>
+    /// <QualityBand>Stable</QualityBand>
+    public class PopulatingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Gets the text that is used to determine which items to display in
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The text that is used to determine which items to display in
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />.</value>
+        public string Parameter { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.PopulatingEventArgs" />.
+        /// </summary>
+        /// <param name="parameter">The value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// property, which is used to filter items for the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</param>
+        public PopulatingEventArgs(string parameter)
+        {
+            Parameter = parameter;
+        }
+    }
+
+    /// <summary>
+    /// Represents the filter used by the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control to
+    /// determine whether an item is a possible match for the specified text.
+    /// </summary>
+    /// <returns>true to indicate <paramref name="item" /> is a possible match
+    /// for <paramref name="search" />; otherwise false.</returns>
+    /// <param name="search">The string used as the basis for filtering.</param>
+    /// <param name="item">The item that is compared with the
+    /// <paramref name="search" /> parameter.</param>
+    /// <typeparam name="T">The type used for filtering the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />. This type can
+    /// be either a string or an object.</typeparam>
+    /// <QualityBand>Stable</QualityBand>
+    public delegate bool AutoCompleteFilterPredicate<T>(string search, T item);
+
+    /// <summary>
+    /// Specifies how text in the text box portion of the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control is used
+    /// to filter items specified by the
+    /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+    /// property for display in the drop-down.
+    /// </summary>
+    /// <QualityBand>Stable</QualityBand>
+    public enum AutoCompleteFilterMode
+    {
+        /// <summary>
+        /// Specifies that no filter is used. All items are returned.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCultureIgnoreCase" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        StartsWith = 1,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCulture" /> as the string
+        /// comparison criteria.
+        /// </summary>
+        StartsWithCaseSensitive = 2,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.OrdinalIgnoreCase" /> as the
+        /// string comparison criteria.
+        /// </summary>
+        StartsWithOrdinal = 3,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// start with the specified text. The filter uses the
+        /// <see cref="M:System.String.StartsWith(System.String,System.StringComparison)" />
+        /// method, specifying <see cref="P:System.StringComparer.Ordinal" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        StartsWithOrdinalCaseSensitive = 4,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items contain the specified text.
+        /// </summary>
+        Contains = 5,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items contain the specified text.
+        /// </summary>
+        ContainsCaseSensitive = 6,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items contain the specified text.
+        /// </summary>
+        ContainsOrdinal = 7,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// contain the specified text.
+        /// </summary>
+        ContainsOrdinalCaseSensitive = 8,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-insensitive filter where the
+        /// returned items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCultureIgnoreCase" /> as
+        /// the search comparison criteria.
+        /// </summary>
+        Equals = 9,
+
+        /// <summary>
+        /// Specifies a culture-sensitive, case-sensitive filter where the
+        /// returned items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.CurrentCulture" /> as the string
+        /// comparison criteria.
+        /// </summary>
+        EqualsCaseSensitive = 10,
+
+        /// <summary>
+        /// Specifies an ordinal, case-insensitive filter where the returned
+        /// items equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying
+        /// <see cref="P:System.StringComparer.OrdinalIgnoreCase" /> as the
+        /// string comparison criteria.
+        /// </summary>
+        EqualsOrdinal = 11,
+
+        /// <summary>
+        /// Specifies an ordinal, case-sensitive filter where the returned items
+        /// equal the specified text. The filter uses the
+        /// <see cref="M:System.String.Equals(System.String,System.StringComparison)" />
+        /// method, specifying <see cref="P:System.StringComparer.Ordinal" /> as
+        /// the string comparison criteria.
+        /// </summary>
+        EqualsOrdinalCaseSensitive = 12,
+
+        /// <summary>
+        /// Specifies that a custom filter is used. This mode is used when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// or
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// properties are set.
+        /// </summary>
+        Custom = 13,
+    }
+
+    /// <summary>
+    /// Represents a control that provides a text box for user input and a
+    /// drop-down that contains possible matches based on the input in the text
+    /// box.
+    /// </summary>
+    public class AutoCompleteBox : TemplatedControl
+    {
+        /// <summary>
+        /// Specifies the name of the selection adapter TemplatePart.
+        /// </summary>
+        private const string ElementSelectionAdapter = "PART_SelectionAdapter";
+
+        /// <summary>
+        /// Specifies the name of the Selector TemplatePart.
+        /// </summary>
+        private const string ElementSelector = "PART_SelectingItemsControl";
+
+        /// <summary>
+        /// Specifies the name of the Popup TemplatePart.
+        /// </summary>
+        private const string ElementPopup = "PART_Popup";
+
+        /// <summary>
+        /// The name for the text box part.
+        /// </summary>
+        private const string ElementTextBox = "PART_TextBox";
+
+        private IEnumerable _itemsEnumerable;
+
+        /// <summary>
+        /// Gets or sets a local cached copy of the items data.
+        /// </summary>
+        private List<object> _items;
+
+        /// <summary>
+        /// Gets or sets the observable collection that contains references to
+        /// all of the items in the generated view of data that is provided to
+        /// the selection-style control adapter.
+        /// </summary>
+        private AvaloniaList<object> _view;
+
+        /// <summary>
+        /// Gets or sets a value to ignore a number of pending change handlers.
+        /// The value is decremented after each use. This is used to reset the
+        /// value of properties without performing any of the actions in their
+        /// change handlers.
+        /// </summary>
+        /// <remarks>The int is important as a value because the TextBox
+        /// TextChanged event does not immediately fire, and this will allow for
+        /// nested property changes to be ignored.</remarks>
+        private int _ignoreTextPropertyChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to ignore calling a pending
+        /// change handlers.
+        /// </summary>
+        private bool _ignorePropertyChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to ignore the selection
+        /// changed event.
+        /// </summary>
+        private bool _ignoreTextSelectionChange;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to skip the text update
+        /// processing when the selected item is updated.
+        /// </summary>
+        private bool _skipSelectedItemTextUpdate;
+
+        /// <summary>
+        /// Gets or sets the last observed text box selection start location.
+        /// </summary>
+        private int _textSelectionStart;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the user initiated the
+        /// current populate call.
+        /// </summary>
+        private bool _userCalledPopulate;
+
+        /// <summary>
+        /// A value indicating whether the popup has been opened at least once.
+        /// </summary>
+        private bool _popupHasOpened;
+
+        /// <summary>
+        /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay
+        /// condition for auto completion.
+        /// </summary>
+        private DispatcherTimer _delayTimer;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether a read-only dependency
+        /// property change handler should allow the value to be set.  This is
+        /// used to ensure that read-only properties cannot be changed via
+        /// SetValue, etc.
+        /// </summary>
+        private bool _allowWrite;
+
+        /// <summary>
+        /// The TextBox template part.
+        /// </summary>
+        private TextBox _textBox;
+        private IDisposable _textBoxSubscriptions;
+
+        /// <summary>
+        /// The SelectionAdapter.
+        /// </summary>
+        private ISelectionAdapter _adapter;
+
+        /// <summary>
+        /// A control that can provide updated string values from a binding.
+        /// </summary>
+        private BindingEvaluator<string> _valueBindingEvaluator;
+
+        /// <summary>
+        /// A weak subscription for the collection changed event.
+        /// </summary>
+        private IDisposable _collectionChangeSubscription;
+
+        private IMemberSelector _valueMemberSelector;
+        private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
+        private CancellationTokenSource _populationCancellationTokenSource;
+
+        private bool _itemTemplateIsFromValueMemeberBinding = true;
+        private bool _settingItemTemplateFromValueMemeberBinding;
+
+        private object _selectedItem;
+        private bool _isDropDownOpen;
+        private bool _isFocused = false;
+
+        private string _text = string.Empty;
+        private string _searchText = string.Empty;
+
+        private AutoCompleteFilterPredicate<object> _itemFilter;
+        private AutoCompleteFilterPredicate<string> _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
+
+        public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
+            RoutedEvent.Register<SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
+
+        public static readonly StyledProperty<string> WatermarkProperty =
+            TextBox.WatermarkProperty.AddOwner<AutoCompleteBox>();
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPrefixLength" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPrefixLength" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, int>(
+                nameof(MinimumPrefixLength), 1,
+                validate: ValidateMinimumPrefixLength);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPopulateDelay" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MinimumPopulateDelay" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
+                nameof(MinimumPopulateDelay),
+                TimeSpan.Zero,
+                validate: ValidateMinimumPopulateDelay);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MaxDropDownHeight" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.MaxDropDownHeight" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<double> MaxDropDownHeightProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, double>(
+                nameof(MaxDropDownHeight),
+                double.PositiveInfinity,
+                validate: ValidateMaxDropDownHeight);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsTextCompletionEnabled" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsTextCompletionEnabled" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<bool> IsTextCompletionEnabledProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, bool>(nameof(IsTextCompletionEnabled));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemTemplate" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemTemplate" />
+        /// dependency property.</value>
+        public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, IDataTemplate>(nameof(ItemTemplate));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, bool> IsDropDownOpenProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, bool>(
+                nameof(IsDropDownOpen),
+                o => o.IsDropDownOpen,
+                (o, v) => o.IsDropDownOpen = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SelectedItem" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SelectedItem" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, object> SelectedItemProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
+                nameof(SelectedItem),
+                o => o.SelectedItem,
+                (o, v) => o.SelectedItem = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, string>(
+                nameof(Text),
+                o => o.Text,
+                (o, v) => o.Text = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.SearchText" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, string> SearchTextProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, string>(
+                nameof(SearchText),
+                o => o.SearchText,
+                unsetValue: string.Empty);
+
+        /// <summary>
+        /// Gets the identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.FilterMode" />
+        /// dependency property.
+        /// </summary>
+        public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
+            AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
+                nameof(FilterMode),
+                defaultValue: AutoCompleteFilterMode.StartsWith,
+                validate: ValidateFilterMode);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemFilter" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<object>> ItemFilterProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<object>>(
+                nameof(ItemFilter),
+                o => o.ItemFilter,
+                (o, v) => o.ItemFilter = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextFilter" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteFilterPredicate<string>> TextFilterProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteFilterPredicate<string>>(
+                nameof(TextFilter),
+                o => o.TextFilter,
+                (o, v) => o.TextFilter = v,
+                unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, IEnumerable> ItemsProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, IEnumerable>(
+                nameof(Items),
+                o => o.Items,
+                (o, v) => o.Items = v);
+
+        public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
+                nameof(ValueMemberSelector),
+                o => o.ValueMemberSelector,
+                (o, v) => o.ValueMemberSelector = v);
+
+        public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
+                nameof(AsyncPopulator),
+                o => o.AsyncPopulator,
+                (o, v) => o.AsyncPopulator = v);
+
+        private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value >= -1);
+
+            return value;
+        }
+
+        private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value.TotalMilliseconds >= 0.0);
+
+            return value;
+        }
+
+        private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
+        {
+            Contract.Requires<ArgumentOutOfRangeException>(value >= 0.0);
+
+            return value;
+        }
+
+        private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
+        {
+            switch (mode)
+            {
+                case AutoCompleteFilterMode.None:
+                case AutoCompleteFilterMode.StartsWith:
+                case AutoCompleteFilterMode.StartsWithCaseSensitive:
+                case AutoCompleteFilterMode.StartsWithOrdinal:
+                case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Contains:
+                case AutoCompleteFilterMode.ContainsCaseSensitive:
+                case AutoCompleteFilterMode.ContainsOrdinal:
+                case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Equals:
+                case AutoCompleteFilterMode.EqualsCaseSensitive:
+                case AutoCompleteFilterMode.EqualsOrdinal:
+                case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+                case AutoCompleteFilterMode.Custom:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+        private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
+        {
+            Contract.Requires<ArgumentException>(IsValidFilterMode(value));
+
+            return value;
+        }
+
+        /// <summary>
+        /// Handle the change of the IsEnabled property.
+        /// </summary>
+        /// <param name="e">The event data.</param>
+        private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            bool isEnabled = (bool)e.NewValue;
+            if (!isEnabled)
+            {
+                IsDropDownOpen = false;
+            }
+        }
+
+        /// <summary>
+        /// MinimumPopulateDelayProperty property changed handler. Any current
+        /// dispatcher timer will be stopped. The timer will not be restarted
+        /// until the next TextUpdate call by the user.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var newValue = (TimeSpan)e.NewValue;
+
+            // Stop any existing timer
+            if (_delayTimer != null)
+            {
+                _delayTimer.Stop();
+
+                if (newValue == TimeSpan.Zero)
+                {
+                    _delayTimer = null;
+                }
+            }
+
+            if (newValue > TimeSpan.Zero)
+            {
+                // Create or clear a dispatcher timer instance
+                if (_delayTimer == null)
+                {
+                    _delayTimer = new DispatcherTimer();
+                    _delayTimer.Tick += PopulateDropDown;
+                }
+
+                // Set the new tick interval
+                _delayTimer.Interval = newValue;
+            }
+        }
+
+        /// <summary>
+        /// IsDropDownOpenProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            // Ignore the change if requested
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            bool oldValue = (bool)e.OldValue;
+            bool newValue = (bool)e.NewValue;
+
+            if (newValue)
+            {
+                TextUpdated(Text, true);
+            }
+            else
+            {
+                ClosingDropDown(oldValue);
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            // Update the text display
+            if (_skipSelectedItemTextUpdate)
+            {
+                _skipSelectedItemTextUpdate = false;
+            }
+            else
+            {
+                OnSelectedItemChanged(e.NewValue);
+            }
+
+            // Fire the SelectionChanged event
+            List<object> removed = new List<object>();
+            if (e.OldValue != null)
+            {
+                removed.Add(e.OldValue);
+            }
+
+            List<object> added = new List<object>();
+            if (e.NewValue != null)
+            {
+                added.Add(e.NewValue);
+            }
+
+            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+        }
+
+        /// <summary>
+        /// TextProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            TextUpdated((string)e.NewValue, false);
+        }
+
+        private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_ignorePropertyChange)
+            {
+                _ignorePropertyChange = false;
+                return;
+            }
+
+            // Ensure the property is only written when expected
+            if (!_allowWrite)
+            {
+                // Reset the old value before it was incorrectly written
+                _ignorePropertyChange = true;
+                SetValue(e.Property, e.OldValue);
+
+                throw new InvalidOperationException("Cannot set read-only property SearchText.");
+            }
+        }
+
+        /// <summary>
+        /// FilterModeProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue;
+
+            // Sets the filter predicate for the new value
+            TextFilter = AutoCompleteSearch.GetFilter(mode);
+        }
+
+        /// <summary>
+        /// ItemFilterProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            AutoCompleteFilterPredicate<object> value = e.NewValue as AutoCompleteFilterPredicate<object>;
+
+            // If null, revert to the "None" predicate
+            if (value == null)
+            {
+                FilterMode = AutoCompleteFilterMode.None;
+            }
+            else
+            {
+                FilterMode = AutoCompleteFilterMode.Custom;
+                TextFilter = null;
+            }
+        }
+
+        /// <summary>
+        /// ItemsSourceProperty property changed handler.
+        /// </summary>
+        /// <param name="e">Event arguments.</param>
+        private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            OnItemsChanged((IEnumerable)e.NewValue);
+        }
+
+        private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_settingItemTemplateFromValueMemeberBinding)
+                _itemTemplateIsFromValueMemeberBinding = false;
+        }
+        private void OnValueMemberBindingChanged(IBinding value)
+        {
+            if(_itemTemplateIsFromValueMemeberBinding)
+            {
+                var template =
+                    new FuncDataTemplate(
+                        typeof(object),
+                        o =>
+                        {
+                            var control = new ContentControl();
+                            control.Bind(ContentControl.ContentProperty, value);
+                            return control;
+                        });
+
+                _settingItemTemplateFromValueMemeberBinding = true;
+                ItemTemplate = template;
+                _settingItemTemplateFromValueMemeberBinding = false;
+            }
+        }
+
+        static AutoCompleteBox()
+        {
+            FocusableProperty.OverrideDefaultValue<AutoCompleteBox>(true);
+
+            MinimumPopulateDelayProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnMinimumPopulateDelayChanged);
+            IsDropDownOpenProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnIsDropDownOpenChanged);
+            SelectedItemProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnSelectedItemPropertyChanged);
+            TextProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnTextPropertyChanged);
+            SearchTextProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnSearchTextPropertyChanged);
+            FilterModeProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnFilterModePropertyChanged);
+            ItemFilterProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnItemFilterPropertyChanged);
+            ItemsProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnItemsPropertyChanged);
+            IsEnabledProperty.Changed.AddClassHandler<AutoCompleteBox>(x => x.OnControlIsEnabledChanged);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> class.
+        /// </summary>
+        public AutoCompleteBox()
+        {
+            ClearView();
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum number of characters required to be entered
+        /// in the text box before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> displays
+        /// possible matches.
+        /// matches.
+        /// </summary>
+        /// <value>
+        /// The minimum number of characters to be entered in the text box
+        /// before the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// displays possible matches. The default is 1.
+        /// </value>
+        /// <remarks>
+        /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
+        /// not provide possible matches. There is no maximum value, but
+        /// setting MinimumPrefixLength to value that is too large will
+        /// prevent the AutoCompleteBox from providing possible matches as well.
+        /// </remarks>
+        public int MinimumPrefixLength
+        {
+            get { return GetValue(MinimumPrefixLengthProperty); }
+            set { SetValue(MinimumPrefixLengthProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the first possible match
+        /// found during the filtering process will be displayed automatically
+        /// in the text box.
+        /// </summary>
+        /// <value>
+        /// True if the first possible match found will be displayed
+        /// automatically in the text box; otherwise, false. The default is
+        /// false.
+        /// </value>
+        public bool IsTextCompletionEnabled
+        {
+            get { return GetValue(IsTextCompletionEnabledProperty); }
+            set { SetValue(IsTextCompletionEnabledProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:Avalonia.DataTemplate" /> used
+        /// to display each item in the drop-down portion of the control.
+        /// </summary>
+        /// <value>The <see cref="T:Avalonia.DataTemplate" /> used to
+        /// display each item in the drop-down. The default is null.</value>
+        /// <remarks>
+        /// You use the ItemTemplate property to specify the visualization
+        /// of the data objects in the drop-down portion of the AutoCompleteBox
+        /// control. If your AutoCompleteBox is bound to a collection and you
+        /// do not provide specific display instructions by using a
+        /// DataTemplate, the resulting UI of each item is a string
+        /// representation of each object in the underlying collection.
+        /// </remarks>
+        public IDataTemplate ItemTemplate
+        {
+            get { return GetValue(ItemTemplateProperty); }
+            set { SetValue(ItemTemplateProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum delay, after text is typed
+        /// in the text box before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
+        /// populates the list of possible matches in the drop-down.
+        /// </summary>
+        /// <value>The minimum delay, after text is typed in
+        /// the text box, but before the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> populates
+        /// the list of possible matches in the drop-down. The default is 0.</value>
+        public TimeSpan MinimumPopulateDelay
+        {
+            get { return GetValue(MinimumPopulateDelayProperty); }
+            set { SetValue(MinimumPopulateDelayProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum height of the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The maximum height of the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// The default is <see cref="F:System.Double.PositiveInfinity" />.</value>
+        /// <exception cref="T:System.ArgumentException">The specified value is less than 0.</exception>
+        public double MaxDropDownHeight
+        {
+            get { return GetValue(MaxDropDownHeightProperty); }
+            set { SetValue(MaxDropDownHeightProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the drop-down portion of
+        /// the control is open.
+        /// </summary>
+        /// <value>
+        /// True if the drop-down is open; otherwise, false. The default is
+        /// false.
+        /// </value>
+        public bool IsDropDownOpen
+        {
+            get { return  _isDropDownOpen; }
+            set { SetAndRaise(IsDropDownOpenProperty, ref  _isDropDownOpen, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the  <see cref="T:Avalonia.Data.Binding" /> that
+        /// is used to get the values for display in the text portion of
+        /// the <see cref="T:Avalonia.Controls.AutoCompleteBox" />
+        /// control.
+        /// </summary>
+        /// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
+        /// when binding to a collection property.</value>
+        [AssignBinding]
+        public IBinding ValueMemberBinding
+        {
+            get { return _valueBindingEvaluator?.ValueBinding; }
+            set
+            {
+                if (ValueMemberBinding != value)
+                {
+                    _valueBindingEvaluator = new BindingEvaluator<string>(value);
+                    OnValueMemberBindingChanged(value);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the MemberSelector that is used to get values for
+        /// display in the text portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The MemberSelector that is used to get values for display in
+        /// the text portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public IMemberSelector ValueMemberSelector
+        {
+            get { return _valueMemberSelector; }
+            set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the selected item in the drop-down.
+        /// </summary>
+        /// <value>The selected item in the drop-down.</value>
+        /// <remarks>
+        /// If the IsTextCompletionEnabled property is true and text typed by
+        /// the user matches an item in the ItemsSource collection, which is
+        /// then displayed in the text box, the SelectedItem property will be
+        /// a null reference.
+        /// </remarks>
+        public object SelectedItem
+        {
+            get { return _selectedItem; }
+            set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        /// <summary>
+        /// Gets the text that is used to filter items in the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// item collection.
+        /// </summary>
+        /// <value>The text that is used to filter items in the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// item collection.</value>
+        /// <remarks>
+        /// The SearchText value is typically the same as the
+        /// Text property, but is set after the TextChanged event occurs
+        /// and before the Populating event.
+        /// </remarks>
+        public string SearchText
+        {
+            get { return _searchText; }
+            private set
+            {
+                try
+                {
+                    _allowWrite = true;
+                    SetAndRaise(SearchTextProperty, ref _searchText, value);
+                }
+                finally
+                {
+                    _allowWrite = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets how the text in the text box is used to filter items
+        /// specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property for display in the drop-down.
+        /// </summary>
+        /// <value>One of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteFilterMode" />
+        /// values The default is
+        /// <see cref="F:Avalonia.Controls.AutoCompleteFilterMode.StartsWith" />.</value>
+        /// <exception cref="T:System.ArgumentException">The specified value is
+        /// not a valid
+        /// <see cref="T:Avalonia.Controls.AutoCompleteFilterMode" />.</exception>
+        /// <remarks>
+        /// Use the FilterMode property to specify how possible matches are
+        /// filtered. For example, possible matches can be filtered in a
+        /// predefined or custom way. The search mode is automatically set to
+        /// Custom if you set the ItemFilter property.
+        /// </remarks>
+        public AutoCompleteFilterMode FilterMode
+        {
+            get { return GetValue(FilterModeProperty); }
+            set { SetValue(FilterModeProperty, value); }
+        }
+
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the custom method that uses user-entered text to filter
+        /// the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property for display in the drop-down.
+        /// </summary>
+        /// <value>The custom method that uses the user-entered text to filter
+        /// the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property. The default is null.</value>
+        /// <remarks>
+        /// The filter mode is automatically set to Custom if you set the
+        /// ItemFilter property.
+        /// </remarks>
+        public AutoCompleteFilterPredicate<object> ItemFilter
+        {
+            get { return _itemFilter; }
+            set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the custom method that uses the user-entered text to
+        /// filter items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property in a text-based way for display in the drop-down.
+        /// </summary>
+        /// <value>The custom method that uses the user-entered text to filter
+        /// items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// property in a text-based way for display in the drop-down.</value>
+        /// <remarks>
+        /// The search mode is automatically set to Custom if you set the
+        /// TextFilter property.
+        /// </remarks>
+        public AutoCompleteFilterPredicate<string> TextFilter
+        {
+            get { return _textFilter; }
+            set { SetAndRaise(TextFilterProperty, ref _textFilter, value); }
+        }
+
+        public Func<string, CancellationToken, Task<IEnumerable<object>>> AsyncPopulator
+        {
+            get { return _asyncPopulator; }
+            set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a collection that is used to generate the items for the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+        /// </summary>
+        /// <value>The collection that is used to generate the items of the
+        /// drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
+        public IEnumerable Items
+        {
+            get { return _itemsEnumerable; }
+            set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the drop down popup control.
+        /// </summary>
+        private Popup DropDownPopup { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Text template part.
+        /// </summary>
+        private TextBox TextBox
+        {
+            get { return _textBox; }
+            set
+            {
+                _textBoxSubscriptions?.Dispose();
+                _textBox = value;
+
+                // Attach handlers
+                if (_textBox != null)
+                {
+                    _textBoxSubscriptions =
+                        _textBox.GetObservable(TextBox.TextProperty)
+                                .Subscribe(_ => OnTextBoxTextChanged());
+
+                    if (Text != null)
+                    {
+                        UpdateTextValue(Text);
+                    }
+                }
+            }
+        }
+
+        private int TextBoxSelectionStart
+        {
+            get
+            {
+                if (TextBox != null)
+                {
+                    return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd);
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+        private int TextBoxSelectionLength
+        {
+            get
+            {
+                if (TextBox != null)
+                {
+                    return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart);
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the selection adapter used to populate the drop-down
+        /// with a list of selectable items.
+        /// </summary>
+        /// <value>The selection adapter used to populate the drop-down with a
+        /// list of selectable items.</value>
+        /// <remarks>
+        /// You can use this property when you create an automation peer to
+        /// use with AutoCompleteBox or deriving from AutoCompleteBox to
+        /// create a custom control.
+        /// </remarks>
+        protected ISelectionAdapter SelectionAdapter
+        {
+            get { return _adapter; }
+            set
+            {
+                if (_adapter != null)
+                {
+                    _adapter.SelectionChanged -= OnAdapterSelectionChanged;
+                    _adapter.Commit -= OnAdapterSelectionComplete;
+                    _adapter.Cancel -= OnAdapterSelectionCanceled;
+                    _adapter.Cancel -= OnAdapterSelectionComplete;
+                    _adapter.Items = null;
+                }
+
+                _adapter = value;
+
+                if (_adapter != null)
+                {
+                    _adapter.SelectionChanged += OnAdapterSelectionChanged;
+                    _adapter.Commit += OnAdapterSelectionComplete;
+                    _adapter.Cancel += OnAdapterSelectionCanceled;
+                    _adapter.Cancel += OnAdapterSelectionComplete;
+                    _adapter.Items = _view;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the
+        /// <see cref="T:Avalonia.Controls.ISelectionAdapter" /> part, if
+        /// possible.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="T:Avalonia.Controls.ISelectionAdapter" /> object,
+        /// if possible. Otherwise, null.
+        /// </returns>
+        protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope)
+        {
+            ISelectionAdapter adapter = null;
+            SelectingItemsControl selector = nameScope.Find<SelectingItemsControl>(ElementSelector);
+            if (selector != null)
+            {
+                // Check if it is already an IItemsSelector
+                adapter = selector as ISelectionAdapter;
+                if (adapter == null)
+                {
+                    // Built in support for wrapping a Selector control
+                    adapter = new SelectingItemsControlSelectionAdapter(selector);
+                }
+            }
+            if (adapter == null)
+            {
+                adapter = nameScope.Find<ISelectionAdapter>(ElementSelectionAdapter);
+            }
+            return adapter;
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
+        /// when a new template is applied.
+        /// </summary>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+
+            if (DropDownPopup != null)
+            {
+                DropDownPopup.Closed -= DropDownPopup_Closed;
+                DropDownPopup = null;
+            }
+
+            // Set the template parts. Individual part setters remove and add
+            // any event handlers.
+            Popup popup = e.NameScope.Find<Popup>(ElementPopup);
+            if (popup != null)
+            {
+                DropDownPopup = popup;
+                DropDownPopup.Closed += DropDownPopup_Closed;
+            }
+
+            SelectionAdapter = GetSelectionAdapterPart(e.NameScope);
+            TextBox = e.NameScope.Find<TextBox>(ElementTextBox);
+
+            // If the drop down property indicates that the popup is open,
+            // flip its value to invoke the changed handler.
+            if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen)
+            {
+                OpeningDropDown(false);
+            }
+
+            base.OnTemplateApplied(e);
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.InputElement.KeyDown" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            Contract.Requires<ArgumentNullException>(e != null);
+
+            base.OnKeyDown(e);
+
+            if (e.Handled || !IsEnabled)
+            {
+                return;
+            }
+
+            // The drop down is open, pass along the key event arguments to the
+            // selection adapter. If it isn't handled by the adapter's logic,
+            // then we handle some simple navigation scenarios for controlling
+            // the drop down.
+            if (IsDropDownOpen)
+            {
+                if (SelectionAdapter != null)
+                {
+                    SelectionAdapter.HandleKeyDown(e);
+                    if (e.Handled)
+                    {
+                        return;
+                    }
+                }
+
+                if (e.Key == Key.Escape)
+                {
+                    OnAdapterSelectionCanceled(this, new RoutedEventArgs());
+                    e.Handled = true;
+                }
+            }
+            else
+            {
+                // The drop down is not open, the Down key will toggle it open.
+                if (e.Key == Key.Down)
+                {
+                    IsDropDownOpen = true;
+                    e.Handled = true;
+                }
+            }
+
+            // Standard drop down navigation
+            switch (e.Key)
+            {
+                case Key.F4:
+                    IsDropDownOpen = !IsDropDownOpen;
+                    e.Handled = true;
+                    break;
+
+                case Key.Enter:
+                    OnAdapterSelectionComplete(this, new RoutedEventArgs());
+                    e.Handled = true;
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.UIElement.GotFocus" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            base.OnGotFocus(e);
+            FocusChanged(HasFocus());
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.UIElement.LostFocus" /> event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+            FocusChanged(HasFocus());
+        }
+
+        /// <summary>
+        /// Determines whether the text box or drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control has
+        /// focus.
+        /// </summary>
+        /// <returns>true to indicate the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus;
+        /// otherwise, false.</returns>
+        protected bool HasFocus()
+        {
+            IVisual focused = FocusManager.Instance.Current;
+
+            while (focused != null)
+            {
+                if (object.ReferenceEquals(focused, this))
+                {
+                    return true;
+                }
+
+                // This helps deal with popups that may not be in the same
+                // visual tree
+                IVisual parent = focused.GetVisualParent();
+                if (parent == null)
+                {
+                    // Try the logical parent.
+                    IControl element = focused as IControl;
+                    if (element != null)
+                    {
+                        parent = element.Parent;
+                    }
+                }
+                focused = parent;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Handles the FocusChanged event.
+        /// </summary>
+        /// <param name="hasFocus">A value indicating whether the control
+        /// currently has the focus.</param>
+        private void FocusChanged(bool hasFocus)
+        {
+            // The OnGotFocus & OnLostFocus are asynchronously and cannot
+            // reliably tell you that have the focus.  All they do is let you
+            // know that the focus changed sometime in the past.  To determine
+            // if you currently have the focus you need to do consult the
+            // FocusManager (see HasFocus()).
+
+            bool wasFocused = _isFocused;
+            _isFocused = hasFocus;
+
+            if (hasFocus)
+            {
+
+                if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0)
+                {
+                    TextBox.Focus();
+                    TextBox.SelectionStart = 0;
+                    TextBox.SelectionEnd = TextBox.Text?.Length ?? 0;
+                }
+            }
+            else
+            {
+                IsDropDownOpen = false;
+                _userCalledPopulate = false;
+                ClearTextBoxSelection();
+            }
+
+            _isFocused = hasFocus;
+        }
+
+        /// <summary>
+        /// Occurs when the text in the text box portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> changes.
+        /// </summary>
+        public event EventHandler TextChanged;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> is
+        /// populating the drop-down with possible matches based on the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// property.
+        /// </summary>
+        /// <remarks>
+        /// If the event is canceled, by setting the PopulatingEventArgs.Cancel
+        /// property to true, the AutoCompleteBox will not automatically
+        /// populate the selection adapter contained in the drop-down.
+        /// In this case, if you want possible matches to appear, you must
+        /// provide the logic for populating the selection adapter.
+        /// </remarks>
+        public event EventHandler<PopulatingEventArgs> Populating;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has
+        /// populated the drop-down with possible matches based on the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Text" />
+        /// property.
+        /// </summary>
+        public event EventHandler<PopulatedEventArgs> Populated;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property is changing from false to true.
+        /// </summary>
+        public event EventHandler<CancelEventArgs> DropDownOpening;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property has changed from false to true and the drop-down is open.
+        /// </summary>
+        public event EventHandler DropDownOpened;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property is changing from true to false.
+        /// </summary>
+        public event EventHandler<CancelEventArgs> DropDownClosing;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.IsDropDownOpen" />
+        /// property was changed from true to false and the drop-down is open.
+        /// </summary>
+        public event EventHandler DropDownClosed;
+
+        /// <summary>
+        /// Occurs when the selected item in the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has
+        /// changed.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged
+        {
+            add { AddHandler(SelectionChangedEvent, value); }
+            remove { RemoveHandler(SelectionChangedEvent, value); }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populating" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.PopulatingEventArgs" /> that
+        /// contains the event data.</param>
+        protected virtual void OnPopulating(PopulatingEventArgs e)
+        {
+            Populating?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.PopulatedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnPopulated(PopulatedEventArgs e)
+        {
+            Populated?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.SelectionChanged" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.SelectionChangedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
+        {
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownOpening" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.CancelEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownOpening(CancelEventArgs e)
+        {
+            DropDownOpening?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownOpened" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:System.EventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownOpened(EventArgs e)
+        {
+            DropDownOpened?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownClosing" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:Avalonia.Controls.CancelEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnDropDownClosing(CancelEventArgs e)
+        {
+            DropDownClosing?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.DropDownClosed" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A
+        /// <see cref="T:System.EventArgs" />
+        /// which contains the event data.</param>
+        protected virtual void OnDropDownClosed(EventArgs e)
+        {
+            DropDownClosed?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.AutoCompleteBox.TextChanged" />
+        /// event.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.RoutedEventArgs" />
+        /// that contains the event data.</param>
+        protected virtual void OnTextChanged(RoutedEventArgs e)
+        {
+            TextChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Begin closing the drop-down.
+        /// </summary>
+        /// <param name="oldValue">The original value.</param>
+        private void ClosingDropDown(bool oldValue)
+        {
+            var args = new CancelEventArgs();
+            OnDropDownClosing(args);
+
+            if (args.Cancel)
+            {
+                _ignorePropertyChange = true;
+                SetValue(IsDropDownOpenProperty, oldValue);
+            }
+            else
+            {
+                CloseDropDown();
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        /// <summary>
+        /// Begin opening the drop down by firing cancelable events, opening the
+        /// drop-down or reverting, depending on the event argument values.
+        /// </summary>
+        /// <param name="oldValue">The original value, if needed for a revert.</param>
+        private void OpeningDropDown(bool oldValue)
+        {
+            var args = new CancelEventArgs();
+
+            // Opening
+            OnDropDownOpening(args);
+
+            if (args.Cancel)
+            {
+                _ignorePropertyChange = true;
+                SetValue(IsDropDownOpenProperty, oldValue);
+            }
+            else
+            {
+                OpenDropDown();
+            }
+
+            UpdatePseudoClasses();
+        }
+
+        /// <summary>
+        /// Connects to the DropDownPopup Closed event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void DropDownPopup_Closed(object sender, EventArgs e)
+        {
+            // Force the drop down dependency property to be false.
+            if (IsDropDownOpen)
+            {
+                IsDropDownOpen = false;
+            }
+
+            // Fire the DropDownClosed event
+            if (_popupHasOpened)
+            {
+                OnDropDownClosed(EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Handles the timer tick when using a populate delay.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event arguments.</param>
+        private void PopulateDropDown(object sender, EventArgs e)
+        {
+            if (_delayTimer != null)
+            {
+                _delayTimer.Stop();
+            }
+
+            // Update the prefix/search text.
+            SearchText = Text;
+
+            if(TryPopulateAsync(SearchText))
+            {
+                return;
+            }
+
+            // The Populated event enables advanced, custom filtering. The
+            // client needs to directly update the ItemsSource collection or
+            // call the Populate method on the control to continue the
+            // display process if Cancel is set to true.
+            PopulatingEventArgs populating = new PopulatingEventArgs(SearchText);
+            OnPopulating(populating);
+            if (!populating.Cancel)
+            {
+                PopulateComplete();
+            }
+        }
+        private bool TryPopulateAsync(string searchText)
+        {
+            _populationCancellationTokenSource?.Cancel(false);
+            _populationCancellationTokenSource?.Dispose();
+            _populationCancellationTokenSource = null;
+
+            if(_asyncPopulator == null)
+            {
+                return false;
+            }
+
+            _populationCancellationTokenSource = new CancellationTokenSource();
+            var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token);
+            if (task.Status == TaskStatus.Created)
+                task.Start();
+
+            return true;
+        }
+        private async Task PopulateAsync(string searchText, CancellationToken cancellationToken)
+        {
+
+            try
+            {
+                IEnumerable<object> result = await _asyncPopulator.Invoke(searchText, cancellationToken);
+                var resultList = result.ToList();
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    return;
+                }
+
+                await Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    if (!cancellationToken.IsCancellationRequested)
+                    {
+                        Items = resultList;
+                        PopulateComplete();
+                    }
+                });
+            }
+            catch (TaskCanceledException)
+            { }
+            finally
+            {
+                _populationCancellationTokenSource?.Dispose();
+                _populationCancellationTokenSource = null;
+            }
+
+        }
+
+        /// <summary>
+        /// Private method that directly opens the popup, checks the expander
+        /// button, and then fires the Opened event.
+        /// </summary>
+        private void OpenDropDown()
+        {
+            if (DropDownPopup != null)
+            {
+                DropDownPopup.IsOpen = true;
+            }
+            _popupHasOpened = true;
+            OnDropDownOpened(EventArgs.Empty);
+        }
+
+        /// <summary>
+        /// Private method that directly closes the popup, flips the Checked
+        /// value, and then fires the Closed event.
+        /// </summary>
+        private void CloseDropDown()
+        {
+            if (_popupHasOpened)
+            {
+                if (SelectionAdapter != null)
+                {
+                    SelectionAdapter.SelectedItem = null;
+                }
+                if (DropDownPopup != null)
+                {
+                    DropDownPopup.IsOpen = false;
+                }
+                OnDropDownClosed(EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Formats an Item for text comparisons based on Converter
+        /// and ConverterCulture properties.
+        /// </summary>
+        /// <param name="value">The object to format.</param>
+        /// <param name="clearDataContext">A value indicating whether to clear
+        /// the data context after the lookup is performed.</param>
+        /// <returns>Formatted Value.</returns>
+        private string FormatValue(object value, bool clearDataContext)
+        {
+            string result = FormatValue(value);
+            if(clearDataContext && _valueBindingEvaluator != null)
+            {
+                _valueBindingEvaluator.ClearDataContext();
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Converts the specified object to a string by using the
+        /// <see cref="P:Avalonia.Data.Binding.Converter" /> and
+        /// <see cref="P:Avalonia.Data.Binding.ConverterCulture" /> values
+        /// of the binding object specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ValueMemberBinding" />
+        /// property.
+        /// </summary>
+        /// <param name="value">The object to format as a string.</param>
+        /// <returns>The string representation of the specified object.</returns>
+        /// <remarks>
+        /// Override this method to provide a custom string conversion.
+        /// </remarks>
+        protected virtual string FormatValue(object value)
+        {
+            if (_valueBindingEvaluator != null)
+            {
+                return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
+            }
+
+            if (_valueMemberSelector != null)
+            {
+                value = _valueMemberSelector.Select(value);
+            }
+
+            return value == null ? String.Empty : value.ToString();
+        }
+
+        /// <summary>
+        /// Handle the TextChanged event that is directly attached to the
+        /// TextBox part. This ensures that only user initiated actions will
+        /// result in an AutoCompleteBox suggestion and operation.
+        /// </summary>
+        private void OnTextBoxTextChanged()
+        {
+            //Uses Dispatcher.Post to allow the TextBox selection to update before processing
+            Dispatcher.UIThread.Post(() =>
+            {
+                // Call the central updated text method as a user-initiated action
+                TextUpdated(_textBox.Text, true);
+            });
+        }
+
+        /// <summary>
+        /// Updates both the text box value and underlying text dependency
+        /// property value if and when they change. Automatically fires the
+        /// text changed events when there is a change.
+        /// </summary>
+        /// <param name="value">The new string value.</param>
+        private void UpdateTextValue(string value)
+        {
+            UpdateTextValue(value, null);
+        }
+
+        /// <summary>
+        /// Updates both the text box value and underlying text dependency
+        /// property value if and when they change. Automatically fires the
+        /// text changed events when there is a change.
+        /// </summary>
+        /// <param name="value">The new string value.</param>
+        /// <param name="userInitiated">A nullable bool value indicating whether
+        /// the action was user initiated. In a user initiated mode, the
+        /// underlying text dependency property is updated. In a non-user
+        /// interaction, the text box value is updated. When user initiated is
+        /// null, all values are updated.</param>
+        private void UpdateTextValue(string value, bool? userInitiated)
+        {
+            bool callTextChanged = false;
+            // Update the Text dependency property
+            if ((userInitiated == null || userInitiated == true) && Text != value)
+            {
+                _ignoreTextPropertyChange++;
+                Text = value;
+                callTextChanged = true;
+            }
+
+            // Update the TextBox's Text dependency property
+            if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value)
+            {
+                _ignoreTextPropertyChange++;
+                TextBox.Text = value ?? string.Empty;
+
+                // Text dependency property value was set, fire event
+                if (!callTextChanged && (Text == value || Text == null))
+                {
+                    callTextChanged = true;
+                }
+            }
+
+            if (callTextChanged)
+            {
+                OnTextChanged(new RoutedEventArgs());
+            }
+        }
+
+        /// <summary>
+        /// Handle the update of the text for the control from any source,
+        /// including the TextBox part and the Text dependency property.
+        /// </summary>
+        /// <param name="newText">The new text.</param>
+        /// <param name="userInitiated">A value indicating whether the update
+        /// is a user-initiated action. This should be a True value when the
+        /// TextUpdated method is called from a TextBox event handler.</param>
+        private void TextUpdated(string newText, bool userInitiated)
+        {
+            // Only process this event if it is coming from someone outside
+            // setting the Text dependency property directly.
+            if (_ignoreTextPropertyChange > 0)
+            {
+                _ignoreTextPropertyChange--;
+                return;
+            }
+
+            if (newText == null)
+            {
+                newText = string.Empty;
+            }
+
+            // The TextBox.TextChanged event was not firing immediately and
+            // was causing an immediate update, even with wrapping. If there is
+            // a selection currently, no update should happen.
+            if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length)
+            {
+                return;
+            }
+
+            // Evaluate the conditions needed for completion.
+            // 1. Minimum prefix length
+            // 2. If a delay timer is in use, use it
+            bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
+            _userCalledPopulate = populateReady ? userInitiated : false;
+
+            // Update the interface and values only as necessary
+            UpdateTextValue(newText, userInitiated);
+
+            if (populateReady)
+            {
+                _ignoreTextSelectionChange = true;
+
+                if (_delayTimer != null)
+                {
+                    _delayTimer.Start();
+                }
+                else
+                {
+                    PopulateDropDown(this, EventArgs.Empty);
+                }
+            }
+            else
+            {
+                SearchText = string.Empty;
+                if (SelectedItem != null)
+                {
+                    _skipSelectedItemTextUpdate = true;
+                }
+                SelectedItem = null;
+                if (IsDropDownOpen)
+                {
+                    IsDropDownOpen = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// A simple helper method to clear the view and ensure that a view
+        /// object is always present and not null.
+        /// </summary>
+        private void ClearView()
+        {
+            if (_view == null)
+            {
+                _view = new AvaloniaList<object>();
+            }
+            else
+            {
+                _view.Clear();
+            }
+        }
+
+        /// <summary>
+        /// Walks through the items enumeration. Performance is not going to be
+        /// perfect with the current implementation.
+        /// </summary>
+        private void RefreshView()
+        {
+            if (_items == null)
+            {
+                ClearView();
+                return;
+            }
+
+            // Cache the current text value
+            string text = Text ?? string.Empty;
+
+            // Determine if any filtering mode is on
+            bool stringFiltering = TextFilter != null;
+            bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null;
+
+            int view_index = 0;
+            int view_count = _view.Count;
+            List<object> items = _items;
+            foreach (object item in items)
+            {
+                bool inResults = !(stringFiltering || objectFiltering);
+                if (!inResults)
+                {
+                    inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
+                }
+
+                if (view_count > view_index && inResults && _view[view_index] == item)
+                {
+                    // Item is still in the view
+                    view_index++;
+                }
+                else if (inResults)
+                {
+                    // Insert the item
+                    if (view_count > view_index && _view[view_index] != item)
+                    {
+                        // Replace item
+                        // Unfortunately replacing via index throws a fatal
+                        // exception: View[view_index] = item;
+                        // Cost: O(n) vs O(1)
+                        _view.RemoveAt(view_index);
+                        _view.Insert(view_index, item);
+                        view_index++;
+                    }
+                    else
+                    {
+                        // Add the item
+                        if (view_index == view_count)
+                        {
+                            // Constant time is preferred (Add).
+                            _view.Add(item);
+                        }
+                        else
+                        {
+                            _view.Insert(view_index, item);
+                        }
+                        view_index++;
+                        view_count++;
+                    }
+                }
+                else if (view_count > view_index && _view[view_index] == item)
+                {
+                    // Remove the item
+                    _view.RemoveAt(view_index);
+                    view_count--;
+                }
+            }
+
+            // Clear the evaluator to discard a reference to the last item
+            if (_valueBindingEvaluator != null)
+            {
+                _valueBindingEvaluator.ClearDataContext();
+            }
+        }
+
+        /// <summary>
+        /// Handle any change to the ItemsSource dependency property, update
+        /// the underlying ObservableCollection view, and set the selection
+        /// adapter's ItemsSource to the view if appropriate.
+        /// </summary>
+        /// <param name="newValue">The new enumerable reference.</param>
+        private void OnItemsChanged(IEnumerable newValue)
+        {
+            // Remove handler for oldValue.CollectionChanged (if present)
+            _collectionChangeSubscription?.Dispose();
+            _collectionChangeSubscription = null;
+
+            // Add handler for newValue.CollectionChanged (if possible)
+            if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
+            {
+                _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged);
+            }
+
+            // Store a local cached copy of the data
+            _items = newValue == null ? null : new List<object>(newValue.Cast<object>().ToList());
+
+            // Clear and set the view on the selection adapter
+            ClearView();
+            if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+            {
+                SelectionAdapter.Items = _view;
+            }
+            if (IsDropDownOpen)
+            {
+                RefreshView();
+            }
+        }
+
+        /// <summary>
+        /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
+        /// </summary>
+        /// <param name="sender">The object that raised the event.</param>
+        /// <param name="e">The event data.</param>
+        private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            // Update the cache
+            if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
+            {
+                for (int index = 0; index < e.OldItems.Count; index++)
+                {
+                    _items.RemoveAt(e.OldStartingIndex);
+                }
+            }
+            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex)
+            {
+                for (int index = 0; index < e.NewItems.Count; index++)
+                {
+                    _items.Insert(e.NewStartingIndex + index, e.NewItems[index]);
+                }
+            }
+            if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null)
+            {
+                for (int index = 0; index < e.NewItems.Count; index++)
+                {
+                    _items[e.NewStartingIndex] = e.NewItems[index];
+                }
+            }
+
+            // Update the view
+            if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
+            {
+                for (int index = 0; index < e.OldItems.Count; index++)
+                {
+                    _view.Remove(e.OldItems[index]);
+                }
+            }
+
+            if (e.Action == NotifyCollectionChangedAction.Reset)
+            {
+                // Significant changes to the underlying data.
+                ClearView();
+                if (Items != null)
+                {
+                    _items = new List<object>(Items.Cast<object>().ToList());
+                }
+            }
+
+            // Refresh the observable collection used in the selection adapter.
+            RefreshView();
+        }
+
+        /// <summary>
+        /// Notifies the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> that the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.Items" />
+        /// property has been set and the data can be filtered to provide
+        /// possible matches in the drop-down.
+        /// </summary>
+        /// <remarks>
+        /// Call this method when you are providing custom population of
+        /// the drop-down portion of the AutoCompleteBox, to signal the control
+        /// that you are done with the population process.
+        /// Typically, you use PopulateComplete when the population process
+        /// is a long-running process and you want to cancel built-in filtering
+        ///  of the ItemsSource items. In this case, you can handle the
+        /// Populated event and set PopulatingEventArgs.Cancel to true.
+        /// When the long-running process has completed you call
+        /// PopulateComplete to indicate the drop-down is populated.
+        /// </remarks>
+        public void PopulateComplete()
+        {
+            // Apply the search filter
+            RefreshView();
+
+            // Fire the Populated event containing the read-only view data.
+            PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection<object>(_view));
+            OnPopulated(populated);
+
+            if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+            {
+                SelectionAdapter.Items = _view;
+            }
+
+            bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0);
+            if (isDropDownOpen != IsDropDownOpen)
+            {
+                _ignorePropertyChange = true;
+                IsDropDownOpen = isDropDownOpen;
+            }
+            if (IsDropDownOpen)
+            {
+                OpeningDropDown(false);
+            }
+            else
+            {
+                ClosingDropDown(true);
+            }
+
+            UpdateTextCompletion(_userCalledPopulate);
+        }
+
+        /// <summary>
+        /// Performs text completion, if enabled, and a lookup on the underlying
+        /// item values for an exact match. Will update the SelectedItem value.
+        /// </summary>
+        /// <param name="userInitiated">A value indicating whether the operation
+        /// was user initiated. Text completion will not be performed when not
+        /// directly initiated by the user.</param>
+        private void UpdateTextCompletion(bool userInitiated)
+        {
+            // By default this method will clear the selected value
+            object newSelectedItem = null;
+            string text = Text;
+
+            // Text search is StartsWith explicit and only when enabled, in
+            // line with WPF's ComboBox lookup. When in use it will associate
+            // a Value with the Text if it is found in ItemsSource. This is
+            // only valid when there is data and the user initiated the action.
+            if (_view.Count > 0)
+            {
+                if (IsTextCompletionEnabled && TextBox != null && userInitiated)
+                {
+                    int currentLength = TextBox.Text.Length;
+                    int selectionStart = TextBoxSelectionStart;
+                    if (selectionStart == text.Length && selectionStart > _textSelectionStart)
+                    {
+                        // When the FilterMode dependency property is set to
+                        // either StartsWith or StartsWithCaseSensitive, the
+                        // first item in the view is used. This will improve
+                        // performance on the lookup. It assumes that the
+                        // FilterMode the user has selected is an acceptable
+                        // case sensitive matching function for their scenario.
+                        object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive
+                            ? _view[0]
+                            : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+                        // If the search was successful, update SelectedItem
+                        if (top != null)
+                        {
+                            newSelectedItem = top;
+                            string topString = FormatValue(top, true);
+
+                            // Only replace partially when the two words being the same
+                            int minLength = Math.Min(topString.Length, Text.Length);
+                            if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength)))
+                            {
+                                // Update the text
+                                UpdateTextValue(topString);
+
+                                // Select the text past the user's caret
+                                TextBox.SelectionStart = currentLength;
+                                TextBox.SelectionEnd = topString.Length;
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // Perform an exact string lookup for the text. This is a
+                    // design change from the original Toolkit release when the
+                    // IsTextCompletionEnabled property behaved just like the
+                    // WPF ComboBox's IsTextSearchEnabled property.
+                    //
+                    // This change provides the behavior that most people expect
+                    // to find: a lookup for the value is always performed.
+                    newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive));
+                }
+            }
+
+            // Update the selected item property
+
+            if (SelectedItem != newSelectedItem)
+            {
+                _skipSelectedItemTextUpdate = true;
+            }
+            SelectedItem = newSelectedItem;
+
+            // Restore updates for TextSelection
+            if (_ignoreTextSelectionChange)
+            {
+                _ignoreTextSelectionChange = false;
+                if (TextBox != null)
+                {
+                    _textSelectionStart = TextBoxSelectionStart;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Attempts to look through the view and locate the specific exact
+        /// text match.
+        /// </summary>
+        /// <param name="searchText">The search text.</param>
+        /// <param name="view">The view reference.</param>
+        /// <param name="predicate">The predicate to use for the partial or
+        /// exact match.</param>
+        /// <returns>Returns the object or null.</returns>
+        private object TryGetMatch(string searchText, AvaloniaList<object> view, AutoCompleteFilterPredicate<string> predicate)
+        {
+            if (view != null && view.Count > 0)
+            {
+                foreach (object o in view)
+                {
+                    if (predicate(searchText, FormatValue(o)))
+                    {
+                        return o;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private void UpdatePseudoClasses()
+        {
+            PseudoClasses.Set(":dropdownopen", IsDropDownOpen);
+        }
+
+        private void ClearTextBoxSelection()
+        {
+            if (TextBox != null)
+            {
+                int length = TextBox.Text?.Length ?? 0;
+                TextBox.SelectionStart = length;
+                TextBox.SelectionEnd = length;
+            }
+        }
+
+        /// <summary>
+        /// Called when the selected item is changed, updates the text value
+        /// that is displayed in the text box part.
+        /// </summary>
+        /// <param name="newItem">The new item.</param>
+        private void OnSelectedItemChanged(object newItem)
+        {
+            string text;
+
+            if (newItem == null)
+            {
+                text = SearchText;
+            }
+            else
+            {
+                text = FormatValue(newItem, true);
+            }
+
+            // Update the Text property and the TextBox values
+            UpdateTextValue(text);
+
+            // Move the caret to the end of the text box
+            ClearTextBoxSelection();
+        }
+
+        /// <summary>
+        /// Handles the SelectionChanged event of the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The selection changed event data.</param>
+        private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            SelectedItem = _adapter.SelectedItem;
+        }
+
+        //TODO Check UpdateTextCompletion
+        /// <summary>
+        /// Handles the Commit event on the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e)
+        {
+            IsDropDownOpen = false;
+
+            // Completion will update the selected value
+            //UpdateTextCompletion(false);
+
+            // Text should not be selected
+            ClearTextBoxSelection();
+
+            TextBox.Focus();
+        }
+
+        /// <summary>
+        /// Handles the Cancel event on the selection adapter.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e)
+        {
+            UpdateTextValue(SearchText);
+
+            // Completion will update the selected value
+            UpdateTextCompletion(false);
+        }
+
+        /// <summary>
+        /// A predefined set of filter functions for the known, built-in
+        /// AutoCompleteFilterMode enumeration values.
+        /// </summary>
+        private static class AutoCompleteSearch
+        {
+            /// <summary>
+            /// Index function that retrieves the filter for the provided
+            /// AutoCompleteFilterMode.
+            /// </summary>
+            /// <param name="FilterMode">The built-in search mode.</param>
+            /// <returns>Returns the string-based comparison function.</returns>
+            public static AutoCompleteFilterPredicate<string> GetFilter(AutoCompleteFilterMode FilterMode)
+            {
+                switch (FilterMode)
+                {
+                    case AutoCompleteFilterMode.Contains:
+                        return Contains;
+
+                    case AutoCompleteFilterMode.ContainsCaseSensitive:
+                        return ContainsCaseSensitive;
+
+                    case AutoCompleteFilterMode.ContainsOrdinal:
+                        return ContainsOrdinal;
+
+                    case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+                        return ContainsOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.Equals:
+                        return Equals;
+
+                    case AutoCompleteFilterMode.EqualsCaseSensitive:
+                        return EqualsCaseSensitive;
+
+                    case AutoCompleteFilterMode.EqualsOrdinal:
+                        return EqualsOrdinal;
+
+                    case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+                        return EqualsOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.StartsWith:
+                        return StartsWith;
+
+                    case AutoCompleteFilterMode.StartsWithCaseSensitive:
+                        return StartsWithCaseSensitive;
+
+                    case AutoCompleteFilterMode.StartsWithOrdinal:
+                        return StartsWithOrdinal;
+
+                    case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+                        return StartsWithOrdinalCaseSensitive;
+
+                    case AutoCompleteFilterMode.None:
+                    case AutoCompleteFilterMode.Custom:
+                    default:
+                        return null;
+                }
+            }
+
+            /// <summary>
+            /// An implementation of the Contains member of string that takes in a
+            /// string comparison. The traditional .NET string Contains member uses
+            /// StringComparison.Ordinal.
+            /// </summary>
+            /// <param name="s">The string.</param>
+            /// <param name="value">The string value to search for.</param>
+            /// <param name="comparison">The string comparison type.</param>
+            /// <returns>Returns true when the substring is found.</returns>
+            private static bool Contains(string s, string value, StringComparison comparison)
+            {
+                return s.IndexOf(value, comparison) >= 0;
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWith(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithCaseSensitive(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithOrdinal(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string value begins with the text.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool StartsWithOrdinalCaseSensitive(string text, string value)
+            {
+                return value.StartsWith(text, StringComparison.Ordinal);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value. The current
+            /// culture's case insensitive string comparison operator is used.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool Contains(string text, string value)
+            {
+                return Contains(value, text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsCaseSensitive(string text, string value)
+            {
+                return Contains(value, text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsOrdinal(string text, string value)
+            {
+                return Contains(value, text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the prefix is contained in the string value.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool ContainsOrdinalCaseSensitive(string text, string value)
+            {
+                return Contains(value, text, StringComparison.Ordinal);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool Equals(string text, string value)
+            {
+                return value.Equals(text, StringComparison.CurrentCultureIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsCaseSensitive(string text, string value)
+            {
+                return value.Equals(text, StringComparison.CurrentCulture);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsOrdinal(string text, string value)
+            {
+                return value.Equals(text, StringComparison.OrdinalIgnoreCase);
+            }
+
+            /// <summary>
+            /// Check if the string values are equal.
+            /// </summary>
+            /// <param name="text">The AutoCompleteBox prefix text.</param>
+            /// <param name="value">The item's string value.</param>
+            /// <returns>Returns true if the condition is met.</returns>
+            public static bool EqualsOrdinalCaseSensitive(string text, string value)
+            {
+                return value.Equals(text, StringComparison.Ordinal);
+            }
+        }
+
+        /// <summary>
+        /// A framework element that permits a binding to be evaluated in a new data
+        /// context leaf node.
+        /// </summary>
+        /// <typeparam name="T">The type of dynamic binding to return.</typeparam>
+        public class BindingEvaluator<T> : Control
+        {
+            /// <summary>
+            /// Gets or sets the string value binding used by the control.
+            /// </summary>
+            private IBinding _binding;
+
+            #region public T Value
+
+            /// <summary>
+            /// Identifies the Value dependency property.
+            /// </summary>
+            public static readonly StyledProperty<T> ValueProperty =
+                AvaloniaProperty.Register<BindingEvaluator<T>, T>(nameof(Value));
+
+            /// <summary>
+            /// Gets or sets the data item value.
+            /// </summary>
+            public T Value
+            {
+                get { return GetValue(ValueProperty); }
+                set { SetValue(ValueProperty, value); }
+            }
+
+            #endregion public string Value
+
+            /// <summary>
+            /// Gets or sets the value binding.
+            /// </summary>
+            public IBinding ValueBinding
+            {
+                get { return _binding; }
+                set
+                {
+                    _binding = value;
+                    AvaloniaObjectExtensions.Bind(this, ValueProperty, value);
+                }
+            }
+
+            /// <summary>
+            /// Initializes a new instance of the BindingEvaluator class.
+            /// </summary>
+            public BindingEvaluator()
+            { }
+
+            /// <summary>
+            /// Initializes a new instance of the BindingEvaluator class,
+            /// setting the initial binding to the provided parameter.
+            /// </summary>
+            /// <param name="binding">The initial string value binding.</param>
+            public BindingEvaluator(IBinding binding)
+                : this()
+            {
+                ValueBinding = binding;
+            }
+
+            /// <summary>
+            /// Clears the data context so that the control does not keep a
+            /// reference to the last-looked up item.
+            /// </summary>
+            public void ClearDataContext()
+            {
+                DataContext = null;
+            }
+
+            /// <summary>
+            /// Updates the data context of the framework element and returns the
+            /// updated binding value.
+            /// </summary>
+            /// <param name="o">The object to use as the data context.</param>
+            /// <param name="clearDataContext">If set to true, this parameter will
+            /// clear the data context immediately after retrieving the value.</param>
+            /// <returns>Returns the evaluated T value of the bound dependency
+            /// property.</returns>
+            public T GetDynamicValue(object o, bool clearDataContext)
+            {
+                DataContext = o;
+                T value = Value;
+                if (clearDataContext)
+                {
+                    DataContext = null;
+                }
+                return value;
+            }
+
+            /// <summary>
+            /// Updates the data context of the framework element and returns the
+            /// updated binding value.
+            /// </summary>
+            /// <param name="o">The object to use as the data context.</param>
+            /// <returns>Returns the evaluated T value of the bound dependency
+            /// property.</returns>
+            public T GetDynamicValue(object o)
+            {
+                DataContext = o;
+                return Value;
+            }
+        }
+    }
+}

+ 21 - 30
src/Avalonia.Controls/Border.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia;
+using Avalonia.Controls.Utils;
 using Avalonia.Media;
 
 namespace Avalonia.Controls
@@ -8,7 +10,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control which decorates a child with a border and background.
     /// </summary>
-    public class Border : Decorator
+    public partial class Border : Decorator
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -25,21 +27,24 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
-            AvaloniaProperty.Register<Border, double>(nameof(BorderThickness));
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
+            AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness));
 
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
-            AvaloniaProperty.Register<Border, float>(nameof(CornerRadius));
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
+
+        private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="Border"/> class.
         /// </summary>
         static Border()
         {
-            AffectsRender(BackgroundProperty, BorderBrushProperty);
+            AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
+            AffectsMeasure(BorderThicknessProperty);
         }
 
         /// <summary>
@@ -63,7 +68,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -72,7 +77,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -84,21 +89,7 @@ namespace Avalonia.Controls
         /// <param name="context">The drawing context.</param>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -120,10 +111,12 @@ namespace Avalonia.Controls
         {
             if (Child != null)
             {
-                var padding = Padding + new Thickness(BorderThickness);
+                var padding = Padding + BorderThickness;
                 Child.Arrange(new Rect(finalSize).Deflate(padding));
             }
 
+            _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
+
             return finalSize;
         }
 
@@ -131,19 +124,17 @@ namespace Avalonia.Controls
             Size availableSize,
             IControl child,
             Thickness padding,
-            double borderThickness)
+            Thickness borderThickness)
         {
-            padding += new Thickness(borderThickness);
+            padding += borderThickness;
 
             if (child != null)
             {
                 child.Measure(availableSize.Deflate(padding));
                 return child.DesiredSize.Inflate(padding);
             }
-            else
-            {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
-            }
+
+            return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Button.cs

@@ -245,7 +245,7 @@ namespace Avalonia.Controls
         {
             base.OnPointerReleased(e);
 
-            if (e.MouseButton == MouseButton.Left)
+            if (IsPressed && e.MouseButton == MouseButton.Left)
             {
                 e.Device.Capture(null);
                 IsPressed = false;

+ 263 - 0
src/Avalonia.Controls/ButtonSpinner.cs

@@ -0,0 +1,263 @@
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    public enum Location
+    {
+        Left,
+        Right
+    }
+
+    /// <summary>
+    /// Represents a spinner control that includes two Buttons.
+    /// </summary>
+    public class ButtonSpinner : Spinner
+    {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(AllowSpin), true);
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(ShowButtonSpinner), true);
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
+
+        private Button _decreaseButton;
+        /// <summary>
+        /// Gets or sets the DecreaseButton template part.
+        /// </summary>
+        private Button DecreaseButton
+        {
+            get { return _decreaseButton; }
+            set
+            {
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click  -= OnButtonClick;
+                }
+                _decreaseButton = value;
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        private Button _increaseButton;
+        /// <summary>
+        /// Gets or sets the IncreaseButton template part.
+        /// </summary>
+        private Button IncreaseButton
+        {
+            get
+            {
+                return _increaseButton;
+            }
+            set
+            {
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click -= OnButtonClick;
+                }
+                _increaseButton = value;
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ButtonSpinner"/> class.
+        /// </summary>
+        static ButtonSpinner()
+        {
+            AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the <see cref="ButtonSpinner"/> should allow to spin.
+        /// </summary>
+        public bool AllowSpin
+        {
+            get { return GetValue(AllowSpinProperty); }
+            set { SetValue(AllowSpinProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the spin buttons should be shown.
+        /// </summary>
+        public bool ShowButtonSpinner
+        {
+            get { return GetValue(ShowButtonSpinnerProperty); }
+            set { SetValue(ShowButtonSpinnerProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
+        /// </summary>
+        public Location ButtonSpinnerLocation
+        {
+            get { return GetValue(ButtonSpinnerLocationProperty); }
+            set { SetValue(ButtonSpinnerLocationProperty, value); }
+        }
+
+        /// <inheritdoc />
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            IncreaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
+            DecreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
+            SetButtonUsage();
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+            Point mousePosition;
+            if (IncreaseButton != null && IncreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(IncreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < IncreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < IncreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+
+            if (DecreaseButton != null && DecreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(DecreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < DecreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < DecreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Up:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Increase));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Down:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Decrease));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Enter:
+                {
+                    //Do not Spin on enter Key when spinners have focus
+                    if (((IncreaseButton != null) && (IncreaseButton.IsFocused))
+                        || ((DecreaseButton != null) && DecreaseButton.IsFocused))
+                    {
+                        e.Handled = true;
+                    }
+                    break;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+            if (!e.Handled && AllowSpin)
+            {
+                if (e.Delta.Y != 0)
+                {
+                    var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
+                    OnSpin(spinnerEventArgs);
+                    e.Handled = spinnerEventArgs.Handled;
+                }
+            }
+        }
+
+        protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+        {
+            SetButtonUsage();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnAllowSpinChanged(bool oldValue, bool newValue)
+        {
+            SetButtonUsage();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void AllowSpinChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is ButtonSpinner spinner)
+            {
+                var oldValue = (bool)e.OldValue;
+                var newValue = (bool)e.NewValue;
+                spinner.OnAllowSpinChanged(oldValue, newValue);
+            }
+        }
+        
+        /// <summary>
+        /// Disables or enables the buttons based on the valid spin direction.
+        /// </summary>
+        private void SetButtonUsage()
+        {
+            if (IncreaseButton != null)
+            {
+                IncreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase);
+            }
+
+            if (DecreaseButton != null)
+            {
+                DecreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease);
+            }
+        }
+
+        /// <summary>
+        /// Called when user clicks one of the spin buttons.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void OnButtonClick(object sender, RoutedEventArgs e)
+        {
+            if (AllowSpin)
+            {
+                var direction = sender == IncreaseButton ? SpinDirection.Increase : SpinDirection.Decrease;
+                OnSpin(new SpinEventArgs(SpinEvent, direction));
+            }
+        }
+    }
+}

+ 1188 - 0
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -0,0 +1,1188 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+    /// event.
+    /// </summary>
+    public class DatePickerDateValidationErrorEventArgs : EventArgs
+    {
+        private bool _throwException;
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" />
+        /// class.
+        /// </summary>
+        /// <param name="exception">
+        /// The initial exception from the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </param>
+        /// <param name="text">
+        /// The text that caused the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </param>
+        public DatePickerDateValidationErrorEventArgs(Exception exception, string text)
+        {
+            this.Text = text;
+            this.Exception = exception;
+        }
+
+        /// <summary>
+        /// Gets the initial exception associated with the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <value>
+        /// The exception associated with the validation failure.
+        /// </value>
+        public Exception Exception { get; private set; }
+
+        /// <summary>
+        /// Gets the text that caused the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <value>
+        /// The text that caused the validation failure.
+        /// </value>
+        public string Text { get; private set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether
+        /// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" />
+        /// should be thrown.
+        /// </summary>
+        /// <value>
+        /// True if the exception should be thrown; otherwise, false.
+        /// </value>
+        /// <exception cref="T:System.ArgumentException">
+        /// If set to true and
+        /// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" />
+        /// is null.
+        /// </exception>
+        public bool ThrowException
+        {
+            get { return this._throwException; }
+            set
+            {
+                if (value && this.Exception == null)
+                {
+                    throw new ArgumentException("Cannot Throw Null Exception");
+                }
+                this._throwException = value;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Specifies date formats for a
+    /// <see cref="T:Avalonia.Controls.DatePicker" />.
+    /// </summary>
+    public enum DatePickerFormat
+    {
+        /// <summary>
+        /// Specifies that the date should be displayed using unabbreviated days
+        /// of the week and month names.
+        /// </summary>
+        Long = 0,
+
+        /// <summary>
+        /// Specifies that the date should be displayed using abbreviated days
+        /// of the week and month names.
+        /// </summary>
+        Short = 1,
+
+        /// <summary>
+        /// Specifies that the date should be displayed using a custom format string.
+        /// </summary>
+        Custom = 2
+    }
+
+    public class DatePicker : TemplatedControl
+    {
+        private const string ElementTextBox = "PART_TextBox";
+        private const string ElementButton = "PART_Button";
+        private const string ElementPopup = "PART_Popup";
+        private const string ElementCalendar = "PART_Calendar";
+
+        private Calendar _calendar;
+        private string _defaultText;
+        private Button _dropDownButton;
+        //private Canvas _outsideCanvas;
+        //private Canvas _outsidePopupCanvas;
+        private Popup _popUp;
+        private TextBox _textBox;
+        private IDisposable _textBoxTextChangedSubscription;
+        private IDisposable _buttonPointerPressedSubscription;
+
+        private DateTime? _onOpenSelectedDate;
+        private bool _settingSelectedDate;
+
+        private DateTime _displayDate;
+        private DateTime? _displayDateStart;
+        private DateTime? _displayDateEnd;
+        private bool _isDropDownOpen;
+        private DateTime? _selectedDate;
+        private string _text;
+        private bool _suspendTextChangeHandler = false;
+        private bool _isPopupClosing = false;
+        private bool _ignoreButtonClick = false;
+
+        /// <summary>
+        /// Gets a collection of dates that are marked as not selectable.
+        /// </summary>
+        /// <value>
+        /// A collection of dates that cannot be selected. The default value is
+        /// an empty collection.
+        /// </value>
+        public CalendarBlackoutDatesCollection BlackoutDates { get; private set; }
+
+        public static readonly DirectProperty<DatePicker, DateTime> DisplayDateProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime>(
+                nameof(DisplayDate),
+                o => o.DisplayDate,
+                (o, v) => o.DisplayDate = v);
+        public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateStartProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(DisplayDateStart),
+                o => o.DisplayDateStart,
+                (o, v) => o.DisplayDateStart = v);
+        public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateEndProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(DisplayDateEnd),
+                o => o.DisplayDateEnd,
+                (o, v) => o.DisplayDateEnd = v);
+        public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
+            AvaloniaProperty.Register<DatePicker, DayOfWeek>(nameof(FirstDayOfWeek));
+
+        public static readonly DirectProperty<DatePicker, bool> IsDropDownOpenProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, bool>(
+                nameof(IsDropDownOpen),
+                o => o.IsDropDownOpen,
+                (o, v) => o.IsDropDownOpen = v);
+
+        public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
+            AvaloniaProperty.Register<DatePicker, bool>(nameof(IsTodayHighlighted));
+        public static readonly DirectProperty<DatePicker, DateTime?> SelectedDateProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(SelectedDate),
+                o => o.SelectedDate,
+                (o, v) => o.SelectedDate = v);
+
+        public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty =
+            AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
+                nameof(SelectedDateFormat),
+                defaultValue: DatePickerFormat.Short,
+                validate: ValidateSelectedDateFormat);
+
+        public static readonly StyledProperty<string> CustomDateFormatStringProperty =
+            AvaloniaProperty.Register<DatePicker, string>(
+                nameof(CustomDateFormatString),
+                defaultValue: "d",
+                validate: ValidateDateFormatString);
+
+        public static readonly DirectProperty<DatePicker, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, string>(
+                nameof(Text),
+                o => o.Text,
+                (o, v) => o.Text = v);
+        public static readonly StyledProperty<string> WatermarkProperty =
+            TextBox.WatermarkProperty.AddOwner<DatePicker>();
+        public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
+            TextBox.UseFloatingWatermarkProperty.AddOwner<DatePicker>();
+
+
+        /// <summary>
+        /// Gets or sets the date to display.
+        /// </summary>
+        /// <value>
+        /// The date to display. The default 
+        /// <see cref="P:System.DateTime.Today" />.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The specified date is not in the range defined by
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateStart" />
+        /// and
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateEnd" />.
+        /// </exception>
+        public DateTime DisplayDate
+        {
+            get { return _displayDate; }
+            set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); }
+        }
+        
+        /// <summary>
+        /// Gets or sets the first date to be displayed.
+        /// </summary>
+        /// <value>The first date to display.</value>
+        public DateTime? DisplayDateStart
+        {
+            get { return _displayDateStart; }
+            set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the last date to be displayed.
+        /// </summary>
+        /// <value>The last date to display.</value>
+        public DateTime? DisplayDateEnd
+        {
+            get { return _displayDateEnd; }
+            set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the day that is considered the beginning of the week.
+        /// </summary>
+        /// <value>
+        /// A <see cref="T:System.DayOfWeek" /> representing the beginning of
+        /// the week. The default is <see cref="F:System.DayOfWeek.Sunday" />.
+        /// </value>
+        public DayOfWeek FirstDayOfWeek
+        {
+            get { return GetValue(FirstDayOfWeekProperty); }
+            set { SetValue(FirstDayOfWeekProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is open or closed.
+        /// </summary>
+        /// <value>
+        /// True if the <see cref="T:Avalonia.Controls.Calendar" /> is
+        /// open; otherwise, false. The default is false.
+        /// </value>
+        public bool IsDropDownOpen
+        {
+            get { return _isDropDownOpen; }
+            set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the current date will be
+        /// highlighted.
+        /// </summary>
+        /// <value>
+        /// True if the current date is highlighted; otherwise, false. The
+        /// default is true.
+        /// </value>
+        public bool IsTodayHighlighted
+        {
+            get { return GetValue(IsTodayHighlightedProperty); }
+            set { SetValue(IsTodayHighlightedProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the currently selected date.
+        /// </summary>
+        /// <value>
+        /// The date currently selected. The default is null.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The specified date is not in the range defined by
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateStart" />
+        /// and
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateEnd" />,
+        /// or the specified date is in the
+        /// <see cref="P:Avalonia.Controls.DatePicker.BlackoutDates" />
+        /// collection.
+        /// </exception>
+        public DateTime? SelectedDate
+        {
+            get { return _selectedDate; }
+            set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the format that is used to display the selected date.
+        /// </summary>
+        /// <value>
+        /// The format that is used to display the selected date. The default is
+        /// <see cref="F:Avalonia.Controls.DatePickerFormat.Short" />.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// An specified format is not valid.
+        /// </exception>
+        public DatePickerFormat SelectedDateFormat
+        {
+            get { return GetValue(SelectedDateFormatProperty); }
+            set { SetValue(SelectedDateFormatProperty, value); }
+        }
+
+        public string CustomDateFormatString
+        {
+            get { return GetValue(CustomDateFormatStringProperty); }
+            set { SetValue(CustomDateFormatStringProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the text that is displayed by the
+        /// <see cref="T:Avalonia.Controls.DatePicker" />.
+        /// </summary>
+        /// <value>
+        /// The text displayed by the
+        /// <see cref="T:Avalonia.Controls.DatePicker" />.
+        /// </value>
+        /// <exception cref="T:System.FormatException">
+        /// The text entered cannot be parsed to a valid date, and the exception
+        /// is not suppressed.
+        /// </exception>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The text entered parses to a date that is not selectable.
+        /// </exception>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+        public bool UseFloatingWatermark
+        {
+            get { return GetValue(UseFloatingWatermarkProperty); }
+            set { SetValue(UseFloatingWatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Occurs when the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is closed.
+        /// </summary>
+        public event EventHandler CalendarClosed;
+
+        /// <summary>
+        /// Occurs when the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is opened.
+        /// </summary>
+        public event EventHandler CalendarOpened;
+
+        /// <summary>
+        /// Occurs when <see cref="P:Avalonia.Controls.DatePicker.Text" />
+        /// is assigned a value that cannot be interpreted as a date.
+        /// </summary>
+        public event EventHandler<DatePickerDateValidationErrorEventArgs> DateValidationError;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.DatePicker.SelectedDate" />
+        /// property is changed.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectedDateChanged;
+
+        static DatePicker()
+        {
+            FocusableProperty.OverrideDefaultValue<DatePicker>(true);
+
+            DisplayDateProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateChanged);
+            DisplayDateStartProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateStartChanged);
+            DisplayDateEndProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateEndChanged);
+            IsDropDownOpenProperty.Changed.AddClassHandler<DatePicker>(x => x.OnIsDropDownOpenChanged);
+            SelectedDateProperty.Changed.AddClassHandler<DatePicker>(x => x.OnSelectedDateChanged);
+            SelectedDateFormatProperty.Changed.AddClassHandler<DatePicker>(x => x.OnSelectedDateFormatChanged);
+            CustomDateFormatStringProperty.Changed.AddClassHandler<DatePicker>(x => x.OnCustomDateFormatStringChanged);
+            TextProperty.Changed.AddClassHandler<DatePicker>(x => x.OnTextChanged);
+        }
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.DatePicker" /> class.
+        /// </summary>
+        public DatePicker()
+        {
+            FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;
+            _defaultText = string.Empty;
+            DisplayDate = DateTime.Today;
+        }
+
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                _calendar.DayButtonMouseUp -= Calendar_DayButtonMouseUp;
+                _calendar.DisplayDateChanged -= Calendar_DisplayDateChanged;
+                _calendar.SelectedDatesChanged -= Calendar_SelectedDatesChanged;
+                _calendar.PointerPressed -= Calendar_PointerPressed;
+                _calendar.KeyDown -= Calendar_KeyDown;
+            }
+            _calendar = e.NameScope.Find<Calendar>(ElementCalendar);
+            if (_calendar != null)
+            {
+                _calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+                _calendar.SelectedDate = SelectedDate;
+                SetCalendarDisplayDate(DisplayDate);
+                SetCalendarDisplayDateStart(DisplayDateStart);
+                SetCalendarDisplayDateEnd(DisplayDateEnd);
+
+                _calendar.DayButtonMouseUp += Calendar_DayButtonMouseUp;
+                _calendar.DisplayDateChanged += Calendar_DisplayDateChanged;
+                _calendar.SelectedDatesChanged += Calendar_SelectedDatesChanged;
+                _calendar.PointerPressed += Calendar_PointerPressed;
+                _calendar.KeyDown += Calendar_KeyDown;
+                //_calendar.SizeChanged += new SizeChangedEventHandler(Calendar_SizeChanged);
+                //_calendar.IsTabStop = true;
+
+                var currentBlackoutDays = BlackoutDates;
+                BlackoutDates = _calendar.BlackoutDates;
+                if(currentBlackoutDays != null)
+                {
+                    foreach (var range in currentBlackoutDays)
+                    {
+                        BlackoutDates.Add(range);
+                    }
+                }
+            }
+
+            if (_popUp != null)
+            {
+                _popUp.Child = null;
+                _popUp.Closed -= PopUp_Closed;
+            }
+            _popUp = e.NameScope.Find<Popup>(ElementPopup);
+            if(_popUp != null)
+            {
+                _popUp.Closed += PopUp_Closed;
+                if (IsDropDownOpen)
+                {
+                    OpenDropDown();
+                }
+            }
+
+            if(_dropDownButton != null)
+            {
+                _dropDownButton.Click -= DropDownButton_Click;
+                _buttonPointerPressedSubscription?.Dispose();
+            }
+            _dropDownButton = e.NameScope.Find<Button>(ElementButton);
+            if(_dropDownButton != null)
+            {
+                _dropDownButton.Click += DropDownButton_Click;
+                _buttonPointerPressedSubscription =
+                    _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
+            }
+
+            if (_textBox != null)
+            {
+                _textBox.KeyDown -= TextBox_KeyDown;
+                _textBox.GotFocus -= TextBox_GotFocus;
+                _textBoxTextChangedSubscription?.Dispose();
+            }
+            _textBox = e.NameScope.Find<TextBox>(ElementTextBox);
+
+            if(!SelectedDate.HasValue)
+            {
+                SetWaterMarkText();
+            }
+
+            if(_textBox != null)
+            {
+                _textBox.KeyDown += TextBox_KeyDown;
+                _textBox.GotFocus += TextBox_GotFocus;
+                _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBox_TextChanged());
+
+                if(SelectedDate.HasValue)
+                {
+                    _textBox.Text = DateTimeToString(SelectedDate.Value);
+                }
+                else if(!String.IsNullOrEmpty(_defaultText))
+                {
+                    _textBox.Text = _defaultText;
+                    SetSelectedDate();
+                }
+            }
+
+            base.OnTemplateApplied(e);
+        }
+
+        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        {
+            if (property == SelectedDateProperty)
+            {
+                DataValidationErrors.SetError(this, status.Error);
+            }
+        }
+
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+            if (!e.Handled && SelectedDate.HasValue && _calendar != null)
+            {
+                DateTime selectedDate = this.SelectedDate.Value;
+                DateTime? newDate = DateTimeHelper.AddDays(selectedDate, e.Delta.Y > 0 ? -1 : 1);
+                if (newDate.HasValue && Calendar.IsValidDateSelection(_calendar, newDate.Value))
+                {
+                    SelectedDate = newDate;
+                    e.Handled = true;
+                }
+            }
+        }
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            base.OnGotFocus(e);
+            if(IsEnabled && _textBox != null && e.NavigationMethod == NavigationMethod.Tab)
+            {
+                _textBox.Focus();
+                var text = _textBox.Text;
+                if(!string.IsNullOrEmpty(text))
+                {
+                    _textBox.SelectionStart = 0;
+                    _textBox.SelectionEnd = text.Length;
+                }
+            }
+        }
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+
+            SetSelectedDate();
+        }
+        
+        private void SetCalendarDisplayDate(DateTime value)
+        {
+            if (DateTimeHelper.CompareYearMonth(_calendar.DisplayDate, value) != 0)
+            {
+                _calendar.DisplayDate = DisplayDate;
+                if (DateTime.Compare(_calendar.DisplayDate, DisplayDate) != 0)
+                {
+                    DisplayDate = _calendar.DisplayDate;
+                }
+            }
+        }
+        private void OnDisplayDateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime)e.NewValue;
+                SetCalendarDisplayDate(value);
+            }
+        }
+        private void SetCalendarDisplayDateStart(DateTime? value)
+        {
+            _calendar.DisplayDateStart = value;
+            if (_calendar.DisplayDateStart.HasValue && DisplayDateStart.HasValue && DateTime.Compare(_calendar.DisplayDateStart.Value, DisplayDateStart.Value) != 0)
+            {
+                DisplayDateStart = _calendar.DisplayDateStart;
+            }
+        }
+        private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime?)e.NewValue;
+                SetCalendarDisplayDateStart(value);
+            }
+        }
+        private void SetCalendarDisplayDateEnd(DateTime? value)
+        {
+            _calendar.DisplayDateEnd = value;
+            if (_calendar.DisplayDateEnd.HasValue && DisplayDateEnd.HasValue && DateTime.Compare(_calendar.DisplayDateEnd.Value, DisplayDateEnd.Value) != 0)
+            {
+                DisplayDateEnd = _calendar.DisplayDateEnd;
+            }
+
+        }
+        private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime?)e.NewValue;
+                SetCalendarDisplayDateEnd(value);
+            }
+        }
+        private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (bool)e.OldValue;
+            var value = (bool)e.NewValue;
+
+            if (_popUp != null && _popUp.Child != null)
+            {
+                if (value != oldValue)
+                {
+                    if (_calendar.DisplayMode != CalendarMode.Month)
+                    {
+                        _calendar.DisplayMode = CalendarMode.Month;
+                    }
+
+                    if (value)
+                    {
+                        OpenDropDown();
+                    }
+                    else
+                    {
+                        _popUp.IsOpen = false;
+                        OnCalendarClosed(new RoutedEventArgs());
+                    }
+                }
+            }
+        }
+        private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var addedDate = (DateTime?)e.NewValue;
+            var removedDate = (DateTime?)e.OldValue;
+
+            if (_calendar != null && addedDate != _calendar.SelectedDate)
+            {
+                _calendar.SelectedDate = addedDate;
+            }
+
+            if (SelectedDate != null)
+            {
+                DateTime day = SelectedDate.Value;
+
+                // When the SelectedDateProperty change is done from
+                // OnTextPropertyChanged method, two-way binding breaks if
+                // BeginInvoke is not used:
+                Threading.Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    _settingSelectedDate = true;
+                    Text = DateTimeToString(day);
+                    _settingSelectedDate = false;
+                    OnDateSelected(addedDate, removedDate);
+                });
+
+                // When DatePickerDisplayDateFlag is TRUE, the SelectedDate
+                // change is coming from the Calendar UI itself, so, we
+                // shouldn't change the DisplayDate since it will automatically
+                // be changed by the Calendar
+                if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.DatePickerDisplayDateFlag))
+                {
+                    DisplayDate = day;
+                }
+                if(_calendar != null)
+                    _calendar.DatePickerDisplayDateFlag = false;
+            }
+            else
+            {
+                _settingSelectedDate = true;
+                SetWaterMarkText();
+                _settingSelectedDate = false;
+                OnDateSelected(addedDate, removedDate);
+            }
+        }
+        private void OnDateFormatChanged()
+        {
+            if (_textBox != null)
+            {
+                if (SelectedDate.HasValue)
+                {
+                    Text = DateTimeToString(SelectedDate.Value);
+                }
+                else if (string.IsNullOrEmpty(_textBox.Text))
+                {
+                    SetWaterMarkText();
+                }
+                else
+                {
+                    DateTime? date = ParseText(_textBox.Text);
+
+                    if (date != null)
+                    {
+                        string s = DateTimeToString((DateTime)date);
+                        Text = s;
+                    }
+                }
+            }
+        }
+        private void OnSelectedDateFormatChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            OnDateFormatChanged();
+        }
+        private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if(SelectedDateFormat == DatePickerFormat.Custom)
+            {
+                OnDateFormatChanged();
+            }
+        }
+        private void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (string)e.OldValue;
+            var value = (string)e.NewValue;
+
+            if (!_suspendTextChangeHandler)
+            {
+                if (value != null)
+                {
+                    if (_textBox != null)
+                    {
+                        _textBox.Text = value;
+                    }
+                    else
+                    {
+                        _defaultText = value;
+                    }
+                    if (!_settingSelectedDate)
+                    {
+                        SetSelectedDate();
+                    }
+                }
+                else
+                {
+                    if (!_settingSelectedDate)
+                    {
+                        _settingSelectedDate = true;
+                        SelectedDate = null;
+                        _settingSelectedDate = false;
+                    }
+                }
+            }
+            else
+            {
+                SetWaterMarkText();
+            }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <param name="e">
+        /// A
+        /// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" />
+        /// that contains the event data.
+        /// </param>
+        protected virtual void OnDateValidationError(DatePickerDateValidationErrorEventArgs e)
+        {
+            DateValidationError?.Invoke(this, e);
+        }
+        private void OnDateSelected(DateTime? addedDate, DateTime? removedDate)
+        {
+            EventHandler<SelectionChangedEventArgs> handler = this.SelectedDateChanged;
+            if (null != handler)
+            {
+                Collection<DateTime> addedItems = new Collection<DateTime>();
+                Collection<DateTime> removedItems = new Collection<DateTime>();
+
+                if (addedDate.HasValue)
+                {
+                    addedItems.Add(addedDate.Value);
+                }
+
+                if (removedDate.HasValue)
+                {
+                    removedItems.Add(removedDate.Value);
+                }
+
+                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, addedItems, removedItems));
+            }
+        }
+        private void OnCalendarClosed(EventArgs e)
+        {
+            CalendarClosed?.Invoke(this, e);
+        }
+        private void OnCalendarOpened(EventArgs e)
+        {
+            CalendarOpened?.Invoke(this, e);
+        }
+
+        private void Calendar_DayButtonMouseUp(object sender, PointerReleasedEventArgs e)
+        {
+            Focus();
+            IsDropDownOpen = false;
+        }      
+        private void Calendar_DisplayDateChanged(object sender, CalendarDateChangedEventArgs e)
+        {
+            if (e.AddedDate != this.DisplayDate)
+            {
+                SetValue(DisplayDateProperty, (DateTime) e.AddedDate);
+            }
+        }
+        private void Calendar_SelectedDatesChanged(object sender, SelectionChangedEventArgs e)
+        {
+            Debug.Assert(e.AddedItems.Count < 2, "There should be less than 2 AddedItems!");
+
+            if (e.AddedItems.Count > 0 && SelectedDate.HasValue && DateTime.Compare((DateTime)e.AddedItems[0], SelectedDate.Value) != 0)
+            {
+                SelectedDate = (DateTime?)e.AddedItems[0];
+            }
+            else
+            {
+                if (e.AddedItems.Count == 0)
+                {
+                    SelectedDate = null;
+                    return;
+                }
+
+                if (!SelectedDate.HasValue)
+                {
+                    if (e.AddedItems.Count > 0)
+                    {
+                        SelectedDate = (DateTime?)e.AddedItems[0];
+                    }
+                }
+            }
+        }
+        private void Calendar_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if (e.MouseButton == MouseButton.Left)
+            {
+                e.Handled = true;
+            }
+        }
+        private void Calendar_KeyDown(object sender, KeyEventArgs e)
+        {
+            Calendar c = sender as Calendar;
+            Debug.Assert(c != null, "The Calendar should not be null!");
+
+            if (!e.Handled && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape) && c.DisplayMode == CalendarMode.Month)
+            {
+                Focus();
+                IsDropDownOpen = false;
+
+                if (e.Key == Key.Escape)
+                {
+                    SelectedDate = _onOpenSelectedDate;
+                }
+            }
+        }
+        private void TextBox_GotFocus(object sender, RoutedEventArgs e)
+        {
+            IsDropDownOpen = false;
+        }
+        private void TextBox_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (!e.Handled)
+            {
+                e.Handled = ProcessDatePickerKey(e);
+            }
+        }
+        private void TextBox_TextChanged()
+        {
+            if (_textBox != null)
+            {
+                _suspendTextChangeHandler = true;
+                Text = _textBox.Text;
+                _suspendTextChangeHandler = false;
+            }
+        }
+        private void DropDownButton_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            _ignoreButtonClick = _isPopupClosing;
+        }
+        private void DropDownButton_Click(object sender, RoutedEventArgs e)
+        {
+            if (!_ignoreButtonClick)
+            {
+                HandlePopUp();
+            }
+            else
+            {
+                _ignoreButtonClick = false;
+            }
+        }
+        private void PopUp_Closed(object sender, EventArgs e)
+        {
+            IsDropDownOpen = false;
+
+            if(!_isPopupClosing)
+            {
+                _isPopupClosing = true;
+                Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
+            }
+        }
+
+        private void HandlePopUp()
+        {
+            if (IsDropDownOpen)
+            {
+                Focus();
+                IsDropDownOpen = false;
+            }
+            else
+            {
+                ProcessTextBox();
+            }
+        }
+        private void OpenDropDown()
+        {
+            if (_calendar != null)
+            {
+                _calendar.Focus();
+                OpenPopUp();
+                _calendar.ResetStates();
+                OnCalendarOpened(new RoutedEventArgs());
+            }
+        }
+        private void OpenPopUp()
+        {
+            _onOpenSelectedDate = SelectedDate;
+            _popUp.IsOpen = true;
+        }
+
+        /// <summary>
+        /// Input text is parsed in the correct format and changed into a
+        /// DateTime object.  If the text can not be parsed TextParseError Event
+        /// is thrown.
+        /// </summary>
+        /// <param name="text">Inherited code: Requires comment.</param>
+        /// <returns>
+        /// IT SHOULD RETURN NULL IF THE STRING IS NOT VALID, RETURN THE
+        /// DATETIME VALUE IF IT IS VALID.
+        /// </returns>
+        private DateTime? ParseText(string text)
+        {
+            DateTime newSelectedDate;
+
+            // TryParse is not used in order to be able to pass the exception to
+            // the TextParseError event
+            try
+            {
+                newSelectedDate = DateTime.Parse(text, DateTimeHelper.GetCurrentDateFormat());
+
+                if (Calendar.IsValidDateSelection(this._calendar, newSelectedDate))
+                {
+                    return newSelectedDate;
+                }
+                else
+                {
+                    var dateValidationError = new DatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException("text", "SelectedDate value is not valid."), text);
+                    OnDateValidationError(dateValidationError);
+
+                    if (dateValidationError.ThrowException)
+                    {
+                        throw dateValidationError.Exception;
+                    }
+                }
+            }
+            catch (FormatException ex)
+            {
+                DatePickerDateValidationErrorEventArgs textParseError = new DatePickerDateValidationErrorEventArgs(ex, text);
+                OnDateValidationError(textParseError);
+
+                if (textParseError.ThrowException)
+                {
+                    throw textParseError.Exception;
+                }
+            }
+            return null;
+        }
+        private string DateTimeToString(DateTime d)
+        {
+            DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
+
+            switch (SelectedDateFormat)
+            {
+                case DatePickerFormat.Short:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi));
+                case DatePickerFormat.Long:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi));
+                case DatePickerFormat.Custom:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi));
+            }
+            return null;
+        }
+        private bool ProcessDatePickerKey(KeyEventArgs e)
+        {
+            
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    {
+                        SetSelectedDate();
+                        return true;
+                    }
+                case Key.Down:
+                    { 
+                        if ((e.Modifiers & InputModifiers.Control) == InputModifiers.Control)
+                        {
+                            HandlePopUp();
+                            return true;
+                        }
+                        break;
+                    }
+            }
+            return false;
+        }
+        private void ProcessTextBox()
+        {
+            SetSelectedDate();
+            IsDropDownOpen = true;
+            _calendar.Focus();
+        }
+        private void SetSelectedDate()
+        {
+            if (_textBox != null)
+            {
+                if (!string.IsNullOrEmpty(_textBox.Text))
+                {
+                    string s = _textBox.Text;
+
+                    if (SelectedDate != null)
+                    {
+                        // If the string value of the SelectedDate and the
+                        // TextBox string value are equal, we do not parse the
+                        // string again if we do an extra parse, we lose data in
+                        // M/d/yy format.
+                        // ex: SelectedDate = DateTime(1008,12,19) but when
+                        // "12/19/08" is parsed it is interpreted as
+                        // DateTime(2008,12,19)
+                        string selectedDate = DateTimeToString(SelectedDate.Value);
+                        if (selectedDate == s)
+                        {
+                            return;
+                        }
+                    }
+                    DateTime? d = SetTextBoxValue(s);
+                    if (!SelectedDate.Equals(d))
+                    {
+                        SelectedDate = d;
+                    }
+                }
+                else
+                {
+                    if (SelectedDate != null)
+                    {
+                        SelectedDate = null;
+                    }
+                }
+            }
+            else
+            {
+                DateTime? d = SetTextBoxValue(_defaultText);
+                if (!SelectedDate.Equals(d))
+                {
+                    SelectedDate = d;
+                }
+            }
+        }
+        private DateTime? SetTextBoxValue(string s)
+        {
+            if (string.IsNullOrEmpty(s))
+            {
+                SetValue(TextProperty, s);
+                return SelectedDate;
+            }
+            else
+            {
+                DateTime? d = ParseText(s);
+                if (d != null)
+                {
+                    SetValue(TextProperty, s);
+                    return d;
+                }
+                else
+                {
+                    // If parse error: TextBox should have the latest valid
+                    // selecteddate value:
+                    if (SelectedDate != null)
+                    {
+                        string newtext = this.DateTimeToString(SelectedDate.Value);
+                        SetValue(TextProperty, newtext);
+                        return SelectedDate;
+                    }
+                    else
+                    {
+                        SetWaterMarkText();
+                        return null;
+                    }
+                }
+            }
+        }
+        private void SetWaterMarkText()
+        {
+            if (_textBox != null)
+            {
+                if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark)
+                {
+                    DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
+                    Text = string.Empty;
+                    _defaultText = string.Empty;
+                    var watermarkFormat = "<{0}>";
+                    string watermarkText;
+
+                    switch (SelectedDateFormat)
+                    {
+                        case DatePickerFormat.Long:
+                            {
+                                watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString());
+                                break;
+                            }
+                        case DatePickerFormat.Short:
+                        default:
+                            {
+                                watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString());
+                                break;
+                            }
+                    }
+                    _textBox.Watermark = watermarkText;
+                }
+                else
+                {
+                    _textBox.ClearValue(TextBox.WatermarkProperty);
+                }
+            }
+        }
+
+        private static bool IsValidSelectedDateFormat(DatePickerFormat value)
+        {
+            return value == DatePickerFormat.Long
+                || value == DatePickerFormat.Short
+                || value == DatePickerFormat.Custom;
+        }
+        private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format)
+        {
+            if(IsValidSelectedDateFormat(format))
+            {
+                return format;
+            }
+            else
+            {
+                throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid.");
+            }
+        }
+        private static string ValidateDateFormatString(DatePicker dp, string formatString)
+        {
+            if(string.IsNullOrWhiteSpace(formatString))
+            {
+                throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString));
+            }
+            else
+            {
+                return formatString;
+            }
+        }
+        private static DateTime DiscardDayTime(DateTime d)
+        {
+            int year = d.Year;
+            int month = d.Month;
+            DateTime newD = new DateTime(year, month, 1, 0, 0, 0);
+            return newD;
+        }
+        private static DateTime? DiscardTime(DateTime? d)
+        {
+            if (d == null)
+            {
+                return null;
+            }
+            else
+            {
+                DateTime discarded = (DateTime) d;
+                int year = discarded.Year;
+                int month = discarded.Month;
+                int day = discarded.Day;
+                DateTime newD = new DateTime(year, month, day, 0, 0, 0);
+                return newD;
+            }
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Controls/ColumnDefinitions.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
         public ColumnDefinitions(string s)
             : this()
         {
-            AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new ColumnDefinition(x)));
+            AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
         }
     }
 }

+ 4 - 3
src/Avalonia.Controls/ContextMenu.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Controls
         {
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
 
-            MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);            
+            MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
         }
 
         /// <summary>
@@ -75,13 +75,14 @@ namespace Avalonia.Controls
         {
             if (control != null)
             {
-                if(_popup == null)
+                if (_popup == null)
                 {
                     _popup = new Popup()
                     {
                         PlacementMode = PlacementMode.Pointer,
                         PlacementTarget = control,
-                        StaysOpen = false                                         
+                        StaysOpen = false,
+                        ObeyScreenEdges = true
                     };
 
                     _popup.Closed += PopupClosed;

+ 1 - 0
src/Avalonia.Controls/DropDown.cs

@@ -164,6 +164,7 @@ namespace Avalonia.Controls
                 else
                 {
                     IsDropDownOpen = !IsDropDownOpen;
+                    e.Handled = true;
                 }
             }
             base.OnPointerPressed(e);

+ 12 - 7
src/Avalonia.Controls/GridLength.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -179,9 +180,8 @@ namespace Avalonia.Controls
         /// Parses a string to return a <see cref="GridLength"/>.
         /// </summary>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="GridLength"/>.</returns>
-        public static GridLength Parse(string s, CultureInfo culture)
+        public static GridLength Parse(string s)
         {
             s = s.ToUpperInvariant();
 
@@ -192,12 +192,12 @@ namespace Avalonia.Controls
             else if (s.EndsWith("*"))
             {
                 var valueString = s.Substring(0, s.Length - 1).Trim();
-                var value = valueString.Length > 0 ? double.Parse(valueString, culture) : 1;
+                var value = valueString.Length > 0 ? double.Parse(valueString, CultureInfo.InvariantCulture) : 1;
                 return new GridLength(value, GridUnitType.Star);
             }
             else
             {
-                var value = double.Parse(s, culture);
+                var value = double.Parse(s, CultureInfo.InvariantCulture);
                 return new GridLength(value, GridUnitType.Pixel);
             }
         }
@@ -206,11 +206,16 @@ namespace Avalonia.Controls
         /// Parses a string to return a collection of <see cref="GridLength"/>s.
         /// </summary>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="GridLength"/>.</returns>
-        public static IEnumerable<GridLength> ParseLengths(string s, CultureInfo culture)
+        public static IEnumerable<GridLength> ParseLengths(string s)
         {
-            return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Parse(x, culture));
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture))
+            {
+                while (tokenizer.TryReadString(out var item))
+                {
+                    yield return Parse(item);
+                }
+            }
         }
     }
 }

+ 5 - 7
src/Avalonia.Controls/ItemsControl.cs

@@ -1,6 +1,7 @@
 // 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;
 using System.Collections.Generic;
 using System.Collections.Specialized;
@@ -54,6 +55,7 @@ namespace Avalonia.Controls
 
         private IEnumerable _items = new AvaloniaList<object>();
         private IItemContainerGenerator _itemContainerGenerator;
+        private IDisposable _itemsCollectionChangedSubscription;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsControl"/> class.
@@ -326,12 +328,8 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            var incc = e.OldValue as INotifyCollectionChanged;
-
-            if (incc != null)
-            {
-                incc.CollectionChanged -= ItemsCollectionChanged;
-            }
+            _itemsCollectionChangedSubscription?.Dispose();
+            _itemsCollectionChangedSubscription = null;
 
             var oldValue = e.OldValue as IEnumerable;
             var newValue = e.NewValue as IEnumerable;
@@ -428,7 +426,7 @@ namespace Avalonia.Controls
 
             if (incc != null)
             {
-                incc.CollectionChanged += ItemsCollectionChanged;
+                _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
             }
         }
 

+ 6 - 0
src/Avalonia.Controls/LayoutTransformControl.cs

@@ -15,6 +15,9 @@ using System.Reactive.Linq;
 
 namespace Avalonia.Controls
 {
+    /// <summary>
+    /// Control that implements support for transformations as if applied by LayoutTransform.
+    /// </summary>
     public class LayoutTransformControl : ContentControl
     {
         public static readonly AvaloniaProperty<Transform> LayoutTransformProperty =
@@ -26,6 +29,9 @@ namespace Avalonia.Controls
                 .AddClassHandler<LayoutTransformControl>(x => x.OnLayoutTransformChanged);
         }
 
+        /// <summary>
+        /// Gets or sets a graphics transformation that should apply to this element when layout is performed.
+        /// </summary>
         public Transform LayoutTransform
         {
             get { return GetValue(LayoutTransformProperty); }

+ 35 - 0
src/Avalonia.Controls/MenuItem.cs

@@ -93,6 +93,7 @@ namespace Avalonia.Controls
         static MenuItem()
         {
             SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
+            CommandProperty.Changed.Subscribe(CommandChanged);
             FocusableProperty.OverrideDefaultValue<MenuItem>(true);
             IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
             ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
@@ -424,6 +425,40 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called when the <see cref="Command"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is MenuItem menuItem)
+            {
+                if (e.OldValue is ICommand oldCommand)
+                {
+                    oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
+                }
+
+                if (e.NewValue is ICommand newCommand)
+                {
+                    newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                }
+
+                menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void CanExecuteChanged(object sender, EventArgs e)
+        {
+            // HACK: Just set the IsEnabled property for the moment. This needs to be changed to
+            // use IsEnabledCore etc. but it will do for now.
+            IsEnabled = Command == null || Command.CanExecute(CommandParameter);
+        }
+
         /// <summary>
         /// Called when the <see cref="Icon"/> property changes.
         /// </summary>

+ 998 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -0,0 +1,998 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
+    /// </summary>
+    public class NumericUpDown : TemplatedControl
+    {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ClipValueToMinMax"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
+                updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
+
+        /// <summary>
+        /// Defines the <see cref="CultureInfo"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo,
+                (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
+
+        /// <summary>
+        /// Defines the <see cref="FormatString"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string> FormatStringProperty =
+            AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty);
+
+        /// <summary>
+        /// Defines the <see cref="Increment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> IncrementProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
+
+        /// <summary>
+        /// Defines the <see cref="IsReadOnly"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsReadOnlyProperty =
+            AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly));
+
+        /// <summary>
+        /// Defines the <see cref="Maximum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MaximumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
+
+        /// <summary>
+        /// Defines the <see cref="Minimum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MinimumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
+
+        /// <summary>
+        /// Defines the <see cref="ParsingNumberStyle"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
+                updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
+
+        /// <summary>
+        /// Defines the <see cref="Text"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="Value"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
+                (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="Watermark"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string> WatermarkProperty =
+            AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark));
+
+        private IDisposable _textBoxTextChangedSubscription;
+
+        private double _value;
+        private string _text;
+        private bool _internalValueSet;
+        private bool _clipValueToMinMax;
+        private bool _isSyncingTextAndValueProperties;
+        private bool _isTextChangedFromUI;
+        private CultureInfo _cultureInfo;
+        private NumberStyles _parsingNumberStyle = NumberStyles.Any;
+        
+        /// <summary>
+        /// Gets the Spinner template part.
+        /// </summary>
+        private Spinner Spinner { get; set; }
+
+        /// <summary>
+        /// Gets the TextBox template part.
+        /// </summary>
+        private TextBox TextBox { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
+        /// </summary>
+        public bool AllowSpin
+        {
+            get { return GetValue(AllowSpinProperty); }
+            set { SetValue(AllowSpinProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
+        /// </summary>
+        public Location ButtonSpinnerLocation
+        {
+            get { return GetValue(ButtonSpinnerLocationProperty); }
+            set { SetValue(ButtonSpinnerLocationProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the spin buttons should be shown.
+        /// </summary>
+        public bool ShowButtonSpinner
+        {
+            get { return GetValue(ShowButtonSpinnerProperty); }
+            set { SetValue(ShowButtonSpinnerProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets if the value should be clipped when minimum/maximum is reached.
+        /// </summary>
+        public bool ClipValueToMinMax
+        {
+            get { return _clipValueToMinMax; }
+            set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the current CultureInfo.
+        /// </summary>
+        public CultureInfo CultureInfo
+        {
+            get { return _cultureInfo; }
+            set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the display format of the <see cref="Value"/>.
+        /// </summary>
+        public string FormatString
+        {
+            get { return GetValue(FormatStringProperty); }
+            set { SetValue(FormatStringProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the amount in which to increment the <see cref="Value"/>.
+        /// </summary>
+        public double Increment
+        {
+            get { return GetValue(IncrementProperty); }
+            set { SetValue(IncrementProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets if the control is read only.
+        /// </summary>
+        public bool IsReadOnly
+        {
+            get { return GetValue(IsReadOnlyProperty); }
+            set { SetValue(IsReadOnlyProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum allowed value.
+        /// </summary>
+        public double Maximum
+        {
+            get { return GetValue(MaximumProperty); }
+            set { SetValue(MaximumProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum allowed value.
+        /// </summary>
+        public double Minimum
+        {
+            get { return GetValue(MinimumProperty); }
+            set { SetValue(MinimumProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
+        /// </summary>
+        public NumberStyles ParsingNumberStyle
+        {
+            get { return _parsingNumberStyle; }
+            set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the formatted string representation of the value.
+        /// </summary>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the value.
+        /// </summary>
+        public double Value
+        {
+            get { return _value; }
+            set
+            {
+                value = OnCoerceValue(value);
+                SetAndRaise(ValueProperty, ref _value, value);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
+        /// </summary>
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Initializes new instance of <see cref="NumericUpDown"/> class.
+        /// </summary>
+        public NumericUpDown()
+        {
+            Initialized += (sender, e) =>
+            {
+                if (!_internalValueSet && IsInitialized)
+                {
+                    SyncTextAndValueProperties(false, null, true);
+                }
+
+                SetValidSpinDirection();
+            };
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="NumericUpDown"/> class.
+        /// </summary>
+        static NumericUpDown()
+        {
+            CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
+            FormatStringProperty.Changed.Subscribe(FormatStringChanged);
+            IncrementProperty.Changed.Subscribe(IncrementChanged);
+            IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
+            MaximumProperty.Changed.Subscribe(OnMaximumChanged);
+            MinimumProperty.Changed.Subscribe(OnMinimumChanged);
+            TextProperty.Changed.Subscribe(OnTextChanged);
+            ValueProperty.Changed.Subscribe(OnValueChanged);
+        }
+
+        /// <inheritdoc />
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            if (TextBox != null)
+            {
+                TextBox.PointerPressed -= TextBoxOnPointerPressed;
+                _textBoxTextChangedSubscription?.Dispose();
+            }
+            TextBox = e.NameScope.Find<TextBox>("PART_TextBox");
+            if (TextBox != null)
+            {
+                TextBox.Text = Text;
+                TextBox.PointerPressed += TextBoxOnPointerPressed;
+                _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
+            }
+
+            if (Spinner != null)
+            {
+                Spinner.Spin -= OnSpinnerSpin;
+            }
+
+            Spinner = e.NameScope.Find<Spinner>("PART_Spinner");
+
+            if (Spinner != null)
+            {
+                Spinner.Spin += OnSpinnerSpin;
+            }
+
+            SetValidSpinDirection();
+        }
+
+        /// <inheritdoc />
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    var commitSuccess = CommitInput();
+                    e.Handled = !commitSuccess;
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="CultureInfo"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="FormatString"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnFormatStringChanged(string oldValue, string newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnIncrementChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="IsReadOnly"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
+        {
+            SetValidSpinDirection();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMaximumChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+            if (ClipValueToMinMax)
+            {
+                Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMinimumChanged(double oldValue, double newValue)
+        {
+            if (IsInitialized)
+            {
+                SetValidSpinDirection();
+            }
+            if (ClipValueToMinMax)
+            {
+                Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Text"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnTextChanged(string oldValue, string newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(true, Text);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValueChanged(double oldValue, double newValue)
+        {
+            if (!_internalValueSet && IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null, true);
+            }
+
+            SetValidSpinDirection();
+
+            RaiseValueChangedEvent(oldValue, newValue);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceIncrement(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMaximum(double baseValue)
+        {
+            return Math.Max(baseValue, Minimum);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMinimum(double baseValue)
+        {
+            return Math.Min(baseValue, Maximum);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceValue(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Raises the OnSpin event when spinning is initiated by the end-user.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected virtual void OnSpin(SpinEventArgs e)
+        {
+            if (e == null)
+            {
+                throw new ArgumentNullException("e");
+            }
+
+            var handler = Spinned;
+            handler?.Invoke(this, e);
+
+            if (e.Direction == SpinDirection.Increase)
+            {
+                DoIncrement();
+            }
+            else
+            {
+                DoDecrement();
+            }
+        }
+
+        /// <summary>
+        /// Raises the <see cref="ValueChanged"/> event.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void RaiseValueChangedEvent(double oldValue, double newValue)
+        {
+            var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Converts the formatted text to a value.
+        /// </summary>
+        private double ConvertTextToValue(string text)
+        {
+            double result = 0;
+
+            if (string.IsNullOrEmpty(text))
+            {
+                return result;
+            }
+
+            // Since the conversion from Value to text using a FormartString may not be parsable,
+            // we verify that the already existing text is not the exact same value.
+            var currentValueText = ConvertValueToText();
+            if (Equals(currentValueText, text))
+            {
+                return Value;
+            }
+
+            result = ConvertTextToValueCore(currentValueText, text);
+
+            if (ClipValueToMinMax)
+            {
+                return MathUtilities.Clamp(result, Minimum, Maximum);
+            }
+
+            ValidateMinMax(result);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Converts the value to formatted text.
+        /// </summary>
+        /// <returns></returns>
+        private string ConvertValueToText()
+        {
+            //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
+            if (FormatString.Contains("{0"))
+            {
+                return string.Format(CultureInfo, FormatString, Value);
+            }
+
+            return Value.ToString(FormatString, CultureInfo);
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Increase.
+        /// </summary>
+        private void OnIncrement()
+        {
+            var result = Value + Increment;
+            Value = MathUtilities.Clamp(result, Minimum, Maximum);
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Descrease.
+        /// </summary>
+        private void OnDecrement()
+        {
+            var result = Value - Increment;
+            Value = MathUtilities.Clamp(result, Minimum, Maximum);
+        }
+
+        /// <summary>
+        /// Sets the valid spin directions.
+        /// </summary>
+        private void SetValidSpinDirection()
+        {
+            var validDirections = ValidSpinDirections.None;
+
+            // Zero increment always prevents spin.
+            if (Increment != 0 && !IsReadOnly)
+            {
+                if (Value < Maximum)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Increase;
+                }
+
+                if (Value > Minimum)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Decrease;
+                }
+            }
+
+            if (Spinner != null)
+            {
+                Spinner.ValidSpinDirection = validDirections;
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="CultureInfo"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (CultureInfo)e.OldValue;
+                var newValue = (CultureInfo)e.NewValue;
+                upDown.OnCultureInfoChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnIncrementChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="FormatString"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (string)e.OldValue;
+                var newValue = (string)e.NewValue;
+                upDown.OnFormatStringChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="IsReadOnly"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (bool)e.OldValue;
+                var newValue = (bool)e.NewValue;
+                upDown.OnIsReadOnlyChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnMaximumChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnMinimumChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Text"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (string)e.OldValue;
+                var newValue = (string)e.NewValue;
+                upDown.OnTextChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnValueChanged(oldValue, newValue);
+            }
+        }
+
+        private void SetValueInternal(double value)
+        {
+            _internalValueSet = true;
+            try
+            {
+                Value = value;
+            }
+            finally
+            {
+                _internalValueSet = false;
+            }
+        }
+
+        private static double OnCoerceMaximum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMaximum(value);
+        }
+
+        private static double OnCoerceMinimum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMinimum(value);
+        }
+
+        private static double OnCoerceIncrement(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceIncrement(value);
+        }
+
+        private void TextBoxOnTextChanged()
+        {
+            try
+            {
+                _isTextChangedFromUI = true;
+                if (TextBox != null)
+                {
+                    Text = TextBox.Text;
+                }
+            }
+            finally
+            {
+                _isTextChangedFromUI = false;
+            }
+        }
+
+        private void OnSpinnerSpin(object sender, SpinEventArgs e)
+        {
+            if (AllowSpin && !IsReadOnly)
+            {
+                var spin = !e.UsingMouseWheel;
+                spin |= ((TextBox != null) && TextBox.IsFocused);
+
+                if (spin)
+                {
+                    e.Handled = true;
+                    OnSpin(e);
+                }
+            }
+        }
+
+        private void DoDecrement()
+        {
+            if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
+            {
+                OnDecrement();
+            }
+        }
+
+        private void DoIncrement()
+        {
+            if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
+            {
+                OnIncrement();
+            }
+        }
+
+        public event EventHandler<SpinEventArgs> Spinned;
+
+        private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if (e.Device.Captured != Spinner)
+            {
+                Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
+            }
+        }
+
+        /// <summary>
+        /// Defines the <see cref="ValueChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent =
+            RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Raised when the <see cref="Value"/> changes.
+        /// </summary>
+        public event EventHandler<SpinEventArgs> ValueChanged
+        {
+            add { AddHandler(ValueChangedEvent, value); }
+            remove { RemoveHandler(ValueChangedEvent, value); }
+        }
+
+        private bool CommitInput()
+        {
+            return SyncTextAndValueProperties(true, Text);
+        }
+
+        /// <summary>
+        /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
+        /// </summary>
+        /// <param name="updateValueFromText">If value should be updated from text.</param>
+        /// <param name="text">The text.</param>
+        private bool SyncTextAndValueProperties(bool updateValueFromText, string text)
+        {
+            return SyncTextAndValueProperties(updateValueFromText, text, false);
+        }
+
+        /// <summary>
+        /// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
+        /// </summary>
+        /// <param name="updateValueFromText">If value should be updated from text.</param>
+        /// <param name="text">The text.</param>
+        /// <param name="forceTextUpdate">Force text update.</param>
+        private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
+        {
+            if (_isSyncingTextAndValueProperties)
+                return true;
+
+            _isSyncingTextAndValueProperties = true;
+            var parsedTextIsValid = true;
+            try
+            {
+                if (updateValueFromText)
+                {
+                    if (!string.IsNullOrEmpty(text))
+                    {
+                        try
+                        {
+                            var newValue = ConvertTextToValue(text);
+                            if (!Equals(newValue, Value))
+                            {
+                                SetValueInternal(newValue);
+                            }
+                        }
+                        catch
+                        {
+                            parsedTextIsValid = false;
+                        }
+                    }
+                }
+
+                // Do not touch the ongoing text input from user.
+                if (!_isTextChangedFromUI)
+                {
+                    var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text);
+                    if (!keepEmpty)
+                    {
+                        var newText = ConvertValueToText();
+                        if (!Equals(Text, newText))
+                        {
+                            Text = newText;
+                        }
+                    }
+
+                    // Sync Text and textBox
+                    if (TextBox != null)
+                    {
+                        TextBox.Text = Text;
+                    }
+                }
+
+                if (_isTextChangedFromUI && !parsedTextIsValid)
+                {
+                    // Text input was made from the user and the text
+                    // repesents an invalid value. Disable the spinner in this case.
+                    if (Spinner != null)
+                    {
+                        Spinner.ValidSpinDirection = ValidSpinDirections.None;
+                    }
+                }
+                else
+                {
+                    SetValidSpinDirection();
+                }
+            }
+            finally
+            {
+                _isSyncingTextAndValueProperties = false;
+            }
+            return parsedTextIsValid;
+        }
+
+        private double ConvertTextToValueCore(string currentValueText, string text)
+        {
+            double result;
+
+            if (IsPercent(FormatString))
+            {
+                result = decimal.ToDouble(ParsePercent(text, CultureInfo));
+            }
+            else
+            {
+                // Problem while converting new text
+                if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
+                {
+                    var shouldThrow = true;
+
+                    // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
+                    if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
+                    {
+                        // extract non-digit characters
+                        var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
+                        var textSpecialCharacters = text.Where(c => !char.IsDigit(c));
+                        // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
+                        if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0)
+                        {
+                            foreach (var character in textSpecialCharacters)
+                            {
+                                text = text.Replace(character.ToString(), string.Empty);
+                            }
+                            // if without the special characters, parsing is good, do not throw
+                            if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue))
+                            {
+                                shouldThrow = false;
+                            }
+                        }
+                    }
+
+                    if (shouldThrow)
+                    {
+                        throw new InvalidDataException("Input string was not in a correct format.");
+                    }
+                }
+                result = outputValue;
+            }
+            return result;
+        }
+
+        private void ValidateMinMax(double value)
+        {
+            if (value < Minimum)
+            {
+                throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
+            }
+            else if (value > Maximum)
+            {
+                throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
+            }
+        }
+
+        /// <summary>
+        /// Parse percent format text
+        /// </summary>
+        /// <param name="text">Text to parse.</param>
+        /// <param name="cultureInfo">The culture info.</param>
+        private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
+        {
+            var info = NumberFormatInfo.GetInstance(cultureInfo);
+            text = text.Replace(info.PercentSymbol, null);
+            var result = decimal.Parse(text, NumberStyles.Any, info);
+            result = result / 100;
+            return result;
+        }
+
+
+        private bool IsPercent(string stringToTest)
+        {
+            var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal);
+            if (PIndex >= 0)
+            {
+                //stringToTest contains a "P" between 2 "'", it's considered as text, not percent
+                var isText = stringToTest.Substring(0, PIndex).Contains("'")
+                             && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'");
+
+                return !isText;
+            }
+            return false;
+        }
+    }
+}

+ 16 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

@@ -0,0 +1,16 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
+    {
+        public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue,  double newValue) : base(routedEvent)
+        {
+            OldValue = oldValue;
+            NewValue = newValue;
+        }
+
+        public double OldValue { get; }
+        public double NewValue { get; }
+    }
+}

+ 8 - 2
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@@ -55,7 +55,7 @@ namespace Avalonia.Platform
         /// Gets the platform window handle.
         /// </summary>
         IPlatformHandle Handle { get; }
-        
+       
         /// <summary>
         /// Gets the maximum size of a window on the system.
         /// </summary>
@@ -65,7 +65,13 @@ namespace Avalonia.Platform
         /// Sets the client size of the toplevel.
         /// </summary>
         void Resize(Size clientSize);
-        
+
+        /// <summary>
+        /// Minimum width of the window.
+        /// </summary>
+        /// 
+        void SetMinMaxSize(Size minSize, Size maxSize);
+
         /// <summary>
         /// Gets platform specific display information
         /// </summary>

+ 11 - 0
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -44,5 +44,16 @@ namespace Avalonia.Platform
         /// Enables or disables the taskbar icon
         /// </summary>
         void ShowTaskbarIcon(bool value);
+
+        /// <summary>
+        /// Enables or disables resizing of the window
+        /// </summary>
+        void CanResize(bool value);
+
+        /// <summary>
+        /// Gets or sets a method called before the underlying implementation is destroyed.
+        /// Return true to prevent the underlying implementation from closing.
+        /// </summary>
+        Func<bool> Closing { get; set; }
     }
 }

+ 210 - 0
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -0,0 +1,210 @@
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Platform
+{
+    class InProcessDragSource : IPlatformDragSource
+    {
+        private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton;
+        private readonly IDragDropDevice _dragDrop;
+        private readonly IInputManager _inputManager;
+        private readonly Subject<DragDropEffects> _result = new Subject<DragDropEffects>();
+
+        private DragDropEffects _allowedEffects;
+        private IDataObject _draggedData;
+        private IInputElement _lastRoot;
+        private Point _lastPosition;
+        private StandardCursorType _lastCursorType;
+        private object _originalCursor;
+        private InputModifiers? _initialInputModifiers;
+
+        public InProcessDragSource()
+        {
+            _inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
+            _dragDrop = AvaloniaLocator.Current.GetService<IDragDropDevice>();
+        }
+
+        public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+        {
+            Dispatcher.UIThread.VerifyAccess();
+            if (_draggedData == null)
+            {
+                _draggedData = data;
+                _lastRoot = null;
+                _lastPosition = default(Point);
+                _allowedEffects = allowedEffects;
+
+                using (_inputManager.PreProcess.OfType<RawMouseEventArgs>().Subscribe(ProcessMouseEvents))
+                {
+                    using (_inputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(ProcessKeyEvents))
+                    {
+                        var effect = await _result.FirstAsync();
+                        return effect;
+                    }
+                }
+            }
+            return DragDropEffects.None;
+        }
+
+
+        private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, InputModifiers modifiers)
+        {
+            _lastPosition = pt;
+
+            RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects);
+            var tl = root.GetSelfAndVisualAncestors().OfType<TopLevel>().FirstOrDefault();
+            tl.PlatformImpl.Input(rawEvent);
+
+            var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers);
+            UpdateCursor(root, effect);
+            return effect;
+        }
+
+        private DragDropEffects GetPreferredEffect(DragDropEffects effect, InputModifiers modifiers)
+        {
+            if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None)
+                return effect; // No need to check for the modifiers.
+            if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(InputModifiers.Alt))
+                return DragDropEffects.Link;
+            if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(InputModifiers.Control))
+                return DragDropEffects.Copy;
+            return DragDropEffects.Move;
+        }
+
+        private StandardCursorType GetCursorForDropEffect(DragDropEffects effects)
+        {
+            if (effects.HasFlag(DragDropEffects.Copy))
+                return StandardCursorType.DragCopy;
+            if (effects.HasFlag(DragDropEffects.Move))
+                return StandardCursorType.DragMove;
+            if (effects.HasFlag(DragDropEffects.Link))
+                return StandardCursorType.DragLink;
+            return StandardCursorType.No;
+        }
+        
+        private void UpdateCursor(IInputElement root, DragDropEffects effect)
+        {
+            if (_lastRoot != root)
+            {
+                if (_lastRoot is InputElement ieLast)
+                {
+                    if (_originalCursor == AvaloniaProperty.UnsetValue)
+                        ieLast.ClearValue(InputElement.CursorProperty);
+                    else
+                        ieLast.Cursor = _originalCursor as Cursor;
+                }
+
+                if (root is InputElement ieNew)
+                {
+                    if (!ieNew.IsSet(InputElement.CursorProperty))
+                        _originalCursor = AvaloniaProperty.UnsetValue;
+                    else
+                        _originalCursor = root.Cursor;
+                }
+                else
+                    _originalCursor = null;
+
+                _lastCursorType = StandardCursorType.Arrow;
+                _lastRoot = root;
+            }
+
+            if (root is InputElement ie)
+            {
+                var ct = GetCursorForDropEffect(effect);
+                if (ct != _lastCursorType)
+                {
+                    _lastCursorType = ct;
+                    ie.Cursor = new Cursor(ct);
+                }
+            }  
+        }
+
+        private void CancelDragging()
+        {
+            if (_lastRoot != null)
+                RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, InputModifiers.None);
+            UpdateCursor(null, DragDropEffects.None);
+            _result.OnNext(DragDropEffects.None);
+        }
+
+        private void ProcessKeyEvents(RawKeyEventArgs e)
+        {
+            if (e.Type == RawKeyEventType.KeyDown && e.Key == Key.Escape)
+            {
+                if (_lastRoot != null)
+                    RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, e.Modifiers);
+                UpdateCursor(null, DragDropEffects.None);
+                _result.OnNext(DragDropEffects.None);
+                e.Handled = true;
+            }
+            else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
+                RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers);
+        }
+
+        private void ProcessMouseEvents(RawMouseEventArgs e)
+        {
+            if (!_initialInputModifiers.HasValue)
+                _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS;
+
+            
+            void CheckDraggingAccepted(InputModifiers changedMouseButton)
+            {
+                if (_initialInputModifiers.Value.HasFlag(changedMouseButton))
+                {
+                    var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers);
+                    UpdateCursor(null, DragDropEffects.None);
+                    _result.OnNext(result);
+                }
+                else
+                    CancelDragging();
+                e.Handled = true;
+            }
+            
+            switch (e.Type)
+            {
+                case RawMouseEventType.LeftButtonDown:
+                case RawMouseEventType.RightButtonDown:
+                case RawMouseEventType.MiddleButtonDown:
+                case RawMouseEventType.NonClientLeftButtonDown:
+                    CancelDragging();
+                    e.Handled = true;
+                    return;
+                case RawMouseEventType.LeaveWindow:
+                    RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position,  e.InputModifiers); break;
+                case RawMouseEventType.LeftButtonUp:
+                    CheckDraggingAccepted(InputModifiers.LeftMouseButton); break;
+                case RawMouseEventType.MiddleButtonUp:
+                    CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break;
+                case RawMouseEventType.RightButtonUp:
+                    CheckDraggingAccepted(InputModifiers.RightMouseButton); break;
+                case RawMouseEventType.Move:
+                    var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS;
+                    if (_initialInputModifiers.Value != mods)
+                    {
+                        CancelDragging();
+                        e.Handled = true;
+                        return;
+                    }
+
+                    if (e.Root != _lastRoot)
+                    {
+                        if (_lastRoot != null)
+                            RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), e.InputModifiers);
+                        RaiseEventAndUpdateCursor(RawDragEventType.DragEnter, e.Root, e.Position, e.InputModifiers);
+                    }
+                    else
+                        RaiseEventAndUpdateCursor(RawDragEventType.DragOver, e.Root, e.Position, e.InputModifiers);
+                    break;
+            }
+        }
+    }
+}

+ 71 - 82
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="CornerRadius"/> property.
         /// </summary>
-        public static readonly StyledProperty<float> CornerRadiusProperty =
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
             Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
 
         /// <summary>
@@ -76,17 +77,20 @@ namespace Avalonia.Controls.Presenters
         /// Defines the <see cref="Padding"/> property.
         /// </summary>
         public static readonly StyledProperty<Thickness> PaddingProperty =
-            Border.PaddingProperty.AddOwner<ContentPresenter>();
+            Decorator.PaddingProperty.AddOwner<ContentPresenter>();
 
         private IControl _child;
         private bool _createdChild;
         private IDataTemplate _dataTemplate;
+        private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
         /// <summary>
         /// Initializes static members of the <see cref="ContentPresenter"/> class.
         /// </summary>
         static ContentPresenter()
         {
+            AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
+            AffectsMeasure(BorderThicknessProperty);
             ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
             ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
             TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged);
@@ -120,7 +124,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the thickness of the border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -157,7 +161,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets or sets the radius of the border rounded corners.
         /// </summary>
-        public float CornerRadius
+        public CornerRadius CornerRadius
         {
             get { return GetValue(CornerRadiusProperty); }
             set { SetValue(CornerRadiusProperty, value); }
@@ -221,7 +225,7 @@ namespace Avalonia.Controls.Presenters
         {
             var content = Content;
             var oldChild = Child;
-            var newChild = CreateChild();            
+            var newChild = CreateChild();
 
             // Remove the old child if we're not recycling it.
             if (oldChild != null && newChild != oldChild)
@@ -277,21 +281,7 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public override void Render(DrawingContext context)
         {
-            var background = Background;
-            var borderBrush = BorderBrush;
-            var borderThickness = BorderThickness;
-            var cornerRadius = CornerRadius;
-            var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
-            if (background != null)
-            {
-                context.FillRectangle(background, rect, cornerRadius);
-            }
-
-            if (borderBrush != null && borderThickness > 0)
-            {
-                context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
-            }
+            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
         }
 
         /// <summary>
@@ -344,7 +334,11 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            return ArrangeOverrideImpl(finalSize, new Vector());
+            finalSize = ArrangeOverrideImpl(finalSize, new Vector());
+
+            _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
+
+            return finalSize;
         }
 
         /// <summary>
@@ -372,74 +366,69 @@ namespace Avalonia.Controls.Presenters
 
         internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
         {
-            if (Child != null)
-            {
-                var padding = Padding;
-                var borderThickness = BorderThickness;
-                var horizontalContentAlignment = HorizontalContentAlignment;
-                var verticalContentAlignment = VerticalContentAlignment;
-                var useLayoutRounding = UseLayoutRounding;
-                var availableSizeMinusMargins = new Size(
-                    Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
-                    Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
-                var size = availableSizeMinusMargins;
-                var scale = GetLayoutScale();
-                var originX = offset.X + padding.Left + borderThickness;
-                var originY = offset.Y + padding.Top + borderThickness;
-
-                if (horizontalContentAlignment != HorizontalAlignment.Stretch)
-                {
-                    size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
-                }
+            if (Child == null) return finalSize;
 
-                if (verticalContentAlignment != VerticalAlignment.Stretch)
-                {
-                    size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
-                }
-
-                size = LayoutHelper.ApplyLayoutConstraints(Child, size);
+            var padding = Padding;
+            var borderThickness = BorderThickness;
+            var horizontalContentAlignment = HorizontalContentAlignment;
+            var verticalContentAlignment = VerticalContentAlignment;
+            var useLayoutRounding = UseLayoutRounding;
+            var availableSizeMinusMargins = new Size(
+                Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right),
+                Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom));
+            var size = availableSizeMinusMargins;
+            var scale = GetLayoutScale();
+            var originX = offset.X + padding.Left + borderThickness.Left;
+            var originY = offset.Y + padding.Top + borderThickness.Top;
+
+            if (horizontalContentAlignment != HorizontalAlignment.Stretch)
+            {
+                size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
+            }
 
-                if (useLayoutRounding)
-                {
-                    size = new Size(
-                        Math.Ceiling(size.Width * scale) / scale,
-                        Math.Ceiling(size.Height * scale) / scale);
-                    availableSizeMinusMargins = new Size(
-                        Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
-                        Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
-                }
+            if (verticalContentAlignment != VerticalAlignment.Stretch)
+            {
+                size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
+            }
 
-                switch (horizontalContentAlignment)
-                {
-                    case HorizontalAlignment.Center:
-                    case HorizontalAlignment.Stretch:
-                        originX += (availableSizeMinusMargins.Width - size.Width) / 2;
-                        break;
-                    case HorizontalAlignment.Right:
-                        originX += availableSizeMinusMargins.Width - size.Width;
-                        break;
-                }
+            if (useLayoutRounding)
+            {
+                size = new Size(
+                    Math.Ceiling(size.Width * scale) / scale,
+                    Math.Ceiling(size.Height * scale) / scale);
+                availableSizeMinusMargins = new Size(
+                    Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
+                    Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+            }
 
-                switch (verticalContentAlignment)
-                {
-                    case VerticalAlignment.Center:
-                    case VerticalAlignment.Stretch:
-                        originY += (availableSizeMinusMargins.Height - size.Height) / 2;
-                        break;
-                    case VerticalAlignment.Bottom:
-                        originY += availableSizeMinusMargins.Height - size.Height;
-                        break;
-                }
+            switch (horizontalContentAlignment)
+            {
+                case HorizontalAlignment.Center:
+                    originX += (availableSizeMinusMargins.Width - size.Width) / 2;
+                    break;
+                case HorizontalAlignment.Right:
+                    originX += availableSizeMinusMargins.Width - size.Width;
+                    break;
+            }
 
-                if (useLayoutRounding)
-                {
-                    originX = Math.Floor(originX * scale) / scale;
-                    originY = Math.Floor(originY * scale) / scale;
-                }
+            switch (verticalContentAlignment)
+            {
+                case VerticalAlignment.Center:
+                    originY += (availableSizeMinusMargins.Height - size.Height) / 2;
+                    break;
+                case VerticalAlignment.Bottom:
+                    originY += availableSizeMinusMargins.Height - size.Height;
+                    break;
+            }
 
-                Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+            if (useLayoutRounding)
+            {
+                originX = Math.Floor(originX * scale) / scale;
+                originY = Math.Floor(originY * scale) / scale;
             }
 
+            Child.Arrange(new Rect(originX, originY, Math.Max(0, size.Width), Math.Max(0, size.Height)));
+
             return finalSize;
         }
 

+ 1 - 1
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -224,7 +224,7 @@ namespace Avalonia.Controls.Presenters
                 CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
             ArrangeOverrideImpl(size, -Offset);
             Viewport = finalSize;
-            Extent = Child.Bounds.Size;
+            Extent = Child.Bounds.Size.Inflate(Child.Margin);
             return finalSize;
         }
 

+ 62 - 4
src/Avalonia.Controls/Primitives/Popup.cs

@@ -40,6 +40,12 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
             AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
 
+        /// <summary>
+        /// Defines the <see cref="ObeyScreenEdges"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
+            AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
+
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
         /// </summary>
@@ -136,6 +142,16 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(PlacementModeProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
+        /// when its opened at a position where it would otherwise overlap the screen edge.
+        /// </summary>
+        public bool ObeyScreenEdges
+        {
+            get => GetValue(ObeyScreenEdgesProperty);
+            set => SetValue(ObeyScreenEdgesProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
         /// </summary>
@@ -215,7 +231,17 @@ namespace Avalonia.Controls.Primitives
             {
                 var window = _topLevel as Window;
                 if (window != null)
+                {
                     window.Deactivated += WindowDeactivated;
+                }
+                else
+                {
+                    var parentPopuproot = _topLevel as PopupRoot;
+                    if (parentPopuproot != null && parentPopuproot.Parent != null)
+                    {
+                        ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed;
+                    }
+                }
                 _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
                 _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
             }
@@ -224,6 +250,11 @@ namespace Avalonia.Controls.Primitives
 
             _popupRoot.Show();
 
+            if (ObeyScreenEdges)
+            {
+                _popupRoot.SnapInsideScreenEdges();
+            }
+
             _ignoreIsOpenChanged = true;
             IsOpen = true;
             _ignoreIsOpenChanged = false;
@@ -244,6 +275,14 @@ namespace Avalonia.Controls.Primitives
                     var window = _topLevel as Window;
                     if (window != null)
                         window.Deactivated -= WindowDeactivated;
+                    else
+                    {
+                        var parentPopuproot = _topLevel as PopupRoot;
+                        if (parentPopuproot != null && parentPopuproot.Parent != null)
+                        {
+                            ((Popup)parentPopuproot.Parent).Closed -= ParentClosed;
+                        }
+                    }
                     _nonClientListener?.Dispose();
                     _nonClientListener = null;
                 }
@@ -328,8 +367,10 @@ namespace Avalonia.Controls.Primitives
         /// <returns>The popup's position in screen coordinates.</returns>
         protected virtual Point GetPosition()
         {
-            return GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot, 
+            var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
                 HorizontalOffset, VerticalOffset);
+
+            return result;
         }
 
         internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
@@ -381,9 +422,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (!StaysOpen)
             {
-                var root = ((IVisual)e.Source).GetVisualRoot();
-
-                if (root != this.PopupRoot)
+                if (!IsChildOrThis((IVisual)e.Source))
                 {
                     Close();
                     e.Handled = true;
@@ -391,6 +430,17 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        private bool IsChildOrThis(IVisual child)
+        {
+            IVisual root = child.GetVisualRoot();
+            while (root is PopupRoot)
+            {
+                if (root == PopupRoot) return true;
+                root = ((PopupRoot)root).Parent.GetVisualRoot();
+            }
+            return false;
+        }
+
         private void WindowDeactivated(object sender, EventArgs e)
         {
             if (!StaysOpen)
@@ -398,5 +448,13 @@ namespace Avalonia.Controls.Primitives
                 Close();
             }
         }
+
+        private void ParentClosed(object sender, EventArgs e)
+        {
+            if (!StaysOpen)
+            {
+                Close();
+            }
+        }
     }
 }

+ 26 - 0
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -2,10 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Linq;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Presenters;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
+using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Styling;
@@ -75,6 +77,30 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         public void Dispose() => PlatformImpl?.Dispose();
 
+        /// <summary>
+        /// Moves the Popups position so that it doesnt overlap screen edges.
+        /// This method can be called immediately after Show has been called.
+        /// </summary>
+        public void SnapInsideScreenEdges()
+        {
+            var window = this.GetSelfAndLogicalAncestors().OfType<Window>().First();
+            
+            var screen = window.Screens.ScreenFromPoint(Position);
+
+            var screenX = Position.X + Bounds.Width - screen.Bounds.X;
+            var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
+
+            if (screenX > screen.Bounds.Width)
+            {
+                Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
+            }
+
+            if (screenY > screen.Bounds.Height)
+            {
+                Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {

+ 4 - 4
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="BorderThickness"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> BorderThicknessProperty =
+        public static readonly StyledProperty<Thickness> BorderThicknessProperty =
             Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
 
         /// <summary>
@@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the thickness of the control's border.
         /// </summary>
-        public double BorderThickness
+        public Thickness BorderThickness
         {
             get { return GetValue(BorderThicknessProperty); }
             set { SetValue(BorderThicknessProperty, value); }
@@ -207,7 +207,7 @@ namespace Avalonia.Controls.Primitives
         /// <param name="control">The control.</param>
         /// <returns>The property value.</returns>
         /// <see cref="SetIsTemplateFocusTarget(Control, bool)"/>
-        public bool GetIsTemplateFocusTarget(Control control)
+        public static bool GetIsTemplateFocusTarget(Control control)
         {
             return control.GetValue(IsTemplateFocusTargetProperty);
         }
@@ -223,7 +223,7 @@ namespace Avalonia.Controls.Primitives
         /// attached property is set to true on an element in the control template, then the focus
         /// adorner will be shown around that control instead.
         /// </remarks>
-        public void SetIsTemplateFocusTarget(Control control, bool value)
+        public static void SetIsTemplateFocusTarget(Control control, bool value)
         {
             control.SetValue(IsTemplateFocusTargetProperty, value);
         }

+ 3 - 23
src/Avalonia.Controls/ProgressBar.cs

@@ -26,12 +26,13 @@ namespace Avalonia.Controls
 
         static ProgressBar()
         {
+            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
+            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+
             ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
 
             IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
                 (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
-            OrientationProperty.Changed.AddClassHandler<ProgressBar>(
-                (p, e) => { if (p._indicator != null) p.UpdateOrientation((Orientation)e.NewValue); });
         }
 
         public bool IsIndeterminate
@@ -59,7 +60,6 @@ namespace Avalonia.Controls
             _indicator = e.NameScope.Get<Border>("PART_Indicator");
 
             UpdateIndicator(Bounds.Size);
-            UpdateOrientation(Orientation);
             UpdateIsIndeterminate(IsIndeterminate);
         }
 
@@ -86,26 +86,6 @@ namespace Avalonia.Controls
             }
         }
 
-        private void UpdateOrientation(Orientation orientation)
-        {
-            if (orientation == Orientation.Horizontal)
-            {
-                MinHeight = 14;
-                MinWidth = 200;
-
-                _indicator.HorizontalAlignment = HorizontalAlignment.Left;
-                _indicator.VerticalAlignment = VerticalAlignment.Stretch;
-            }
-            else
-            {
-                MinHeight = 200;
-                MinWidth = 14;
-
-                _indicator.HorizontalAlignment = HorizontalAlignment.Stretch;
-                _indicator.VerticalAlignment = VerticalAlignment.Bottom;
-            }
-        }
-
         private void UpdateIsIndeterminate(bool isIndeterminate)
         {
             if (isIndeterminate)

+ 2 - 2
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls.Remote
     {
         private readonly IAvaloniaRemoteTransportConnection _connection;
         private FrameMessage _lastFrame;
-        private WritableBitmap _bitmap;
+        private WriteableBitmap _bitmap;
         public RemoteWidget(IAvaloniaRemoteTransportConnection connection)
         {
             _connection = connection;
@@ -62,7 +62,7 @@ namespace Avalonia.Controls.Remote
                 var fmt = (PixelFormat) _lastFrame.Format;
                 if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width ||
                     _bitmap.PixelHeight != _lastFrame.Height)
-                    _bitmap = new WritableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
+                    _bitmap = new WriteableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
                 using (var l = _bitmap.Lock())
                 {
                     var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width;

+ 27 - 2
src/Avalonia.Controls/RepeatButton.cs

@@ -6,17 +6,32 @@ namespace Avalonia.Controls
 {
     public class RepeatButton : Button
     {
+        /// <summary>
+        /// Defines the <see cref="Interval"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> IntervalProperty =
+            AvaloniaProperty.Register<Button, int>(nameof(Interval), 100);
+
         /// <summary>
         /// Defines the <see cref="Delay"/> property.
         /// </summary>
         public static readonly StyledProperty<int> DelayProperty =
-            AvaloniaProperty.Register<Button, int>(nameof(Delay), 100);
+            AvaloniaProperty.Register<Button, int>(nameof(Delay), 300);
 
         private DispatcherTimer _repeatTimer;
 
         /// <summary>
         /// Gets or sets the amount of time, in milliseconds, of repeating clicks.
         /// </summary>
+        public int Interval
+        {
+            get { return GetValue(IntervalProperty); }
+            set { SetValue(IntervalProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the amount of time, in milliseconds, to wait before repeating begins.
+        /// </summary>
         public int Delay
         {
             get { return GetValue(DelayProperty); }
@@ -28,7 +43,7 @@ namespace Avalonia.Controls
             if (_repeatTimer == null)
             {
                 _repeatTimer = new DispatcherTimer();
-                _repeatTimer.Tick += (o, e) => OnClick();
+                _repeatTimer.Tick += RepeatTimerOnTick;
             }
 
             if (_repeatTimer.IsEnabled) return;
@@ -37,6 +52,16 @@ namespace Avalonia.Controls
             _repeatTimer.Start();
         }
 
+        private void RepeatTimerOnTick(object sender, EventArgs e)
+        {
+            var interval = TimeSpan.FromMilliseconds(Interval);
+            if (_repeatTimer.Interval != interval)
+            {
+                _repeatTimer.Interval = interval;
+            }
+            OnClick();
+        }
+
         private void StopTimer()
         {
             _repeatTimer?.Stop();

+ 1 - 1
src/Avalonia.Controls/RowDefinitions.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
         public RowDefinitions(string s)
             : this()
         {
-            AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x)));
+            AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x)));
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Screens.cs

@@ -39,7 +39,7 @@ namespace Avalonia.Controls
             return currMaxScreen;
         }
         
-        public Screen SceenFromPoint(Point point)
+        public Screen ScreenFromPoint(Point point)
         {
             return All.FirstOrDefault(x=>x.Bounds.Contains(point));        
         }

+ 174 - 0
src/Avalonia.Controls/Spinner.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents spin directions that are valid.
+    /// </summary>
+    [Flags]
+    public enum ValidSpinDirections
+    {
+        /// <summary>
+        /// Can not increase nor decrease.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Can increase.
+        /// </summary>
+        Increase = 1,
+
+        /// <summary>
+        /// Can decrease.
+        /// </summary>
+        Decrease = 2
+    }
+
+    /// <summary>
+    /// Represents spin directions that could be initiated by the end-user.
+    /// </summary>
+    public enum SpinDirection
+    {
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Increase a value.
+        /// </summary>
+        Increase = 0,
+
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Decrease a value.
+        /// </summary>
+        Decrease = 1
+    }
+
+    /// <summary>
+    /// Provides data for the Spinner.Spin event.
+    /// </summary>
+    public class SpinEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Gets the SpinDirection for the spin that has been initiated by the end-user.
+        /// </summary>
+        public SpinDirection Direction { get; }
+
+        /// <summary>
+        /// Get or set whheter the spin event originated from a mouse wheel event.
+        /// </summary>
+        public bool UsingMouseWheel{ get; }
+
+        /// <summary>
+        /// Initializes a new instance of the SpinEventArgs class.
+        /// </summary>
+        /// <param name="direction">Spin direction.</param>
+        public SpinEventArgs(SpinDirection direction)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction)
+            : base(routedEvent)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(SpinDirection direction, bool usingMouseWheel)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction, bool usingMouseWheel)
+            : base(routedEvent)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+    }
+
+    /// <summary>
+    /// Base class for controls that represents controls that can spin.
+    /// </summary>
+    public abstract class Spinner : ContentControl
+    {
+        /// <summary>
+        /// Defines the <see cref="ValidSpinDirection"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ValidSpinDirections> ValidSpinDirectionProperty =
+            AvaloniaProperty.Register<Spinner, ValidSpinDirections>(nameof(ValidSpinDirection),
+                ValidSpinDirections.Increase | ValidSpinDirections.Decrease);
+
+        /// <summary>
+        /// Defines the <see cref="Spin"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<SpinEventArgs> SpinEvent =
+            RoutedEvent.Register<Spinner, SpinEventArgs>(nameof(Spin), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Initializes static members of the <see cref="Spinner"/> class.
+        /// </summary>
+        static Spinner()
+        {
+            ValidSpinDirectionProperty.Changed.Subscribe(OnValidSpinDirectionPropertyChanged);
+        }
+
+        /// <summary>
+        /// Occurs when spinning is initiated by the end-user.
+        /// </summary>
+        public event EventHandler<SpinEventArgs> Spin
+        {
+            add { AddHandler(SpinEvent, value); }
+            remove { RemoveHandler(SpinEvent, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets <see cref="ValidSpinDirections"/> allowed for this control.
+        /// </summary>
+        public ValidSpinDirections ValidSpinDirection
+        {
+            get { return GetValue(ValidSpinDirectionProperty); }
+            set { SetValue(ValidSpinDirectionProperty, value); }
+        }
+
+        /// <summary>
+        /// Called when valid spin direction changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+        {
+        }
+
+        /// <summary>
+        /// Raises the OnSpin event when spinning is initiated by the end-user.
+        /// </summary>
+        /// <param name="e">Spin event args.</param>
+        protected virtual void OnSpin(SpinEventArgs e)
+        {
+            var valid = e.Direction == SpinDirection.Increase
+                ? ValidSpinDirections.Increase
+                : ValidSpinDirections.Decrease;
+
+            //Only raise the event if spin is allowed.
+            if ((ValidSpinDirection & valid) == valid)
+            {
+                RaiseEvent(e);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="ValidSpinDirection"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnValidSpinDirectionPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is Spinner spinner)
+            {
+                var oldValue = (ValidSpinDirections)e.OldValue;
+                var newValue = (ValidSpinDirections)e.NewValue;
+                spinner.OnValidSpinDirectionChanged(oldValue, newValue);
+            }
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Controls/TextBlock.cs

@@ -120,6 +120,7 @@ namespace Avalonia.Controls
                 .Subscribe(_ =>
                 {
                     InvalidateFormattedText();
+                    InvalidateMeasure();
                 });
         }
 
@@ -370,8 +371,6 @@ namespace Avalonia.Controls
                 _constraint = _formattedText.Constraint;
                 _formattedText = null;
             }
-
-            InvalidateMeasure();
         }
 
         /// <summary>
@@ -402,6 +401,7 @@ namespace Avalonia.Controls
         {
             base.OnAttachedToLogicalTree(e);
             InvalidateFormattedText();
+            InvalidateMeasure();
         }
     }
 }

+ 32 - 5
src/Avalonia.Controls/TextBox.cs

@@ -85,6 +85,7 @@ namespace Avalonia.Controls
         private int _selectionEnd;
         private TextPresenter _presenter;
         private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
+        private bool _isUndoingRedoing;
         private bool _ignoreTextChanges;
         private static readonly string[] invalidCharacters = new String[1]{"\u007f"};
 
@@ -198,7 +199,11 @@ namespace Avalonia.Controls
                 if (!_ignoreTextChanges)
                 {
                     CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0);
-                    SetAndRaise(TextProperty, ref _text, value);
+
+                    if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
+                    {
+                        _undoRedoHelper.Clear();
+                    }
                 }
             }
         }
@@ -256,6 +261,8 @@ namespace Avalonia.Controls
             {
                 _presenter?.ShowCaret();
             }
+
+            e.Handled = true;
         }
 
         protected override void OnLostFocus(RoutedEventArgs e)
@@ -268,7 +275,11 @@ namespace Avalonia.Controls
 
         protected override void OnTextInput(TextInputEventArgs e)
         {
-            HandleTextInput(e.Text);
+            if (!e.Handled)
+            {
+                HandleTextInput(e.Text);
+                e.Handled = true;
+            }
         }
 
         private void HandleTextInput(string input)
@@ -364,14 +375,30 @@ namespace Avalonia.Controls
                 case Key.Z:
                     if (modifiers == InputModifiers.Control)
                     {
-                        _undoRedoHelper.Undo();
+                        try
+                        {
+                            _isUndoingRedoing = true;
+                            _undoRedoHelper.Undo();
+                        }
+                        finally
+                        {
+                            _isUndoingRedoing = false;
+                        }
                         handled = true;
                     }
                     break;
                 case Key.Y:
                     if (modifiers == InputModifiers.Control)
                     {
-                        _undoRedoHelper.Redo();
+                        try
+                        {
+                            _isUndoingRedoing = true;
+                            _undoRedoHelper.Redo();
+                        }
+                        finally
+                        {
+                            _isUndoingRedoing = false;
+                        }
                         handled = true;
                     }
                     break;
@@ -788,7 +815,7 @@ namespace Avalonia.Controls
             int pos = 0;
             int i;
 
-            for (i = 0; i < lines.Count; ++i)
+            for (i = 0; i < lines.Count - 1; ++i)
             {
                 var line = lines[i];
                 pos += line.Length;

+ 4 - 0
src/Avalonia.Controls/UserControl.cs

@@ -6,6 +6,9 @@ using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
+    /// <summary>
+    /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic.
+    /// </summary>
     public class UserControl : ContentControl, IStyleable, INameScope
     {
         private readonly NameScope _nameScope = new NameScope();
@@ -24,6 +27,7 @@ namespace Avalonia.Controls
             remove { _nameScope.Unregistered -= value; }
         }
 
+        /// <inheritdoc/>
         Type IStyleable.StyleKey => typeof(ContentControl);
 
         /// <inheritdoc/>

+ 279 - 0
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -0,0 +1,279 @@
+// 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 Avalonia.Media;
+
+namespace Avalonia.Controls.Utils
+{
+    internal class BorderRenderHelper
+    {
+        private bool _useComplexRendering;
+        private StreamGeometry _backgroundGeometryCache;
+        private StreamGeometry _borderGeometryCache;
+
+        public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
+        {
+            if (borderThickness.IsUniform && cornerRadius.IsUniform)
+            {
+                _backgroundGeometryCache = null;
+                _borderGeometryCache = null;
+                _useComplexRendering = false;
+            }
+            else
+            {
+                _useComplexRendering = true;
+
+                var boundRect = new Rect(finalSize);
+                var innerRect = boundRect.Deflate(borderThickness);
+                var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false);
+
+                StreamGeometry backgroundGeometry = null;
+
+                if (innerRect.Width != 0 && innerRect.Height != 0)
+                {
+                    backgroundGeometry = new StreamGeometry();
+
+                    using (var ctx = backgroundGeometry.Open())
+                    {
+                        CreateGeometry(ctx, innerRect, innerCoordinates);
+                    }
+
+                    _backgroundGeometryCache = backgroundGeometry;
+                }
+                else
+                {
+                    _backgroundGeometryCache = null;
+                }
+
+                if (boundRect.Width != 0 && innerRect.Height != 0)
+                {
+                    var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true);
+                    var borderGeometry = new StreamGeometry();
+
+                    using (var ctx = borderGeometry.Open())
+                    {
+                        CreateGeometry(ctx, boundRect, outerCoordinates);
+
+                        if (backgroundGeometry != null)
+                        {
+                            CreateGeometry(ctx, innerRect, innerCoordinates);
+                        }
+                    }
+
+                    _borderGeometryCache = borderGeometry;
+                }
+                else
+                {
+                    _borderGeometryCache = null;
+                }
+            }
+        }
+
+        public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
+        {
+            if (_useComplexRendering)
+            {
+                var backgroundGeometry = _backgroundGeometryCache;
+                if (backgroundGeometry != null)
+                {
+                    context.DrawGeometry(background, null, backgroundGeometry);
+                }
+
+                var borderGeometry = _borderGeometryCache;
+                if (borderGeometry != null)
+                {
+                    context.DrawGeometry(borderBrush, null, borderGeometry);
+                }
+            }
+            else
+            {
+                var borderThickness = borders.Left;
+                var cornerRadius = (float)radii.TopLeft;
+                var rect = new Rect(size);
+
+                if (background != null)
+                {
+                    context.FillRectangle(background, rect.Deflate(borders), cornerRadius);
+                }
+
+                if (borderBrush != null && borderThickness > 0)
+                {
+                    context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius);
+                }
+            }
+        }
+
+        private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates)
+        {
+            var topLeft = new Point(borderCoordinates.LeftTop, 0);
+            var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0);
+            var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight);
+            var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight);
+            var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height);
+            var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height);
+            var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft);
+            var leftTop = new Point(0, borderCoordinates.TopLeft);
+
+
+            if (topLeft.X > topRight.X)
+            {
+                var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width;
+                topLeft = new Point(scaledX, topLeft.Y);
+                topRight = new Point(scaledX, topRight.Y);
+            }
+
+            if (rightTop.Y > rightBottom.Y)
+            {
+                var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height;
+                rightTop = new Point(rightTop.X, scaledY);
+                rightBottom = new Point(rightBottom.X, scaledY);
+            }
+
+            if (bottomRight.X < bottomLeft.X)
+            {
+                var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width;
+                bottomRight = new Point(scaledX, bottomRight.Y);
+                bottomLeft = new Point(scaledX, bottomLeft.Y);
+            }
+
+            if (leftBottom.Y < leftTop.Y)
+            {
+                var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height;
+                leftBottom = new Point(leftBottom.X, scaledY);
+                leftTop = new Point(leftTop.X, scaledY);
+            }
+
+            var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y);
+            topLeft += offset;
+            topRight += offset;
+            rightTop += offset;
+            rightBottom += offset;
+            bottomRight += offset;
+            bottomLeft += offset;
+            leftBottom += offset;
+            leftTop += offset;
+
+            context.BeginFigure(topLeft, true);
+
+            //Top
+            context.LineTo(topRight);
+
+            //TopRight corner
+            var radiusX = boundRect.TopRight.X - topRight.X;
+            var radiusY = rightTop.Y - boundRect.TopRight.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Right
+            context.LineTo(rightBottom);
+
+            //BottomRight corner
+            radiusX = boundRect.BottomRight.X - bottomRight.X;
+            radiusY = boundRect.BottomRight.Y - rightBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Bottom
+            context.LineTo(bottomLeft);
+
+            //BottomLeft corner
+            radiusX = bottomLeft.X - boundRect.BottomLeft.X;
+            radiusY = boundRect.BottomLeft.Y - leftBottom.Y;
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            //Left
+            context.LineTo(leftTop);
+
+            //TopLeft corner
+            radiusX = topLeft.X - boundRect.TopLeft.X;
+            radiusY = leftTop.Y - boundRect.TopLeft.Y;
+
+            if (radiusX != 0 || radiusY != 0)
+            {
+                context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+            }
+
+            context.EndFigure(true);
+        }
+
+        private struct BorderCoordinates
+        {
+            internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter)
+            {
+                var left = 0.5 * borderThickness.Left;
+                var top = 0.5 * borderThickness.Top;
+                var right = 0.5 * borderThickness.Right;
+                var bottom = 0.5 * borderThickness.Bottom;
+
+                if (isOuter)
+                {
+                    if (cornerRadius.TopLeft == 0)
+                    {
+                        LeftTop = TopLeft = 0.0;
+                    }
+                    else
+                    {
+                        LeftTop = cornerRadius.TopLeft + left;
+                        TopLeft = cornerRadius.TopLeft + top;
+                    }
+                    if (cornerRadius.TopRight == 0)
+                    {
+                        TopRight = RightTop = 0;
+                    }
+                    else
+                    {
+                        TopRight = cornerRadius.TopRight + top;
+                        RightTop = cornerRadius.TopRight + right;
+                    }
+                    if (cornerRadius.BottomRight == 0)
+                    {
+                        RightBottom = BottomRight = 0;
+                    }
+                    else
+                    {
+                        RightBottom = cornerRadius.BottomRight + right;
+                        BottomRight = cornerRadius.BottomRight + bottom;
+                    }
+                    if (cornerRadius.BottomLeft == 0)
+                    {
+                        BottomLeft = LeftBottom = 0;
+                    }
+                    else
+                    {
+                        BottomLeft = cornerRadius.BottomLeft + bottom;
+                        LeftBottom = cornerRadius.BottomLeft + left;
+                    }
+                }
+                else
+                {
+                    LeftTop = Math.Max(0, cornerRadius.TopLeft - left);
+                    TopLeft = Math.Max(0, cornerRadius.TopLeft - top);
+                    TopRight = Math.Max(0, cornerRadius.TopRight - top);
+                    RightTop = Math.Max(0, cornerRadius.TopRight - right);
+                    RightBottom = Math.Max(0, cornerRadius.BottomRight - right);
+                    BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom);
+                    BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom);
+                    LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left);
+                }
+            }
+
+            internal readonly double LeftTop;
+            internal readonly double TopLeft;
+            internal readonly double TopRight;
+            internal readonly double RightTop;
+            internal readonly double RightBottom;
+            internal readonly double BottomRight;
+            internal readonly double BottomLeft;
+            internal readonly double LeftBottom;
+        }
+
+    }
+}

+ 64 - 0
src/Avalonia.Controls/Utils/ISelectionAdapter.cs

@@ -0,0 +1,64 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Utils
+{
+    /// <summary>
+    /// Defines an item collection, selection members, and key handling for the
+    /// selection adapter contained in the drop-down portion of an
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+    /// </summary>
+    public interface ISelectionAdapter
+    {
+        /// <summary>
+        /// Gets or sets the selected item.
+        /// </summary>
+        /// <value>The currently selected item.</value>
+        object SelectedItem { get; set; }
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.Utils.ISelectionAdapter.SelectedItem" />
+        /// property value changes.
+        /// </summary>
+        event EventHandler<SelectionChangedEventArgs> SelectionChanged;
+        
+        /// <summary>
+        /// Gets or sets a collection that is used to generate content for the
+        /// selection adapter.
+        /// </summary>
+        /// <value>The collection that is used to generate content for the
+        /// selection adapter.</value>
+        IEnumerable Items { get; set; }
+
+        /// <summary>
+        /// Occurs when a selected item is not cancelled and is committed as the
+        /// selected item.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> Commit;
+
+        /// <summary>
+        /// Occurs when a selection has been canceled.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> Cancel;
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
+        /// when a key is pressed while the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains data about the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
+        void HandleKeyDown(KeyEventArgs e);
+    }
+
+}

+ 342 - 0
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@@ -0,0 +1,342 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using System.Collections;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Utils
+{
+    /// <summary>
+    /// Represents the selection adapter contained in the drop-down portion of
+    /// an <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
+    /// </summary>
+    public class SelectingItemsControlSelectionAdapter : ISelectionAdapter
+    {
+        /// <summary>
+        /// The SelectingItemsControl instance.
+        /// </summary>
+        private SelectingItemsControl _selector;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the selection change event 
+        /// should not be fired.
+        /// </summary>
+        private bool IgnoringSelectionChanged { get; set; }
+
+        /// <summary>
+        /// Gets or sets the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        /// <value>The underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.</value>
+        public SelectingItemsControl SelectorControl
+        {
+            get { return _selector; }
+
+            set
+            {
+                if (_selector != null)
+                {
+                    _selector.SelectionChanged -= OnSelectionChanged;
+                    _selector.PointerReleased -= OnSelectorPointerReleased;
+                }
+
+                _selector = value;
+
+                if (_selector != null)
+                {
+                    _selector.SelectionChanged += OnSelectionChanged;
+                    _selector.PointerReleased += OnSelectorPointerReleased;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
+        /// property value changes.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
+
+        /// <summary>
+        /// Occurs when an item is selected and is committed to the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> Commit;
+
+        /// <summary>
+        /// Occurs when a selection is canceled before it is committed.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> Cancel;
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
+        /// class.
+        /// </summary>
+        public SelectingItemsControlSelectionAdapter()
+        {
+
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
+        /// class with the specified
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        /// <param name="selector">The
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
+        /// to wrap as a
+        /// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.</param>
+        public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector)
+        {
+            SelectorControl = selector;
+        }
+
+        /// <summary>
+        /// Gets or sets the selected item of the selection adapter.
+        /// </summary>
+        /// <value>The selected item of the underlying selection adapter.</value>
+        public object SelectedItem
+        {
+            get
+            {
+                return SelectorControl?.SelectedItem;
+            }
+
+            set
+            {
+                IgnoringSelectionChanged = true;
+                if (SelectorControl != null)
+                {
+                    SelectorControl.SelectedItem = value;
+                }
+
+                // Attempt to reset the scroll viewer's position
+                if (value == null)
+                {
+                    ResetScrollViewer();
+                }
+
+                IgnoringSelectionChanged = false;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a collection that is used to generate the content of
+        /// the selection adapter.
+        /// </summary>
+        /// <value>The collection used to generate content for the selection
+        /// adapter.</value>
+        public IEnumerable Items
+        {
+            get
+            {
+                return SelectorControl?.Items;
+            }
+            set
+            {
+                if (SelectorControl != null)
+                {
+                    SelectorControl.Items = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// If the control contains a ScrollViewer, this will reset the viewer 
+        /// to be scrolled to the top.
+        /// </summary>
+        private void ResetScrollViewer()
+        {
+            if (SelectorControl != null)
+            {
+                ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault();
+                if (sv != null)
+                {
+                    sv.Offset = new Vector(0, 0);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Handles the mouse left button up event on the selector control.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            if (e.MouseButton == MouseButton.Left)
+            {
+                OnCommit();
+            }
+        }
+
+        /// <summary>
+        /// Handles the SelectionChanged event on the SelectingItemsControl control.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The selection changed event data.</param>
+        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (IgnoringSelectionChanged)
+            {
+                return;
+            }
+
+            SelectionChanged?.Invoke(sender, e);
+        }
+
+        /// <summary>
+        /// Increments the
+        /// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
+        /// property of the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        protected void SelectedIndexIncrement()
+        {
+            if (SelectorControl != null)
+            {
+                SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1;
+            }
+        }
+
+        /// <summary>
+        /// Decrements the
+        /// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
+        /// property of the underlying
+        /// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
+        /// control.
+        /// </summary>
+        protected void SelectedIndexDecrement()
+        {
+            if (SelectorControl != null)
+            {
+                int index = SelectorControl.SelectedIndex;
+                if (index >= 0)
+                {
+                    SelectorControl.SelectedIndex--;
+                }
+                else if (index == -1)
+                {
+                    SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Provides handling for the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
+        /// when a key is pressed while the drop-down portion of the
+        /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
+        /// </summary>
+        /// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
+        /// that contains data about the
+        /// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
+        public void HandleKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    OnCommit();
+                    e.Handled = true;
+                    break;
+
+                case Key.Up:
+                    SelectedIndexDecrement();
+                    e.Handled = true;
+                    break;
+
+                case Key.Down:
+                    if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None)
+                    {
+                        SelectedIndexIncrement();
+                        e.Handled = true;
+                    }
+                    break;
+
+                case Key.Escape:
+                    OnCancel();
+                    e.Handled = true;
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
+        /// event.
+        /// </summary>
+        protected virtual void OnCommit()
+        {
+            OnCommit(this, new RoutedEventArgs());
+        }
+
+        /// <summary>
+        /// Fires the Commit event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnCommit(object sender, RoutedEventArgs e)
+        {
+            Commit?.Invoke(sender, e);
+
+            AfterAdapterAction();
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
+        /// event.
+        /// </summary>
+        protected virtual void OnCancel()
+        {
+            OnCancel(this, new RoutedEventArgs());
+        }
+
+        /// <summary>
+        /// Fires the Cancel event.
+        /// </summary>
+        /// <param name="sender">The source object.</param>
+        /// <param name="e">The event data.</param>
+        private void OnCancel(object sender, RoutedEventArgs e)
+        {
+            Cancel?.Invoke(sender, e);
+
+            AfterAdapterAction();
+        }
+
+        /// <summary>
+        /// Change the selection after the actions are complete.
+        /// </summary>
+        private void AfterAdapterAction()
+        {
+            IgnoringSelectionChanged = true;
+            if (SelectorControl != null)
+            {
+                SelectorControl.SelectedItem = null;
+                SelectorControl.SelectedIndex = -1;
+            }
+            IgnoringSelectionChanged = false;
+        }
+    }
+}

+ 7 - 1
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@@ -59,7 +59,7 @@ namespace Avalonia.Controls.Utils
 
         public void UpdateLastState()
         {
-            _states.Last.Value = _host.UndoRedoState;
+            UpdateLastState(_host.UndoRedoState);
         }
 
         public void DiscardRedo()
@@ -91,6 +91,12 @@ namespace Avalonia.Controls.Utils
             }
         }
 
+        public void Clear()
+        {
+            _states.Clear();
+            _currentNode = null;
+        }
+
         bool WeakTimer.IWeakTimerSubscriber.Tick()
         {
             Snapshot();

+ 93 - 6
src/Avalonia.Controls/Window.cs

@@ -13,6 +13,7 @@ using Avalonia.Styling;
 using System.Collections.Generic;
 using System.Linq;
 using JetBrains.Annotations;
+using System.ComponentModel;
 
 namespace Avalonia.Controls
 {
@@ -85,9 +86,22 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<WindowIcon> IconProperty =
             AvaloniaProperty.Register<Window, WindowIcon>(nameof(Icon));
 
+        /// <summary>
+        /// Defines the <see cref="WindowStartupLocation"/> proeprty.
+        /// </summary>
+        public static readonly DirectProperty<Window, WindowStartupLocation> WindowStartupLocationProperty =
+            AvaloniaProperty.RegisterDirect<Window, WindowStartupLocation>(
+                nameof(WindowStartupLocation),
+                o => o.WindowStartupLocation,
+                (o, v) => o.WindowStartupLocation = v);
+
+        public static readonly StyledProperty<bool> CanResizeProperty =
+            AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
+
         private readonly NameScope _nameScope = new NameScope();
         private object _dialogResult;
         private readonly Size _maxPlatformClientSize;
+        private WindowStartupLocation _windowStartupLoction;
 
         /// <summary>
         /// Initializes static members of the <see cref="Window"/> class.
@@ -102,6 +116,8 @@ namespace Avalonia.Controls
             ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
 
             IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue).PlatformImpl));
+
+            CanResizeProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue));
         }
 
         /// <summary>
@@ -119,6 +135,7 @@ namespace Avalonia.Controls
         public Window(IWindowImpl impl)
             : base(impl)
         {
+            impl.Closing = HandleClosing;
             _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
             Screens = new Screens(PlatformImpl?.Screen);
         }
@@ -196,6 +213,15 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Enables or disables resizing of the window
+        /// </summary>
+        public bool CanResize
+        {
+            get { return GetValue(CanResizeProperty); }
+            set { SetValue(CanResizeProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the icon of the window.
         /// </summary>
@@ -205,26 +231,38 @@ namespace Avalonia.Controls
             set { SetValue(IconProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the startup location of the window.
+        /// </summary>
+        public WindowStartupLocation WindowStartupLocation
+        {
+            get { return _windowStartupLoction; }
+            set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLoction, value); }
+        }
+
         /// <inheritdoc/>
         Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
 
         /// <inheritdoc/>
         Type IStyleable.StyleKey => typeof(Window);
 
+        /// <summary>
+        /// Fired before a window is closed.
+        /// </summary>
+        public event EventHandler<CancelEventArgs> Closing;
+
         /// <summary>
         /// Closes the window.
         /// </summary>
         public void Close()
         {
-            s_windows.Remove(this);
-            PlatformImpl?.Dispose();
-            IsVisible = false;
+            Close(false);
         }
 
         protected override void HandleApplicationExiting()
         {
             base.HandleApplicationExiting();
-            Close();
+            Close(true);
         }
 
         /// <summary>
@@ -239,7 +277,35 @@ namespace Avalonia.Controls
         public void Close(object dialogResult)
         {
             _dialogResult = dialogResult;
-            Close();
+            Close(false);
+        }
+
+        internal void Close(bool ignoreCancel)
+        {
+            var cancelClosing = false;
+            try
+            {
+                cancelClosing = HandleClosing();
+            }
+            finally
+            {
+                if (ignoreCancel || !cancelClosing)
+                {
+                    s_windows.Remove(this);
+                    PlatformImpl?.Dispose();
+                    IsVisible = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
+        /// </summary>
+        protected virtual bool HandleClosing()
+        {
+            var args = new CancelEventArgs();
+            Closing?.Invoke(this, args);
+            return args.Cancel;
         }
 
         /// <summary>
@@ -274,6 +340,7 @@ namespace Avalonia.Controls
             s_windows.Add(this);
 
             EnsureInitialized();
+            SetWindowStartupLocation();
             IsVisible = true;
             LayoutManager.Instance.ExecuteInitialLayoutPass(this);
 
@@ -314,6 +381,7 @@ namespace Avalonia.Controls
             s_windows.Add(this);
 
             EnsureInitialized();
+            SetWindowStartupLocation();
             IsVisible = true;
             LayoutManager.Instance.ExecuteInitialLayoutPass(this);
 
@@ -337,7 +405,7 @@ namespace Avalonia.Controls
                         modal?.Dispose();
                         SetIsEnabled(affectedWindows, true);
                         activated?.Activate();
-                        result.SetResult((TResult)_dialogResult);
+                        result.SetResult((TResult)(_dialogResult ?? default(TResult)));
                     });
 
                 return result.Task;
@@ -352,6 +420,25 @@ namespace Avalonia.Controls
             }
         }
 
+        void SetWindowStartupLocation()
+        {
+            if (WindowStartupLocation == WindowStartupLocation.CenterScreen)
+            {
+                var screen = Screens.ScreenFromPoint(Bounds.Position);
+
+                if (screen != null)
+                    Position = screen.WorkingArea.CenterRect(new Rect(ClientSize)).Position;
+            }
+            else if (WindowStartupLocation == WindowStartupLocation.CenterOwner)
+            {
+                if (Owner != null)
+                {
+                    var positionAsSize = Owner.ClientSize / 2 - ClientSize / 2;
+                    Position = Owner.Position + new Point(positionAsSize.Width, positionAsSize.Height);
+                }
+            }
+        }
+
         /// <inheritdoc/>
         void INameScope.Register(string name, object element)
         {

+ 24 - 0
src/Avalonia.Controls/WindowBase.cs

@@ -29,14 +29,29 @@ namespace Avalonia.Controls
         public static readonly DirectProperty<WindowBase, bool> IsActiveProperty =
             AvaloniaProperty.RegisterDirect<WindowBase, bool>(nameof(IsActive), o => o.IsActive);
 
+        /// <summary>
+        /// Defines the <see cref="Owner"/> property.
+        /// </summary>
+        public static readonly DirectProperty<WindowBase, WindowBase> OwnerProperty =
+            AvaloniaProperty.RegisterDirect<WindowBase, WindowBase>(
+                nameof(Owner),
+                o => o.Owner,
+                (o, v) => o.Owner = v);
+
         private bool _hasExecutedInitialLayoutPass;
         private bool _isActive;
         private bool _ignoreVisibilityChange;
+        private WindowBase _owner;
 
         static WindowBase()
         {
             IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
             IsVisibleProperty.Changed.AddClassHandler<WindowBase>(x => x.IsVisibleChanged);
+
+            MinWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
+            MinHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
+            MaxWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
+            MaxHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
         }
 
         public WindowBase(IWindowBaseImpl impl) : this(impl, AvaloniaLocator.Current)
@@ -100,6 +115,15 @@ namespace Avalonia.Controls
             private set;
         }
 
+        /// <summary>
+        /// Gets or sets the owner of the window.
+        /// </summary>
+        public WindowBase Owner
+        {
+            get { return _owner; }
+            set { SetAndRaise(OwnerProperty, ref _owner, value); }
+        }
+
         /// <summary>
         /// Activates the window.
         /// </summary>

+ 23 - 0
src/Avalonia.Controls/WindowStartupLocation.cs

@@ -0,0 +1,23 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Determines the startup location of the window.
+    /// </summary>
+    public enum WindowStartupLocation
+    {
+        /// <summary>
+        /// The startup location is defined by the Position property.
+        /// </summary>
+        Manual,
+
+        /// <summary>
+        /// The startup location is the center of the screen.
+        /// </summary>
+        CenterScreen,
+
+        /// <summary>
+        /// The startup location is the center of the owner window. If the owner window is not specified, the startup location will be <see cref="Manual"/>.
+        /// </summary>
+        CenterOwner
+    }
+}

+ 2 - 2
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@@ -56,7 +56,7 @@ namespace Avalonia.DesignerSupport
                             }
                         };
                 }
-                if (loaded is Application)
+                else if (loaded is Application)
                     control = new TextBlock {Text = "Application can't be previewed in design view"};
                 else
                     control = (Control) loaded;
@@ -75,4 +75,4 @@ namespace Avalonia.DesignerSupport
             return window;
         }
     }
-}
+}

+ 9 - 0
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -39,6 +39,7 @@ namespace Avalonia.DesignerSupport.Remote
         public Action<Point> PositionChanged { get; set; }
         public Action Deactivated { get; set; }
         public Action Activated { get; set; }
+        public Func<bool> Closing { get; set; }
         public IPlatformHandle Handle { get; }
         public WindowState WindowState { get; set; }
         public Size MaxClientSize { get; } = new Size(4096, 4096);
@@ -66,6 +67,10 @@ namespace Avalonia.DesignerSupport.Remote
             RenderIfNeeded();
         }
 
+        public void SetMinMaxSize(Size minSize, Size maxSize)
+        {
+        }
+
         public IScreenImpl Screen { get; } = new ScreenStub();
 
         public void Activate()
@@ -92,5 +97,9 @@ namespace Avalonia.DesignerSupport.Remote
         public void ShowTaskbarIcon(bool value)
         {
         }
+
+        public void CanResize(bool value)
+        {
+        }
     }
 }

+ 9 - 0
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -26,6 +26,7 @@ namespace Avalonia.DesignerSupport.Remote
         public Action<Rect> Paint { get; set; }
         public Action<Size> Resized { get; set; }
         public Action<double> ScalingChanged { get; set; }
+        public Func<bool> Closing { get; set; }
         public Action Closed { get; set; }
         public IMouseDevice MouseDevice { get; } = new MouseDevice();
         public Point Position { get; set; }
@@ -77,6 +78,10 @@ namespace Avalonia.DesignerSupport.Remote
 
         public IScreenImpl Screen { get; } = new ScreenStub();
 
+        public void SetMinMaxSize(Size minSize, Size maxSize)
+        {
+        }
+
         public void SetTitle(string title)
         {
         }
@@ -94,6 +99,10 @@ namespace Avalonia.DesignerSupport.Remote
         public void ShowTaskbarIcon(bool value)
         {
         }
+
+        public void CanResize(bool value)
+        {
+        }
     }
 
     class ClipboardStub : IClipboard

+ 1 - 1
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -86,7 +86,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         private void UpdateFocusedControl()
         {
-            _focusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
+            FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
         }
     }
 }

+ 3 - 0
src/Avalonia.DotNetCoreRuntime/AppBuilder.cs

@@ -10,6 +10,9 @@ using Avalonia.Shared.PlatformSupport;
 
 namespace Avalonia
 {
+    /// <summary>
+    /// Initializes platform-specific services for an <see cref="Application"/>.
+    /// </summary>
     public sealed class AppBuilder : AppBuilderBase<AppBuilder>
     {
         /// <summary>

+ 1 - 1
src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj

@@ -19,7 +19,7 @@
     <ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" />
     <ProjectReference Include="..\OSX\Avalonia.MonoMac\Avalonia.MonoMac.csproj" />
     <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
-    <ProjectReference Include="..\Windows\Avalonia.Win32.NetStandard\Avalonia.Win32.NetStandard.csproj" />
+    <ProjectReference Include="..\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\NetCore.props" />
   <Import Project="..\Shared\PlatformSupport\PlatformSupport.projitems" />

+ 4 - 1
src/Avalonia.Input/Cursors.cs

@@ -38,7 +38,10 @@ namespace Avalonia.Input
         TopLeftCorner,
         TopRightCorner,
         BottomLeftCorner,
-        BottomRightCorner
+        BottomRightCorner,
+        DragMove,
+        DragCopy,
+        DragLink,
 
         // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ 
         // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image

+ 15 - 0
src/Avalonia.Input/DataFormats.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Input
+{
+    public static class DataFormats
+    {
+        /// <summary>
+        /// Dataformat for plaintext
+        /// </summary>
+        public static string Text = nameof(Text);
+
+        /// <summary>
+        /// Dataformat for one or more filenames
+        /// </summary>
+        public static string FileNames = nameof(FileNames);
+    }
+}

+ 43 - 0
src/Avalonia.Input/DataObject.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Input
+{
+    public class DataObject : IDataObject
+    {
+        private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
+
+        public bool Contains(string dataFormat)
+        {
+            return _items.ContainsKey(dataFormat);
+        }
+
+        public object Get(string dataFormat)
+        {
+            if (_items.ContainsKey(dataFormat))
+                return _items[dataFormat];
+            return null;
+        }
+
+        public IEnumerable<string> GetDataFormats()
+        {
+            return _items.Keys;
+        }
+
+        public IEnumerable<string> GetFileNames()
+        {
+            return Get(DataFormats.FileNames) as IEnumerable<string>;
+        }
+
+        public string GetText()
+        {
+            return Get(DataFormats.Text) as string;
+        }
+
+        public void Set(string dataFormat, object value)
+        {
+            _items[dataFormat] = value;
+        }
+    }
+}

+ 54 - 0
src/Avalonia.Input/DragDrop.cs

@@ -0,0 +1,54 @@
+using System.Threading.Tasks;
+using Avalonia.Interactivity;
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Input
+{
+    public static class DragDrop
+    {
+        /// <summary>
+        /// Event which is raised, when a drag-and-drop operation enters the element.
+        /// </summary>
+        public static RoutedEvent<DragEventArgs> DragEnterEvent = RoutedEvent.Register<DragEventArgs>("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop));
+        /// <summary>
+        /// Event which is raised, when a drag-and-drop operation leaves the element.
+        /// </summary>
+        public static RoutedEvent<RoutedEventArgs> DragLeaveEvent = RoutedEvent.Register<RoutedEventArgs>("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop));
+        /// <summary>
+        /// Event which is raised, when a drag-and-drop operation is updated while over the element.
+        /// </summary>
+        public static RoutedEvent<DragEventArgs> DragOverEvent = RoutedEvent.Register<DragEventArgs>("DragOver", RoutingStrategies.Bubble, typeof(DragDrop));
+        /// <summary>
+        /// Event which is raised, when a drag-and-drop operation should complete over the element.
+        /// </summary>
+        public static RoutedEvent<DragEventArgs> DropEvent = RoutedEvent.Register<DragEventArgs>("Drop", RoutingStrategies.Bubble, typeof(DragDrop));
+
+        public static AvaloniaProperty<bool> AllowDropProperty = AvaloniaProperty.RegisterAttached<Interactive, bool>("AllowDrop", typeof(DragDrop), inherits: true);
+
+        /// <summary>
+        /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. 
+        /// </summary>
+        public static bool GetAllowDrop(Interactive interactive)
+        {
+            return interactive.GetValue(AllowDropProperty);
+        }
+
+        /// <summary>
+        /// Sets a value indicating whether the given interactive can be used as the target of a drag-and-drop operation. 
+        /// </summary>
+        public static void SetAllowDrop(Interactive interactive, bool value)
+        {
+            interactive.SetValue(AllowDropProperty, value);
+        }
+
+        /// <summary>
+        /// Starts a dragging operation with the given <see cref="IDataObject"/> and returns the applied drop effect from the target.
+        /// <seealso cref="DataObject"/>
+        /// </summary>
+        public static Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+        {
+            var src = AvaloniaLocator.Current.GetService<IPlatformDragSource>();
+            return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None);
+        }
+    }
+}

+ 111 - 0
src/Avalonia.Input/DragDropDevice.cs

@@ -0,0 +1,111 @@
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+using System.Linq;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input
+{
+    public class DragDropDevice : IDragDropDevice
+    {
+        public static readonly DragDropDevice Instance = new DragDropDevice();
+        
+        private Interactive _lastTarget = null;
+        
+        private Interactive GetTarget(IInputElement root, Point local)
+        {
+            var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType<Interactive>()?.FirstOrDefault();
+            if (target != null && DragDrop.GetAllowDrop(target))
+                return target;
+            return null;
+        }
+        
+        private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data)
+        {
+            if (target == null)
+                return DragDropEffects.None;
+            var args = new DragEventArgs(routedEvent, data)
+            {
+                RoutedEvent = routedEvent,
+                DragEffects = operation
+            };
+            target.RaiseEvent(args);
+            return args.DragEffects;
+        }
+        
+        private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+        {
+            _lastTarget = GetTarget(inputRoot, point);
+            return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data);
+        }
+
+        private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+        {
+            var target = GetTarget(inputRoot, point);
+
+            if (target == _lastTarget)
+                return RaiseDragEvent(target, DragDrop.DragOverEvent, effects, data);
+            
+            try
+            {
+                if (_lastTarget != null)
+                    _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent));
+                return RaiseDragEvent(target, DragDrop.DragEnterEvent, effects, data);
+            }
+            finally
+            {
+                _lastTarget = target;
+            }            
+        }
+
+        private void DragLeave(IInputElement inputRoot)
+        {
+            if (_lastTarget == null)
+                return;
+            try
+            {
+                _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent));
+            }
+            finally 
+            {
+                _lastTarget = null;
+            }
+        }
+
+        private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+        {
+            try
+            {
+                return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, effects, data);
+            }
+            finally 
+            {
+                _lastTarget = null;
+            }
+        }
+
+        public void ProcessRawEvent(RawInputEventArgs e)
+        {
+            if (!e.Handled && e is RawDragEvent margs)
+                ProcessRawEvent(margs);
+        }
+
+        private void ProcessRawEvent(RawDragEvent e)
+        {
+            switch (e.Type)
+            {
+                case RawDragEventType.DragEnter:
+                    e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects);
+                    break;
+                case RawDragEventType.DragOver:
+                    e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects);
+                    break;
+                case RawDragEventType.DragLeave:
+                    DragLeave(e.InputRoot);
+                    break;
+                case RawDragEventType.Drop:
+                    e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects);
+                    break;
+            }
+        }
+    }
+}

+ 13 - 0
src/Avalonia.Input/DragDropEffects.cs

@@ -0,0 +1,13 @@
+using System;
+
+namespace Avalonia.Input
+{
+    [Flags]
+    public enum DragDropEffects
+    {
+        None = 0,
+        Copy = 1,
+        Move = 2,
+        Link = 4,
+    }
+}

+ 18 - 0
src/Avalonia.Input/DragEventArgs.cs

@@ -0,0 +1,18 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+    public class DragEventArgs : RoutedEventArgs
+    {
+        public DragDropEffects DragEffects { get; set; }
+
+        public IDataObject Data { get; private set; }
+
+        public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data)
+            : base(routedEvent)
+        {
+            this.Data = data;
+        }
+
+    }
+}

+ 39 - 0
src/Avalonia.Input/IDataObject.cs

@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Input
+{
+    /// <summary>
+    /// Interface to access information about the data of a drag-and-drop operation.
+    /// </summary>
+    public interface IDataObject
+    {
+        /// <summary>
+        /// Lists all formats which are present in the DataObject.
+        /// <seealso cref="DataFormats"/>
+        /// </summary>
+        IEnumerable<string> GetDataFormats();
+
+        /// <summary>
+        /// Checks wether a given DataFormat is present in this object
+        /// <seealso cref="DataFormats"/>
+        /// </summary>
+        bool Contains(string dataFormat);
+
+        /// <summary>
+        /// Returns the dragged text if the DataObject contains any text.
+        /// <seealso cref="DataFormats.Text"/>
+        /// </summary>
+        string GetText();
+
+        /// <summary>
+        /// Returns a list of filenames if the DataObject contains filenames.
+        /// <seealso cref="DataFormats.FileNames"/>
+        /// </summary>
+        IEnumerable<string> GetFileNames();
+        
+        /// <summary>
+        /// Tries to get the data of the given DataFormat.
+        /// </summary>
+        object Get(string dataFormat);
+    }
+}

+ 1 - 1
src/Avalonia.Input/KeyboardDevice.cs

@@ -46,13 +46,13 @@ namespace Avalonia.Input
             if (element != FocusedElement)
             {
                 var interactive = FocusedElement as IInteractive;
+                FocusedElement = element;
 
                 interactive?.RaiseEvent(new RoutedEventArgs
                 {
                     RoutedEvent = InputElement.LostFocusEvent,
                 });
 
-                FocusedElement = element;
                 interactive = element as IInteractive;
 
                 interactive?.RaiseEvent(new GotFocusEventArgs

+ 10 - 7
src/Avalonia.Input/Navigation/TabNavigation.cs

@@ -221,17 +221,16 @@ namespace Avalonia.Input.Navigation
                     return parent;
                 }
 
-                var siblings = parent.GetVisualChildren()
+                var allSiblings = parent.GetVisualChildren()
                     .OfType<IInputElement>()
                     .Where(FocusExtensions.CanFocusDescendants);
-                var sibling = direction == NavigationDirection.Next ? 
-                    siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : 
-                    siblings.TakeWhile(x => x != container).LastOrDefault();
+                var siblings = direction == NavigationDirection.Next ?
+                    allSiblings.SkipWhile(x => x != container).Skip(1) :
+                    allSiblings.TakeWhile(x => x != container).Reverse();
 
-                if (sibling != null)
+                foreach (var sibling in siblings)
                 {
                     var customNext = GetCustomNext(sibling, direction);
-
                     if (customNext.handled)
                     {
                         return customNext.next;
@@ -239,13 +238,17 @@ namespace Avalonia.Input.Navigation
 
                     if (sibling.CanFocus())
                     {
-                        next = sibling;
+                        return sibling;
                     }
                     else
                     {
                         next = direction == NavigationDirection.Next ?
                             GetFocusableDescendants(sibling, direction).FirstOrDefault() :
                             GetFocusableDescendants(sibling, direction).LastOrDefault();
+                        if(next != null)
+                        {
+                            return next;
+                        }
                     }
                 }
 

+ 14 - 0
src/Avalonia.Input/Platform/IPlatformDragSource.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.Platform
+{
+    public interface IPlatformDragSource
+    {
+        Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects);
+    }
+}

+ 8 - 0
src/Avalonia.Input/Raw/IDragDropDevice.cs

@@ -0,0 +1,8 @@
+using Avalonia.Input;
+
+namespace Avalonia.Input.Raw
+{
+    public interface IDragDropDevice : IInputDevice
+    {
+    }
+}

+ 26 - 0
src/Avalonia.Input/Raw/RawDragEvent.cs

@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input.Raw
+{
+    public class RawDragEvent : RawInputEventArgs
+    {
+        public IInputElement InputRoot { get; }
+        public Point Location { get; }
+        public IDataObject Data { get; }
+        public DragDropEffects Effects { get; set; }
+        public RawDragEventType Type { get; }
+
+        public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, 
+            IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects)
+            :base(inputDevice, 0)
+        {
+            Type = type;
+            InputRoot = inputRoot;
+            Location = location;
+            Data = data;
+            Effects = effects;
+        }
+    }
+}

+ 10 - 0
src/Avalonia.Input/Raw/RawDragEventType.cs

@@ -0,0 +1,10 @@
+namespace Avalonia.Input.Raw
+{
+    public enum RawDragEventType
+    {
+        DragEnter,
+        DragOver,
+        DragLeave,
+        Drop
+    }
+}

+ 1 - 1
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@@ -20,7 +20,7 @@
     <SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
     <SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
 
-    <sys:Double x:Key="ThemeBorderThickness">2</sys:Double>
+    <Thickness x:Key="ThemeBorderThickness">2</Thickness>
     <sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>
 
     <sys:Double x:Key="FontSizeSmall">10</sys:Double>

+ 43 - 0
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@@ -0,0 +1,43 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="AutoCompleteBox">
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="Padding" Value="4"/>
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Panel>
+          <TextBox Name="PART_TextBox"
+                   Background="{TemplateBinding Background}" 
+                   BorderBrush="{TemplateBinding BorderBrush}" 
+                   BorderThickness="{TemplateBinding BorderThickness}"
+                   Padding="{TemplateBinding Padding}"
+                   Watermark="{TemplateBinding Watermark}"
+                   DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" />
+          
+          <Popup Name="PART_Popup"
+                 MinWidth="{TemplateBinding Bounds.Width}"
+                 MaxHeight="{TemplateBinding MaxDropDownHeight}"
+                 PlacementTarget="{TemplateBinding}"
+                 StaysOpen="False">
+            <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    BorderThickness="1">
+              <ListBox Name="PART_SelectingItemsControl"
+                       BorderThickness="0"
+                       Background="{TemplateBinding Background}"
+                       Foreground="{TemplateBinding Foreground}"
+                       ItemTemplate="{TemplateBinding ItemTemplate}"
+                       MemberSelector="{TemplateBinding ValueMemberSelector}"
+                       ScrollViewer.HorizontalScrollBarVisibility="Auto"
+                       ScrollViewer.VerticalScrollBarVisibility="Auto" />
+            </Border>
+          </Popup>
+        </Panel>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  
+  <Style Selector="AutoCompleteBox ListBoxItem:pointerover">
+    <Setter Property="Background" Value="#ffd0d0d0"/>
+  </Style>
+</Styles>

+ 86 - 0
src/Avalonia.Themes.Default/ButtonSpinner.xaml

@@ -0,0 +1,86 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="ButtonSpinner">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
+    <Setter Property="VerticalContentAlignment" Value="Center"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton">
+    <Setter Property="RepeatButton.Background" Value="Transparent"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="Transparent"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton:pointerover">
+    <Setter Property="RepeatButton.Background" Value="{DynamicResource ThemeControlMidBrush}"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,5 L4.5,.5 9,5 6,5 4.5,3.5 3,5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_DecreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,0 L3,0 4.5,1.5 6,0 9,0 4.5,4.5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:right">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="*,Auto">
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="0"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+            <Grid Grid.Column="1" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:left">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="Auto,*">
+            <Grid Grid.Column="0" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="1"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 126 - 0
src/Avalonia.Themes.Default/DatePicker.xaml

@@ -0,0 +1,126 @@
+<!--
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+-->
+
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <Style Selector="DatePicker">
+
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="Padding" Value="4"/>
+    
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="*,Auto">
+
+          <Grid.Styles>
+
+            <Style Selector="Button.CalendarDropDown">
+              <Setter Property="Template">
+                <ControlTemplate>
+                  <Grid Height="18"
+                        Width="19"
+                        HorizontalAlignment="Center"
+                        VerticalAlignment="Center"
+                        Margin="0"
+                        Background="#FFFFFFFF"
+                        ColumnDefinitions="*,*,*,*"
+                        RowDefinitions="23*,19*,19*,19*"
+                        ClipToBounds="False">
+
+                    <Border Name="Highlight"
+                            Margin="-1"
+                            Grid.ColumnSpan="4"
+                            Grid.Row="0"
+                            Grid.RowSpan="4"
+                            BorderThickness="1"
+                            BorderBrush="{DynamicResource HighlightBrush}" />
+                    <Border Name="Background"
+                            Margin="0,-1,0,0"
+                            Grid.ColumnSpan="4"
+                            Grid.Row="1"
+                            Grid.RowSpan="3"
+                            BorderThickness="1"
+                            BorderBrush="{DynamicResource ThemeBorderDarkBrush}"
+                            CornerRadius=".5" />
+                    <Rectangle Grid.ColumnSpan="4"
+                               Grid.RowSpan="1"
+                               StrokeThickness="1"
+                               Stroke="{DynamicResource ThemeBorderDarkBrush}"
+                               Fill="{DynamicResource ThemeAccentBrush}">
+                    </Rectangle>
+                    <Path HorizontalAlignment="Center"
+                          Margin="4,3,4,3"
+                          VerticalAlignment="Center"
+                          RenderTransformOrigin="0.5,0.5"
+                          Grid.Column="0"
+                          Grid.Row="1"
+                          Grid.ColumnSpan="4"
+                          Grid.RowSpan="3"
+                          Fill="{DynamicResource ThemeBorderDarkBrush}"
+                          Stretch="Fill"
+                          Data="M11.426758,8.4305077 L11.749023,8.4305077 L11.749023,16.331387 L10.674805,16.331387 L10.674805,10.299648 L9.0742188,11.298672 L9.0742188,10.294277 C9.4788408,10.090176 9.9094238,9.8090878 10.365967,9.4510155 C10.82251,9.0929432 11.176106,8.7527733 11.426758,8.4305077 z M14.65086,8.4305077 L18.566387,8.4305077 L18.566387,9.3435936 L15.671368,9.3435936 L15.671368,11.255703 C15.936341,11.058764 16.27293,10.960293 16.681133,10.960293 C17.411602,10.960293 17.969301,11.178717 18.354229,11.615566 C18.739157,12.052416 18.931622,12.673672 18.931622,13.479336 C18.931622,15.452317 18.052553,16.438808 16.294415,16.438808 C15.560365,16.438808 14.951641,16.234707 14.468243,15.826504 L14.881817,14.929531 C15.368796,15.326992 15.837872,15.525723 16.289043,15.525723 C17.298809,15.525723 17.803692,14.895514 17.803692,13.635098 C17.803692,12.460618 17.305971,11.873379 16.310528,11.873379 C15.83071,11.873379 15.399232,12.079271 15.016094,12.491055 L14.65086,12.238613 z" />
+
+                    <Ellipse HorizontalAlignment="Center" VerticalAlignment="Center" Fill="#FFFFFFFF" StrokeThickness="0" Grid.ColumnSpan="4" Width="3" Height="3"/>
+                  </Grid>
+                </ControlTemplate>
+              </Setter>
+            </Style>
+
+            <Style Selector="Button.CalendarDropDown /template/ Border#Highlight">
+              <Setter Property="IsVisible" Value="False"/>
+            </Style>
+            <Style Selector="Button.CalendarDropDown:pressed /template/ Border#Highlight">
+              <Setter Property="IsVisible" Value="True"/>
+            </Style>
+
+            <Style Selector="Button.CalendarDropDown:pointerover /template/ Border#Background">
+              <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
+            </Style>
+            
+          </Grid.Styles>
+          
+          <TextBox Name="PART_TextBox"
+                   Background="{TemplateBinding Background}" 
+                   BorderBrush="{TemplateBinding BorderBrush}" 
+                   BorderThickness="{TemplateBinding BorderThickness}" 
+                   Padding="{TemplateBinding Padding}"
+                   Watermark="{TemplateBinding Watermark}"
+                   UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}"
+                   DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
+                   Grid.Column="0"/>
+
+          <Button Name="PART_Button"
+                  Grid.Column="1"
+                  Width="20"
+                  Classes="CalendarDropDown"
+                  Foreground="{TemplateBinding Foreground}"
+                  Background="#00FFFFFF"
+                  BorderThickness="0"
+                  Margin="2,0,2,0"
+                  Padding="0"
+                  ClipToBounds="False"
+                  Focusable="False"/>
+
+          <Popup Name="PART_Popup"
+                 PlacementTarget="{TemplateBinding}"
+                 StaysOpen="False">
+            <Calendar Name="PART_Calendar"
+                      FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
+                      IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}"/>
+          </Popup>
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DatePicker:focus /template/ TextBox#PART_TextBox">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
+  </Style>
+  
+</Styles>

Some files were not shown because too many files changed in this diff