Browse Source

Merge branch 'master' into fix/allow_opening_popup_when_not_attached

Steven Kirk 5 years ago
parent
commit
a182f106e3
100 changed files with 1674 additions and 677 deletions
  1. 1 0
      .gitignore
  2. 1 0
      .ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject
  3. 5 0
      .ncrunch/Sandbox.v3.ncrunchproject
  4. 53 0
      Avalonia.sln
  5. 3 0
      Avalonia.v3.ncrunchsolution
  6. 8 17
      azure-pipelines.yml
  7. 1 1
      build/NetFX.props
  8. 2 2
      build/SkiaSharp.props
  9. 1 0
      dirs.proj
  10. 1 1
      global.json
  11. 1 1
      native/Avalonia.Native/src/OSX/rendertarget.mm
  12. 52 3
      nukebuild/Build.cs
  13. 5 0
      nukebuild/BuildParameters.cs
  14. 1 0
      nukebuild/_build.csproj
  15. 4 1
      samples/ControlCatalog/MainView.xaml
  16. 5 0
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  17. 57 1
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  18. 32 3
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  19. 20 30
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  20. 1 0
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  21. 43 14
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  22. 6 2
      samples/RenderDemo/MainWindow.xaml
  23. 1 2
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  24. 42 6
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  25. 8 0
      samples/Sandbox/App.axaml
  26. 22 0
      samples/Sandbox/App.axaml.cs
  27. 4 0
      samples/Sandbox/MainWindow.axaml
  28. 20 0
      samples/Sandbox/MainWindow.axaml.cs
  29. 17 0
      samples/Sandbox/Program.cs
  30. 18 0
      samples/Sandbox/Sandbox.csproj
  31. 3 0
      src/Avalonia.Base/ApiCompatBaseline.txt
  32. 3 14
      src/Avalonia.Base/AvaloniaProperty.cs
  33. 45 3
      src/Avalonia.Base/AvaloniaProperty`1.cs
  34. 67 1
      src/Avalonia.Base/Collections/AvaloniaList.cs
  35. 16 1
      src/Avalonia.Base/DirectPropertyBase.cs
  36. 35 19
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  37. 7 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  38. 6 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  39. 2 0
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  40. 1 1
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  41. 54 45
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  42. 5 0
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  43. 2 0
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  44. 2 0
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  45. 2 0
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  46. 2 2
      src/Avalonia.Controls.DataGrid/EventArgs.cs
  47. 3 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  48. 10 4
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  49. 7 0
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  50. 96 0
      src/Avalonia.Controls/AutoCompleteBox.cs
  51. 2 0
      src/Avalonia.Controls/Button.cs
  52. 2 0
      src/Avalonia.Controls/ButtonSpinner.cs
  53. 2 0
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  54. 2 0
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  55. 2 0
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  56. 2 0
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  57. 2 0
      src/Avalonia.Controls/Chrome/TitleBar.cs
  58. 8 2
      src/Avalonia.Controls/ColumnDefinitions.cs
  59. 2 0
      src/Avalonia.Controls/DataValidationErrors.cs
  60. 3 1
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  61. 3 1
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  62. 2 1
      src/Avalonia.Controls/Expander.cs
  63. 9 0
      src/Avalonia.Controls/IconElement.cs
  64. 2 0
      src/Avalonia.Controls/ItemsControl.cs
  65. 1 0
      src/Avalonia.Controls/ListBox.cs
  66. 2 0
      src/Avalonia.Controls/ListBoxItem.cs
  67. 2 0
      src/Avalonia.Controls/MenuItem.cs
  68. 2 2
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  69. 1 1
      src/Avalonia.Controls/NativeMenu.Export.cs
  70. 1 1
      src/Avalonia.Controls/NativeMenuItem.cs
  71. 2 0
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  72. 2 0
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  73. 21 0
      src/Avalonia.Controls/PathIcon.cs
  74. 3 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  75. 2 2
      src/Avalonia.Controls/Primitives/AccessText.cs
  76. 2 1
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  77. 1 1
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  78. 3 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  79. 258 81
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  80. 2 0
      src/Avalonia.Controls/Primitives/Thumb.cs
  81. 2 0
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  82. 2 0
      src/Avalonia.Controls/Primitives/Track.cs
  83. 2 0
      src/Avalonia.Controls/ProgressBar.cs
  84. 19 5
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  85. 34 2
      src/Avalonia.Controls/ScrollViewer.cs
  86. 278 0
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  87. 75 38
      src/Avalonia.Controls/Selection/SelectionModel.cs
  88. 2 0
      src/Avalonia.Controls/Slider.cs
  89. 6 1
      src/Avalonia.Controls/SplitView.cs
  90. 14 41
      src/Avalonia.Controls/TabControl.cs
  91. 2 0
      src/Avalonia.Controls/TabItem.cs
  92. 6 3
      src/Avalonia.Controls/TextBlock.cs
  93. 2 0
      src/Avalonia.Controls/TextBox.cs
  94. 3 1
      src/Avalonia.Controls/ToggleSwitch.cs
  95. 2 0
      src/Avalonia.Controls/ToolTip.cs
  96. 2 0
      src/Avalonia.Controls/TreeViewItem.cs
  97. 0 283
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  98. 4 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  99. 37 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  100. 31 20
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

+ 1 - 0
.gitignore

@@ -117,6 +117,7 @@ ClientBin/
 *.[Pp]ublish.xml
 *.pfx
 *.publishsettings
+Events_Avalonia.cs
 
 # RIA/Silverlight projects
 Generated_Code/

+ 1 - 0
.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject

@@ -3,5 +3,6 @@
     <HiddenComponentWarnings>
       <Value>MissingOrIgnoredProjectReference</Value>
     </HiddenComponentWarnings>
+        <FixtureName>Avalonia.Controls.UnitTests.TimePickerTests</FixtureName>
   </Settings>
 </ProjectConfiguration>

+ 5 - 0
.ncrunch/Sandbox.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 53 - 0
Avalonia.sln

@@ -222,6 +222,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -2012,6 +2016,54 @@ Global
 		{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU
 		{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -2070,6 +2122,7 @@ Global
 		{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
 		{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
+		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 3 - 0
Avalonia.v3.ncrunchsolution

@@ -6,6 +6,9 @@
       <Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value>
     </AdditionalFilesToIncludeForSolution>
     <AllowParallelTestExecution>True</AllowParallelTestExecution>
+    <CustomBuildProperties>
+      <Value>RunApiCompat = false</Value>
+    </CustomBuildProperties>
     <ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
     <SolutionConfigured>True</SolutionConfigured>
   </Settings>

+ 8 - 17
azure-pipelines.yml

@@ -35,16 +35,9 @@ jobs:
     vmImage: 'macOS-10.14'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.101'
+    displayName: 'Use .NET Core SDK 3.1.401'
     inputs:
-      packageType: sdk
-      version: 3.1.101
-
-  - task: UseDotNet@2
-    displayName: 'Use .NET Core Runtime 3.1.1'
-    inputs:
-      packageType: runtime
-      version: 3.1.1
+      version: 3.1.401
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'
@@ -63,13 +56,6 @@ jobs:
       xcodeVersion: '10' # Options: 8, 9, default, specifyPath
       args: '-derivedDataPath ./'
 
-  - task: CmdLine@2
-    displayName: 'Install CastXML'
-    inputs:
-      script: |
-        brew update
-        brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
-
   - task: CmdLine@2
     displayName: 'Install Nuke'
     inputs:
@@ -88,7 +74,7 @@ jobs:
         export PATH="$PATH:$HOME/.dotnet/tools"
         dotnet --info
         printenv
-        nuke --target CiAzureOSX --configuration Release
+        nuke --target CiAzureOSX --configuration Release --skip-previewer
 
   - task: PublishTestResults@2
     inputs:
@@ -112,6 +98,11 @@ jobs:
   pool:
     vmImage: 'windows-2019'
   steps:
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 3.1.401'
+    inputs:
+      version: 3.1.401
+
   - task: CmdLine@2
     displayName: 'Install Nuke'
     inputs:

+ 1 - 1
build/NetFX.props

@@ -1,7 +1,7 @@
 <Project>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
   </ItemGroup>
 
 </Project>

+ 2 - 2
build/SkiaSharp.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="2.80.2-preview.33" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2-preview.33" />
+    <PackageReference Include="SkiaSharp" Version="2.80.2" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
   </ItemGroup>
 </Project>

+ 1 - 0
dirs.proj

@@ -7,6 +7,7 @@
     <ProjectReference Remove="**/*.shproj" />
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
+    <ProjectReference Remove="tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj" />
   </ItemGroup>
   <!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
   <ItemGroup>

+ 1 - 1
global.json

@@ -1,6 +1,6 @@
 {
 	"sdk": {
-		"version": "3.1.101"
+		"version": "3.1.401"
 	},
     "msbuild-sdks": {
         "Microsoft.Build.Traversal": "1.0.43",

+ 1 - 1
native/Avalonia.Native/src/OSX/rendertarget.mm

@@ -110,7 +110,7 @@
         if(_renderbuffer != 0)
             glDeleteRenderbuffers(1, &_renderbuffer);
     }
-    IOSurfaceDecrementUseCount(surface);
+    CFRelease(surface);
 }
 @end
 

+ 52 - 3
nukebuild/Build.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading;
+using System.Threading.Tasks;
 using System.Xml.Linq;
 using Nuke.Common;
 using Nuke.Common.Git;
@@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild;
 using Nuke.Common.Tools.Npm;
 using Nuke.Common.Utilities;
 using Nuke.Common.Utilities.Collections;
+using Pharmacist.Core;
 using static Nuke.Common.EnvironmentInfo;
 using static Nuke.Common.IO.FileSystemTasks;
 using static Nuke.Common.IO.PathConstruction;
@@ -124,6 +126,7 @@ partial class Build : NukeBuild
 
     Target CompileHtmlPreviewer => _ => _
         .DependsOn(Clean)
+        .OnlyWhenStatic(() => !Parameters.SkipPreviewer)
         .Executes(() =>
         {
             var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
@@ -135,11 +138,21 @@ partial class Build : NukeBuild
                 .SetWorkingDirectory(webappDir)
                 .SetCommand("dist"));
         });
-    
-    Target Compile => _ => _
+
+    Target CompileNative => _ => _
         .DependsOn(Clean)
-        .DependsOn(CompileHtmlPreviewer)
+        .OnlyWhenStatic(() => EnvironmentInfo.IsOsx)
         .Executes(() =>
+        {
+            var project = $"{RootDirectory}/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/";
+            var args = $"-project {project} -configuration {Parameters.Configuration} CONFIGURATION_BUILD_DIR={RootDirectory}/Build/Products/Release";
+            ProcessTasks.StartProcess("xcodebuild", args).AssertZeroExitCode();
+        });
+
+    Target Compile => _ => _
+        .DependsOn(Clean, CompileNative)
+        .DependsOn(CompileHtmlPreviewer)
+        .Executes(async () =>
         {
             if (Parameters.IsRunningOnWindows)
                 MsBuildCommon(Parameters.MSBuildSolution, c => c
@@ -153,8 +166,44 @@ partial class Build : NukeBuild
                     .AddProperty("PackageVersion", Parameters.Version)
                     .SetConfiguration(Parameters.Configuration)
                 );
+
+            await CompileReactiveEvents();
         });
 
+    async Task CompileReactiveEvents()
+    {
+        var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration);
+        var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll")
+            .Where(file => !file.Contains("Avalonia.Build.Tasks") &&
+                            !file.Contains("Avalonia.Remote.Protocol"));
+
+        var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First();
+        var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs");
+        if (File.Exists(eventsBuildFile))
+            File.Delete(eventsBuildFile);
+
+        using (var stream = File.Create(eventsBuildFile))
+        using (var writer = new StreamWriter(stream))
+        {
+            await ObservablesForEventGenerator.ExtractEventsFromAssemblies(
+                writer, avaloniaAssemblies, new string[0], "netstandard2.0"
+            );
+        }
+
+        var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj");
+        if (Parameters.IsRunningOnWindows)
+            MsBuildCommon(eventsProject, c => c
+                .SetArgumentConfigurator(a => a.Add("/r"))
+                .AddTargets("Build")
+            );
+        else
+            DotNetBuild(c => c
+                .SetProjectFile(eventsProject)
+                .AddProperty("PackageVersion", Parameters.Version)
+                .SetConfiguration(Parameters.Configuration)
+            );
+    }
+
     void RunCoreTest(string projectName)
     {
         Information($"Running tests from {projectName}");

+ 5 - 0
nukebuild/BuildParameters.cs

@@ -19,10 +19,14 @@ public partial class Build
     [Parameter("force-nuget-version")]
     public string ForceNugetVersion { get; set; }
 
+    [Parameter("skip-previewer")]
+    public bool SkipPreviewer { get; set; }
+
     public class BuildParameters
     {
         public string Configuration { get; }
         public bool SkipTests { get; }
+        public bool SkipPreviewer {get;}
         public string MainRepo { get; }
         public string MasterBranch { get; }
         public string RepositoryName { get; }
@@ -63,6 +67,7 @@ public partial class Build
             // ARGUMENTS
             Configuration = b.Configuration ?? "Release";
             SkipTests = b.SkipTests;
+            SkipPreviewer = b.SkipPreviewer;
 
             // CONFIGURATION
             MainRepo = "https://github.com/AvaloniaUI/Avalonia";

+ 1 - 0
nukebuild/_build.csproj

@@ -17,6 +17,7 @@
     <PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
     <!-- Keep in sync with Avalonia.Build.Tasks -->
     <PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" />
+    <PackageReference Include="Pharmacist.Core" Version="1.8.1" />
   </ItemGroup>
 
   <ItemGroup>

+ 4 - 1
samples/ControlCatalog/MainView.xaml

@@ -45,7 +45,10 @@
         <pages:ItemsRepeaterPage/>
       </TabItem>
       <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
-      <TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
+      <TabItem Header="ListBox"
+               ScrollViewer.VerticalScrollBarVisibility="Disabled">
+        <pages:ListBoxPage/>
+      </TabItem>
       <TabItem Header="Menu"><pages:MenuPage/></TabItem>
       <TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
 	    <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>

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

@@ -51,6 +51,11 @@
                          Width="200"
                          Margin="0,0,0,8"
                          FilterMode="None"/>
+        <TextBlock Text="Custom Autocomplete"/>
+        <AutoCompleteBox Name="CustomAutocompleteBox"
+                         Width="200"
+                         Margin="0,0,0,8"
+                         FilterMode="None"/>
       </StackPanel>
     </StackPanel>
   </StackPanel>

+ 57 - 1
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@@ -92,13 +92,28 @@ namespace ControlCatalog.Pages
         }
         public StateData[] States { get; private set; }
         
+        private LinkedList<string>[] BuildAllSentences()
+        {
+            return new string[]
+            {
+                "Hello world",
+                "No this is Patrick",
+                "Never gonna give you up",
+                "How does one patch KDE2 under FreeBSD"
+            }
+            .Select(x => new LinkedList<string>(x.Split(' ')))
+            .ToArray();
+        }
+        public LinkedList<string>[] Sentences { get; private set; }
+
         public AutoCompleteBoxPage()
         {
             this.InitializeComponent();
 
             States = BuildAllStates();
+            Sentences = BuildAllSentences();
 
-            foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
+            foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox"))
             {
                 box.Items = States;
             }
@@ -116,6 +131,11 @@ namespace ControlCatalog.Pages
 
             var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
             asyncBox.AsyncPopulator = PopulateAsync;
+
+            var customAutocompleteBox = this.FindControl<AutoCompleteBox>("CustomAutocompleteBox");
+            customAutocompleteBox.Items = Sentences.SelectMany(x => x);
+            customAutocompleteBox.TextFilter = LastWordContains;
+            customAutocompleteBox.TextSelector = AppendWord;
         }
         private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox()
         {
@@ -137,6 +157,42 @@ namespace ControlCatalog.Pages
                       .ToList();
         }
 
+        private bool LastWordContains(string searchText, string item)
+        {
+            var words = searchText.Split(' ');
+            var options = Sentences.Select(x => x.First).ToArray();
+            for (var i = 0; i < words.Length; ++i)
+            {
+                var word = words[i];
+                for (var j = 0; j < options.Length; ++j)
+                {
+                    var option = options[j];
+                    if (option == null)
+                        continue;
+
+                    if (i == words.Length - 1)
+                    {
+                        options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null;
+                    }
+                    else
+                    {
+                        options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null;
+                    }
+                }
+            }
+
+            return options.Any(x => x != null && x.Value == item);
+        }
+        private string AppendWord(string text, string item)
+        {
+            string[] parts = text.Split(' ');
+            if (parts.Length == 0)
+                return item;
+
+            parts[parts.Length - 1] = item;
+            return string.Join(" ", parts);
+        }
+
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);

+ 32 - 3
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@@ -1,8 +1,12 @@
+using System.Collections;
 using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using ControlCatalog.Models;
 using Avalonia.Collections;
+using Avalonia.Data;
 
 namespace ControlCatalog.Pages
 {
@@ -11,12 +15,22 @@ namespace ControlCatalog.Pages
         public DataGridPage()
         {
             this.InitializeComponent();
+
+            var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer());
+            var collectionView1 = new DataGridCollectionView(Countries.All);
+            collectionView1.SortDescriptions.Add(dataGridSortDescription);
             var dg1 = this.FindControl<DataGrid>("dataGrid1");
             dg1.IsReadOnly = true;
             dg1.LoadingRow += Dg1_LoadingRow;
-            var collectionView1 = new DataGridCollectionView(Countries.All);
-            //collectionView.GroupDescriptions.Add(new PathGroupDescription("Region"));
-
+            dg1.Sorting += (s, a) =>
+            {
+                var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path;
+                if (property == dataGridSortDescription.PropertyPath
+                    && !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
+                {
+                    collectionView1.SortDescriptions.Add(dataGridSortDescription);
+                }
+            };
             dg1.Items = collectionView1;
 
             var dg2 = this.FindControl<DataGrid>("dataGridGrouping");
@@ -53,5 +67,20 @@ namespace ControlCatalog.Pages
         {
             AvaloniaXamlLoader.Load(this);
         }
+
+        private class ReversedStringComparer : IComparer<object>, IComparer
+        {
+            public int Compare(object x, object y)
+            {
+                if (x is string left && y is string right)
+                {
+                    var reversedLeft = new string(left.Reverse().ToArray());
+                    var reversedRight = new string(right.Reverse().ToArray());
+                    return reversedLeft.CompareTo(reversedRight);
+                }
+
+                return Comparer.Default.Compare(x, y);
+            }
+        }
     }
 }

+ 20 - 30
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -1,35 +1,25 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ListBoxPage">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock Classes="h1">ListBox</TextBlock>
-    <TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
-
-    <StackPanel Orientation="Horizontal"
-              Margin="0,16,0,0"
-              HorizontalAlignment="Center"
-              Spacing="16">
-      <StackPanel Orientation="Vertical" Spacing="8">
-        <ListBox Items="{Binding Items}"
-                 Selection="{Binding Selection}"
-                 AutoScrollToSelectedItem="True"
-                 SelectionMode="{Binding SelectionMode}"
-                 Width="250"
-                 Height="350"/>
-
-        <Button Command="{Binding AddItemCommand}">Add</Button>
-
-        <Button Command="{Binding RemoveItemCommand}">Remove</Button>
-
-        <Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
-
-        <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
-          <ComboBoxItem>Single</ComboBoxItem>
-          <ComboBoxItem>Multiple</ComboBoxItem>
-          <ComboBoxItem>Toggle</ComboBoxItem>
-          <ComboBoxItem>AlwaysSelected</ComboBoxItem>
-        </ComboBox>
-      </StackPanel>
+  <DockPanel>
+    <StackPanel DockPanel.Dock="Top" Margin="4">
+      <TextBlock Classes="h1">ListBox</TextBlock>
+      <TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
     </StackPanel>
-  </StackPanel>
+    <StackPanel DockPanel.Dock="Right" Margin="4">
+      <CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
+      <CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
+      <CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
+      <CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
+    </StackPanel>
+    <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4">
+      <Button Command="{Binding AddItemCommand}">Add</Button>
+      <Button Command="{Binding RemoveItemCommand}">Remove</Button>
+      <Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
+    </StackPanel>
+    <ListBox Items="{Binding Items}"
+             Selection="{Binding Selection}"
+             AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
+             SelectionMode="{Binding SelectionMode}"/>
+  </DockPanel>
 </UserControl>

+ 1 - 0
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.OpenGL;
+using Avalonia.OpenGL.Controls;
 using Avalonia.Platform.Interop;
 using Avalonia.Threading;
 using static Avalonia.OpenGL.GlConsts;

+ 43 - 14
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@@ -10,22 +10,39 @@ namespace ControlCatalog.ViewModels
 {
     public class ListBoxPageViewModel : ReactiveObject
     {
+        private bool _multiple;
+        private bool _toggle;
+        private bool _alwaysSelected;
+        private bool _autoScrollToSelectedItem = true;
         private int _counter;
-        private SelectionMode _selectionMode;
+        private ObservableAsPropertyHelper<SelectionMode> _selectionMode;
 
         public ListBoxPageViewModel()
         {
             Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
+            
             Selection = new SelectionModel<string>();
             Selection.Select(1);
 
+            _selectionMode = this.WhenAnyValue(
+                x => x.Multiple,
+                x => x.Toggle,
+                x => x.AlwaysSelected,
+                (m, t, a) =>
+                    (m ? SelectionMode.Multiple : 0) |
+                    (t ? SelectionMode.Toggle : 0) |
+                    (a ? SelectionMode.AlwaysSelected : 0))
+                .ToProperty(this, x => x.SelectionMode);
+
             AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
 
             RemoveItemCommand = ReactiveCommand.Create(() =>
             {
-                while (Selection.Count > 0)
+                var items = Selection.SelectedItems.ToList();
+
+                foreach (var item in items)
                 {
-                    Items.Remove(Selection.SelectedItems.First());
+                    Items.Remove(item);
                 }
             });
 
@@ -42,25 +59,37 @@ namespace ControlCatalog.ViewModels
         }
 
         public ObservableCollection<string> Items { get; }
-
         public SelectionModel<string> Selection { get; }
+        public SelectionMode SelectionMode => _selectionMode.Value;
 
-        public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+        public bool Multiple
+        {
+            get => _multiple;
+            set => this.RaiseAndSetIfChanged(ref _multiple, value);
+        }
 
-        public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+        public bool Toggle
+        {
+            get => _toggle;
+            set => this.RaiseAndSetIfChanged(ref _toggle, value);
+        }
 
-        public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
+        public bool AlwaysSelected
+        {
+            get => _alwaysSelected;
+            set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value);
+        }
 
-        public SelectionMode SelectionMode
+        public bool AutoScrollToSelectedItem
         {
-            get => _selectionMode;
-            set
-            {
-                Selection.Clear();
-                this.RaiseAndSetIfChanged(ref _selectionMode, value);
-            }
+            get => _autoScrollToSelectedItem;
+            set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
         }
 
+        public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+        public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+        public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
+
         private string GenerateItem() => $"Item {_counter++.ToString()}";
     }
 }

+ 6 - 2
samples/RenderDemo/MainWindow.xaml

@@ -3,8 +3,8 @@
         x:Class="RenderDemo.MainWindow"
         Title="AvaloniaUI Rendering Test"
         xmlns:pages="clr-namespace:RenderDemo.Pages"
-        Width="800"
-        Height="600">
+        Width="{Binding Width, Mode=TwoWay}"
+        Height="{Binding Height, Mode=TwoWay}">
   <DockPanel>
     <Menu DockPanel.Dock="Top">
       <MenuItem Header="Rendering">
@@ -24,6 +24,10 @@
           </MenuItem.Icon>
         </MenuItem>
       </MenuItem>
+      <MenuItem Header="Tests">
+        <MenuItem Header="Resize window"
+                  Command="{Binding ResizeWindow}"/>
+      </MenuItem>
     </Menu>
     <TabControl Classes="sidebar">
       <TabItem Header="Animations">

+ 1 - 2
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@@ -61,7 +61,6 @@ namespace RenderDemo.Pages
             {
                 Foreground = Brushes.Black,
                 GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
-                BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
             };
 
             drawingGroup.Children.Add(glyphRunDrawing);
@@ -69,7 +68,7 @@ namespace RenderDemo.Pages
             var geometryDrawing = new GeometryDrawing
             {
                 Pen = new Pen(Brushes.Black),
-                Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
+                Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
             };
 
             drawingGroup.Children.Add(geometryDrawing);

+ 42 - 6
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@@ -1,5 +1,6 @@
-using System;
-using System.Reactive;
+using System.Reactive;
+using System.Threading.Tasks;
+
 using ReactiveUI;
 
 namespace RenderDemo.ViewModels
@@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels
     {
         private bool drawDirtyRects = false;
         private bool drawFps = true;
+        private double width = 800;
+        private double height = 600;
 
         public MainWindowViewModel()
         {
             ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
             ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps);
+            ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync);
         }
 
         public bool DrawDirtyRects
         {
-            get { return drawDirtyRects; }
-            set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); }
+            get => drawDirtyRects;
+            set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value);
         }
 
         public bool DrawFps
         {
-            get { return drawFps; }
-            set { this.RaiseAndSetIfChanged(ref drawFps, value); }
+            get => drawFps;
+            set => this.RaiseAndSetIfChanged(ref drawFps, value);
+        }
+
+        public double Width
+        {
+            get => width;
+            set => this.RaiseAndSetIfChanged(ref width, value);
+        }
+
+        public double Height
+        {
+            get => height;
+            set => this.RaiseAndSetIfChanged(ref height, value);
         }
 
         public ReactiveCommand<Unit, bool> ToggleDrawDirtyRects { get; }
         public ReactiveCommand<Unit, bool> ToggleDrawFps { get; }
+        public ReactiveCommand<Unit, Unit> ResizeWindow { get; }
+
+        private async Task ResizeWindowAsync()
+        {
+            for (int i = 0; i < 30; i++)
+            {
+                Width += 10;
+                Height += 5;
+                await Task.Delay(10);
+            }
+
+            await Task.Delay(10);
+
+            for (int i = 0; i < 30; i++)
+            {
+                Width -= 10;
+                Height -= 5;
+                await Task.Delay(10);
+            }
+        }
     }
 }

+ 8 - 0
samples/Sandbox/App.axaml

@@ -0,0 +1,8 @@
+<Application
+    xmlns="https://github.com/avaloniaui" 
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    x:Class="Sandbox.App">
+    <Application.Styles>
+        <StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml"/>
+    </Application.Styles>
+</Application>

+ 22 - 0
samples/Sandbox/App.axaml.cs

@@ -0,0 +1,22 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace Sandbox
+{
+    public class App : Application
+    {
+        public override void Initialize()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        public override void OnFrameworkInitializationCompleted()
+        {
+            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+            {
+                desktopLifetime.MainWindow = new MainWindow();
+            }
+        }
+    }
+}

+ 4 - 0
samples/Sandbox/MainWindow.axaml

@@ -0,0 +1,4 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        x:Class="Sandbox.MainWindow">
+</Window>

+ 20 - 0
samples/Sandbox/MainWindow.axaml.cs

@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Sandbox
+{
+    public class MainWindow : Window
+    {
+        public MainWindow()
+        {
+            this.InitializeComponent();
+            this.AttachDevTools();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 17 - 0
samples/Sandbox/Program.cs

@@ -0,0 +1,17 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace Sandbox
+{
+    public class Program
+    {
+        static void Main(string[] args)
+        {
+            AppBuilder.Configure<App>()
+                .UsePlatformDetect()
+                .UseReactiveUI()
+                .LogToDebug()
+                .StartWithClassicDesktopLifetime(args);
+        }
+    }
+}

+ 18 - 0
samples/Sandbox/Sandbox.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
+  </ItemGroup>
+  
+  <Import Project="..\..\build\SampleApp.props" />
+  <Import Project="..\..\build\ReferenceCoreLibraries.props" />
+  <Import Project="..\..\build\BuildTargets.targets" />
+</Project>

+ 3 - 0
src/Avalonia.Base/ApiCompatBaseline.txt

@@ -0,0 +1,3 @@
+Compat issues with assembly Avalonia.Base:
+CannotAddAbstractMembers : Member 'protected System.IObservable<Avalonia.AvaloniaPropertyChangedEventArgs> Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract.
+Total Issues: 1

+ 3 - 14
src/Avalonia.Base/AvaloniaProperty.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Utilities;
@@ -18,7 +17,6 @@ namespace Avalonia
         public static readonly object UnsetValue = new UnsetValueType();
 
         private static int s_nextId;
-        private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
         private readonly PropertyMetadata _defaultMetadata;
         private readonly Dictionary<Type, PropertyMetadata> _metadata;
         private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>();
@@ -50,7 +48,6 @@ namespace Avalonia
                 throw new ArgumentException("'name' may not contain periods.");
             }
 
-            _changed = new Subject<AvaloniaPropertyChangedEventArgs>();
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
             Name = name;
@@ -77,7 +74,6 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(source != null);
             Contract.Requires<ArgumentNullException>(ownerType != null);
 
-            _changed = source._changed;
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
             Name = source.Name;
@@ -139,7 +135,7 @@ namespace Avalonia
         /// An observable that is fired when this property changes on any
         /// <see cref="AvaloniaObject"/> instance.
         /// </value>
-        public IObservable<AvaloniaPropertyChangedEventArgs> Changed => _changed;
+        public IObservable<AvaloniaPropertyChangedEventArgs> Changed => GetChanged();
 
         /// <summary>
         /// Gets a method that gets called before and after the property starts being notified on an
@@ -474,15 +470,6 @@ namespace Avalonia
         public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
             where TData : struct;
 
-        /// <summary>
-        /// Notifies the <see cref="Changed"/> observable.
-        /// </summary>
-        /// <param name="e">The observable arguments.</param>
-        internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e)
-        {
-            _changed.OnNext(e);
-        }
-
         /// <summary>
         /// Routes an untyped ClearValue call to a typed call.
         /// </summary>
@@ -553,6 +540,8 @@ namespace Avalonia
             _hasMetadataOverrides = true;
         }
 
+        protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
+
         private PropertyMetadata GetMetadataWithOverrides(Type type)
         {
             if (type is null)

+ 45 - 3
src/Avalonia.Base/AvaloniaProperty`1.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Utilities;
 
@@ -10,6 +11,8 @@ namespace Avalonia
     /// <typeparam name="TValue">The value type of the property.</typeparam>
     public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
     {
+        private readonly Subject<AvaloniaPropertyChangedEventArgs<TValue>> _changed;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
         /// </summary>
@@ -24,22 +27,61 @@ namespace Avalonia
             Action<IAvaloniaObject, bool> notifying = null)
             : base(name, typeof(TValue), ownerType, metadata, notifying)
         {
+            _changed = new Subject<AvaloniaPropertyChangedEventArgs<TValue>>();
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
+        /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
         /// </summary>
         /// <param name="source">The property to copy.</param>
         /// <param name="ownerType">The new owner type.</param>
         /// <param name="metadata">Optional overridden metadata.</param>
+        [Obsolete("Use constructor with AvaloniaProperty<TValue> instead.", true)]
         protected AvaloniaProperty(
-            AvaloniaProperty source, 
-            Type ownerType, 
+            AvaloniaProperty source,
+            Type ownerType,
+            PropertyMetadata metadata)
+            : this(source as AvaloniaProperty<TValue> ?? throw new InvalidOperationException(), ownerType, metadata)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
+        /// </summary>
+        /// <param name="source">The property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        /// <param name="metadata">Optional overridden metadata.</param>
+        protected AvaloniaProperty(
+            AvaloniaProperty<TValue> source,
+            Type ownerType,
             PropertyMetadata metadata)
             : base(source, ownerType, metadata)
         {
+            _changed = source._changed;
+        }
+
+        /// <summary>
+        /// Gets an observable that is fired when this property changes on any
+        /// <see cref="AvaloniaObject"/> instance.
+        /// </summary>
+        /// <value>
+        /// An observable that is fired when this property changes on any
+        /// <see cref="AvaloniaObject"/> instance.
+        /// </value>
+
+        public new IObservable<AvaloniaPropertyChangedEventArgs<TValue>> Changed => _changed;
+
+        /// <summary>
+        /// Notifies the <see cref="Changed"/> observable.
+        /// </summary>
+        /// <param name="e">The observable arguments.</param>
+        internal void NotifyChanged(AvaloniaPropertyChangedEventArgs<TValue> e)
+        {
+            _changed.OnNext(e);
         }
 
+        protected override IObservable<AvaloniaPropertyChangedEventArgs> GetChanged() => Changed;
+
         protected BindingValue<object> TryConvert(object value)
         {
             if (value == UnsetValue)

+ 67 - 1
src/Avalonia.Base/Collections/AvaloniaList.cs

@@ -543,7 +543,73 @@ namespace Avalonia.Collections
         /// <inheritdoc/>
         void ICollection.CopyTo(Array array, int index)
         {
-            _inner.CopyTo((T[])array, index);
+            if (array == null)
+            {
+                throw new ArgumentNullException(nameof(array));
+            }
+
+            if (array.Rank != 1)
+            {
+                throw new ArgumentException("Multi-dimensional arrays are not supported.");
+            }
+
+            if (array.GetLowerBound(0) != 0)
+            {
+                throw new ArgumentException("Non-zero lower bounds are not supported.");
+            }
+
+            if (index < 0)
+            {
+                throw new ArgumentException("Invalid index.");
+            }
+
+            if (array.Length - index < Count)
+            {
+                throw new ArgumentException("The target array is too small.");
+            }
+
+            if (array is T[] tArray)
+            {
+                _inner.CopyTo(tArray, index);
+            }
+            else
+            {
+                //
+                // Catch the obvious case assignment will fail.
+                // We can't find all possible problems by doing the check though.
+                // For example, if the element type of the Array is derived from T,
+                // we can't figure out if we can successfully copy the element beforehand.
+                //
+                Type targetType = array.GetType().GetElementType()!;
+                Type sourceType = typeof(T);
+                if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType)))
+                {
+                    throw new ArgumentException("Invalid array type");
+                }
+
+                //
+                // We can't cast array of value type to object[], so we don't support
+                // widening of primitive types here.
+                //
+                object[] objects = array as object[];
+                if (objects == null)
+                {
+                    throw new ArgumentException("Invalid array type");
+                }
+
+                int count = _inner.Count;
+                try
+                {
+                    for (int i = 0; i < count; i++)
+                    {
+                        objects[index++] = _inner[i];
+                    }
+                }
+                catch (ArrayTypeMismatchException)
+                {
+                    throw new ArgumentException("Invalid array type");
+                }
+            }
         }
 
         /// <inheritdoc/>

+ 16 - 1
src/Avalonia.Base/DirectPropertyBase.cs

@@ -32,15 +32,30 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
+        /// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
         /// </summary>
         /// <param name="source">The property to copy.</param>
         /// <param name="ownerType">The new owner type.</param>
         /// <param name="metadata">Optional overridden metadata.</param>
+        [Obsolete("Use constructor with DirectPropertyBase<TValue> instead.", true)]
         protected DirectPropertyBase(
             AvaloniaProperty source,
             Type ownerType,
             PropertyMetadata metadata)
+            : this(source as DirectPropertyBase<TValue> ?? throw new InvalidOperationException(), ownerType, metadata)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
+        /// </summary>
+        /// <param name="source">The property to copy.</param>
+        /// <param name="ownerType">The new owner type.</param>
+        /// <param name="metadata">Optional overridden metadata.</param>
+        protected DirectPropertyBase(
+            DirectPropertyBase<TValue> source,
+            Type ownerType,
+            PropertyMetadata metadata)
             : base(source, ownerType, metadata)
         {
         }

+ 35 - 19
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@@ -1,19 +1,21 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Globalization;
 using System.Linq;
-using System.Text;
-using Avalonia.Controls;
 using Avalonia.Controls.Utils;
-using Avalonia.Utilities;
 
 namespace Avalonia.Collections
 {
     public abstract class DataGridSortDescription
     {
         public virtual string PropertyPath => null;
-        public virtual bool Descending => false;
+
+        [Obsolete("Use Direction property to read or override sorting direction.")]
+        public virtual bool Descending => Direction == ListSortDirection.Descending;
+
+        public virtual ListSortDirection Direction => ListSortDirection.Ascending;
         public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath);
         public abstract IComparer<object> Comparer { get; }
 
@@ -26,7 +28,7 @@ namespace Avalonia.Collections
             return seq.ThenBy(o => o, Comparer);
         }
 
-        internal virtual DataGridSortDescription SwitchSortDirection()
+        public virtual DataGridSortDescription SwitchSortDirection()
         {
             return this;
         }
@@ -105,7 +107,7 @@ namespace Avalonia.Collections
 
         private class DataGridPathSortDescription : DataGridSortDescription
         {
-            private readonly bool _descending;
+            private readonly ListSortDirection _direction;
             private readonly string _propertyPath;
             private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer;
             private readonly Lazy<IComparer<object>> _comparer;
@@ -118,7 +120,7 @@ namespace Avalonia.Collections
                 {
                     if (_internalComparerTyped == null && _internalComparer != null)
                     {
-                        if (_internalComparerTyped is IComparer<object> c)
+                        if (_internalComparer is IComparer<object> c)
                             _internalComparerTyped = c;
                         else
                             _internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y));
@@ -130,19 +132,20 @@ namespace Avalonia.Collections
 
             public override string PropertyPath => _propertyPath;
             public override IComparer<object> Comparer => _comparer.Value;
-            public override bool Descending => _descending;
+            public override ListSortDirection Direction => _direction;
 
-            public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture)
+            public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture)
             {
                 _propertyPath = propertyPath;
-                _descending = descending;
+                _direction = direction;
                 _cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture));
+                _internalComparer = internalComparer;
                 _comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y)));
             }
-            private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending)
+            private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction)
             {
                 _propertyPath = inner._propertyPath;
-                _descending = descending;
+                _direction = direction;
                 _propertyType = inner._propertyType;
                 _cultureSensitiveComparer = inner._cultureSensitiveComparer;
                 _internalComparer = inner._internalComparer;
@@ -201,7 +204,7 @@ namespace Avalonia.Collections
 
                 result = _internalComparer?.Compare(v1, v2) ?? 0;
 
-                if (_descending)
+                if (Direction == ListSortDirection.Descending)
                     return -result;
                 else
                     return result;
@@ -218,7 +221,7 @@ namespace Avalonia.Collections
             }
             public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq)
             {
-                if(_descending)
+                if (Direction == ListSortDirection.Descending)
                 {
                     return seq.OrderByDescending(o => GetValue(o), InternalComparer);
                 }
@@ -229,7 +232,7 @@ namespace Avalonia.Collections
             }
             public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq)
             {
-                if (_descending)
+                if (Direction == ListSortDirection.Descending)
                 {
                     return seq.ThenByDescending(o => GetValue(o), InternalComparer);
                 }
@@ -239,15 +242,28 @@ namespace Avalonia.Collections
                 }
             }
 
-            internal override DataGridSortDescription SwitchSortDirection()
+            public override DataGridSortDescription SwitchSortDirection()
             {
-                return new DataGridPathSortDescription(this, !_descending);
+                var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
+                return new DataGridPathSortDescription(this, newDirection);
             }
         }
 
-        public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null)
+        public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null)
+        {
+            return new DataGridPathSortDescription(propertyPath, direction, null, culture);
+        }
+
+
+        [Obsolete("Use overload taking a ListSortDirection.")]
+        public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null)
+        {
+            return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture);
+        }
+
+        public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer)
         {
-            return new DataGridPathSortDescription(propertyPath, descending, culture);
+            return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
         }
     }
 

+ 7 - 0
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -24,12 +24,14 @@ using Avalonia.Input.Platform;
 using System.ComponentModel.DataAnnotations;
 using Avalonia.Controls.Utils;
 using Avalonia.Layout;
+using Avalonia.Controls.Metadata;
 
 namespace Avalonia.Controls
 {
     /// <summary>
     /// Displays data in a customizable grid.
     /// </summary>
+    [PseudoClasses(":invalid")]
     public partial class DataGrid : TemplatedControl
     {
         private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter";
@@ -1229,6 +1231,11 @@ namespace Avalonia.Controls
             remove { AddHandler(SelectionChangedEvent, value); }
         }
 
+        /// <summary>
+        /// Occurs when the <see cref="DataGridColumn"/> sorting request is triggered.
+        /// </summary>
+        public event EventHandler<DataGridColumnEventArgs> Sorting;
+
         /// <summary>
         /// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" /> 
         /// object becomes available for reuse.

+ 6 - 2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -49,8 +49,12 @@ namespace Avalonia.Controls
                     {
                         if(_binding is Avalonia.Data.Binding binding)
                         {
-                            // Force the TwoWay binding mode if there is a Path present.  TwoWay binding requires a Path.
-                            if (!String.IsNullOrEmpty(binding.Path))
+                            if (binding.Mode == BindingMode.OneWayToSource)
+                            {
+                                throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
+                            }
+
+                            if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
                             {
                                 binding.Mode = BindingMode.TwoWay;
                             } 

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@@ -3,6 +3,7 @@
 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 // All other rights reserved.
 
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Input;
@@ -12,6 +13,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell.
     /// </summary>
+    [PseudoClasses(":selected", ":current", ":edited", ":invalid")]
     public class DataGridCell : ContentControl
     {
         private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine";

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -1047,4 +1047,4 @@ namespace Avalonia.Controls
 
     }
 
-}
+}

+ 54 - 45
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -14,12 +14,14 @@ using Avalonia.Utilities;
 using System;
 using Avalonia.Controls.Utils;
 using Avalonia.Controls.Mixins;
+using Avalonia.Controls.Metadata;
 
 namespace Avalonia.Controls
 {
     /// <summary>
     /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> column header.
     /// </summary>
+    [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")]
     public class DataGridColumnHeader : ContentControl
     {
         private enum DragMode
@@ -161,13 +163,14 @@ namespace Avalonia.Controls
                 var sort = OwningColumn.GetSortDescription();
                 if (sort != null)
                 {
-                    CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending;
+                    CurrentSortingState = sort.Direction;
                 }
             }
+
             PseudoClasses.Set(":sortascending",
-                CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending);
+                CurrentSortingState == ListSortDirection.Ascending);
             PseudoClasses.Set(":sortdescending",
-                CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending);
+                CurrentSortingState == ListSortDirection.Descending);
         }
 
         internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn)
@@ -215,70 +218,76 @@ namespace Avalonia.Controls
         internal void ProcessSort(KeyModifiers keyModifiers)
         {
             // if we can sort:
-            //  - DataConnection.AllowSort is true, and
             //  - AllowUserToSortColumns and CanSort are true, and
-            //  - OwningColumn is bound, and
-            //  - SortDescriptionsCollection exists, and
-            //  - the column's data type is comparable
+            //  - OwningColumn is bound
             // then try to sort
             if (OwningColumn != null
                 && OwningGrid != null
                 && OwningGrid.EditingRow == null
                 && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn
-                && OwningGrid.DataConnection.AllowSort
                 && OwningGrid.CanUserSortColumns
-                && OwningColumn.CanUserSort
-                && OwningGrid.DataConnection.SortDescriptions != null)
+                && OwningColumn.CanUserSort)
             {
-                DataGrid owningGrid = OwningGrid;
+                var ea = new DataGridColumnEventArgs(OwningColumn);
+                OwningGrid.OnColumnSorting(ea);
 
-                DataGridSortDescription newSort;
+                if (!ea.Handled && OwningGrid.DataConnection.AllowSort && OwningGrid.DataConnection.SortDescriptions != null)
+                {
+                    // - DataConnection.AllowSort is true, and
+                    // - SortDescriptionsCollection exists, and
+                    // - the column's data type is comparable
 
-                KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift);
+                    DataGrid owningGrid = OwningGrid;
+                    DataGridSortDescription newSort;
 
-                DataGridSortDescription sort = OwningColumn.GetSortDescription();
-                IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
-                Debug.Assert(collectionView != null);
-                using (collectionView.DeferRefresh())
-                {
-                    // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
-                    if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
-                    {
-                        owningGrid.DataConnection.SortDescriptions.Clear();
-                    }
+                    KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift);
+
+                    DataGridSortDescription sort = OwningColumn.GetSortDescription();
+                    IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
+                    Debug.Assert(collectionView != null);
 
-                    // if ctrl is held down, we only clear the sort directions
-                    if (!ctrl)
+                    using (collectionView.DeferRefresh())
                     {
-                        if (sort != null)
+                        // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
+                        if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
                         {
-                            newSort = sort.SwitchSortDirection();
+                            owningGrid.DataConnection.SortDescriptions.Clear();
+                        }
 
-                            // changing direction should not affect sort order, so we replace this column's
-                            // sort description instead of just adding it to the end of the collection
-                            int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
-                            if (oldIndex >= 0)
+                        // if ctrl is held down, we only clear the sort directions
+                        if (!ctrl)
+                        {
+                            if (sort != null)
                             {
-                                owningGrid.DataConnection.SortDescriptions.Remove(sort);
-                                owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                                newSort = sort.SwitchSortDirection();
+
+                                // changing direction should not affect sort order, so we replace this column's
+                                // sort description instead of just adding it to the end of the collection
+                                int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
+                                if (oldIndex >= 0)
+                                {
+                                    owningGrid.DataConnection.SortDescriptions.Remove(sort);
+                                    owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
+                                }
+                                else
+                                {
+                                    owningGrid.DataConnection.SortDescriptions.Add(newSort);
+                                }
                             }
                             else
                             {
+                                string propertyName = OwningColumn.GetSortPropertyName();
+                                // no-opt if we couldn't find a property to sort on
+                                if (string.IsNullOrEmpty(propertyName))
+                                {
+                                    return;
+                                }
+
+                                newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
+
                                 owningGrid.DataConnection.SortDescriptions.Add(newSort);
                             }
                         }
-                        else
-                        {
-                            string propertyName = OwningColumn.GetSortPropertyName();
-                            // no-opt if we couldn't find a property to sort on
-                            if (string.IsNullOrEmpty(propertyName))
-                            {
-                                return;
-                            }
-
-                            newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
-                            owningGrid.DataConnection.SortDescriptions.Add(newSort);
-                        }
                     }
                 }
             }

+ 5 - 0
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@@ -33,6 +33,11 @@ namespace Avalonia.Controls
             ColumnReordering?.Invoke(this, e);
         }
 
+        protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e)
+        {
+            Sorting?.Invoke(this, e);
+        }
+
         /// <summary>
         /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total
         /// width is adjusted by the given amount, if possible.  If the total desired adjustment amount

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@@ -3,6 +3,7 @@
 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 // All other rights reserved.
 
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
@@ -20,6 +21,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
     /// </summary>
+    [PseudoClasses(":selected", ":editing", ":invalid")]
     public class DataGridRow : TemplatedControl
     {
 

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@@ -3,6 +3,7 @@
 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 // All other rights reserved.
 
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
@@ -13,6 +14,7 @@ using System.Reactive.Linq;
 
 namespace Avalonia.Controls
 {
+    [PseudoClasses(":pressed", ":current", ":expanded")]
     public class DataGridRowGroupHeader : TemplatedControl
     {
         private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton";

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@@ -3,6 +3,7 @@
 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 // All other rights reserved.
 
+using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 using Avalonia.Media;
 using System.Diagnostics;
@@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header. 
     /// </summary>
+    [PseudoClasses(":invalid", ":selected", ":editing", ":current")]
     public class DataGridRowHeader : ContentControl
     {
         private const string DATAGRIDROWHEADER_elementRootName = "PART_Root";

+ 2 - 2
src/Avalonia.Controls.DataGrid/EventArgs.cs

@@ -289,7 +289,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events.
     /// </summary>
-    public class DataGridColumnEventArgs : EventArgs
+    public class DataGridColumnEventArgs : HandledEventArgs
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class.
@@ -566,4 +566,4 @@ namespace Avalonia.Controls
             private set;
         }
     }
-}
+}

+ 3 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Tree
 MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not in the contract.
 MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 16
+Total Issues: 18

+ 10 - 4
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
         /// <inheritdoc/>
         public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;
 
+        /// <summary>
+        /// Gets the arguments passed to the AppBuilder Start method.
+        /// </summary>
+        public string[] Args { get; set; }
+        
         /// <inheritdoc/>
         public ShutdownMode ShutdownMode { get; set; }
         
@@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes
             else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
                 Shutdown();
         }
-        
-        
-
 
         public void Shutdown(int exitCode = 0)
         {
@@ -123,7 +125,11 @@ namespace Avalonia
             this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
             where T : AppBuilderBase<T>, new()
         {
-            var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode};
+            var lifetime = new ClassicDesktopStyleApplicationLifetime()
+            {
+                Args = args,
+                ShutdownMode = shutdownMode
+            };
             builder.SetupWithLifetime(lifetime);
             return lifetime.Start(args);
         }

+ 7 - 0
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes
     /// </summary>
     public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
     {
+        /// <summary>
+        /// Gets the arguments passed to the
+        /// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
+        /// method.
+        /// </summary>
+        string[] Args { get; }
+        
         /// <summary>
         /// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly. 
         /// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called.

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

@@ -14,6 +14,7 @@ using System.Reactive.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Collections;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
@@ -30,6 +31,7 @@ namespace Avalonia.Controls
     /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
     /// event.
     /// </summary>
+    [PseudoClasses(":dropdownopen")]
     public class PopulatedEventArgs : EventArgs
     {
         /// <summary>
@@ -225,6 +227,27 @@ namespace Avalonia.Controls
         Custom = 13,
     }
 
+    /// <summary>
+    /// Represents the selector used by the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control to
+    /// determine how the specified text should be modified with an item.
+    /// </summary>
+    /// <returns>
+    /// Modified text that will be used by the
+    /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />.
+    /// </returns>
+    /// <param name="search">The string used as the basis for filtering.</param>
+    /// <param name="item">
+    /// The selected item that should be combined 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>
+    public delegate string AutoCompleteSelector<T>(string search, T item);
+
     /// <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
@@ -362,6 +385,9 @@ namespace Avalonia.Controls
         private AutoCompleteFilterPredicate<object> _itemFilter;
         private AutoCompleteFilterPredicate<string> _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
 
+        private AutoCompleteSelector<object> _itemSelector;
+        private AutoCompleteSelector<string> _textSelector;
+
         public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
             RoutedEvent.Register<SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
 
@@ -528,6 +554,34 @@ namespace Avalonia.Controls
                 (o, v) => o.TextFilter = v,
                 unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
 
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemSelector" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemSelector" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<object>> ItemSelectorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<object>>(
+                nameof(ItemSelector),
+                o => o.ItemSelector,
+                (o, v) => o.ItemSelector = v);
+
+        /// <summary>
+        /// Identifies the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextSelector" />
+        /// dependency property.
+        /// </summary>
+        /// <value>The identifier for the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextSelector" />
+        /// dependency property.</value>
+        public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<string>> TextSelectorProperty =
+            AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<string>>(
+                nameof(TextSelector),
+                o => o.TextSelector,
+                (o, v) => o.TextSelector = v);
+
         /// <summary>
         /// Identifies the
         /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
@@ -1061,6 +1115,40 @@ namespace Avalonia.Controls
             set { SetAndRaise(TextFilterProperty, ref _textFilter, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the custom method that combines the user-entered
+        /// text and one of the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />.
+        /// </summary>
+        /// <value>
+        /// The custom method that combines the user-entered
+        /// text and one of the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />.
+        /// </value>
+        public AutoCompleteSelector<object> ItemSelector
+        {
+            get { return _itemSelector; }
+            set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the custom method that combines the user-entered
+        /// text and one of the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// in a text-based way.
+        /// </summary>
+        /// <value>
+        /// The custom method that combines the user-entered
+        /// text and one of the items specified by the
+        /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
+        /// in a text-based way.
+        /// </value>
+        public AutoCompleteSelector<string> TextSelector
+        {
+            get { return _textSelector; }
+            set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); }
+        }
+
         public Func<string, CancellationToken, Task<IEnumerable<object>>> AsyncPopulator
         {
             get { return _asyncPopulator; }
@@ -2329,6 +2417,14 @@ namespace Avalonia.Controls
             {
                 text = SearchText;
             }
+            else if (TextSelector != null)
+            {
+                text = TextSelector(SearchText, FormatValue(newItem, true));
+            }
+            else if (ItemSelector != null)
+            {
+                text = ItemSelector(SearchText, newItem);
+            }
             else
             {
                 text = FormatValue(newItem, true);

+ 2 - 0
src/Avalonia.Controls/Button.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Windows.Input;
+using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -28,6 +29,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A button control.
     /// </summary>
+    [PseudoClasses(":pressed")]
     public class Button : ContentControl
     {
         /// <summary>

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

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Input;
@@ -15,6 +16,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Represents a spinner control that includes two Buttons.
     /// </summary>
+    [PseudoClasses(":left", ":right")]
     public class ButtonSpinner : Spinner
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/Calendar/CalendarButton.cs

@@ -3,6 +3,7 @@
 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 // All other rights reserved.
 
+using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 using System;
 
@@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives
     /// Represents a button on a
     /// <see cref="T:Avalonia.Controls.Calendar" />.
     /// </summary>
+    [PseudoClasses(":selected", ":inactive", ":btnfocused")]
     public sealed class CalendarButton : Button
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@@ -5,10 +5,12 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 
 namespace Avalonia.Controls.Primitives
 {
+    [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")]
     public sealed class CalendarDayButton : Button
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -7,6 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
+using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives
     /// Represents the currently displayed month or year on a
     /// <see cref="T:Avalonia.Controls.Calendar" />.
     /// </summary>
+    [PseudoClasses(":calendardisabled")]
     public sealed class CalendarItem : TemplatedControl
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Reactive.Disposables;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 
 #nullable enable
@@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome
     /// <summary>
     /// Draws window minimize / maximize / close buttons in a <see cref="TitleBar"/> when managed client decorations are enabled.
     /// </summary>
+    [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
     public class CaptionButtons : TemplatedControl
     {
         private CompositeDisposable? _disposables;

+ 2 - 0
src/Avalonia.Controls/Chrome/TitleBar.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Reactive.Disposables;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 
 #nullable enable
@@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome
     /// <summary>
     /// Draws a titlebar when managed client decorations are enabled.
     /// </summary>
+    [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
     public class TitleBar : TemplatedControl
     {
         private CompositeDisposable? _disposables;

+ 8 - 2
src/Avalonia.Controls/ColumnDefinitions.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Specialized;
 using System.Linq;
+using System.Text;
 using Avalonia.Collections;
 
 namespace Avalonia.Controls
@@ -13,7 +14,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Initializes a new instance of the <see cref="ColumnDefinitions"/> class.
         /// </summary>
-        public ColumnDefinitions() : base ()
+        public ColumnDefinitions()
         {
         }
 
@@ -27,6 +28,11 @@ namespace Avalonia.Controls
             AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
         }
 
+        public override string ToString()
+        {
+            return string.Join(",", this.Select(x => x.Width));
+        }
+
         /// <summary>
         /// Parses a string representation of column definitions collection.
         /// </summary>
@@ -34,4 +40,4 @@ namespace Avalonia.Controls
         /// <returns>The <see cref="ColumnDefinitions"/>.</returns>
         public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s);
     }
-}
+}

+ 2 - 0
src/Avalonia.Controls/DataValidationErrors.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 
@@ -14,6 +15,7 @@ namespace Avalonia.Controls
     /// <remarks>
     /// You will probably only want to create instances inside of control templates.
     /// </remarks>
+    [PseudoClasses(":error")]
     public class DataValidationErrors : ContentControl
     {
         /// <summary>

+ 3 - 1
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Interactivity;
@@ -11,6 +12,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control to allow the user to select a date
     /// </summary>
+    [PseudoClasses(":hasnodate")]
     public class DatePicker : TemplatedControl
     {
         /// <summary>

+ 3 - 1
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using System;
@@ -9,6 +10,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control to allow the user to select a time
     /// </summary>
+    [PseudoClasses(":hasnotime")]
     public class TimePicker : TemplatedControl
     {
         /// <summary>

+ 2 - 1
src/Avalonia.Controls/Expander.cs

@@ -1,6 +1,6 @@
 using Avalonia.Animation;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
-using Avalonia.Data;
 
 namespace Avalonia.Controls
 {
@@ -12,6 +12,7 @@ namespace Avalonia.Controls
         Right
     }
 
+    [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
     public class Expander : HeaderedContentControl
     {
         public static readonly StyledProperty<IPageTransition> ContentTransitionProperty =

+ 9 - 0
src/Avalonia.Controls/IconElement.cs

@@ -0,0 +1,9 @@
+using Avalonia.Controls.Primitives;
+
+namespace Avalonia.Controls
+{
+    public abstract class IconElement : TemplatedControl
+    {
+
+    }
+}

+ 2 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -18,6 +19,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Displays a collection of items.
     /// </summary>
+    [PseudoClasses(":empty", ":singleitem")]
     public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
     {
         /// <summary>

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

@@ -163,6 +163,7 @@ namespace Avalonia.Controls
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
+            base.OnApplyTemplate(e);
             Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
         }
     }

+ 2 - 0
src/Avalonia.Controls/ListBoxItem.cs

@@ -1,3 +1,4 @@
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Input;
 
@@ -6,6 +7,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A selectable item in a <see cref="ListBox"/>.
     /// </summary>
+    [PseudoClasses(":pressed", ":selected")]
     public class ListBoxItem : ContentControl, ISelectable
     {
         /// <summary>

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

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reactive.Linq;
 using System.Windows.Input;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -20,6 +21,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A menu item control.
     /// </summary>
+    [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
     public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
     {
         /// <summary>

+ 2 - 2
src/Avalonia.Controls/Mixins/SelectableMixin.cs

@@ -48,7 +48,7 @@ namespace Avalonia.Controls.Mixins
 
                 if (sender != null)
                 {
-                    ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue);
+                    ((IPseudoClasses)sender.Classes).Set(":selected", x.NewValue.GetValueOrDefault());
 
                     sender.RaiseEvent(new RoutedEventArgs
                     {
@@ -58,4 +58,4 @@ namespace Avalonia.Controls.Mixins
             });
         }
     }
-}
+}

+ 1 - 1
src/Avalonia.Controls/NativeMenu.Export.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Controls
             {
                 if (args.Sender is TopLevel tl)
                 {
-                    GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue);
+                    GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
                 }
             });
         }

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

@@ -23,7 +23,7 @@ namespace Avalonia.Controls
             MenuProperty.Changed.Subscribe(args =>
             {
                 var item = (NativeMenuItem)args.Sender;
-                var value = (NativeMenu)args.NewValue;
+                var value = args.NewValue.GetValueOrDefault();
                 if (value.Parent != null && value.Parent != item)
                     throw new InvalidOperationException("NativeMenu already has a parent");
                 value.Parent = item;

+ 2 - 0
src/Avalonia.Controls/Notifications/NotificationCard.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Controls.Metadata;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
 
@@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications
     /// <summary>
     /// Control that represents and displays a notification.
     /// </summary>
+    [PseudoClasses(":error", ":information", ":success", ":warning")]
     public class NotificationCard : ContentControl
     {
         private bool _isClosed;

+ 2 - 0
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Rendering;
 using Avalonia.Data;
 using Avalonia.VisualTree;
+using Avalonia.Controls.Metadata;
 
 namespace Avalonia.Controls.Notifications
 {
     /// <summary>
     /// An <see cref="INotificationManager"/> that displays notifications in a <see cref="Window"/>.
     /// </summary>
+    [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")]
     public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest
     {
         private IList _items;

+ 21 - 0
src/Avalonia.Controls/PathIcon.cs

@@ -0,0 +1,21 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+    public class PathIcon : IconElement
+    {
+        static PathIcon()
+        {
+            AffectsRender<PathIcon>(DataProperty);
+        }
+
+        public static readonly StyledProperty<Geometry> DataProperty =
+            AvaloniaProperty.Register<PathIcon, Geometry>(nameof(Data));
+
+        public Geometry Data
+        {
+            get { return GetValue(DataProperty); }
+            set { SetValue(DataProperty, value); }
+        }
+    }
+}

+ 3 - 3
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -82,7 +82,7 @@ namespace Avalonia.Controls.Presenters
                 TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
                 TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
 
-            Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
+            Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
                 TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
                 TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, 
                 TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
@@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters
             return new FormattedText
             {
                 Constraint = constraint,
-                Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
+                Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
                 FontSize = FontSize,
                 Text = text ?? string.Empty,
                 TextAlignment = TextAlignment,
@@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters
                 return new FormattedText
                 {
                     Text = "X",
-                    Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
+                    Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
                     FontSize = FontSize,
                     TextAlignment = TextAlignment,
                     Constraint = availableSize,

+ 2 - 2
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -126,7 +126,7 @@ namespace Avalonia.Controls.Primitives
 
                     if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
                     {
-                        currentX += shapedTextCharacters.GlyphRun.Bounds.Width;
+                        currentX += shapedTextCharacters.Size.Width;
 
                         continue;
                     }
@@ -143,7 +143,7 @@ namespace Avalonia.Controls.Primitives
                         width = 0.0;
                     }
 
-                    return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height);
+                    return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height);
                 }
             }
 

+ 2 - 1
src/Avalonia.Controls/Primitives/IPopupHost.cs

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Primitives
@@ -13,7 +14,7 @@ namespace Avalonia.Controls.Primitives
     /// (<see cref="PopupRoot"/>) or an <see cref="OverlayPopupHost"/> which is created
     /// on an <see cref="OverlayLayer"/>.
     /// </remarks>
-    public interface IPopupHost : IDisposable
+    public interface IPopupHost : IDisposable, IFocusScope
     {
         /// <summary>
         /// Sets the control to display in the popup.

+ 1 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
                 if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
                 {
-                    unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y);
+                    unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y);
                 }
 
                 if (IsValid(unconstrainedRect))

+ 3 - 1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -4,6 +4,7 @@ using Avalonia.Interactivity;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Threading;
+using Avalonia.Controls.Metadata;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// A scrollbar control.
     /// </summary>
+    [PseudoClasses(":vertical", ":horizontal")]
     public class ScrollBar : RangeBase
     {
         /// <summary>
@@ -141,7 +143,7 @@ namespace Avalonia.Controls.Primitives
                 _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
             };
 
-            SetValue(IsVisibleProperty, isVisible, BindingPriority.Style);
+            SetValue(IsVisibleProperty, isVisible);
         }
 
         protected override void OnKeyDown(KeyEventArgs e)

+ 258 - 81
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -6,7 +6,6 @@ using System.ComponentModel;
 using System.Linq;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Selection;
-using Avalonia.Controls.Utils;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
@@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
-        protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
-            AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>(
+        protected static readonly DirectProperty<SelectingItemsControl, IList?> SelectedItemsProperty =
+            AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList?>(
                 nameof(SelectedItems),
                 o => o.SelectedItems,
                 (o, v) => o.SelectedItems = v);
@@ -111,12 +110,13 @@ namespace Avalonia.Controls.Primitives
                 RoutingStrategies.Bubble);
 
         private static readonly IList Empty = Array.Empty<object>();
-        private SelectedItemsSync? _selectedItemsSync;
         private ISelectionModel? _selection;
         private int _oldSelectedIndex;
         private object? _oldSelectedItem;
-        private int _initializing;
+        private IList? _oldSelectedItems;
         private bool _ignoreContainerSelectionChanged;
+        private UpdateState? _updateState;
+        private bool _hasScrolledToSelectedItem;
 
         /// <summary>
         /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@@ -149,8 +149,27 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public int SelectedIndex
         {
-            get => Selection.SelectedIndex;
-            set => Selection.SelectedIndex = value;
+            get
+            {
+                // When a Begin/EndInit/DataContext update is in place we return the value to be
+                // updated here, even though it's not yet active and the property changed notification
+                // has not yet been raised. If we don't do this then the old value will be written back
+                // to the source when two-way bound, and the update value will be lost.
+                return _updateState?.SelectedIndex.HasValue == true ?
+                    _updateState.SelectedIndex.Value :
+                    Selection.SelectedIndex;
+            }
+            set
+            {
+                if (_updateState is object)
+                {
+                    _updateState.SelectedIndex = value;
+                }
+                else
+                {
+                    Selection.SelectedIndex = value;
+                }
+            }
         }
 
         /// <summary>
@@ -158,17 +177,67 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public object? SelectedItem
         {
-            get => Selection.SelectedItem;
-            set => Selection.SelectedItem = value;
+            get
+            {
+                // See SelectedIndex setter for more information.
+                return _updateState?.SelectedItem.HasValue == true ?
+                    _updateState.SelectedItem.Value :
+                    Selection.SelectedItem;
+            }
+            set
+            {
+                if (_updateState is object)
+                {
+                    _updateState.SelectedItem = value;
+                }
+                else
+                {
+                    Selection.SelectedItem = value;
+                }
+            }
         }
 
         /// <summary>
         /// Gets or sets the selected items.
         /// </summary>
-        protected IList SelectedItems
+        /// <remarks>
+        /// By default returns a collection that can be modified in order to manipulate the control
+        /// selection, however this property will return null if <see cref="Selection"/> is
+        /// re-assigned; you should only use _either_ Selection or SelectedItems.
+        /// </remarks>
+        protected IList? SelectedItems
         {
-            get => SelectedItemsSync.SelectedItems;
-            set => SelectedItemsSync.SelectedItems = value;
+            get
+            {
+                // See SelectedIndex setter for more information.
+                if (_updateState?.SelectedItems.HasValue == true)
+                {
+                    return _updateState.SelectedItems.Value;
+                }
+                else if (Selection is InternalSelectionModel ism)
+                {
+                    var result = ism.WritableSelectedItems;
+                    _oldSelectedItems = result;
+                    return result;
+                }
+
+                return null;
+            }
+            set
+            {
+                if (_updateState is object)
+                {
+                    _updateState.SelectedItems = new Optional<IList?>(value);
+                }
+                else if (Selection is InternalSelectionModel i)
+                {
+                    i.WritableSelectedItems = value;
+                }
+                else
+                {
+                    throw new InvalidOperationException("Cannot set both Selection and SelectedItems.");
+                }
+            }
         }
 
         /// <summary>
@@ -178,19 +247,30 @@ namespace Avalonia.Controls.Primitives
         {
             get
             {
-                if (_selection is null)
+                if (_updateState?.Selection.HasValue == true)
                 {
-                    _selection = CreateDefaultSelectionModel();
-                    InitializeSelectionModel(_selection);
+                    return _updateState.Selection.Value;
                 }
+                else
+                {
+                    if (_selection is null)
+                    {
+                        _selection = CreateDefaultSelectionModel();
+                        InitializeSelectionModel(_selection);
+                    }
 
-                return _selection;
+                    return _selection;
+                }
             }
             set
             {
                 value ??= CreateDefaultSelectionModel();
 
-                if (_selection != value)
+                if (_updateState is object)
+                {
+                    _updateState.Selection = new Optional<ISelectionModel>(value);
+                }
+                else if (_selection != value)
                 {
                     if (value.Source != null && value.Source != Items)
                     {
@@ -212,6 +292,15 @@ namespace Avalonia.Controls.Primitives
                     }
 
                     InitializeSelectionModel(_selection);
+
+                    if (_oldSelectedItems != SelectedItems)
+                    {
+                        RaisePropertyChanged(
+                            SelectedItemsProperty,
+                            new Optional<IList?>(_oldSelectedItems),
+                            new BindingValue<IList?>(SelectedItems));
+                        _oldSelectedItems = SelectedItems;
+                    }
                 }
             }
         }
@@ -234,20 +323,18 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
 
-        private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
-
         /// <inheritdoc/>
         public override void BeginInit()
         {
             base.BeginInit();
-            ++_initializing;
+            BeginUpdating();
         }
 
         /// <inheritdoc/>
         public override void EndInit()
         {
             base.EndInit();
-            --_initializing;
+            EndUpdating();
         }
 
         /// <summary>
@@ -295,6 +382,28 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            AutoScrollToSelectedItemIfNecessary();
+        }
+
+        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+        {
+            base.OnApplyTemplate(e);
+
+            void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e)
+            {
+                LayoutUpdated -= ExecuteScrollWhenLayoutUpdated;
+                AutoScrollToSelectedItemIfNecessary();
+            }
+
+            if (AutoScrollToSelectedItem)
+            {
+                LayoutUpdated += ExecuteScrollWhenLayoutUpdated;
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnContainersMaterialized(ItemContainerEventArgs e)
         {
@@ -351,30 +460,14 @@ namespace Avalonia.Controls.Primitives
         protected override void OnDataContextBeginUpdate()
         {
             base.OnDataContextBeginUpdate();
-            ++_initializing;
-
-            if (_selection is object)
-            {
-                _selection.Source = null;
-            }
+            BeginUpdating();
         }
 
         /// <inheritdoc/>
         protected override void OnDataContextEndUpdate()
         {
             base.OnDataContextEndUpdate();
-            --_initializing;
-
-            if (_selection is object && _initializing == 0)
-            {
-                _selection.Source = Items;
-
-                if (Items is null)
-                {
-                    _selection.Clear();
-                    _selectedItemsSync?.SelectedItems?.Clear();
-                }
-            }
+            EndUpdating();
         }
 
         protected override void OnInitialized()
@@ -398,8 +491,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (ItemCount > 0 &&
                     Match(keymap.SelectAll) &&
-                    (((SelectionMode & SelectionMode.Multiple) != 0) ||
-                      (SelectionMode & SelectionMode.Toggle) != 0))
+                    SelectionMode.HasFlag(SelectionMode.Multiple))
                 {
                     Selection.SelectAll();
                     e.Handled = true;
@@ -411,9 +503,11 @@ namespace Avalonia.Controls.Primitives
         {
             base.OnPropertyChanged(change);
 
-            if (change.Property == ItemsProperty &&
-                _initializing == 0 &&
-                _selection is object)
+            if (change.Property == AutoScrollToSelectedItemProperty)
+            {
+                AutoScrollToSelectedItemIfNecessary();
+            }
+            if (change.Property == ItemsProperty && _updateState is null && _selection is object)
             {
                 var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
                 _selection.Source = newValue;
@@ -601,23 +695,30 @@ namespace Avalonia.Controls.Primitives
         /// <param name="e">The event args.</param>
         private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
         {
-            if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
+            if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
             {
-                if (Selection.AnchorIndex > 0)
-                {
-                    ScrollIntoView(Selection.AnchorIndex);
-                }
+                _hasScrolledToSelectedItem = false;
+                AutoScrollToSelectedItemIfNecessary();
             }
-            else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex))
+            else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)
             {
                 RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
                 _oldSelectedIndex = SelectedIndex;
             }
-            else if (e.PropertyName == nameof(ISelectionModel.SelectedItem))
+            else if (e.PropertyName == nameof(ISelectionModel.SelectedItem) && _oldSelectedItem != SelectedItem)
             {
                 RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
                 _oldSelectedItem = SelectedItem;
             }
+            else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
+                _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
+            {
+                RaisePropertyChanged(
+                    SelectedItemsProperty,
+                    new Optional<IList?>(_oldSelectedItems),
+                    new BindingValue<IList?>(SelectedItems));
+                _oldSelectedItems = SelectedItems;
+            }
         }
 
         /// <summary>
@@ -674,6 +775,19 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        private void AutoScrollToSelectedItemIfNecessary()
+        {
+            if (AutoScrollToSelectedItem &&
+                !_hasScrolledToSelectedItem &&
+                Presenter is object &&
+                Selection.AnchorIndex >= 0 &&
+                ((IVisual)this).IsAttachedToVisualTree)
+            {
+                ScrollIntoView(Selection.AnchorIndex);
+                _hasScrolledToSelectedItem = true;
+            }
+        }
+
         /// <summary>
         /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
         /// </summary>
@@ -734,14 +848,6 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private void MarkContainersUnselected()
-        {
-            foreach (var container in ItemContainerGenerator.Containers)
-            {
-                MarkContainerSelected(container.ContainerControl, false);
-            }
-        }
-
         /// <summary>
         /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
         /// </summary>
@@ -757,23 +863,6 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        /// <summary>
-        /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="selected">Whether the item should be selected or deselected.</param>
-        private int MarkItemSelected(object item, bool selected)
-        {
-            var index = IndexOf(Items, item);
-
-            if (index != -1)
-            {
-                MarkItemSelected(index, selected);
-            }
-
-            return index;
-        }
-
         private void UpdateContainerSelection()
         {
             if (Presenter?.Panel is IPanel panel)
@@ -789,7 +878,7 @@ namespace Avalonia.Controls.Primitives
 
         private ISelectionModel CreateDefaultSelectionModel()
         {
-            return new SelectionModel<object>
+            return new InternalSelectionModel
             {
                 SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
             };
@@ -797,7 +886,7 @@ namespace Avalonia.Controls.Primitives
 
         private void InitializeSelectionModel(ISelectionModel model)
         {
-            if (_initializing == 0)
+            if (_updateState is null)
             {
                 model.Source = Items;
             }
@@ -825,9 +914,6 @@ namespace Avalonia.Controls.Primitives
 
             UpdateContainerSelection();
 
-            _selectedItemsSync ??= new SelectedItemsSync(model);
-            _selectedItemsSync.SelectionModel = model;
-
             if (SelectedIndex != -1)
             {
                 RaiseEvent(new SelectionChangedEventArgs(
@@ -845,5 +931,96 @@ namespace Avalonia.Controls.Primitives
                 model.SelectionChanged -= OnSelectionModelSelectionChanged;
             }
         }
+
+        private void BeginUpdating()
+        {
+            _updateState ??= new UpdateState();
+            _updateState.UpdateCount++;
+        }
+
+        private void EndUpdating()
+        {
+            if (_updateState is object && --_updateState.UpdateCount == 0)
+            {
+                var state = _updateState;
+                _updateState = null;
+
+                if (state.Selection.HasValue)
+                {
+                    Selection = state.Selection.Value;
+                }
+
+                if (state.SelectedItems.HasValue)
+                {
+                    SelectedItems = state.SelectedItems.Value;
+                }
+
+                Selection.Source = Items;
+
+                if (Items is null)
+                {
+                    Selection.Clear();
+                }
+
+                if (state.SelectedIndex.HasValue)
+                {
+                    SelectedIndex = state.SelectedIndex.Value;
+                }
+                else if (state.SelectedItem.HasValue)
+                {
+                    SelectedItem = state.SelectedItem.Value;
+                }
+            }
+        }
+
+        // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
+        // defer changes to the selection model because we have no idea in which order properties
+        // will be set. Consider:
+        //
+        // - Both Items and SelectedItem are bound
+        // - The DataContext changes
+        // - The binding for SelectedItem updates first, producing an item
+        // - Items is searched to find the index of the new selected item
+        // - However Items isn't yet updated; the item is not found
+        // - SelectedIndex is incorrectly set to -1
+        //
+        // This logic cannot be encapsulated in SelectionModel because the selection model can also
+        // be bound, consider:
+        //
+        // - Both Items and Selection are bound
+        // - The DataContext changes
+        // - The binding for Items updates first
+        // - The new items are assigned to Selection.Source
+        // - The binding for Selection updates, producing a new SelectionModel
+        // - Both the old and new SelectionModels have the incorrect Source
+        private class UpdateState
+        {
+            private Optional<int> _selectedIndex;
+            private Optional<object?> _selectedItem;
+
+            public int UpdateCount { get; set; }
+            public Optional<ISelectionModel> Selection { get; set; }
+            public Optional<IList?> SelectedItems { get; set; }
+
+            public Optional<int> SelectedIndex 
+            {
+                get => _selectedIndex;
+                set
+                {
+                    _selectedIndex = value;
+                    _selectedItem = default;
+                }
+            }
+
+            public Optional<object?> SelectedItem
+            {
+                get => _selectedItem;
+                set
+                {
+                    _selectedItem = value;
+                    _selectedIndex = default;
+                }
+            }
+       }
     }
 }

+ 2 - 0
src/Avalonia.Controls/Primitives/Thumb.cs

@@ -1,9 +1,11 @@
 using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
 namespace Avalonia.Controls.Primitives
 {
+    [PseudoClasses(":pressed")]
     public class Thumb : TemplatedControl
     {
         public static readonly RoutedEvent<VectorEventArgs> DragStartedEvent =

+ 2 - 0
src/Avalonia.Controls/Primitives/ToggleButton.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Interactivity;
 
@@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states.
     /// </summary>
+    [PseudoClasses(":checked", ":unchecked", ":indeterminate")]
     public class ToggleButton : Button
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/Primitives/Track.cs

@@ -4,6 +4,7 @@
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Layout;
@@ -12,6 +13,7 @@ using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Primitives
 {
+    [PseudoClasses(":vertical", ":horizontal")]
     public class Track : Control
     {
         public static readonly DirectProperty<Track, double> MinimumProperty =

+ 2 - 0
src/Avalonia.Controls/ProgressBar.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Layout;
 using Avalonia.Media;
@@ -8,6 +9,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control used to indicate the progress of an operation.
     /// </summary>
+    [PseudoClasses(":vertical", ":horizontal", ":indeterminate")]
     public class ProgressBar : RangeBase
     {
         public class ProgressBarTemplateProperties : AvaloniaObject

+ 19 - 5
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -7,10 +7,10 @@ using System;
 using System.Collections;
 using System.Collections.Specialized;
 using Avalonia.Controls.Templates;
-using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Logging;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -681,8 +681,15 @@ namespace Avalonia.Controls
             if (oldValue != null)
             {
                 oldValue.UninitializeForContext(LayoutContext);
-                oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
-                oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
+
+                WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
+                    oldValue,
+                    nameof(AttachedLayout.MeasureInvalidated),
+                    InvalidateMeasureForLayout);
+                WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
+                    oldValue,
+                    nameof(AttachedLayout.ArrangeInvalidated),
+                    InvalidateArrangeForLayout);
 
                 // Walk through all the elements and make sure they are cleared
                 foreach (var element in Children)
@@ -699,8 +706,15 @@ namespace Avalonia.Controls
             if (newValue != null)
             {
                 newValue.InitializeForContext(LayoutContext);
-                newValue.MeasureInvalidated += InvalidateMeasureForLayout;
-                newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
+
+                WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
+                    newValue,
+                    nameof(AttachedLayout.MeasureInvalidated),
+                    InvalidateMeasureForLayout);
+                WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
+                    newValue,
+                    nameof(AttachedLayout.ArrangeInvalidated),
+                    InvalidateArrangeForLayout);
             }
 
             bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;

+ 34 - 2
src/Avalonia.Controls/ScrollViewer.cs

@@ -448,6 +448,38 @@ namespace Avalonia.Controls
             Offset += new Vector(_smallChange.Width, 0);
         }
 
+        /// <summary>
+        /// Scrolls the content upward by one page.
+        /// </summary>
+        public void PageUp()
+        {
+            VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0);
+        }
+
+        /// <summary>
+        /// Scrolls the content downward by one page.
+        /// </summary>
+        public void PageDown()
+        {
+            VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum);
+        }
+
+        /// <summary>
+        /// Scrolls the content left by one page.
+        /// </summary>
+        public void PageLeft()
+        {
+            HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0);
+        }
+
+        /// <summary>
+        /// Scrolls the content tight by one page.
+        /// </summary>
+        public void PageRight()
+        {
+            HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum);
+        }
+
         /// <summary>
         /// Scrolls to the top-left corner of the content.
         /// </summary>
@@ -623,12 +655,12 @@ namespace Avalonia.Controls
         {
             if (e.Key == Key.PageUp)
             {
-                VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0);
+                PageUp();
                 e.Handled = true;
             }
             else if (e.Key == Key.PageDown)
             {
-                VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum);
+                PageDown();
                 e.Handled = true;
             }
         }

+ 278 - 0
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@@ -0,0 +1,278 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Avalonia.Collections;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    internal class InternalSelectionModel : SelectionModel<object?>
+    {
+        private IList? _writableSelectedItems;
+        private bool _ignoreModelChanges;
+        private bool _ignoreSelectedItemsChanges;
+
+        public InternalSelectionModel()
+        {
+            SelectionChanged += OnSelectionChanged;
+            SourceReset += OnSourceReset;
+        }
+
+        [AllowNull]
+        public IList WritableSelectedItems
+        {
+            get
+            {
+                if (_writableSelectedItems is null)
+                {
+                    _writableSelectedItems = new AvaloniaList<object?>();
+                    SubscribeToSelectedItems();
+                }
+
+                return _writableSelectedItems;
+            }
+            set
+            {
+                value ??= new AvaloniaList<object?>();
+
+                if (value.IsFixedSize)
+                {
+                    throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems.");
+                }
+
+                if (_writableSelectedItems != value)
+                {
+                    UnsubscribeFromSelectedItems();
+                    _writableSelectedItems = value;
+                    SyncFromSelectedItems();
+                    SubscribeToSelectedItems();
+                    
+                    if (ItemsView is null)
+                    {
+                        SetInitSelectedItems(value);
+                    }
+
+                    RaisePropertyChanged(nameof(WritableSelectedItems));
+                }
+            }
+        }
+
+        private protected override void SetSource(IEnumerable? value)
+        {
+            if (Source == value)
+            {
+                return;
+            }
+
+            object?[]? oldSelection = null;
+
+            if (Source is object && value is object)
+            {
+                oldSelection = new object?[WritableSelectedItems.Count];
+                WritableSelectedItems.CopyTo(oldSelection, 0);
+            }
+
+            try
+            {
+                _ignoreSelectedItemsChanges = true;
+                base.SetSource(value);
+            }
+            finally
+            {
+                _ignoreSelectedItemsChanges = false;
+            }
+
+            if (oldSelection is null)
+            {
+                SyncToSelectedItems();
+            }
+            else
+            {
+                foreach (var i in oldSelection)
+                {
+                    var index = ItemsView!.IndexOf(i);
+                    Select(index);
+                }
+            }
+        }
+
+        private void SyncToSelectedItems()
+        {
+            if (_writableSelectedItems is object)
+            {
+                try
+                {
+                    _ignoreSelectedItemsChanges = true;
+                    _writableSelectedItems.Clear();
+
+                    foreach (var i in base.SelectedItems)
+                    {
+                        _writableSelectedItems.Add(i);
+                    }
+                }
+                finally
+                {
+                    _ignoreSelectedItemsChanges = false;
+                }
+            }
+        }
+
+        private void SyncFromSelectedItems()
+        {
+            if (Source is null || _writableSelectedItems is null)
+            {
+                return;
+            }
+
+            try
+            {
+                _ignoreModelChanges = true;
+
+                using (BatchUpdate())
+                {
+                    Clear();
+                    Add(_writableSelectedItems);
+                }
+            }
+            finally
+            {
+                _ignoreModelChanges = false;
+            }
+        }
+
+        private void SubscribeToSelectedItems()
+        {
+            if (_writableSelectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged += OnSelectedItemsCollectionChanged;
+            }
+        }
+
+        private void UnsubscribeFromSelectedItems()
+        {
+            if (_writableSelectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged += OnSelectedItemsCollectionChanged;
+            }
+        }
+
+        private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
+        {
+            if (_ignoreModelChanges)
+            {
+                return;
+            }
+
+            try
+            {
+                var items = WritableSelectedItems;
+                var deselected = e.DeselectedItems.ToList();
+                var selected = e.SelectedItems.ToList();
+
+                _ignoreSelectedItemsChanges = true;
+
+                foreach (var i in deselected)
+                {
+                    items.Remove(i);
+                }
+
+                foreach (var i in selected)
+                {
+                    items.Add(i);
+                }
+            }
+            finally
+            {
+                _ignoreSelectedItemsChanges = false;
+            }
+        }
+
+        private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems();
+
+        private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (_ignoreSelectedItemsChanges)
+            {
+                return;
+            }
+
+            if (_writableSelectedItems == null)
+            {
+                throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
+            }
+
+            void Remove()
+            {
+                foreach (var i in e.OldItems)
+                {
+                    var index = IndexOf(Source, i);
+
+                    if (index != -1)
+                    {
+                        Deselect(index);
+                    }
+                }
+            }
+
+            try
+            {
+                using var operation = BatchUpdate();
+
+                _ignoreModelChanges = true;
+
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        Add(e.NewItems);
+                        break;
+                    case NotifyCollectionChangedAction.Remove:
+                        Remove();
+                        break;
+                    case NotifyCollectionChangedAction.Replace:
+                        Remove();
+                        Add(e.NewItems);
+                        break;
+                    case NotifyCollectionChangedAction.Reset:
+                        Clear();
+                        Add(_writableSelectedItems);
+                        break;
+                }
+            }
+            finally
+            {
+                _ignoreModelChanges = false;
+            }
+        }
+
+        private void Add(IList newItems)
+        {
+            foreach (var i in newItems)
+            {
+                var index = IndexOf(Source, i);
+
+                if (index != -1)
+                {
+                    Select(index);
+                }
+            }
+        }
+
+        private static int IndexOf(object? source, object? item)
+        {
+            if (source is IList l)
+            {
+                return l.IndexOf(item);
+            }
+            else if (source is ItemsSourceView v)
+            {
+                return v.IndexOf(item);
+            }
+
+            return -1;
+        }
+    }
+}

+ 75 - 38
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -20,8 +20,7 @@ namespace Avalonia.Controls.Selection
         private SelectedItems<T>? _selectedItems;
         private SelectedItems<T>.Untyped? _selectedItemsUntyped;
         private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
-        [AllowNull] private T _initSelectedItem = default;
-        private bool _hasInitSelectedItem;
+        private IList? _initSelectedItems;
 
         public SelectionModel()
         {
@@ -82,7 +81,19 @@ namespace Avalonia.Controls.Selection
         [MaybeNull, AllowNull]
         public T SelectedItem
         {
-            get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem;
+            get
+            {
+                if (ItemsView is object)
+                {
+                    return GetItemAt(_selectedIndex);
+                }
+                else if (_initSelectedItems is object && _initSelectedItems.Count > 0)
+                {
+                    return (T)_initSelectedItems[0];
+                }
+
+                return default;
+            }
             set
             {
                 if (ItemsView is object)
@@ -92,8 +103,9 @@ namespace Avalonia.Controls.Selection
                 else
                 {
                     Clear();
-                    _initSelectedItem = value;
-                    _hasInitSelectedItem = true;
+#pragma warning disable CS8601
+                    SetInitSelectedItems(new T[] { value });
+#pragma warning restore CS8601
                 }
             }
         }
@@ -102,9 +114,10 @@ namespace Avalonia.Controls.Selection
         {
             get
             {
-                if (ItemsView is null && _hasInitSelectedItem)
+                if (ItemsView is null && _initSelectedItems is object)
                 {
-                    return new[] { _initSelectedItem };
+                    return _initSelectedItems is IReadOnlyList<T> i ?
+                        i : _initSelectedItems.Cast<T>().ToList();
                 }
 
                 return _selectedItems ??= new SelectedItems<T>(this);
@@ -229,12 +242,7 @@ namespace Avalonia.Controls.Selection
         {
             using var update = BatchUpdate();
             var o = update.Operation;
-            var range = CoerceRange(start, end);
-
-            if (range.Begin == -1)
-            {
-                return;
-            }
+            var range = new IndexRange(Math.Max(0, start), end);
 
             if (RangesEnabled)
             {
@@ -258,8 +266,7 @@ namespace Avalonia.Controls.Selection
                 o.SelectedIndex = -1;
             }
 
-            _initSelectedItem = default;
-            _hasInitSelectedItem = false;
+            _initSelectedItems = null;
         }
 
         public void SelectAll() => SelectRange(0, int.MaxValue);
@@ -270,7 +277,7 @@ namespace Avalonia.Controls.Selection
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }
 
-        private void SetSource(IEnumerable? value)
+        private protected virtual void SetSource(IEnumerable? value)
         {
             if (base.Source != value)
             {
@@ -292,11 +299,14 @@ namespace Avalonia.Controls.Selection
                 {
                     update.Operation.IsSourceUpdate = true;
 
-                    if (_hasInitSelectedItem)
+                    if (_initSelectedItems is object && ItemsView is object)
                     {
-                        SelectedItem = _initSelectedItem;
-                        _initSelectedItem = default;
-                        _hasInitSelectedItem = false;
+                        foreach (T i in _initSelectedItems)
+                        {
+                            Select(ItemsView.IndexOf(i));
+                        }
+
+                        _initSelectedItems = null;
                     }
                     else
                     {
@@ -345,7 +355,9 @@ namespace Avalonia.Controls.Selection
                 LostSelection(this, EventArgs.Empty);
             }
 
-            CommitOperation(update.Operation);
+            // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that
+            // let to this method being called will raise them if necessary.
+            CommitOperation(update.Operation, raisePropertyChanged: false);
         }
 
         private protected override CollectionChangeState OnItemsAdded(int index, IList items)
@@ -430,6 +442,11 @@ namespace Avalonia.Controls.Selection
                 RaisePropertyChanged(nameof(SelectedIndex));
             }
 
+            if (e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex)
+            {
+                RaisePropertyChanged(nameof(SelectedItem));
+            }
+
             if (oldAnchorIndex != _anchorIndex)
             {
                 RaisePropertyChanged(nameof(AnchorIndex));
@@ -459,6 +476,16 @@ namespace Avalonia.Controls.Selection
             return true;
         }
 
+        private protected void SetInitSelectedItems(IList items)
+        {
+            if (Source is object)
+            {
+                throw new InvalidOperationException("Cannot set init selected items when Source is set.");
+            }
+
+            _initSelectedItems = items;
+        }
+
         protected override void OnSourceCollectionChangeFinished()
         {
             if (_operation is object)
@@ -532,8 +559,7 @@ namespace Avalonia.Controls.Selection
                 o.SelectedIndex = o.AnchorIndex = start;
             }
 
-            _initSelectedItem = default;
-            _hasInitSelectedItem = false;
+            _initSelectedItems = null;
         }
 
         [return: MaybeNull]
@@ -611,7 +637,7 @@ namespace Avalonia.Controls.Selection
             }
         }
 
-        private void CommitOperation(Operation operation)
+        private void CommitOperation(Operation operation, bool raisePropertyChanged = true)
         {
             try
             {
@@ -679,23 +705,34 @@ namespace Avalonia.Controls.Selection
                     }
                 }
 
-                if (oldSelectedIndex != _selectedIndex)
+                if (raisePropertyChanged)
                 {
-                    indexesChanged = true;
-                    RaisePropertyChanged(nameof(SelectedIndex));
-                    RaisePropertyChanged(nameof(SelectedItem));
-                }
+                    if (oldSelectedIndex != _selectedIndex)
+                    {
+                        indexesChanged = true;
+                        RaisePropertyChanged(nameof(SelectedIndex));
+                    }
 
-                if (oldAnchorIndex != _anchorIndex)
-                {
-                    indexesChanged = true;
-                    RaisePropertyChanged(nameof(AnchorIndex));
-                }
+                    if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate)
+                    {
+                        RaisePropertyChanged(nameof(SelectedItem));
+                    }
 
-                if (indexesChanged)
-                {
-                    RaisePropertyChanged(nameof(SelectedIndexes));
-                    RaisePropertyChanged(nameof(SelectedItems));
+                    if (oldAnchorIndex != _anchorIndex)
+                    {
+                        indexesChanged = true;
+                        RaisePropertyChanged(nameof(AnchorIndex));
+                    }
+
+                    if (indexesChanged)
+                    {
+                        RaisePropertyChanged(nameof(SelectedIndexes));
+                    }
+
+                    if (indexesChanged || operation.IsSourceUpdate)
+                    {
+                        RaisePropertyChanged(nameof(SelectedItems));
+                    }
                 }
             }
             finally

+ 2 - 0
src/Avalonia.Controls/Slider.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Collections;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
@@ -39,6 +40,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control that lets the user select from a range of values by moving a Thumb control along a Track.
     /// </summary>
+    [PseudoClasses(":vertical", ":horizontal", ":pressed")]
     public class Slider : RangeBase
     {
         /// <summary>

+ 6 - 1
src/Avalonia.Controls/SplitView.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
@@ -73,6 +74,10 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control with two views: A collapsible pane and an area for content
     /// </summary>
+    [PseudoClasses(":open", ":closed")]
+    [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")]
+    [PseudoClasses(":left", ":right")]
+    [PseudoClasses(":lightdismiss")]
     public class SplitView : TemplatedControl
     {
         /*

+ 14 - 41
src/Avalonia.Controls/TabControl.cs

@@ -1,3 +1,4 @@
+using System.ComponentModel;
 using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
@@ -66,7 +67,7 @@ namespace Avalonia.Controls
             SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
             ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
             AffectsMeasure<TabControl>(TabStripPlacementProperty);
-            SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e));
+            SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
         }
 
         /// <summary>
@@ -145,55 +146,27 @@ namespace Avalonia.Controls
         protected override void OnContainersMaterialized(ItemContainerEventArgs e)
         {
             base.OnContainersMaterialized(e);
-
-            if (SelectedContent != null || SelectedIndex == -1)
-            {
-                return;
-            }
-
-            var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex);
-
-            if (container == null)
-            {
-                return;
-            }
-
-            UpdateSelectedContent(container);
+            UpdateSelectedContent();
         }
 
-        private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
+        protected override void OnContainersRecycled(ItemContainerEventArgs e)
         {
-            var index = (int)e.NewValue;
-
-            if (index == -1)
-            {
-                SelectedContentTemplate = null;
-
-                SelectedContent = null;
-
-                return;
-            }
-
-            var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index);
-
-            if (container == null)
-            {
-                return;
-            }
-
-            UpdateSelectedContent(container);
+            base.OnContainersRecycled(e);
+            UpdateSelectedContent();
         }
 
-        private void UpdateSelectedContent(IContentControl item)
+        private void UpdateSelectedContent()
         {
-            if (SelectedContentTemplate != item.ContentTemplate)
+            if (SelectedIndex == -1)
             {
-                SelectedContentTemplate = item.ContentTemplate;
+                SelectedContent = SelectedContentTemplate = null;
             }
-
-            if (SelectedContent != item.Content)
+            else
             {
-                SelectedContent = item.Content;
+                var container = SelectedItem as IContentControl ??
+                    ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl;
+                SelectedContentTemplate = container?.ContentTemplate;
+                SelectedContent = container?.Content;
             }
         }
 

+ 2 - 0
src/Avalonia.Controls/TabItem.cs

@@ -1,3 +1,4 @@
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 
@@ -6,6 +7,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// An item in  a <see cref="TabStrip"/> or <see cref="TabControl"/>.
     /// </summary>
+    [PseudoClasses(":pressed", ":selected")]
     public class TabItem : HeaderedContentControl, ISelectable
     {
         /// <summary>

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

@@ -138,7 +138,7 @@ namespace Avalonia.Controls
                 FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
                 TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
 
-            Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed,
+            Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, ForegroundProperty.Changed,
                 TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
                 TextTrimmingProperty.Changed, FontSizeProperty.Changed,
                 FontStyleProperty.Changed, FontWeightProperty.Changed,
@@ -434,7 +434,10 @@ namespace Avalonia.Controls
 
             var padding = Padding;
 
-            TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top));
+            using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, padding.Top)))
+            {
+                TextLayout.Draw(context);
+            }
         }
 
         /// <summary>
@@ -452,7 +455,7 @@ namespace Avalonia.Controls
 
             return new TextLayout(
                 text ?? string.Empty,
-                FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
+                new Typeface(FontFamily, FontStyle, FontWeight),
                 FontSize,
                 Foreground,
                 TextAlignment,

+ 2 - 0
src/Avalonia.Controls/TextBox.cs

@@ -13,9 +13,11 @@ using Avalonia.Metadata;
 using Avalonia.Data;
 using Avalonia.Layout;
 using Avalonia.Utilities;
+using Avalonia.Controls.Metadata;
 
 namespace Avalonia.Controls
 {
+    [PseudoClasses(":empty")]
     public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
     {
         public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current

+ 3 - 1
src/Avalonia.Controls/ToggleSwitch.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.LogicalTree;
@@ -8,6 +9,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A Toggle Switch control.
     /// </summary>
+    [PseudoClasses(":dragging")]
     public class ToggleSwitch : ToggleButton
     {
         private Panel _knobsPanel;

+ 2 - 0
src/Avalonia.Controls/ToolTip.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Reactive.Linq;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.VisualTree;
 
@@ -14,6 +15,7 @@ namespace Avalonia.Controls
     /// To add a tooltip to a control, use the <see cref="TipProperty"/> attached property,
     /// assigning the content that you want displayed.
     /// </remarks>
+    [PseudoClasses(":open")]
     public class ToolTip : ContentControl
     {
         /// <summary>

+ 2 - 0
src/Avalonia.Controls/TreeViewItem.cs

@@ -1,5 +1,6 @@
 using System.Linq;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -11,6 +12,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// An item in a <see cref="TreeView"/>.
     /// </summary>
+    [PseudoClasses(":pressed", ":selected")]
     public class TreeViewItem : HeaderedItemsControl, ISelectable
     {
         /// <summary>

+ 0 - 283
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@@ -1,283 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using System.Linq;
-using Avalonia.Collections;
-using Avalonia.Controls.Selection;
-
-#nullable enable
-
-namespace Avalonia.Controls.Utils
-{
-    /// <summary>
-    /// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
-    /// </summary>
-    internal class SelectedItemsSync : IDisposable
-    {
-        private ISelectionModel _selectionModel;
-        private IList _selectedItems;
-        private bool _updatingItems;
-        private bool _updatingModel;
-
-        public SelectedItemsSync(ISelectionModel model)
-        {
-            _selectionModel = model ?? throw new ArgumentNullException(nameof(model));
-            _selectedItems = new AvaloniaList<object?>();
-            SyncSelectedItemsWithSelectionModel();
-            SubscribeToSelectedItems(_selectedItems);
-            SubscribeToSelectionModel(model);
-        }
-
-        public ISelectionModel SelectionModel 
-        {
-            get => _selectionModel;
-            set
-            {
-                if (_selectionModel != value)
-                {
-                    value = value ?? throw new ArgumentNullException(nameof(value));
-                    UnsubscribeFromSelectionModel(_selectionModel);
-                    _selectionModel = value;
-                    SubscribeToSelectionModel(_selectionModel);
-                    SyncSelectedItemsWithSelectionModel();
-                }
-            }
-        }
-        
-        public IList SelectedItems 
-        {
-            get => _selectedItems;
-            set
-            {
-                value ??= new AvaloniaList<object?>();
-
-                if (_selectedItems != value)
-                {
-                    if (value.IsFixedSize)
-                    {
-                        throw new NotSupportedException(
-                            "Cannot assign fixed size selection to SelectedItems.");
-                    }
-
-                    UnsubscribeFromSelectedItems(_selectedItems);
-                    _selectedItems = value;
-                    SubscribeToSelectedItems(_selectedItems);
-                    SyncSelectionModelWithSelectedItems();
-                }
-            }
-        }
-
-        public void Dispose()
-        {
-            UnsubscribeFromSelectedItems(_selectedItems);
-            UnsubscribeFromSelectionModel(_selectionModel);
-        }
-
-        private void SyncSelectedItemsWithSelectionModel()
-        {
-            _updatingItems = true;
-
-            try
-            {
-                _selectedItems.Clear();
-
-                if (_selectionModel.Source is object)
-                {
-                    foreach (var i in _selectionModel.SelectedItems)
-                    {
-                        _selectedItems.Add(i);
-                    }
-                }
-            }
-            finally
-            {
-                _updatingItems = false;
-            }
-        }
-
-        private void SyncSelectionModelWithSelectedItems()
-        {
-            _updatingModel = true;
-
-            try
-            {
-                if (_selectionModel.Source is object)
-                {
-                    using (_selectionModel.BatchUpdate())
-                    {
-                        SelectionModel.Clear();
-                        Add(_selectedItems);
-                    }
-                }
-            }
-            finally
-            {
-                _updatingModel = false;
-            }
-        }
-
-        private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
-        {
-            if (_updatingItems)
-            {
-                return;
-            }
-
-            if (_selectedItems == null)
-            {
-                throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
-            }
-
-            void Remove()
-            {
-                foreach (var i in e.OldItems)
-                {
-                    var index = IndexOf(SelectionModel.Source, i);
-
-                    if (index != -1)
-                    {
-                        SelectionModel.Deselect(index);
-                    }
-                }
-            }
-
-            try
-            {
-                using var operation = SelectionModel.BatchUpdate();
-
-                _updatingModel = true;
-
-                switch (e.Action)
-                {
-                    case NotifyCollectionChangedAction.Add:
-                        Add(e.NewItems);
-                        break;
-                    case NotifyCollectionChangedAction.Remove:
-                        Remove();
-                        break;
-                    case NotifyCollectionChangedAction.Replace:
-                        Remove();
-                        Add(e.NewItems);
-                        break;
-                    case NotifyCollectionChangedAction.Reset:
-                        SelectionModel.Clear();
-                        Add(_selectedItems);
-                        break;
-                }
-            }
-            finally
-            {
-                _updatingModel = false;
-            }
-        }
-
-        private void Add(IList newItems)
-        {
-            foreach (var i in newItems)
-            {
-                var index = IndexOf(SelectionModel.Source, i);
-
-                if (index != -1)
-                {
-                    SelectionModel.Select(index);
-                }
-            }
-        }
-
-        private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
-        {
-            if (e.PropertyName == nameof(ISelectionModel.Source))
-            {
-                if (_selectedItems.Count > 0)
-                {
-                    SyncSelectionModelWithSelectedItems();
-                }
-                else
-                {
-                    SyncSelectedItemsWithSelectionModel();
-                }
-            }
-        }
-
-        private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
-        {
-            if (_updatingModel || _selectionModel.Source is null)
-            {
-                return;
-            }
-
-            try
-            {
-                var deselected = e.DeselectedItems.ToList();
-                var selected = e.SelectedItems.ToList();
-
-                _updatingItems = true;
-
-                foreach (var i in deselected)
-                {
-                    _selectedItems.Remove(i);
-                }
-
-                foreach (var i in selected)
-                {
-                    _selectedItems.Add(i);
-                }
-            }
-            finally
-            {
-                _updatingItems = false;
-            }
-        }
-
-        private void SelectionModelSourceReset(object sender, EventArgs e)
-        {
-            SyncSelectionModelWithSelectedItems();
-        }
-
-
-        private void SubscribeToSelectedItems(IList selectedItems)
-        {
-            if (selectedItems is INotifyCollectionChanged incc)
-            {
-                incc.CollectionChanged += SelectedItemsCollectionChanged;
-            }
-        }
-
-        private void SubscribeToSelectionModel(ISelectionModel model)
-        {
-            model.PropertyChanged += SelectionModelPropertyChanged;
-            model.SelectionChanged += SelectionModelSelectionChanged;
-            model.SourceReset += SelectionModelSourceReset;
-        }
-
-        private void UnsubscribeFromSelectedItems(IList selectedItems)
-        {
-            if (selectedItems is INotifyCollectionChanged incc)
-            {
-                incc.CollectionChanged -= SelectedItemsCollectionChanged;
-            }
-        }
-
-        private void UnsubscribeFromSelectionModel(ISelectionModel model)
-        {
-            model.PropertyChanged -= SelectionModelPropertyChanged;
-            model.SelectionChanged -= SelectionModelSelectionChanged;
-            model.SourceReset -= SelectionModelSourceReset;
-        }
-
-        private static int IndexOf(object? source, object? item)
-        {
-            if (source is IList l)
-            {
-                return l.IndexOf(item);
-            }
-            else if (source is ItemsSourceView v)
-            {
-                return v.IndexOf(item);
-            }
-
-            return -1;
-        }
-    }
-}

+ 4 - 2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@@ -38,11 +38,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
         public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
         {
             if (listenUri.Scheme != "http")
-                throw new ArgumentException("listenUri");
+                throw new ArgumentException("URI scheme is not HTTP.", nameof(listenUri));
 
             var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
             _resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
-                .Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary(
+                .Where(r => r.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase)
+                         && r.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
+                .ToDictionary(
                     r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
                     r =>
                     {

+ 37 - 6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class MainViewModel : ViewModelBase, IDisposable
     {
-        private readonly IControl _root;
+        private readonly TopLevel _root;
         private readonly TreePageViewModel _logicalTree;
         private readonly TreePageViewModel _visualTree;
         private readonly EventsPageViewModel _events;
@@ -19,8 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
         private string _focusedControl;
         private string _pointerOverElement;
         private bool _shouldVisualizeMarginPadding = true;
+        private bool _shouldVisualizeDirtyRects;
+        private bool _showFpsOverlay;
 
-        public MainViewModel(IControl root)
+        public MainViewModel(TopLevel root)
         {
             _root = root;
             _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@@ -40,12 +42,42 @@ namespace Avalonia.Diagnostics.ViewModels
             get => _shouldVisualizeMarginPadding;
             set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
         }
+        
+        public bool ShouldVisualizeDirtyRects
+        {
+            get => _shouldVisualizeDirtyRects;
+            set
+            {
+                _root.Renderer.DrawDirtyRects = value;
+                RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
+            }
+        }
+
+        public void ToggleVisualizeDirtyRects()
+        {
+            ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
+        }
 
         public void ToggleVisualizeMarginPadding()
         {
             ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
         }
 
+        public bool ShowFpsOverlay
+        {
+            get => _showFpsOverlay;
+            set
+            {
+                _root.Renderer.DrawFps = value;
+                RaiseAndSetIfChanged(ref _showFpsOverlay, value);
+            }
+        }
+
+        public void ToggleFpsOverlay()
+        {
+            ShowFpsOverlay = !ShowFpsOverlay;
+        }
+
         public ConsoleViewModel Console { get; }
 
         public ViewModelBase Content
@@ -128,10 +160,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             var tree = Content as TreePageViewModel;
 
-            if (tree != null)
-            {
-                tree.SelectControl(control);
-            }
+            tree?.SelectControl(control);
         }
 
         public void Dispose()
@@ -140,6 +169,8 @@ namespace Avalonia.Diagnostics.ViewModels
             _pointerOverSubscription.Dispose();
             _logicalTree.Dispose();
             _visualTree.Dispose();
+            _root.Renderer.DrawDirtyRects = false;
+            _root.Renderer.DrawFps = false;
         }
 
         private void UpdateFocusedControl()

+ 31 - 20
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@@ -8,8 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels
     internal abstract class PropertyViewModel : ViewModelBase
     {
         private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
-        private static readonly Type[] StringParameter = new[] { typeof(string) };
-        private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) };
+        private static readonly Type[] StringParameter = { typeof(string) };
+        private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
 
         public abstract object Key { get; }
         public abstract string Name { get; }
@@ -26,35 +26,46 @@ namespace Avalonia.Diagnostics.ViewModels
             }
 
             var converter = TypeDescriptor.GetConverter(value);
-            return converter?.ConvertToString(value) ?? value.ToString();
+
+            //CollectionConverter does not deliver any important information. It just displays "(Collection)".
+            if (!converter.CanConvertTo(typeof(string)) || 
+                converter.GetType() == typeof(CollectionConverter))
+            {
+                return value.ToString();
+            }
+
+            return converter.ConvertToString(value);
         }
 
-        protected static object ConvertFromString(string s, Type targetType)
+        private static object InvokeParse(string s, Type targetType)
         {
-            var converter = TypeDescriptor.GetConverter(targetType);
-            
-            if (converter != null && converter.CanConvertFrom(typeof(string)))
+            var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
+
+            if (method != null)
             {
-                return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
+                return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
             }
-            else
+
+            method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
+
+            if (method != null)
             {
-                var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
+                return method.Invoke(null, new object[] { s });
+            }
 
-                if (method != null)
-                {
-                    return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
-                }
+            throw new InvalidCastException("Unable to convert value.");
+        }
 
-                method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
+        protected static object ConvertFromString(string s, Type targetType)
+        {
+            var converter = TypeDescriptor.GetConverter(targetType);
 
-                if (method != null)
-                {
-                    return method.Invoke(null, new object[] { s });
-                }
+            if (converter.CanConvertFrom(typeof(string)))
+            {
+                return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
             }
 
-            throw new InvalidCastException("Unable to convert value.");
+            return InvokeParse(s, targetType);
         }
     }
 }

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