Browse Source

Merge remote-tracking branch 'upstream/master' into fixes/geometry-impls

Dariusz Komosinski 6 years ago
parent
commit
50b0421a1a
100 changed files with 30326 additions and 531 deletions
  1. 26 0
      Avalonia.sln
  2. 5 1
      samples/ControlCatalog.Desktop/Program.cs
  3. 6 2
      samples/ControlCatalog.NetCore/Program.cs
  4. 1 0
      samples/ControlCatalog/App.xaml
  5. 1 0
      samples/ControlCatalog/ControlCatalog.csproj
  6. 6 5
      samples/ControlCatalog/MainView.xaml
  7. 1 1
      samples/ControlCatalog/MainView.xaml.cs
  8. 256 0
      samples/ControlCatalog/Models/Countries.cs
  9. 41 0
      samples/ControlCatalog/Models/Country.cs
  10. 37 0
      samples/ControlCatalog/Models/GDPValueConverter.cs
  11. 98 0
      samples/ControlCatalog/Models/Person.cs
  12. 9 9
      samples/ControlCatalog/Pages/CarouselPage.xaml
  13. 4 4
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  14. 41 0
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  15. 5 5
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  16. 55 0
      samples/ControlCatalog/Pages/DataGridPage.xaml
  17. 52 0
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  18. 1 0
      samples/ControlCatalog/Pages/DialogsPage.xaml
  19. 24 2
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  20. 0 42
      samples/ControlCatalog/Pages/DropDownPage.xaml
  21. 6 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  22. 1 1
      samples/ControlCatalog/Pages/ScreenPage.cs
  23. 6 6
      samples/ControlCatalog/Pages/TabControlPage.xaml
  24. 5 5
      samples/VirtualizationDemo/MainWindow.xaml
  25. 20 0
      src/Avalonia.Controls.DataGrid/AppBuilderExtensions.cs
  26. 20 0
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  27. 4315 0
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  28. 1366 0
      src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
  29. 259 0
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  30. 233 0
      src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs
  31. 5953 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  32. 145 0
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  33. 222 0
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  34. 71 0
      src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs
  35. 57 0
      src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
  36. 316 0
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  37. 204 0
      src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
  38. 1050 0
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  39. 586 0
      src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs
  40. 806 0
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  41. 1764 0
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  42. 696 0
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  43. 364 0
      src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
  44. 106 0
      src/Avalonia.Controls.DataGrid/DataGridEnumerations.cs
  45. 190 0
      src/Avalonia.Controls.DataGrid/DataGridError.cs
  46. 70 0
      src/Avalonia.Controls.DataGrid/DataGridFillerColumn.cs
  47. 542 0
      src/Avalonia.Controls.DataGrid/DataGridLength.cs
  48. 1056 0
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  49. 449 0
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  50. 57 0
      src/Avalonia.Controls.DataGrid/DataGridRowGroupInfo.cs
  51. 192 0
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  52. 3027 0
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  53. 470 0
      src/Avalonia.Controls.DataGrid/DataGridSelectedItemsCollection.cs
  54. 79 0
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  55. 357 0
      src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs
  56. 40 0
      src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs
  57. 569 0
      src/Avalonia.Controls.DataGrid/EventArgs.cs
  58. 25 0
      src/Avalonia.Controls.DataGrid/Extensions.cs
  59. 850 0
      src/Avalonia.Controls.DataGrid/IndexToValueTable.cs
  60. 315 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  61. 395 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
  62. 134 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs
  63. 45 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs
  64. 182 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  65. 14 0
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  66. 69 0
      src/Avalonia.Controls.DataGrid/Range.cs
  67. 233 0
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  68. 160 0
      src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
  69. 136 0
      src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
  70. 24 0
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  71. 522 0
      src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs
  72. 60 0
      src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs
  73. 167 0
      src/Avalonia.Controls.DataGrid/Utils/ValidationUtil.cs
  74. 0 3
      src/Avalonia.Controls/AutoCompleteBox.cs
  75. 45 24
      src/Avalonia.Controls/Button.cs
  76. 0 1
      src/Avalonia.Controls/Calendar/Calendar.cs
  77. 373 0
      src/Avalonia.Controls/ComboBox.cs
  78. 5 5
      src/Avalonia.Controls/ComboBoxItem.cs
  79. 2 2
      src/Avalonia.Controls/ControlExtensions.cs
  80. 15 360
      src/Avalonia.Controls/DropDown.cs
  81. 29 6
      src/Avalonia.Controls/MenuItem.cs
  82. 5 3
      src/Avalonia.Controls/Platform/IScreenImpl.cs
  83. 2 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  84. 3 2
      src/Avalonia.Controls/Screens.cs
  85. 2 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  86. 1 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  87. 2 1
      src/Avalonia.Native/ScreenImpl.cs
  88. 2 2
      src/Avalonia.Themes.Default/ComboBox.xaml
  89. 6 6
      src/Avalonia.Themes.Default/ComboBoxItem.xaml
  90. 2 2
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  91. 12 0
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  92. 21 4
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  93. 5 0
      src/Avalonia.X11/X11Platform.cs
  94. 1 1
      src/Avalonia.X11/X11Screens.cs
  95. 12 0
      src/Avalonia.X11/X11Window.cs
  96. 3 2
      src/Gtk/Avalonia.Gtk3/ScreenImpl.cs
  97. 2 1
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  98. 44 2
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  99. 9 9
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  100. 54 0
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

+ 26 - 0
Avalonia.sln

@@ -202,6 +202,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1845,6 +1847,30 @@ Global
 		{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU
 		{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

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

@@ -22,7 +22,11 @@ namespace ControlCatalog
         /// This method is needed for IDE previewer infrastructure
         /// </summary>
         public static AppBuilder BuildAvaloniaApp()
-            => AppBuilder.Configure<App>().LogToDebug().UsePlatformDetect().UseReactiveUI();
+            => AppBuilder.Configure<App>()
+                .LogToDebug()
+                .UsePlatformDetect()
+                .UseReactiveUI()
+                .UseDataGrid();
 
         private static void ConfigureAssetAssembly(AppBuilder builder)
         {

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

@@ -9,7 +9,7 @@ namespace ControlCatalog.NetCore
 {
     static class Program
     {
-        
+
         static void Main(string[] args)
         {
             Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA);
@@ -43,7 +43,11 @@ namespace ControlCatalog.NetCore
         /// This method is needed for IDE previewer infrastructure
         /// </summary>
         public static AppBuilder BuildAvaloniaApp()
-            => AppBuilder.Configure<App>().UsePlatformDetect().UseSkia().UseReactiveUI();
+            => AppBuilder.Configure<App>()
+                .UsePlatformDetect()
+                .UseSkia()
+                .UseReactiveUI()
+                .UseDataGrid();
 
         static void ConsoleSilencer()
         {

+ 1 - 0
samples/ControlCatalog/App.xaml

@@ -4,6 +4,7 @@
   <Application.Styles>
       <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
       <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
+      <StyleInclude Source="resm:Avalonia.Controls.DataGrid.Themes.Default.xaml?assembly=Avalonia.Controls.DataGrid"/>
     <Style Selector="TextBlock.h1">
       <Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>
       <Setter Property="FontWeight" Value="Medium"/>

+ 1 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -22,6 +22,7 @@
   <ItemGroup>
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
   </ItemGroup>
   
   <Import Project="..\..\build\Serilog.props" />

+ 6 - 5
samples/ControlCatalog/MainView.xaml

@@ -6,10 +6,10 @@
         Foreground="{DynamicResource ThemeForegroundBrush}"
         FontSize="{DynamicResource FontSizeNormal}">
   <Grid>
-    <DropDown x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
-      <DropDownItem>Light</DropDownItem>
-      <DropDownItem>Dark</DropDownItem>
-    </DropDown>
+    <ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+      <ComboBoxItem>Light</ComboBoxItem>
+      <ComboBoxItem>Dark</ComboBoxItem>
+    </ComboBox>
     <TabControl Classes="sidebar" Name="Sidebar">
       <TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
       <TabItem Header="Border"><pages:BorderPage/></TabItem>
@@ -19,10 +19,11 @@
       <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
       <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
+      <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
+      <TabItem Header="DataGrid"><pages:DataGridPage/></TabItem>
       <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
-      <TabItem Header="DropDown"><pages:DropDownPage/></TabItem>
       <TabItem Header="Ellipse"><pages:EllipsePage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
       <TabItem Header="Image"><pages:ImagePage/></TabItem>

+ 1 - 1
samples/ControlCatalog/MainView.xaml.cs

@@ -30,7 +30,7 @@ namespace ControlCatalog
             }
             var light = AvaloniaXamlLoader.Parse<StyleInclude>(@"<StyleInclude xmlns='https://github.com/avaloniaui' Source='resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default'/>");
             var dark = AvaloniaXamlLoader.Parse<StyleInclude>(@"<StyleInclude xmlns='https://github.com/avaloniaui' Source='resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default'/>");
-            var themes = this.Find<DropDown>("Themes");
+            var themes = this.Find<ComboBox>("Themes");
             themes.SelectionChanged += (sender, e) =>
             {
                 switch (themes.SelectedIndex)

+ 256 - 0
samples/ControlCatalog/Models/Countries.cs

@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+
+namespace ControlCatalog.Models
+{
+    public static class Countries
+    {
+        static IEnumerable<Country> GetCountries()
+        {
+            yield return new Country("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 48, 0, 23.06, 163.07, 700, 36, 3.2, 46.6, 20.34);
+            yield return new Country("Albania", "EASTERN EUROPE", 3581655, 28748, 124.6, 1.26, -4.93, 21.52, 4500, 86.5, 71.2, 15.11, 5.22);
+            yield return new Country("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 13.8, 0.04, -0.39, 31, 6000, 70, 78.1, 17.14, 4.61);
+            yield return new Country("American Samoa", "OCEANIA", 57794, 199, 290.4, 58.29, -20.71, 9.27, 8000, 97, 259.5, 22.46, 3.27);
+            yield return new Country("Andorra", "WESTERN EUROPE", 71201, 468, 152.1, 0, 6.6, 4.05, 19000, 100, 497.2, 8.71, 6.25);
+            yield return new Country("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 9.7, 0.13, 0, 191.19, 1900, 42, 7.8, 45.11, 24.2);
+            yield return new Country("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 132.1, 59.8, 10.76, 21.03, 8600, 95, 460, 14.17, 5.34);
+            yield return new Country("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 156, 34.54, -6.15, 19.46, 11000, 89, 549.9, 16.93, 5.37);
+            yield return new Country("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 14.4, 0.18, 0.61, 15.18, 11200, 97.1, 220.4, 16.73, 7.55);
+            yield return new Country("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 99.9, 0, -6.47, 23.28, 3500, 98.6, 195.7, 12.07, 8.23);
+            yield return new Country("Aruba", "LATIN AMER. & CARIB", 71891, 193, 372.5, 35.49, 0, 5.89, 28000, 97, 516.1, 11.03, 6.68);
+            yield return new Country("Australia", "OCEANIA", 20264082, 7686850, 2.6, 0.34, 3.98, 4.69, 29000, 100, 565.5, 12.14, 7.51);
+            yield return new Country("Austria", "WESTERN EUROPE", 8192880, 83870, 97.7, 0, 2, 4.66, 30000, 98, 452.2, 8.74, 9.76);
+            yield return new Country("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 91.9, 0, -4.9, 81.74, 3400, 97, 137.1, 20.74, 9.75);
+            yield return new Country("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 21.8, 25.41, -2.2, 25.21, 16700, 95.6, 460.6, 17.57, 9.05);
+            yield return new Country("Bahrain", "NEAR EAST", 698585, 665, 1050.5, 24.21, 1.05, 17.27, 16900, 89.1, 281.3, 17.8, 4.14);
+            yield return new Country("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1023.4, 0.4, -0.71, 62.6, 1900, 43.1, 7.3, 29.8, 8.27);
+            yield return new Country("Barbados", "LATIN AMER. & CARIB", 279912, 431, 649.5, 22.51, -0.31, 12.5, 15700, 97.4, 481.9, 12.71, 8.67);
+            yield return new Country("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 49.6, 0, 2.54, 13.37, 6100, 99.6, 319.1, 11.16, 14.02);
+            yield return new Country("Belgium", "WESTERN EUROPE", 10379067, 30528, 340, 0.22, 1.23, 4.68, 29100, 98, 462.6, 10.38, 10.27);
+            yield return new Country("Belize", "LATIN AMER. & CARIB", 287730, 22966, 12.5, 1.68, 0, 25.69, 4900, 94.1, 115.7, 28.84, 5.72);
+            yield return new Country("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 69.8, 0.11, 0, 85, 1100, 40.9, 9.7, 38.85, 12.22);
+            yield return new Country("Bermuda", "NORTHERN AMERICA", 65773, 53, 1241, 194.34, 2.49, 8.53, 36000, 98, 851.4, 11.4, 7.74);
+            yield return new Country("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 48.5, 0, 0, 100.44, 1300, 42.2, 14.3, 33.65, 12.7);
+            yield return new Country("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 8.2, 0, -1.32, 53.11, 2400, 87.2, 71.9, 23.3, 7.53);
+            yield return new Country("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 88, 0.04, 0.31, 21.05, 6100,null, 215.4, 8.77, 8.27);
+            yield return new Country("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 2.7, 0, 0, 54.58, 9000, 79.8, 80.5, 23.08, 29.5);
+            yield return new Country("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 22.1, 0.09, -0.03, 29.61, 7600, 86.4, 225.3, 16.56, 6.17);
+            yield return new Country("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 151, 52.29, 10.01, 18.05, 16000, 97.8, 506.5, 14.89, 4.42);
+            yield return new Country("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 65.8, 2.79, 3.59, 12.61, 18600, 93.9, 237.2, 18.79, 3.45);
+            yield return new Country("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 66.6, 0.32, -4.58, 20.55, 7600, 98.6, 336.3, 9.65, 14.27);
+            yield return new Country("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 50.7, 0, 0, 97.57, 1100, 26.6, 7, 45.62, 15.6);
+            yield return new Country("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 69.8, 0.28, -1.8, 67.24, 1800, 85.3, 10.1, 17.91, 9.83);
+            yield return new Country("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 290.7, 0, -0.06, 69.29, 600, 51.6, 3.4, 42.22, 13.46);
+            yield return new Country("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 76.7, 0.24, 0, 71.48, 1900, 69.4, 2.6, 26.9, 9.06);
+            yield return new Country("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 36.5, 0.08, 0, 68.26, 1800, 79, 5.7, 33.89, 13.47);
+            yield return new Country("Canada", "NORTHERN AMERICA", 33098932, 9984670, 3.3, 2.02, 5.96, 4.75, 29800, 97, 552.2, 10.78, 7.8);
+            yield return new Country("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 104.4, 23.93, -12.07, 47.77, 1400, 76.6, 169.6, 24.87, 6.55);
+            yield return new Country("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 173.4, 61.07, 18.75, 8.19, 35000, 98, 836.3, 12.74, 4.89);
+            yield return new Country("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 6.9, 0, 0, 91, 1100, 51, 2.3, 33.91, 18.65);
+            yield return new Country("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 7.7, 0, -0.11, 93.82, 1200, 47.5, 1.3, 45.73, 16.38);
+            yield return new Country("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 21.3, 0.85, 0, 8.8, 9900, 96.2, 213, 15.23, 5.81);
+            yield return new Country("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 136.9, 0.15, -0.4, 24.18, 5000, 90.9, 266.7, 13.25, 6.97);
+            yield return new Country("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 38.3, 0.28, -0.31, 20.97, 6300, 92.5, 176.2, 20.48, 5.58);
+            yield return new Country("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 318.4, 15.67, 0, 74.93, 700, 56.5, 24.5, 36.93, 8.2);
+            yield return new Country("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 26.7, 0, 0, 94.69, 700, 65.5, 0.2, 43.69, 13.27);
+            yield return new Country("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 10.8, 0.05, -0.17, 93.86, 700, 83.8, 3.7, 42.57, 12.93);
+            yield return new Country("Cook Islands", "OCEANIA", 21388, 240, 89.1, 50,null,null, 5000, 95, 289.9, 21,null);
+            yield return new Country("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 79.8, 2.52, 0.51, 9.95, 9100, 96, 340.7, 18.32, 4.36);
+            yield return new Country("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 54.8, 0.16, -0.07, 90.83, 1400, 50.9, 14.6, 35.11, 14.84);
+            yield return new Country("Croatia", "EASTERN EUROPE", 4494749, 56542, 79.5, 10.32, 1.58, 6.84, 10600, 98.5, 420.4, 9.61, 11.48);
+            yield return new Country("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 102.7, 3.37, -1.58, 6.33, 2900, 97, 74.7, 11.89, 7.22);
+            yield return new Country("Cyprus", "NEAR EAST", 784301, 9250, 84.8, 7.01, 0.43, 7.18, 19200, 97.6,null, 12.56, 7.68);
+            yield return new Country("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 129.8, 0, 0.97, 3.93, 15700, 99.9, 314.3, 9.02, 10.59);
+            yield return new Country("Denmark", "WESTERN EUROPE", 5450661, 43094, 126.5, 16.97, 2.48, 4.56, 31100, 100, 614.6, 11.13, 10.36);
+            yield return new Country("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 21.2, 1.37, 0, 104.13, 1300, 67.9, 22.8, 39.53, 19.31);
+            yield return new Country("Dominica", "LATIN AMER. & CARIB", 68910, 754, 91.4, 19.63, -13.87, 14.15, 5400, 94, 304.8, 15.27, 6.73);
+            yield return new Country("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 188.5, 2.64, -3.22, 32.38, 6000, 84.7, 97.4, 23.22, 5.73);
+            yield return new Country("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 70.8, 4.7, 0, 47.41, 500, 58.6,null, 26.99, 6.24);
+            yield return new Country("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 47.8, 0.79, -8.58, 23.66, 3300, 92.5, 125.6, 22.29, 4.23);
+            yield return new Country("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 78.8, 0.24, -0.22, 32.59, 4000, 57.7, 131.8, 22.94, 5.23);
+            yield return new Country("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 324.3, 1.46, -3.74, 25.1, 4800, 80.2, 142.4, 26.61, 5.78);
+            yield return new Country("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 19.3, 1.06, 0, 85.13, 2700, 85.7, 18.5, 35.59, 15.06);
+            yield return new Country("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 39.5, 1.84, 0, 74.87, 700, 58.6, 7.9, 34.33, 9.6);
+            yield return new Country("Estonia", "BALTICS", 1324333, 45226, 29.3, 8.39, -3.16, 7.87, 12300, 99.8, 333.8, 10.04, 13.25);
+            yield return new Country("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 66.3, 0, 0, 95.32, 700, 42.7, 8.2, 37.98, 14.86);
+            yield return new Country("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 33.8, 79.84, 1.41, 6.24, 22000,null, 503.8, 14.05, 8.7);
+            yield return new Country("Fiji", "OCEANIA", 905949, 18270, 49.6, 6.18, -3.14, 12.62, 5800, 93.7, 112.6, 22.55, 5.65);
+            yield return new Country("Finland", "WESTERN EUROPE", 5231372, 338145, 15.5, 0.37, 0.95, 3.57, 27400, 100, 405.3, 10.45, 9.86);
+            yield return new Country("France", "WESTERN EUROPE", 60876136, 547030, 111.3, 0.63, 0.66, 4.26, 27600, 99, 586.4, 11.99, 9.14);
+            yield return new Country("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 2.2, 0.42, 6.27, 12.07, 8300, 83, 255.6, 20.46, 4.88);
+            yield return new Country("French Polynesia", "OCEANIA", 274578, 4167, 65.9, 60.6, 2.94, 8.44, 17500, 98, 194.5, 16.68, 4.69);
+            yield return new Country("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5.3, 0.33, 0, 53.64, 5500, 63.2, 27.4, 36.16, 12.25);
+            yield return new Country("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 145.3, 0.71, 1.57, 72.02, 1700, 40.1, 26.8, 39.37, 12.25);
+            yield return new Country("Gaza Strip", "NEAR EAST", 1428757, 360, 3968.8, 11.11, 1.6, 22.93, 600,null, 244.3, 39.45, 3.8);
+            yield return new Country("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 66.9, 0.44, -4.7, 18.59, 2500, 99, 146.6, 10.41, 9.23);
+            yield return new Country("Germany", "WESTERN EUROPE", 82422299, 357021, 230.9, 0.67, 2.18, 4.16, 27600, 99, 667.9, 8.25, 10.62);
+            yield return new Country("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 93.6, 0.23, -0.64, 51.43, 2200, 74.8, 14.4, 30.52, 9.72);
+            yield return new Country("Gibraltar", "WESTERN EUROPE", 27928, 7, 3989.7, 171.43, 0, 5.13, 17500,null, 877.7, 10.74, 9.31);
+            yield return new Country("Greece", "WESTERN EUROPE", 10688058, 131940, 81, 10.37, 2.35, 5.53, 20000, 97.5, 589.7, 9.68, 10.24);
+            yield return new Country("Greenland", "NORTHERN AMERICA", 56361, 2166086, 0, 2.04, -8.37, 15.82, 20000,null, 448.9, 15.93, 7.84);
+            yield return new Country("Grenada", "LATIN AMER. & CARIB", 89703, 344, 260.8, 35.17, -13.92, 14.62, 5000, 98, 364.5, 22.08, 6.88);
+            yield return new Country("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 254.4, 17.19, -0.15, 8.6, 8000, 90, 463.8, 15.05, 6.09);
+            yield return new Country("Guam", "OCEANIA", 171019, 541, 316.1, 23.2, 0, 6.94, 21000, 99, 492, 18.79, 4.48);
+            yield return new Country("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 112.9, 0.37, -1.67, 35.93, 4100, 70.6, 92.1, 29.88, 5.2);
+            yield return new Country("Guernsey", "WESTERN EUROPE", 65409, 78, 838.6, 64.1, 3.84, 4.71, 20000,null, 842.4, 8.81, 10.01);
+            yield return new Country("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 39.4, 0.13, -3.06, 90.37, 2100, 35.9, 2.7, 41.76, 15.48);
+            yield return new Country("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 39.9, 0.97, -1.57, 107.17, 800, 42.4, 7.4, 37.22, 16.53);
+            yield return new Country("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 3.6, 0.21, -2.07, 33.26, 4000, 98.8, 143.5, 18.28, 8.28);
+            yield return new Country("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 299.4, 6.38, -3.4, 73.45, 1600, 52.9, 16.9, 36.44, 12.17);
+            yield return new Country("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 65.4, 0.73, -1.99, 29.32, 2600, 76.2, 67.5, 28.24, 5.28);
+            yield return new Country("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 6355.7, 67.12, 5.24, 2.97, 28800, 93.5, 546.7, 7.29, 6.29);
+            yield return new Country("Hungary", "EASTERN EUROPE", 9981334, 93030, 107.3, 0, 0.86, 8.57, 13900, 99.4, 336.2, 9.72, 13.11);
+            yield return new Country("Iceland", "WESTERN EUROPE", 299388, 103000, 2.9, 4.83, 2.38, 3.31, 30900, 99.9, 647.7, 13.64, 6.72);
+            yield return new Country("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 333.2, 0.21, -0.07, 56.29, 2900, 59.5, 45.4, 22.01, 8.18);
+            yield return new Country("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 127.9, 2.85, 0, 35.6, 3200, 87.9, 52, 20.34, 6.25);
+            yield return new Country("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 41.7, 0.15, -0.84, 41.58, 7000, 79.4, 276.4, 17, 5.55);
+            yield return new Country("Iraq", "NEAR EAST", 26783383, 437072, 61.3, 0.01, 0, 50.25, 1500, 40.4, 38.6, 31.98, 5.37);
+            yield return new Country("Ireland", "WESTERN EUROPE", 4062235, 70280, 57.8, 2.06, 4.99, 5.39, 29600, 98, 500.5, 14.45, 7.82);
+            yield return new Country("Isle of Man", "WESTERN EUROPE", 75441, 572, 131.9, 27.97, 5.36, 5.93, 21000,null, 676, 11.05, 11.19);
+            yield return new Country("Israel", "NEAR EAST", 6352117, 20770, 305.8, 1.31, 0.68, 7.03, 19800, 95.4, 462.3, 17.97, 6.18);
+            yield return new Country("Italy", "WESTERN EUROPE", 58133509, 301230, 193, 2.52, 2.07, 5.94, 26700, 98.6, 430.9, 8.72, 10.4);
+            yield return new Country("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 250.9, 9.3, -4.92, 12.36, 3900, 87.9, 124, 20.82, 6.52);
+            yield return new Country("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 337.4, 7.87, 0, 3.26, 28200, 99, 461.2, 9.37, 9.16);
+            yield return new Country("Jersey", "WESTERN EUROPE", 91084, 116, 785.2, 60.34, 2.76, 5.24, 24800,null, 811.3, 9.3, 9.28);
+            yield return new Country("Jordan", "NEAR EAST", 5906760, 92300, 64, 0.03, 6.59, 17.35, 4300, 91.3, 104.5, 21.25, 2.65);
+            yield return new Country("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 5.6, 0, -3.35, 29.21, 6300, 98.4, 164.1, 16, 9.42);
+            yield return new Country("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 59.6, 0.09, -0.1, 61.47, 1000, 85.1, 8.1, 39.72, 14.02);
+            yield return new Country("Kiribati", "OCEANIA", 105432, 811, 130, 140.94, 0, 48.52, 800,null, 42.7, 30.65, 8.26);
+            yield return new Country("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 191.8, 2.07, 0, 24.04, 1300, 99, 42.4, 15.54, 7.13);
+            yield return new Country("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 496, 2.45, 0, 7.05, 17800, 97.9, 486.1, 10, 5.85);
+            yield return new Country("Kuwait", "NEAR EAST", 2418393, 17820, 135.7, 2.8, 14.18, 9.95, 19000, 83.5, 211, 21.94, 2.41);
+            yield return new Country("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 26.3, 0, -2.45, 35.64, 1600, 97, 84, 22.8, 7.08);
+            yield return new Country("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 26.9, 0, 0, 85.22, 1700, 66.4, 14.1, 35.49, 11.55);
+            yield return new Country("Latvia", "BALTICS", 2274735, 64589, 35.2, 0.82, -2.23, 9.55, 10200, 99.8, 321.4, 9.24, 13.66);
+            yield return new Country("Lebanon", "NEAR EAST", 3874050, 10400, 372.5, 2.16, 0, 24.52, 4800, 87.4, 255.6, 18.52, 6.21);
+            yield return new Country("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 66.6, 0, -0.74, 84.23, 3000, 84.8, 23.7, 24.75, 28.71);
+            yield return new Country("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 27.3, 0.52, 0, 128.87, 1000, 57.5, 2.3, 44.77, 23.1);
+            yield return new Country("Libya", "NORTHERN AFRICA", 5900754, 1759540, 3.4, 0.1, 0, 24.6, 6400, 82.6, 127.1, 26.49, 3.48);
+            yield return new Country("Liechtenstein", "WESTERN EUROPE", 33987, 160, 212.4, 0, 4.85, 4.7, 25000, 100, 585.5, 10.21, 7.18);
+            yield return new Country("Lithuania", "BALTICS", 3585906, 65200, 55, 0.14, -0.71, 6.89, 11400, 99.6, 223.4, 8.75, 10.98);
+            yield return new Country("Luxembourg", "WESTERN EUROPE", 474413, 2586, 183.5, 0, 8.97, 4.81, 55100, 100, 515.4, 11.94, 8.41);
+            yield return new Country("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 16183, 146.43, 4.86, 4.39, 19400, 94.5, 384.9, 8.48, 4.47);
+            yield return new Country("Macedonia", "EASTERN EUROPE", 2050554, 25333, 80.9, 0, -1.45, 10.09, 6700,null, 260, 12.02, 8.77);
+            yield return new Country("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 31.7, 0.82, 0, 76.83, 800, 68.9, 3.6, 41.41, 11.11);
+            yield return new Country("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 109.8, 0, 0, 103.32, 600, 62.7, 7.9, 43.13, 19.33);
+            yield return new Country("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 74, 1.42, 0, 17.7, 9000, 88.7, 179, 22.86, 5.05);
+            yield return new Country("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 1196.7, 214.67, 0, 56.52, 3900, 97.2, 90, 34.81, 7.06);
+            yield return new Country("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 9.5, 0, -0.33, 116.79, 900, 46.4, 6.4, 49.82, 16.89);
+            yield return new Country("Malta", "WESTERN EUROPE", 400214, 316, 1266.5, 62.28, 2.07, 3.89, 17700, 92.8, 505, 10.22, 8.1);
+            yield return new Country("Marshall Islands", "OCEANIA", 60422, 11854, 5.1, 3.12, -6.04, 29.45, 1600, 93.7, 91.2, 33.05, 4.78);
+            yield return new Country("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 396.5, 31.82, -0.05, 7.09, 14400, 97.7, 394.4, 13.74, 6.48);
+            yield return new Country("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 3.1, 0.07, 0, 70.89, 1800, 41.7, 12.9, 40.99, 12.16);
+            yield return new Country("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 608.3, 8.68, -0.9, 15.03, 11400, 85.6, 289.3, 15.43, 6.86);
+            yield return new Country("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 538.1, 49.52, 6.78, 62.4, 2600,null, 49.7, 40.95, 7.7);
+            yield return new Country("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 54.5, 0.47, -4.87, 20.91, 9000, 92.2, 181.6, 20.69, 4.74);
+            yield return new Country("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 153.9, 870.66, -20.99, 30.21, 2000, 89, 114.8, 24.68, 4.75);
+            yield return new Country("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 132, 0, -0.26, 40.42, 1800, 99.1, 208.1, 15.7, 12.64);
+            yield return new Country("Monaco", "WESTERN EUROPE", 32543, 2, 16271.5, 205, 7.75, 5.43, 27000, 99, 1035.6, 9.19, 12.91);
+            yield return new Country("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1.8, 0, 0, 53.79, 1800, 97.8, 55.1, 21.59, 6.95);
+            yield return new Country("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 92.5, 39.22, 0, 7.35, 3400, 97,null, 17.59, 7.1);
+            yield return new Country("Morocco", "NORTHERN AFRICA", 33241259, 446550, 74.4, 0.41, -0.98, 41.62, 4000, 51.7, 40.4, 21.98, 5.58);
+            yield return new Country("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 24.6, 0.31, 0, 130.79, 1200, 47.8, 3.5, 35.18, 21.35);
+            yield return new Country("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 2.5, 0.19, 0, 48.98, 7200, 84, 62.6, 24.32, 18.86);
+            yield return new Country("Nauru", "OCEANIA", 13287, 21, 632.7, 142.86, 0, 9.95, 5000,null, 143, 24.76, 6.7);
+            yield return new Country("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 192.2, 0, 0, 66.98, 1400, 45.2, 15.9, 30.98, 9.31);
+            yield return new Country("Netherlands", "WESTERN EUROPE", 16491461, 41526, 397.1, 1.09, 2.91, 5.04, 28600, 99, 460.8, 10.9, 8.68);
+            yield return new Country("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 231, 37.92, -0.41, 10.03, 11400, 96.7, 365.3, 14.78, 6.45);
+            yield return new Country("New Caledonia", "OCEANIA", 219246, 19060, 11.5, 11.83, 0, 7.72, 15000, 91, 252.2, 18.11, 5.69);
+            yield return new Country("New Zealand", "OCEANIA", 4076140, 268680, 15.2, 5.63, 4.05, 5.85, 21600, 99, 441.7, 13.76, 7.53);
+            yield return new Country("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 43, 0.7, -1.22, 29.11, 2300, 67.5, 39.7, 24.51, 4.45);
+            yield return new Country("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 9.9, 0, -0.67, 121.69, 800, 17.6, 1.9, 50.73, 20.91);
+            yield return new Country("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 142.7, 0.09, 0.26, 98.8, 900, 68, 9.3, 40.43, 16.94);
+            yield return new Country("N. Mariana Islands", "OCEANIA", 82459, 477, 172.9, 310.69, 9.61, 7.11, 12500, 97, 254.7, 19.43, 2.29);
+            yield return new Country("Norway", "WESTERN EUROPE", 4610820, 323802, 14.2, 7.77, 1.74, 3.7, 37800, 100, 461.7, 11.46, 9.4);
+            yield return new Country("Oman", "NEAR EAST", 3102229, 212460, 14.6, 0.98, 0.28, 19.51, 13100, 75.8, 85.5, 36.24, 3.81);
+            yield return new Country("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 206.2, 0.13, -2.77, 72.44, 2100, 45.7, 31.8, 29.74, 8.23);
+            yield return new Country("Palau", "OCEANIA", 20579, 458, 44.9, 331.66, 2.85, 14.84, 9000, 92, 325.6, 18.03, 6.8);
+            yield return new Country("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 40.8, 3.18, -0.91, 20.47, 6300, 92.6, 137.9, 21.74, 5.36);
+            yield return new Country("Papua New Guinea", "OCEANIA", 5670544, 462840, 12.3, 1.11, 0, 51.45, 2200, 64.6, 10.9, 29.36, 7.25);
+            yield return new Country("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 16, 0, -0.08, 25.63, 4700, 94, 49.2, 29.1, 4.49);
+            yield return new Country("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 22, 0.19, -1.05, 31.94, 5100, 90.9, 79.5, 20.48, 6.23);
+            yield return new Country("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 298.2, 12.1, -1.5, 23.51, 4600, 92.6, 38.4, 24.89, 5.41);
+            yield return new Country("Poland", "EASTERN EUROPE", 38536869, 312685, 123.3, 0.16, -0.49, 8.51, 11100, 99.8, 306.3, 9.85, 9.89);
+            yield return new Country("Portugal", "WESTERN EUROPE", 10605870, 92391, 114.8, 1.94, 3.57, 5.05, 18000, 93.3, 399.2, 10.72, 10.5);
+            yield return new Country("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 284.8, 3.63, -1.46, 8.24, 16800, 94.1, 283.1, 12.77, 7.65);
+            yield return new Country("Qatar", "NEAR EAST", 885359, 11437, 77.4, 4.92, 16.29, 18.61, 21500, 82.5, 232, 15.56, 4.72);
+            yield return new Country("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 312.9, 8.22, 0, 7.78, 5800, 88.9, 380.9, 18.9, 5.49);
+            yield return new Country("Romania", "EASTERN EUROPE", 22303552, 237500, 93.9, 0.09, -0.13, 26.43, 7000, 98.4, 196.9, 10.7, 11.77);
+            yield return new Country("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8.4, 0.22, 1.02, 15.39, 8900, 99.6, 280.6, 9.95, 14.65);
+            yield return new Country("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 328.4, 0, 0, 91.23, 1300, 70.4, 2.7, 40.37, 16.09);
+            yield return new Country("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 18.2, 14.53, 0, 19, 2500, 97, 293.3, 12.13, 6.53);
+            yield return new Country("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 149.9, 51.72, -7.11, 14.49, 8800, 97, 638.9, 18.02, 8.33);
+            yield return new Country("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 273.5, 25.65, -2.67, 13.53, 5400, 67, 303.3, 19.68, 5.08);
+            yield return new Country("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 29, 49.59, -4.86, 7.54, 6900, 99, 683.2, 13.52, 6.83);
+            yield return new Country("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 303, 21.59, -7.64, 14.78, 2900, 96, 190.9, 16.18, 5.98);
+            yield return new Country("Samoa", "OCEANIA", 176908, 2944, 60.1, 13.69, -11.7, 27.71, 5600, 99.7, 75.2, 16.43, 6.62);
+            yield return new Country("San Marino", "WESTERN EUROPE", 29251, 61, 479.5, 0, 10.98, 5.73, 34600, 96, 704.3, 10.02, 8.17);
+            yield return new Country("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 193.2, 20.88, -2.72, 43.11, 1200, 79.3, 36.2, 40.25, 6.47);
+            yield return new Country("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 13.8, 0.13, -2.71, 13.24, 11800, 78.8, 140.6, 29.34, 2.58);
+            yield return new Country("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 61.1, 0.27, 0.2, 55.51, 1600, 40.2, 22.2, 32.78, 9.42);
+            yield return new Country("Serbia", "EASTERN EUROPE", 9396411, 88361, 106.3, 0, -1.33, 12.89, 2200, 93, 285.8,null,null);
+            yield return new Country("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 179.2, 107.91, -5.69, 15.53, 7800, 58, 262.4, 16.03, 6.29);
+            yield return new Country("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 83.7, 0.56, 0, 143.64, 500, 31.4, 4, 45.76, 23.03);
+            yield return new Country("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 6482.2, 27.85, 11.53, 2.29, 23700, 92.5, 411.4, 9.34, 4.28);
+            yield return new Country("Slovakia", "EASTERN EUROPE", 5439448, 48845, 111.4, 0, 0.3, 7.41, 13300,null, 220.1, 10.65, 9.45);
+            yield return new Country("Slovenia", "EASTERN EUROPE", 2010347, 20273, 99.2, 0.23, 1.12, 4.45, 19000, 99.7, 406.1, 8.98, 10.31);
+            yield return new Country("Solomon Islands", "OCEANIA", 552438, 28450, 19.4, 18.67, 0, 21.29, 1700,null, 13.4, 30.01, 3.92);
+            yield return new Country("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 13.9, 0.47, 5.37, 116.7, 500, 37.8, 11.3, 45.13, 16.63);
+            yield return new Country("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 36.2, 0.23, -0.29, 61.81, 10700, 86.4, 107, 18.2, 22);
+            yield return new Country("Spain", "WESTERN EUROPE", 40397842, 504782, 80, 0.98, 0.99, 4.42, 22000, 97.9, 453.5, 10.06, 9.72);
+            yield return new Country("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 308.2, 2.04, -1.31, 14.35, 3700, 92.3, 61.5, 15.51, 6.52);
+            yield return new Country("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 16.5, 0.03, -0.02, 62.5, 1900, 61.1, 16.3, 34.53, 8.97);
+            yield return new Country("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 2.7, 0.24, -8.81, 23.57, 4000, 93, 184.7, 18.02, 7.27);
+            yield return new Country("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 65.5, 0, 0, 69.27, 4900, 81.6, 30.8, 27.41, 29.74);
+            yield return new Country("Sweden", "WESTERN EUROPE", 9016596, 449964, 20, 0.72, 1.67, 2.77, 26800, 99, 715, 10.27, 10.31);
+            yield return new Country("Switzerland", "WESTERN EUROPE", 7523934, 41290, 182.2, 0, 4.05, 4.39, 32700, 99, 680.9, 9.71, 8.49);
+            yield return new Country("Syria", "NEAR EAST", 18881361, 185180, 102, 0.1, 0, 29.53, 3300, 76.9, 153.8, 27.76, 4.81);
+            yield return new Country("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 640.3, 4.35, 0, 6.4, 23400, 96.1, 591, 12.56, 6.48);
+            yield return new Country("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 51.2, 0, -2.86, 110.76, 1000, 99.4, 33.5, 32.65, 8.25);
+            yield return new Country("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 39.6, 0.15, -2.06, 98.54, 600, 78.2, 4, 37.71, 16.39);
+            yield return new Country("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 125.7, 0.63, 0, 20.48, 7400, 92.6, 108.9, 13.87, 7.04);
+            yield return new Country("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 97.7, 0.1, 0, 66.61, 1500, 60.9, 10.6, 37.01, 9.83);
+            yield return new Country("Tonga", "OCEANIA", 114689, 748, 153.3, 56.02, 0, 12.62, 2200, 98.5, 97.7, 25.37, 5.28);
+            yield return new Country("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 207.9, 7.06, -10.83, 24.31, 9500, 98.6, 303.5, 12.9, 10.57);
+            yield return new Country("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 62.2, 0.7, -0.57, 24.77, 6900, 74.2, 123.6, 15.52, 5.13);
+            yield return new Country("Turkey", "NEAR EAST", 70413958, 780580, 90.2, 0.92, 0, 41.04, 6700, 86.5, 269.5, 16.62, 5.97);
+            yield return new Country("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 10.3, 0, -0.86, 73.08, 5800, 98, 74.6, 27.61, 8.6);
+            yield return new Country("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 49.2, 90.47, 11.68, 15.67, 9600, 98, 269.5, 21.84, 4.21);
+            yield return new Country("Tuvalu", "OCEANIA", 11810, 26, 454.2, 92.31, 0, 20.03, 1100,null, 59.3, 22.18, 7.11);
+            yield return new Country("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 119.5, 0, 0, 67.83, 1400, 69.9, 3.6, 47.35, 12.24);
+            yield return new Country("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 77.4, 0.46, -0.39, 20.34, 5400, 99.7, 259.9, 8.82, 14.39);
+            yield return new Country("United Arab Emirates", "NEAR EAST", 2602713, 82880, 31.4, 1.59, 1.03, 14.51, 23200, 77.9, 475.3, 18.96, 4.4);
+            yield return new Country("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 247.6, 5.08, 2.19, 5.16, 27700, 99, 543.5, 10.71, 10.13);
+            yield return new Country("United States", "NORTHERN AMERICA", 298444215, 9631420, 31, 0.21, 3.41, 6.5, 37800, 97, 898, 14.14, 8.26);
+            yield return new Country("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 19.5, 0.37, -0.32, 11.95, 12800, 98, 291.4, 13.91, 9.05);
+            yield return new Country("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 61, 0, -1.72, 71.1, 1700, 99.3, 62.9, 26.36, 7.84);
+            yield return new Country("Vanuatu", "OCEANIA", 208869, 12200, 17.1, 20.72, 0, 55.16, 2900, 53, 32.6, 22.72, 7.82);
+            yield return new Country("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 28.2, 0.31, -0.04, 22.2, 4800, 93.4, 140.1, 18.71, 4.92);
+            yield return new Country("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 256.1, 1.05, -0.45, 25.95, 2500, 90.3, 187.7, 16.86, 6.22);
+            yield return new Country("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 56.9, 9.84, -8.94, 8.03, 17200,null, 652.8, 13.96, 6.43);
+            yield return new Country("Wallis and Futuna", "OCEANIA", 16025, 274, 58.5, 47.08,null,null, 3700, 50, 118.6,null,null);
+            yield return new Country("West Bank", "NEAR EAST", 2460492, 5860, 419.9, 0, 2.98, 19.62, 800,null, 145.2, 31.67, 3.92);
+            yield return new Country("Yemen", "NEAR EAST", 21456188, 527970, 40.6, 0.36, 0, 61.5, 800, 50.2, 37.2, 42.89, 8.3);
+            yield return new Country("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 15.3, 0, 0, 88.29, 800, 80.6, 8.2, 41, 19.93);
+            yield return new Country("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 31.3, 0, 0, 67.69, 1900, 90.7, 26.8, 28.01, 21.84);
+        }
+
+        static IReadOnlyList<Country> _all;
+
+        public static IReadOnlyList<Country> All
+        {
+            get
+            {
+                if(_all == null)
+                {
+                    _all = GetCountries().ToList().AsReadOnly();
+                }
+
+                return _all;
+            }
+
+        }
+    }
+}

+ 41 - 0
samples/ControlCatalog/Models/Country.cs

@@ -0,0 +1,41 @@
+namespace ControlCatalog.Models
+{
+    public class Country
+    {
+        public string Name { get; private set; }
+        public string Region { get; private set; }
+        public int Population { get; private set; }
+        //Square Miles
+        public int Area { get; private set; }
+        //Per Square Mile
+        public double PopulationDensity { get; private set; }
+        //Coast / Area
+        public double CoastLine { get; private set; }
+        public double? NetMigration { get; private set; }
+        //per 1000 births
+        public double? InfantMortality { get; private set; }
+        public int GDP { get; private set; }
+        public double? LiteracyPercent { get; private set; }
+        //per 1000
+        public double? Phones { get; private set; }
+        public double? BirthRate { get; private set; }
+        public double? DeathRate { get; private set; }
+
+        public Country(string name, string region, int population, int area, double density, double coast, double? migration, 
+                       double? infantMorality, int gdp, double? literacy, double? phones, double? birth, double? death)
+        {
+            Name = name;
+            Region = region;
+            Population = population;
+            Area = area;
+            PopulationDensity = density;
+            CoastLine = coast;
+            NetMigration = migration;
+            InfantMortality = infantMorality;
+            GDP = gdp;
+            LiteracyPercent = literacy;
+            BirthRate = birth;
+            DeathRate = death;
+        }
+    }
+}

+ 37 - 0
samples/ControlCatalog/Models/GDPValueConverter.cs

@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace ControlCatalog.Models
+{
+    public class GDPValueConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is int gdp)
+            {
+                if (gdp <= 5000)
+                    return Brushes.Orange;
+                else if (gdp <= 10000)
+                    return Brushes.Yellow;
+                else
+                    return Brushes.LightGreen;
+            }
+
+            return value;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 98 - 0
samples/ControlCatalog/Models/Person.cs

@@ -0,0 +1,98 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Media;
+
+namespace ControlCatalog.Models
+{
+    public class Person : INotifyDataErrorInfo, INotifyPropertyChanged
+    {
+        string _firstName;
+        string _lastName;
+
+        public string FirstName
+        {
+            get => _firstName;
+            set
+            {
+                if (string.IsNullOrWhiteSpace(value))
+                    SetError(nameof(FirstName), "First Name Required");
+                else
+                    SetError(nameof(FirstName), null);
+
+                _firstName = value;
+                OnPropertyChanged(nameof(FirstName));
+            }
+
+        }
+
+        public string LastName
+        {
+            get => _lastName;
+            set
+            {
+                if (string.IsNullOrWhiteSpace(value))
+                    SetError(nameof(LastName), "Last Name Required");
+                else
+                    SetError(nameof(LastName), null);
+
+                _lastName = value;
+                OnPropertyChanged(nameof(LastName));
+            }
+        }
+
+        Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
+
+        void SetError(string propertyName, string error)
+        {
+            if (string.IsNullOrEmpty(error))
+            {
+                if (_errorLookup.Remove(propertyName))
+                    OnErrorsChanged(propertyName);
+            }
+            else
+            {
+                if (_errorLookup.TryGetValue(propertyName, out List<string> errorList))
+                {
+                    errorList.Clear();
+                    errorList.Add(error);
+                }
+                else
+                {
+                    var errors = new List<string> { error };
+                    _errorLookup.Add(propertyName, errors);
+                }
+
+                OnErrorsChanged(propertyName);
+            }
+        }
+
+        public bool HasErrors => _errorLookup.Count > 0;
+
+        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        void OnErrorsChanged(string propertyName)
+        {
+            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+        }
+        void OnPropertyChanged(string propertyName)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        public IEnumerable GetErrors(string propertyName)
+        {
+            if (_errorLookup.TryGetValue(propertyName, out List<string> errorList))
+                return errorList;
+            else
+                return null;
+        }
+    }
+}

+ 9 - 9
samples/ControlCatalog/Pages/CarouselPage.xaml

@@ -24,19 +24,19 @@
 
     <StackPanel Orientation="Horizontal" Spacing="4">
       <TextBlock VerticalAlignment="Center">Transition</TextBlock>
-      <DropDown Name="transition" SelectedIndex="1" VerticalAlignment="Center">
-        <DropDownItem>None</DropDownItem>
-        <DropDownItem>Slide</DropDownItem>
-        <DropDownItem>Crossfade</DropDownItem>
-      </DropDown>
+      <ComboBox Name="transition" SelectedIndex="1" VerticalAlignment="Center">
+        <ComboBoxItem>None</ComboBoxItem>
+        <ComboBoxItem>Slide</ComboBoxItem>
+        <ComboBoxItem>Crossfade</ComboBoxItem>
+      </ComboBox>
     </StackPanel>
 
     <StackPanel Orientation="Horizontal" Spacing="4">
       <TextBlock VerticalAlignment="Center">Orientation</TextBlock>
-      <DropDown Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
-        <DropDownItem>Horizontal</DropDownItem>
-        <DropDownItem>Vertical</DropDownItem>
-      </DropDown>
+      <ComboBox Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
+        <ComboBoxItem>Horizontal</ComboBoxItem>
+        <ComboBoxItem>Vertical</ComboBoxItem>
+      </ComboBox>
     </StackPanel>
     
   </StackPanel>

+ 4 - 4
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@@ -10,8 +10,8 @@ namespace ControlCatalog.Pages
         private Carousel _carousel;
         private Button _left;
         private Button _right;
-        private DropDown _transition;
-        private DropDown _orientation;
+        private ComboBox _transition;
+        private ComboBox _orientation;
 
         public CarouselPage()
         {
@@ -28,8 +28,8 @@ namespace ControlCatalog.Pages
             _carousel = this.FindControl<Carousel>("carousel");
             _left = this.FindControl<Button>("left");
             _right = this.FindControl<Button>("right");
-            _transition = this.FindControl<DropDown>("transition");
-            _orientation = this.FindControl<DropDown>("orientation");
+            _transition = this.FindControl<ComboBox>("transition");
+            _orientation = this.FindControl<ComboBox>("orientation");
         }
 
         private void TransitionChanged(object sender, SelectionChangedEventArgs e)

+ 41 - 0
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@@ -0,0 +1,41 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.ComboBoxPage">
+  <StackPanel Orientation="Vertical" Spacing="4">
+    <TextBlock Classes="h1">ComboBox</TextBlock>
+    <TextBlock Classes="h2">A drop-down list.</TextBlock>
+
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
+      <ComboBox SelectedIndex="0">
+        <ComboBoxItem>Inline Items</ComboBoxItem>
+        <ComboBoxItem>Inline Item 2</ComboBoxItem>
+        <ComboBoxItem>Inline Item 3</ComboBoxItem>
+        <ComboBoxItem>Inline Item 4</ComboBoxItem>
+      </ComboBox>
+
+      <ComboBox SelectedIndex="0">
+        <ComboBoxItem>
+          <Panel>
+            <Rectangle Fill="{DynamicResource ThemeAccentBrush}"/>
+            <TextBlock Margin="8">Control Items</TextBlock>
+          </Panel>
+        </ComboBoxItem>
+        <ComboBoxItem>
+          <Ellipse Width="50" Height="50" Fill="Yellow"/>
+        </ComboBoxItem>
+        <ComboBoxItem>
+          <TextBox Text="TextBox"/>
+        </ComboBoxItem>
+      </ComboBox>
+
+       <ComboBox x:Name="fontComboBox"  SelectedIndex="0">
+         <ComboBox.ItemTemplate>
+           <DataTemplate>
+             <TextBlock Text="{Binding Name}" FontFamily="{Binding}" />
+           </DataTemplate>
+         </ComboBox.ItemTemplate>
+       </ComboBox>
+    </StackPanel>
+
+  </StackPanel>
+</UserControl>

+ 5 - 5
samples/ControlCatalog/Pages/DropDownPage.xaml.cs → samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@@ -3,9 +3,9 @@ using Avalonia.Markup.Xaml;
 
 namespace ControlCatalog.Pages
 {
-    public class DropDownPage : UserControl
+    public class ComboBoxPage : UserControl
     {
-        public DropDownPage()
+        public ComboBoxPage()
         {
             this.InitializeComponent();
         }
@@ -13,9 +13,9 @@ namespace ControlCatalog.Pages
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
-            var fontDropDown = this.Find<DropDown>("fontDropDown");
-            fontDropDown.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
-            fontDropDown.SelectedIndex = 0;
+            var fontComboBox = this.Find<ComboBox>("fontComboBox");
+            fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
+            fontComboBox.SelectedIndex = 0;
         }
     }
 }

+ 55 - 0
samples/ControlCatalog/Pages/DataGridPage.xaml

@@ -0,0 +1,55 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:local="clr-namespace:ControlCatalog.Models;assembly=ControlCatalog"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.DataGridPage">
+  <UserControl.Resources>
+    <local:GDPValueConverter x:Key="GDPConverter" />
+  </UserControl.Resources>
+  <UserControl.Styles>
+    <Style Selector="DataGridCell.gdp">
+      <Setter Property="FontWeight" Value="Bold" />
+      <Setter Property="Background" Value="{Binding Path=GDP, Mode=OneWay, Converter={StaticResource GDPConverter}}" />
+    </Style>
+  </UserControl.Styles>
+  <Grid RowDefinitions="Auto,Auto">
+    <StackPanel Orientation="Vertical" Spacing="4" Grid.Row="0">
+      <TextBlock Classes="h1">DataGrid</TextBlock>
+      <TextBlock Classes="h2">A control for displaying and interacting with a data source.</TextBlock>
+    </StackPanel>
+    <TabControl Grid.Row="1">
+      <TabItem Header="DataGrid">
+        <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True">
+          <DataGrid.Columns>
+            <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
+            <DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" />
+            <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
+            <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
+            <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" />
+          </DataGrid.Columns>
+        </DataGrid>
+      </TabItem>
+      <TabItem Header="Grouping">
+        <DataGrid Name="dataGridGrouping" Margin="12">
+          <DataGrid.Columns>
+            <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
+            <DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" />
+            <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
+            <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
+            <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" />
+          </DataGrid.Columns>
+        </DataGrid>
+      </TabItem>
+      <TabItem Header="Editable">
+        <Grid RowDefinitions="*,Auto">
+          <DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0">
+            <DataGrid.Columns>
+              <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" />
+              <DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="*" />
+            </DataGrid.Columns>
+          </DataGrid>
+          <Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />
+        </Grid>
+      </TabItem>
+    </TabControl>
+  </Grid>
+</UserControl>

+ 52 - 0
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.Models;
+using Avalonia.Collections;
+
+namespace ControlCatalog.Pages
+{
+    public class DataGridPage : UserControl
+    {
+        public DataGridPage()
+        {
+            this.InitializeComponent();
+            var dg1 = this.FindControl<DataGrid>("dataGrid1");
+            dg1.IsReadOnly = true;
+
+            var collectionView1 = new DataGridCollectionView(Countries.All);
+            //collectionView.GroupDescriptions.Add(new PathGroupDescription("Region"));
+
+            dg1.Items = collectionView1;
+
+            var dg2 = this.FindControl<DataGrid>("dataGridGrouping");
+            dg2.IsReadOnly = true;
+
+            var collectionView2 = new DataGridCollectionView(Countries.All);
+            collectionView2.GroupDescriptions.Add(new DataGridPathGroupDescription("Region"));
+
+            dg2.Items = collectionView2;
+
+            var dg3 = this.FindControl<DataGrid>("dataGridEdit");
+            dg3.IsReadOnly = false;
+
+            var items = new List<Person>
+            {
+                new Person { FirstName = "John", LastName = "Doe" },
+                new Person { FirstName = "Elizabeth", LastName = "Thomas" },
+                new Person { FirstName = "Zack", LastName = "Ward" }
+            };
+            var collectionView3 = new DataGridCollectionView(items);
+
+            dg3.Items = collectionView3;
+
+            var addButton = this.FindControl<Button>("btnAdd");
+            addButton.Click += (a, b) => collectionView3.AddNew();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 1 - 0
samples/ControlCatalog/Pages/DialogsPage.xaml

@@ -2,6 +2,7 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.DialogsPage">
   <StackPanel Orientation="Vertical" Spacing="4" Margin="4">
+      <CheckBox Name="UseFilters">Use filters</CheckBox>
       <Button Name="OpenFile">Open File</Button>
       <Button Name="SaveFile">Save File</Button>
       <Button Name="SelectFolder">Select Folder</Button>

+ 24 - 2
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 #pragma warning disable 4014
@@ -9,18 +10,39 @@ namespace ControlCatalog.Pages
         public DialogsPage()
         {
             this.InitializeComponent();
+
+            List<FileDialogFilter> GetFilters()
+            {
+                if (this.FindControl<CheckBox>("UseFilters").IsChecked != true)
+                    return null;
+                return  new List<FileDialogFilter>
+                {
+                    new FileDialogFilter
+                    {
+                        Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
+                    },
+                    new FileDialogFilter
+                    {
+                        Name = "All files",
+                        Extensions = new List<string> {"*"}
+                    }
+                };
+            }
+
             this.FindControl<Button>("OpenFile").Click += delegate
             {
                 new OpenFileDialog()
                 {
-                    Title = "Open file"
+                    Title = "Open file",
+                    Filters = GetFilters()
                 }.ShowAsync(GetWindow());
             };
             this.FindControl<Button>("SaveFile").Click += delegate
             {
                 new SaveFileDialog()
                 {
-                    Title = "Save file"
+                    Title = "Save file",
+                    Filters = GetFilters()
                 }.ShowAsync(GetWindow());
             };
             this.FindControl<Button>("SelectFolder").Click += delegate

+ 0 - 42
samples/ControlCatalog/Pages/DropDownPage.xaml

@@ -1,42 +0,0 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="ControlCatalog.Pages.DropDownPage">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock Classes="h1">DropDown</TextBlock>
-    <TextBlock Classes="h2">A drop-down list.</TextBlock>
-
-    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
-      <DropDown SelectedIndex="0">
-        <DropDownItem>Inline Items</DropDownItem>
-        <DropDownItem>Inline Item 2</DropDownItem>
-        <DropDownItem>Inline Item 3</DropDownItem>
-        <DropDownItem>Inline Item 4</DropDownItem>
-      </DropDown>
-
-      <DropDown SelectedIndex="0">
-        <DropDownItem>
-          <Panel>
-            <Rectangle Fill="{DynamicResource ThemeAccentBrush}"/>
-            <TextBlock Margin="8">Control Items</TextBlock>
-          </Panel>
-        </DropDownItem>
-        <DropDownItem>
-          <Ellipse Width="50" Height="50" Fill="Yellow"/>
-        </DropDownItem>
-        <DropDownItem>
-          <TextBox Text="TextBox"/>
-        </DropDownItem>
-      </DropDown>
-
-
-       <DropDown x:Name="fontDropDown"  SelectedIndex="0">
-            <DropDown.ItemTemplate>
-                <DataTemplate>
-                    <TextBlock Text="{Binding Name}" FontFamily="{Binding}" />
-                </DataTemplate>
-            </DropDown.ItemTemplate>
-       </DropDown>
-    </StackPanel>
-
-  </StackPanel>
-</UserControl>

+ 6 - 6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -23,9 +23,9 @@
       </Grid>
       <Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
         <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
-        <DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
+        <ComboBox Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
                   VerticalAlignment="Center" Margin="2">
-          <DropDown.ItemTemplate>
+          <ComboBox.ItemTemplate>
             <DataTemplate>
               <StackPanel Orientation="Horizontal" Spacing="2">
                 <TextBlock Text="{Binding Name}"/>
@@ -33,15 +33,15 @@
                 <TextBlock Text="{Binding Value}"/>
               </StackPanel>
             </DataTemplate>
-          </DropDown.ItemTemplate>
-        </DropDown>
+          </ComboBox.ItemTemplate>
+        </ComboBox>
 
         <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
-        <DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
+        <ComboBox Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
                   VerticalAlignment="Center" Margin="2"/>
 
         <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
-        <DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
+        <ComboBox Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
                   VerticalAlignment="Center" Margin="2"/>
 
         <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>

+ 1 - 1
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -23,7 +23,7 @@ namespace ControlCatalog.Pages
         {
             base.Render(context);
             Window w = (Window)VisualRoot;
-            Screen[] screens = w.Screens.All;
+            var screens = w.Screens.All;
             var scaling = ((IRenderRoot)w).RenderScaling;
 
             Pen p = new Pen(Brushes.Black);

+ 6 - 6
samples/ControlCatalog/Pages/TabControlPage.xaml

@@ -88,12 +88,12 @@
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center">
                 <TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
-                <DropDown SelectedIndex="{Binding TabPlacement, Mode=TwoWay}">
-                    <DropDownItem>Left</DropDownItem>
-                    <DropDownItem>Bottom</DropDownItem>
-                    <DropDownItem>Right</DropDownItem>
-                    <DropDownItem>Top</DropDownItem>
-                </DropDown>
+                <ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}">
+                    <ComboBoxItem>Left</ComboBoxItem>
+                    <ComboBoxItem>Bottom</ComboBoxItem>
+                    <ComboBoxItem>Right</ComboBoxItem>
+                    <ComboBoxItem>Top</ComboBoxItem>
+                </ComboBox>
             </StackPanel>
         </Grid>
     </DockPanel>

+ 5 - 5
samples/VirtualizationDemo/MainWindow.xaml

@@ -7,9 +7,9 @@
                     Margin="16 0 0 0" 
                     MinWidth="150"
                     Spacing="4">
-            <DropDown Items="{Binding VirtualizationModes}"
+            <ComboBox Items="{Binding VirtualizationModes}"
                       SelectedItem="{Binding VirtualizationMode}"/>
-            <DropDown Items="{Binding Orientations}"
+            <ComboBox Items="{Binding Orientations}"
                       SelectedItem="{Binding Orientation}"/>
             <TextBox Watermark="Item Count"
                      UseFloatingWatermark="True"
@@ -24,10 +24,10 @@
                      UseFloatingWatermark="True"
                      Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
             <TextBlock>Horiz. ScrollBar</TextBlock>
-            <DropDown Items="{Binding ScrollBarVisibilities}"
+            <ComboBox Items="{Binding ScrollBarVisibilities}"
                       SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
             <TextBlock>Vert. ScrollBar</TextBlock>
-            <DropDown Items="{Binding ScrollBarVisibilities}"
+            <ComboBox Items="{Binding ScrollBarVisibilities}"
                       SelectedItem="{Binding VerticalScrollBarVisibility}"/>
             <TextBox Watermark="Item to Create"
                      UseFloatingWatermark="True"
@@ -58,4 +58,4 @@
             </ListBox.ItemTemplate>
         </ListBox>
     </DockPanel>
-</Window>
+</Window>

+ 20 - 0
src/Avalonia.Controls.DataGrid/AppBuilderExtensions.cs

@@ -0,0 +1,20 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Controls;
+using Avalonia.Threading;
+
+namespace Avalonia
+{
+    public static class AppBuilderExtensions
+    {
+        public static TAppBuilder UseDataGrid<TAppBuilder>(this TAppBuilder builder)
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
+        {
+            // Portable.Xaml doesn't correctly load referenced assemblies and so doesn't
+            // find `DataGrid` when loading XAML. Call this method from AppBuilder as a
+            // temporary workaround until we fix XAML.
+            return builder;
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
+    <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\Avalonia.Input\Avalonia.Input.csproj" />
+    <ProjectReference Include="..\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
+    <ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
+    <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
+    <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
+    <ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />    
+    <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
+  </ItemGroup>
+  <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\EmbedXaml.props" />
+  <Import Project="..\..\build\JetBrains.Annotations.props" />
+</Project>

+ 4315 - 0
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -0,0 +1,4315 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Utils;
+using Avalonia.Utilities;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace Avalonia.Collections
+{
+    /// <summary>
+    /// Event argument used for page index change notifications. The requested page move
+    /// can be canceled by setting e.Cancel to True.
+    /// </summary>
+    public sealed class PageChangingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Constructor that takes the target page index
+        /// </summary>
+        /// <param name="newPageIndex">Index of the requested page</param>
+        public PageChangingEventArgs(int newPageIndex)
+        {
+            NewPageIndex = newPageIndex;
+        }
+
+        /// <summary>
+        /// Gets the index of the requested page
+        /// </summary>
+        public int NewPageIndex
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>Defines a method that enables a collection to provide a custom view for specialized sorting, filtering, grouping, and currency.</summary>
+    internal interface IDataGridCollectionViewFactory
+    {
+        /// <summary>Returns a custom view for specialized sorting, filtering, grouping, and currency.</summary>
+        /// <returns>A custom view for specialized sorting, filtering, grouping, and currency.</returns>
+        IDataGridCollectionView CreateView();
+    }
+
+    /// <summary>
+    /// DataGrid-readable view over an IEnumerable.
+    /// </summary>
+    public sealed class DataGridCollectionView : IDataGridCollectionView, IDataGridEditableCollectionView, INotifyPropertyChanged 
+    {
+        /// <summary>
+        /// Since there's nothing in the un-cancelable event args that is mutable,
+        /// just create one instance to be used universally.
+        /// </summary>
+        private static readonly DataGridCurrentChangingEventArgs uncancelableCurrentChangingEventArgs = new DataGridCurrentChangingEventArgs(false);
+
+        /// <summary>
+        /// Value that we cache for the PageIndex if we are in a DeferRefresh,
+        /// and the user has attempted to move to a different page.
+        /// </summary>
+        private int _cachedPageIndex = -1;
+
+        /// <summary>
+        /// Value that we cache for the PageSize if we are in a DeferRefresh,
+        /// and the user has attempted to change the PageSize.
+        /// </summary>
+        private int _cachedPageSize;
+
+        /// <summary>
+        /// CultureInfo used in this DataGridCollectionView
+        /// </summary>
+        private CultureInfo _culture;
+
+        /// <summary>
+        /// Private accessor for the Monitor we use to prevent recursion
+        /// </summary>
+        private SimpleMonitor _currentChangedMonitor = new SimpleMonitor();
+
+        /// <summary>
+        /// Private accessor for the CurrentItem
+        /// </summary>
+        private object _currentItem;
+
+        /// <summary>
+        /// Private accessor for the CurrentPosition
+        /// </summary>
+        private int _currentPosition;
+
+        /// <summary>
+        /// The number of requests to defer Refresh()
+        /// </summary>
+        private int _deferLevel;
+
+        /// <summary>
+        /// The item we are currently editing
+        /// </summary>
+        private object _editItem;
+
+        /// <summary>
+        /// Private accessor for the Filter
+        /// </summary>
+        private Func<object, bool> _filter;
+
+        /// <summary>
+        /// Private accessor for the CollectionViewFlags
+        /// </summary>
+        private CollectionViewFlags _flags = CollectionViewFlags.ShouldProcessCollectionChanged;
+
+        /// <summary>
+        /// Private accessor for the Grouping data
+        /// </summary>
+        private CollectionViewGroupRoot _group;
+
+        /// <summary>
+        /// Private accessor for the InternalList
+        /// </summary>
+        private IList _internalList;
+
+        /// <summary>
+        /// Keeps track of whether groups have been applied to the
+        /// collection already or not. Note that this can still be set
+        /// to false even though we specify a GroupDescription, as the 
+        /// collection may not have gone through the PrepareGroups function.
+        /// </summary>
+        private bool _isGrouping;
+
+        /// <summary>
+        /// Private accessor for indicating whether we want to point to the temporary grouping data for calculations
+        /// </summary>
+        private bool _isUsingTemporaryGroup;
+
+        /// <summary>
+        /// ConstructorInfo obtained from reflection for generating new items
+        /// </summary>
+        private ConstructorInfo _itemConstructor;
+
+        /// <summary>
+        /// Whether we have the correct ConstructorInfo information for the ItemConstructor
+        /// </summary>
+        private bool _itemConstructorIsValid;
+
+        /// <summary>
+        /// The new item we are getting ready to add to the collection
+        /// </summary>
+        private object _newItem;
+
+        /// <summary>
+        /// Private accessor for the PageIndex
+        /// </summary>
+        private int _pageIndex = -1;
+
+        /// <summary>
+        /// Private accessor for the PageSize
+        /// </summary>
+        private int _pageSize;
+
+        /// <summary>
+        /// Whether the source needs to poll for changes
+        /// (if it did not implement INotifyCollectionChanged)
+        /// </summary>
+        private bool _pollForChanges;
+
+        /// <summary>
+        /// Private accessor for the SortDescriptions
+        /// </summary>
+        private DataGridSortDescriptionCollection _sortDescriptions;
+
+        /// <summary>
+        /// Private accessor for the SourceCollection
+        /// </summary>
+        private IEnumerable _sourceCollection;
+
+        /// <summary>
+        /// Private accessor for the Grouping data on the entire collection
+        /// </summary>
+        private CollectionViewGroupRoot _temporaryGroup;
+
+        /// <summary>
+        /// Timestamp used to see if there was a collection change while 
+        /// processing enumerator changes
+        /// </summary>
+        private int _timestamp;
+
+        /// <summary>
+        /// Private accessor for the TrackingEnumerator
+        /// </summary>
+        private IEnumerator _trackingEnumerator;
+
+        /// <summary>
+        /// Helper constructor that sets default values for isDataSorted and isDataInGroupOrder.
+        /// </summary>
+        /// <param name="source">The source for the collection</param>
+        public DataGridCollectionView(IEnumerable source)
+            : this(source, false /*isDataSorted*/, false /*isDataInGroupOrder*/)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the DataGridCollectionView class.
+        /// </summary>
+        /// <param name="source">The source for the collection</param>
+        /// <param name="isDataSorted">Determines whether the source is already sorted</param>
+        /// <param name="isDataInGroupOrder">Whether the source is already in the correct order for grouping</param>
+        public DataGridCollectionView(IEnumerable source, bool isDataSorted, bool isDataInGroupOrder)
+        {
+            _sourceCollection = source ?? throw new ArgumentNullException(nameof(source));
+
+            SetFlag(CollectionViewFlags.IsDataSorted, isDataSorted);
+            SetFlag(CollectionViewFlags.IsDataInGroupOrder, isDataInGroupOrder);
+
+            _temporaryGroup = new CollectionViewGroupRoot(this, isDataInGroupOrder);
+            _group = new CollectionViewGroupRoot(this, false);
+            _group.GroupDescriptionChanged += OnGroupDescriptionChanged;
+            _group.GroupDescriptions.CollectionChanged += OnGroupByChanged;
+
+            CopySourceToInternalList();
+            _trackingEnumerator = source.GetEnumerator();
+
+            // set currency
+            if (_internalList.Count > 0)
+            {
+                SetCurrent(_internalList[0], 0, 1);
+            }
+            else
+            {
+                SetCurrent(null, -1, 0);
+            }
+
+            // Set flag for whether the collection is empty
+            SetFlag(CollectionViewFlags.CachedIsEmpty, Count == 0);
+
+            // If we implement INotifyCollectionChanged
+            if (source is INotifyCollectionChanged coll)
+            {
+                coll.CollectionChanged += (_, args) => ProcessCollectionChanged(args);
+            }
+            else
+            {
+                // If the source doesn't raise collection change events, try to
+                // detect changes by polling the enumerator
+                _pollForChanges = true;
+            }
+        }
+
+        /// <summary>
+        /// Raise this event when the (filtered) view changes
+        /// </summary>
+        public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+        /// <summary>
+        /// CollectionChanged event (per INotifyCollectionChanged).
+        /// </summary>
+        event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
+        {
+            add { CollectionChanged += value; }
+            remove { CollectionChanged -= value; }
+        }
+
+        /// <summary>
+        /// Raised when the CurrentItem property changed
+        /// </summary>
+        public event EventHandler CurrentChanged;
+
+        /// <summary>
+        /// Raised when the CurrentItem property is changing
+        /// </summary>
+        public event EventHandler<DataGridCurrentChangingEventArgs> CurrentChanging;
+
+        /// <summary>
+        /// Raised when a page index change completed
+        /// </summary>
+        //TODO Paging
+        public event EventHandler<EventArgs> PageChanged;
+
+        /// <summary>
+        /// Raised when a page index change is requested
+        /// </summary>
+        //TODO Paging
+        public event EventHandler<PageChangingEventArgs> PageChanging;
+
+        /// <summary>
+        /// PropertyChanged event.
+        /// </summary>
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        /// <summary>
+        /// PropertyChanged event (per INotifyPropertyChanged)
+        /// </summary>
+        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+        {
+            add { PropertyChanged += value; }
+            remove { PropertyChanged -= value; }
+        }
+
+        /// <summary>
+        /// Enum for CollectionViewFlags
+        /// </summary>
+        //TODO Paging
+        [Flags]
+        private enum CollectionViewFlags
+        {
+            /// <summary>
+            /// Whether the list of items (after applying the sort and filters, if any) 
+            /// is already in the correct order for grouping. 
+            /// </summary>
+            IsDataInGroupOrder = 0x01,
+
+            /// <summary>
+            /// Whether the source collection is already sorted according to the SortDescriptions collection
+            /// </summary>
+            IsDataSorted = 0x02,
+
+            /// <summary>
+            /// Whether we should process the collection changed event
+            /// </summary>
+            ShouldProcessCollectionChanged = 0x04,
+
+            /// <summary>
+            /// Whether the current item is before the first
+            /// </summary>
+            IsCurrentBeforeFirst = 0x08,
+
+            /// <summary>
+            /// Whether the current item is after the last
+            /// </summary>
+            IsCurrentAfterLast = 0x10,
+
+            /// <summary>
+            /// Whether we need to refresh
+            /// </summary>
+            NeedsRefresh = 0x20,
+
+            /// <summary>
+            /// Whether we cache the IsEmpty value
+            /// </summary>
+            CachedIsEmpty = 0x40,
+
+            /// <summary>
+            /// Indicates whether a page index change is in process or not
+            /// </summary>
+            IsPageChanging = 0x80,
+
+            /// <summary>
+            /// Whether we need to move to another page after EndDefer
+            /// </summary>
+            IsMoveToPageDeferred = 0x100,
+
+            /// <summary>
+            /// Whether we need to update the PageSize after EndDefer
+            /// </summary>
+            IsUpdatePageSizeDeferred = 0x200
+        }
+
+        private Type _itemType;
+        private Type ItemType
+        {
+            get
+            {
+                if (_itemType == null)
+                    _itemType = GetItemType(true);
+
+                return _itemType;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the view supports AddNew.
+        /// </summary>
+        public bool CanAddNew
+        {
+            get
+            {
+                return !IsEditingItem &&
+                    (SourceList != null && !SourceList.IsFixedSize && CanConstructItem);
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the view supports the notion of "pending changes" 
+        /// on the current edit item.  This may vary, depending on the view and the particular
+        /// item.  For example, a view might return true if the current edit item
+        /// implements IEditableObject, or if the view has special knowledge about 
+        /// the item that it can use to support rollback of pending changes.
+        /// </summary>
+        public bool CanCancelEdit
+        {
+            get { return _editItem is IEditableObject; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the PageIndex value is allowed to change or not.
+        /// </summary>
+        //TODO Paging
+        public bool CanChangePage
+        {
+            get { return true; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether we support filtering with this ICollectionView.
+        /// </summary>
+        public bool CanFilter
+        {
+            get { return true; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this view supports grouping.
+        /// When this returns false, the rest of the interface is ignored.
+        /// </summary>
+        public bool CanGroup
+        {
+            get { return true; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the view supports Remove and RemoveAt.
+        /// </summary>
+        public bool CanRemove
+        {
+            get
+            {
+                return !IsEditingItem && !IsAddingNew &&
+                    (SourceList != null && !SourceList.IsFixedSize);
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether we support sorting with this ICollectionView.
+        /// </summary>
+        public bool CanSort
+        {
+            get { return true; }
+        }
+
+        /// <summary>
+        /// Gets the number of records in the view after 
+        /// filtering, sorting, and paging.
+        /// </summary>
+        //TODO Paging
+        public int Count
+        {
+            get
+            {
+                EnsureCollectionInSync();
+                VerifyRefreshNotDeferred();
+
+                // if we have paging
+                if (PageSize > 0 && PageIndex > -1)
+                {
+                    if (IsGrouping && !_isUsingTemporaryGroup)
+                    {
+                        return _group.ItemCount;
+                    }
+                    else
+                    {
+                        return Math.Max(0, Math.Min(PageSize, InternalCount - (_pageSize * PageIndex)));
+                    }
+                }
+                else
+                {
+                    if (IsGrouping)
+                    {
+                        if (_isUsingTemporaryGroup)
+                        {
+                            return _temporaryGroup.ItemCount;
+                        }
+                        else
+                        {
+                            return _group.ItemCount;
+                        }
+                    }
+                    else
+                    {
+                        return InternalCount;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets Culture to use during sorting.
+        /// </summary>
+        public CultureInfo Culture
+        {
+            get
+            {
+                return _culture;
+            }
+
+            set
+            {
+                if (value == null)
+                {
+                    throw new ArgumentNullException(nameof(value));
+                }
+
+                if (_culture != value)
+                {
+                    _culture = value;
+                    OnPropertyChanged(nameof(Culture));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the new item when an AddNew transaction is in progress
+        /// Otherwise it returns null.
+        /// </summary>
+        public object CurrentAddItem
+        {
+            get
+            {
+                return _newItem;
+            }
+
+            private set
+            {
+                if (_newItem != value)
+                {
+                    Debug.Assert(value == null || _newItem == null, "Old and new _newItem values are unexpectedly non null");
+                    _newItem = value;
+                    OnPropertyChanged(nameof(IsAddingNew));
+                    OnPropertyChanged(nameof(CurrentAddItem));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the affected item when an EditItem transaction is in progress
+        /// Otherwise it returns null.
+        /// </summary>
+        public object CurrentEditItem
+        {
+            get
+            {
+                return _editItem;
+            }
+
+            private set
+            {
+                if (_editItem != value)
+                {
+                    Debug.Assert(value == null || _editItem == null, "Old and new _editItem values are unexpectedly non null");
+                    bool oldCanCancelEdit = CanCancelEdit;
+                    _editItem = value;
+                    OnPropertyChanged(nameof(IsEditingItem));
+                    OnPropertyChanged(nameof(CurrentEditItem));
+                    if (oldCanCancelEdit != CanCancelEdit)
+                    {
+                        OnPropertyChanged(nameof(CanCancelEdit));
+                    }
+                }
+            }
+        }
+
+        /// <summary> 
+        /// Gets the "current item" for this view 
+        /// </summary>
+        public object CurrentItem
+        {
+            get
+            {
+                VerifyRefreshNotDeferred();
+                return _currentItem;
+            }
+        }
+
+        /// <summary>
+        /// Gets the ordinal position of the CurrentItem within the 
+        /// (optionally sorted and filtered) view.
+        /// </summary>
+        public int CurrentPosition
+        {
+            get
+            {
+                VerifyRefreshNotDeferred();
+                return _currentPosition;
+            }
+        }
+
+        private string GetOperationNotAllowedDuringAddOrEditText(string action)
+        {
+            return $"'{action}' is not allowed during an AddNew or EditItem transaction.";
+        }
+        private string GetOperationNotAllowedText(string action, string transaction = null)
+        {
+            if (String.IsNullOrWhiteSpace(transaction))
+            {
+                return $"'{action}' is not allowed for this view.";
+            }
+            else
+            {
+                return $"'{action}' is not allowed during a transaction started by '{transaction}'.";
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the Filter, which is a callback set by the consumer of the ICollectionView
+        /// and used by the implementation of the ICollectionView to determine if an
+        /// item is suitable for inclusion in the view.
+        /// </summary>        
+        /// <exception cref="NotSupportedException">
+        /// Simpler implementations do not support filtering and will throw a NotSupportedException.
+        /// Use <seealso cref="CanFilter"/> property to test if filtering is supported before
+        /// assigning a non-null value.
+        /// </exception>
+        public Func<object, bool> Filter
+        {
+            get
+            {
+                return _filter;
+            }
+
+            set
+            {
+                if (IsAddingNew || IsEditingItem)
+                {
+                    throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Filter)));
+                }
+
+                if (!CanFilter)
+                {
+                    throw new NotSupportedException("The Filter property cannot be set when the CanFilter property returns false.");
+                }
+
+                if (_filter != value)
+                {
+                    _filter = value;
+                    RefreshOrDefer();
+                    OnPropertyChanged(nameof(Filter));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the description of grouping, indexed by level.
+        /// </summary>
+        public AvaloniaList<DataGridGroupDescription> GroupDescriptions
+        {
+            get
+            {
+                return _group?.GroupDescriptions;
+            }
+        }
+
+        int IDataGridCollectionView.GroupingDepth => GroupDescriptions?.Count ?? 0;
+        string IDataGridCollectionView.GetGroupingPropertyNameAtDepth(int level)
+        {
+            var groups = GroupDescriptions;
+            if(groups != null && level >= 0 && level < groups.Count)
+            {
+                return groups[level].PropertyName;
+            }
+            else
+            {
+                return String.Empty;
+            }
+        }
+
+        /// <summary>
+        /// Gets the top-level groups, constructed according to the descriptions
+        /// given in GroupDescriptions.
+        /// </summary>
+        public IAvaloniaReadOnlyList<object> Groups
+        {
+            get
+            {
+                if (!IsGrouping)
+                {
+                    return null;
+                }
+
+                return RootGroup?.Items;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether an "AddNew" transaction is in progress.
+        /// </summary>
+        public bool IsAddingNew
+        {
+            get { return _newItem != null; }
+        }
+
+        /// <summary> 
+        /// Gets a value indicating whether currency is beyond the end (End-Of-File). 
+        /// </summary>
+        /// <returns>Whether IsCurrentAfterLast</returns>
+        public bool IsCurrentAfterLast
+        {
+            get
+            {
+                VerifyRefreshNotDeferred();
+                return CheckFlag(CollectionViewFlags.IsCurrentAfterLast);
+            }
+        }
+
+        /// <summary> 
+        /// Gets a value indicating whether currency is before the beginning (Beginning-Of-File). 
+        /// </summary>
+        /// <returns>Whether IsCurrentBeforeFirst</returns>
+        public bool IsCurrentBeforeFirst
+        {
+            get
+            {
+                VerifyRefreshNotDeferred();
+                return CheckFlag(CollectionViewFlags.IsCurrentBeforeFirst);
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether an EditItem transaction is in progress.
+        /// </summary>
+        public bool IsEditingItem
+        {
+            get { return _editItem != null; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the resulting (filtered) view is empty.
+        /// </summary>
+        public bool IsEmpty
+        {
+            get
+            {
+                EnsureCollectionInSync();
+                return InternalCount == 0;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether a page index change is in process or not.
+        /// </summary>
+        //TODO Paging
+        public bool IsPageChanging
+        {
+            get
+            {
+                return CheckFlag(CollectionViewFlags.IsPageChanging);
+            }
+
+            private set
+            {
+                if (CheckFlag(CollectionViewFlags.IsPageChanging) != value)
+                {
+                    SetFlag(CollectionViewFlags.IsPageChanging, value);
+                    OnPropertyChanged(nameof(IsPageChanging));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the minimum number of items known to be in the source collection
+        /// that verify the current filter if any
+        /// </summary>
+        public int ItemCount
+        {
+            get
+            {
+                return InternalList.Count;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this view needs to be refreshed.
+        /// </summary>
+        public bool NeedsRefresh
+        {
+            get { return CheckFlag(CollectionViewFlags.NeedsRefresh); }
+        }
+
+        /// <summary>
+        /// Gets the current page we are on. (zero based)
+        /// </summary>
+        //TODO Paging
+        public int PageIndex
+        {
+            get
+            {
+                return _pageIndex;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the number of items to display on a page. If the
+        /// PageSize = 0, then we are not paging, and will display all items
+        /// in the collection. Otherwise, we will have separate pages for 
+        /// the items to display.
+        /// </summary>
+        //TODO Paging
+        public int PageSize
+        {
+            get
+            {
+                return _pageSize;
+            }
+            set
+            {
+                if (value < 0)
+                {
+                    throw new ArgumentOutOfRangeException("PageSize cannot have a negative value.");
+                }
+
+                // if the Refresh is currently deferred, cache the desired PageSize
+                // and set the flag so that once the defer is over, we can then
+                // update the PageSize.
+                if (IsRefreshDeferred)
+                {
+                    // set cached value and flag so that we update the PageSize on EndDefer
+                    _cachedPageSize = value;
+                    SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, true);
+                    return;
+                }
+
+                // to see whether or not to fire an OnPropertyChanged
+                int oldCount = Count;
+
+                if (_pageSize != value)
+                {
+                    // Remember current currency values for upcoming OnPropertyChanged notifications
+                    object oldCurrentItem = CurrentItem;
+                    int oldCurrentPosition = CurrentPosition;
+                    bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                    bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                    // Check if there is a current edited or new item so changes can be committed first.
+                    if (CurrentAddItem != null || CurrentEditItem != null)
+                    {
+                        // Check with the ICollectionView.CurrentChanging listeners if it's OK to
+                        // change the currency. If not, then we can't fire the event to allow them to
+                        // commit their changes. So, we will not be able to change the PageSize.
+                        if (!OkToChangeCurrent())
+                        {
+                            throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction.");
+                        }
+
+                        // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or 
+                        // cancelling an item that is no longer on the current page. That's acceptable and means that
+                        // the potential _newItem or _editItem needs to be committed before this PageSize change.
+                        // The reason why we temporarily reset currency here is to give a chance to the bound
+                        // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit()
+                        // for example as a result of changing currency.
+                        SetCurrentToPosition(-1);
+                        RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+                        // If the bound controls did not successfully end their potential item editing/addition, we 
+                        // need to throw an exception to show that the PageSize change failed. 
+                        if (CurrentAddItem != null || CurrentEditItem != null)
+                        {
+                            throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction.");
+                        }
+                    }
+
+                    _pageSize = value;
+                    OnPropertyChanged(nameof(PageSize));
+
+                    if (_pageSize == 0)
+                    {
+                        // update the groups for the current page
+                        //***************************************
+                        PrepareGroups();
+
+                        // if we are not paging
+                        MoveToPage(-1);
+                    }
+                    else if (_pageIndex != 0)
+                    {
+                        if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred))
+                        {
+                            // if the temporaryGroup was not created yet and is out of sync
+                            // then create it so that we can use it as a refernce while paging.
+                            if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count)
+                            {
+                                PrepareTemporaryGroups();
+                            }
+
+                            MoveToFirstPage();
+                        }
+                    }
+                    else if (IsGrouping)
+                    {
+                        // if the temporaryGroup was not created yet and is out of sync
+                        // then create it so that we can use it as a refernce while paging.
+                        if (_temporaryGroup.ItemCount != InternalList.Count)
+                        {
+                            // update the groups that get created for the
+                            // entire collection as well as the current page
+                            PrepareTemporaryGroups();
+                        }
+
+                        // update the groups for the current page
+                        PrepareGroupsForCurrentPage();
+                    }
+
+                    // if the count has changed
+                    if (Count != oldCount)
+                    {
+                        OnPropertyChanged(nameof(Count));
+                    }
+
+                    // reset currency values
+                    ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+                    // send a notification that our collection has been updated
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Reset));
+
+                    // now raise currency changes at the end
+                    RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the Sort criteria to sort items in collection.
+        /// </summary>
+        /// <remarks>
+        /// <p>
+        /// Clear a sort criteria by assigning SortDescription.Empty to this property.
+        /// One or more sort criteria in form of <seealso cref="SortDescription"/>
+        /// can be used, each specifying a property and direction to sort by.
+        /// </p>
+        /// </remarks>
+        /// <exception cref="NotSupportedException">
+        /// Simpler implementations do not support sorting and will throw a NotSupportedException.
+        /// Use <seealso cref="CanSort"/> property to test if sorting is supported before adding
+        /// to SortDescriptions.
+        /// </exception>
+        public DataGridSortDescriptionCollection SortDescriptions
+        {
+            get
+            {
+                if (_sortDescriptions == null)
+                {
+                    SetSortDescriptions(new DataGridSortDescriptionCollection());
+                }
+
+                return _sortDescriptions;
+            }
+        }
+
+        /// <summary>
+        /// Gets the source of the IEnumerable collection we are using for our view.
+        /// </summary>
+        public IEnumerable SourceCollection
+        {
+            get { return _sourceCollection; }
+        }
+
+        /// <summary>
+        /// Gets the total number of items in the view before paging is applied.
+        /// </summary>
+        public int TotalItemCount
+        {
+            get
+            {
+                return InternalList.Count;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether we have a valid ItemConstructor of the correct type
+        /// </summary>
+        private bool CanConstructItem
+        {
+            get
+            {
+                if (!_itemConstructorIsValid)
+                {
+                    EnsureItemConstructor();
+                }
+
+                return _itemConstructor != null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the private count without taking paging or
+        /// placeholders into account
+        /// </summary>
+        private int InternalCount
+        {
+            get { return InternalList.Count; }
+        }
+
+        /// <summary>
+        /// Gets the InternalList
+        /// </summary>
+        private IList InternalList
+        {
+            get { return _internalList; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether CurrentItem and CurrentPosition are
+        /// up-to-date with the state and content of the collection.
+        /// </summary>
+        private bool IsCurrentInSync
+        {
+            get
+            {
+                if (IsCurrentInView)
+                {
+                    return GetItemAt(CurrentPosition).Equals(CurrentItem);
+                }
+                else
+                {
+                    return CurrentItem == null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the current item is in the view
+        /// </summary>
+        private bool IsCurrentInView
+        {
+            get
+            {
+                VerifyRefreshNotDeferred();
+
+                // Calling IndexOf will check whether the specified currentItem
+                // is within the (paged) view.
+                return IndexOf(CurrentItem) >= 0;
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether or not we have grouping 
+        /// taking place in this collection.
+        /// </summary>
+        private bool IsGrouping
+        {
+            get { return _isGrouping; }
+        }
+
+        bool IDataGridCollectionView.IsGrouping => IsGrouping;
+
+        /// <summary>
+        /// Gets a value indicating whether there
+        /// is still an outstanding DeferRefresh in
+        /// use.  If at all possible, derived classes
+        /// should not call Refresh if IsRefreshDeferred
+        /// is true.
+        /// </summary>
+        private bool IsRefreshDeferred
+        {
+            get { return _deferLevel > 0; }
+        }
+
+        /// <summary>
+        /// Gets whether the current page is empty and we need
+        /// to move to a previous page.
+        /// </summary>
+        //TODO Paging
+        private bool NeedToMoveToPreviousPage
+        {
+            get { return (PageSize > 0 && Count == 0 && PageIndex != 0 && PageCount == PageIndex); }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether we are on the last local page
+        /// </summary>
+        //TODO Paging
+        private bool OnLastLocalPage
+        {
+            get
+            {
+                if (PageSize == 0)
+                {
+                    return false;
+                }
+
+                Debug.Assert(PageCount > 0, "Unexpected PageCount <= 0");
+
+                // if we have no items (PageCount==1) or there is just one page
+                if (PageCount == 1)
+                {
+                    return true;
+                }
+
+                return (PageIndex == PageCount - 1);
+            }
+        }
+
+        /// <summary>
+        /// Gets the number of pages we currently have
+        /// </summary>
+        //TODO Paging
+        private int PageCount
+        {
+            get { return (_pageSize > 0) ? Math.Max(1, (int)Math.Ceiling((double)ItemCount / _pageSize)) : 0; }
+        }
+
+        /// <summary>
+        /// Gets the root of the Group that we expose to the user
+        /// </summary>
+        private CollectionViewGroupRoot RootGroup
+        {
+            get
+            {
+                return _isUsingTemporaryGroup ? _temporaryGroup : _group;
+            }
+        }
+
+        /// <summary>
+        /// Gets the SourceCollection as an IList
+        /// </summary>
+        private IList SourceList
+        {
+            get { return SourceCollection as IList; }
+        }
+
+        /// <summary>
+        /// Gets Timestamp used by the NewItemAwareEnumerator to determine if a
+        /// collection change has occurred since the enumerator began.  (If so,
+        /// MoveNext should throw.)
+        /// </summary>
+        private int Timestamp
+        {
+            get { return _timestamp; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether a private copy of the data 
+        /// is needed for sorting, filtering, and paging. We want any deriving 
+        /// classes to also be able to access this value to see whether or not 
+        /// to use the default source collection, or the internal list.
+        /// </summary>
+        //TODO Paging
+        private bool UsesLocalArray
+        {
+            get { return SortDescriptions.Count > 0 || Filter != null || _pageSize > 0 || GroupDescriptions.Count > 0; }
+        }
+
+        /// <summary>
+        /// Return the item at the specified index
+        /// </summary>
+        /// <param name="index">Index of the item we want to retrieve</param>
+        /// <returns>The item at the specified index</returns>
+        public object this[int index]
+        {
+            get { return GetItemAt(index); }
+        }
+
+        /// <summary>
+        /// Add a new item to the underlying collection.  Returns the new item.
+        /// After calling AddNew and changing the new item as desired, either
+        /// CommitNew or CancelNew" should be called to complete the transaction.
+        /// </summary>
+        /// <returns>The new item we are adding</returns>
+        //TODO Paging
+        public object AddNew()
+        {
+            EnsureCollectionInSync();
+            VerifyRefreshNotDeferred();
+
+            if (IsEditingItem)
+            {
+                // Implicitly close a previous EditItem
+                CommitEdit();
+            }
+
+            // Implicitly close a previous AddNew
+            CommitNew();
+
+            // Checking CanAddNew will validate that we have the correct itemConstructor
+            if (!CanAddNew)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedText(nameof(AddNew)));
+            }
+
+            object newItem = null;
+
+            if (_itemConstructor != null)
+            {
+                newItem = _itemConstructor.Invoke(null);
+            }
+
+            try
+            {
+                // temporarily disable the CollectionChanged event
+                // handler so filtering, sorting, or grouping
+                // doesn't get applied yet
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false);
+
+                if (SourceList != null)
+                {
+                    SourceList.Add(newItem);
+                }
+            }
+            finally
+            {
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true);
+            }
+
+            // Modify our _trackingEnumerator so that it shows that our collection is "up to date" 
+            // and will not refresh for now.
+            _trackingEnumerator = _sourceCollection.GetEnumerator();
+
+            int addIndex;
+            int removeIndex = -1;
+
+            // Adjust index based on where it should be displayed in view.
+            if (PageSize > 0)
+            {
+                // if the page is full (Count==PageSize), then replace last item (Count-1).
+                // otherwise, we just append at end (Count).
+                addIndex = Count - ((Count == PageSize) ? 1 : 0);
+
+                // if the page is full, remove the last item to make space for the new one.
+                removeIndex = (Count == PageSize) ? addIndex : -1;
+            }
+            else
+            {
+                // for non-paged lists, we want to insert the item 
+                // as the last item in the view
+                addIndex = Count;
+            }
+
+            // if we need to remove an item from the view due to paging
+            if (removeIndex > -1)
+            {
+                object removeItem = GetItemAt(removeIndex);
+                if (IsGrouping)
+                {
+                    _group.RemoveFromSubgroups(removeItem);
+                }
+
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Remove,
+                        removeItem,
+                        removeIndex));
+            }
+
+            // add the new item to the internal list
+            _internalList.Insert(ConvertToInternalIndex(addIndex), newItem);
+            OnPropertyChanged(nameof(ItemCount));
+
+            object oldCurrentItem = CurrentItem;
+            int oldCurrentPosition = CurrentPosition;
+            bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+            bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+            AdjustCurrencyForAdd(null, addIndex);
+
+            if (IsGrouping)
+            {
+                _group.InsertSpecialItem(_group.Items.Count, newItem, false);
+                if (PageSize > 0)
+                {
+                    _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, newItem, false);
+                }
+            }
+
+            // fire collection changed.
+            OnCollectionChanged(
+                new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Add,
+                    newItem,
+                    addIndex));
+
+            RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+            // set the current new item
+            CurrentAddItem = newItem;
+
+            MoveCurrentTo(newItem);
+
+            // if the new item is editable, call BeginEdit on it
+            if (newItem is IEditableObject editableObject)
+            {
+                editableObject.BeginEdit();
+            }
+
+            return newItem;
+        }
+
+        /// <summary>
+        /// Complete the transaction started by <seealso cref="EditItem"/>.
+        /// The pending changes (if any) to the item are discarded.
+        /// </summary>
+        public void CancelEdit()
+        {
+            if (IsAddingNew)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelEdit), nameof(AddNew)));
+            }
+            else if (!CanCancelEdit)
+            {
+                throw new InvalidOperationException("CancelEdit is not supported for the current edit item.");
+            }
+
+            VerifyRefreshNotDeferred();
+
+            if (CurrentEditItem == null)
+            {
+                return;
+            }
+
+            object editItem = CurrentEditItem;
+            CurrentEditItem = null;
+
+            if (editItem is IEditableObject ieo)
+            {
+                ieo.CancelEdit();
+            }
+            else
+            {
+                throw new InvalidOperationException("CancelEdit is not supported for the current edit item.");
+            }
+        }
+
+        /// <summary>
+        /// Complete the transaction started by AddNew. The new
+        /// item is removed from the collection.
+        /// </summary>
+        //TODO Paging
+        public void CancelNew()
+        {
+            if (IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelNew), nameof(EditItem)));
+            }
+
+            VerifyRefreshNotDeferred();
+
+            if (CurrentAddItem == null)
+            {
+                return;
+            }
+
+            // get index of item before it is removed
+            int index = IndexOf(CurrentAddItem);
+
+            // remove the new item from the underlying collection
+            try
+            {
+                // temporarily disable the CollectionChanged event
+                // handler so filtering, sorting, or grouping
+                // doesn't get applied yet
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false);
+
+                if (SourceList != null)
+                {
+                    SourceList.Remove(CurrentAddItem);
+                }
+            }
+            finally
+            {
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true);
+            }
+
+            // Modify our _trackingEnumerator so that it shows that our collection is "up to date" 
+            // and will not refresh for now.
+            _trackingEnumerator = _sourceCollection.GetEnumerator();
+
+            // fire the correct events
+            if (CurrentAddItem != null)
+            {
+                object newItem = EndAddNew(true);
+
+                int addIndex = -1;
+
+                // Adjust index based on where it should be displayed in view.
+                if (PageSize > 0 && !OnLastLocalPage)
+                {
+                    // if there is paging and we are not on the last page, we need
+                    // to bring in an item from the next page.
+                    addIndex = Count - 1;
+                }
+
+                // remove the new item from the internal list 
+                InternalList.Remove(newItem);
+
+                if (IsGrouping)
+                {
+                    _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false);
+                    if (PageSize > 0)
+                    {
+                        _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false);
+                    }
+                }
+
+                OnPropertyChanged(nameof(ItemCount));
+
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                AdjustCurrencyForRemove(index);
+
+                // fire collection changed.
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Remove,
+                        newItem,
+                        index));
+
+                RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+                // if we need to add an item into the view due to paging
+                if (addIndex > -1)
+                {
+                    int internalIndex = ConvertToInternalIndex(addIndex);
+                    object addItem = null;
+                    if (IsGrouping)
+                    {
+                        addItem = _temporaryGroup.LeafAt(internalIndex);
+                        _group.AddToSubgroups(addItem, loading: false);
+                    }
+                    else
+                    {
+                        addItem = InternalItemAt(internalIndex);
+                    }
+
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Add,
+                            addItem,
+                            IndexOf(addItem)));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Complete the transaction started by <seealso cref="EditItem"/>.
+        /// The pending changes (if any) to the item are committed.
+        /// </summary>
+        //TODO Paging
+        public void CommitEdit()
+        {
+            if (IsAddingNew)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitEdit), nameof(AddNew)));
+            }
+
+            VerifyRefreshNotDeferred();
+
+            if (CurrentEditItem == null)
+            {
+                return;
+            }
+
+            object editItem = CurrentEditItem;
+            CurrentEditItem = null;
+
+            if (editItem is IEditableObject ieo)
+            {
+                ieo.EndEdit();
+            }
+
+            if (UsesLocalArray)
+            {
+                // first remove the item from the array so that we can insert into the correct position
+                int removeIndex = IndexOf(editItem);
+                int internalRemoveIndex = InternalIndexOf(editItem);
+                _internalList.Remove(editItem);
+
+                // check whether to restore currency to the item being edited
+                object restoreCurrencyTo = (editItem == CurrentItem) ? editItem : null;
+
+                if (removeIndex >= 0 && IsGrouping)
+                {
+                    // we can't just call RemoveFromSubgroups, as the group name
+                    // for the item may have changed during the edit.
+                    _group.RemoveItemFromSubgroupsByExhaustiveSearch(editItem);
+                    if (PageSize > 0)
+                    {
+                        _temporaryGroup.RemoveItemFromSubgroupsByExhaustiveSearch(editItem);
+                    }
+                }
+
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                // only adjust currency and fire the event if we actually removed the item
+                if (removeIndex >= 0)
+                {
+                    AdjustCurrencyForRemove(removeIndex);
+
+                    // raise the remove event so we can next insert it into the correct place
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Remove,
+                            editItem,
+                            removeIndex));
+                }
+
+                // check to see that the item will be added back in
+                bool passedFilter = PassesFilter(editItem);
+
+                // if we removed all items from the current page,
+                // move to the previous page. we do not need to 
+                // fire additional notifications, as moving the page will
+                // trigger a reset.
+                if (NeedToMoveToPreviousPage && !passedFilter)
+                {
+                    MoveToPreviousPage();
+                    return;
+                }
+
+                // next process adding it into the correct location
+                ProcessInsertToCollection(editItem, internalRemoveIndex);
+
+                int pageStartIndex = PageIndex * PageSize;
+                int nextPageStartIndex = pageStartIndex + PageSize;
+
+                if (IsGrouping)
+                {
+                    int leafIndex = -1;
+                    if (passedFilter && PageSize > 0)
+                    {
+                        _temporaryGroup.AddToSubgroups(editItem, false /*loading*/);
+                        leafIndex = _temporaryGroup.LeafIndexOf(editItem);
+                    }
+
+                    // if we are not paging, we should just be able to add the item.
+                    // otherwise, we need to validate that it is within the current page.
+                    if (passedFilter && (PageSize == 0 ||
+                       (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex)))
+                    {
+                        _group.AddToSubgroups(editItem, false /*loading*/);
+                        int addIndex = IndexOf(editItem);
+                        AdjustCurrencyForEdit(restoreCurrencyTo, addIndex);
+                        OnCollectionChanged(
+                            new NotifyCollectionChangedEventArgs(
+                                NotifyCollectionChangedAction.Add,
+                                editItem,
+                                addIndex));
+                    }
+                    else if (PageSize > 0)
+                    {
+                        int addIndex = -1;
+                        if (passedFilter && leafIndex < pageStartIndex)
+                        {
+                            // if the item was added to an earlier page, then we need to bring
+                            // in the item that would have been pushed down to this page
+                            addIndex = pageStartIndex;
+                        }
+                        else if (!OnLastLocalPage && removeIndex >= 0)
+                        {
+                            // if the item was added to a later page, then we need to bring in the
+                            // first item from the next page
+                            addIndex = nextPageStartIndex - 1;
+                        }
+
+                        object addItem = _temporaryGroup.LeafAt(addIndex);
+                        if (addItem != null)
+                        {
+                            _group.AddToSubgroups(addItem, false /*loading*/);
+                            addIndex = IndexOf(addItem);
+                            AdjustCurrencyForEdit(restoreCurrencyTo, addIndex);
+                            OnCollectionChanged(
+                                new NotifyCollectionChangedEventArgs(
+                                    NotifyCollectionChangedAction.Add,
+                                    addItem,
+                                    addIndex));
+                        }
+                    }
+                }
+                else
+                {
+                    // if we are still within the view
+                    int addIndex = IndexOf(editItem);
+                    if (addIndex >= 0)
+                    {
+                        AdjustCurrencyForEdit(restoreCurrencyTo, addIndex);
+                        OnCollectionChanged(
+                            new NotifyCollectionChangedEventArgs(
+                                NotifyCollectionChangedAction.Add,
+                                editItem,
+                                addIndex));
+                    }
+                    else if (PageSize > 0)
+                    {
+                        // calculate whether the item was inserted into the previous page
+                        bool insertedToPreviousPage = PassesFilter(editItem) &&
+                            (InternalIndexOf(editItem) < ConvertToInternalIndex(0));
+                        addIndex = insertedToPreviousPage ? 0 : Count - 1;
+
+                        // don't fire the event if we are on the last page
+                        // and we don't have any items to bring in.
+                        if (insertedToPreviousPage || (!OnLastLocalPage && removeIndex >= 0))
+                        {
+                            AdjustCurrencyForEdit(restoreCurrencyTo, addIndex);
+                            OnCollectionChanged(
+                                new NotifyCollectionChangedEventArgs(
+                                    NotifyCollectionChangedAction.Add,
+                                    GetItemAt(addIndex),
+                                    addIndex));
+                        }
+                    }
+                }
+
+                // now raise currency changes at the end
+                RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+            }
+            else if (!Contains(editItem))
+            {
+                // if the item did not belong to the collection, add it
+                InternalList.Add(editItem);
+            }
+        }
+
+        /// <summary>
+        /// Complete the transaction started by AddNew. We follow the WPF
+        /// convention in that the view's sort, filter, and paging
+        /// specifications (if any) are applied to the new item.
+        /// </summary>
+        //TODO Paging
+        public void CommitNew()
+        {
+            if (IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitNew), nameof(EditItem)));
+            }
+
+            VerifyRefreshNotDeferred();
+
+            if (CurrentAddItem == null)
+            {
+                return;
+            }
+
+            // End the AddNew transaction
+            object newItem = EndAddNew(false);
+
+            // keep track of the current item
+            object previousCurrentItem = CurrentItem;
+
+            // Modify our _trackingEnumerator so that it shows that our collection is "up to date" 
+            // and will not refresh for now.
+            _trackingEnumerator = _sourceCollection.GetEnumerator();
+
+            if (UsesLocalArray)
+            {
+                // first remove the item from the array so that we can insert into the correct position
+                int removeIndex = Count - 1;
+                int internalIndex = _internalList.IndexOf(newItem);
+                _internalList.Remove(newItem);
+
+                if (IsGrouping)
+                {
+                    _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false);
+                    if (PageSize > 0)
+                    {
+                        _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false);
+                    }
+                }
+
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                AdjustCurrencyForRemove(removeIndex);
+
+                // raise the remove event so we can next insert it into the correct place
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Remove,
+                        newItem,
+                        removeIndex));
+
+                // check to see that the item will be added back in
+                bool passedFilter = PassesFilter(newItem);
+
+                // next process adding it into the correct location
+                ProcessInsertToCollection(newItem, internalIndex);
+
+                int pageStartIndex = PageIndex * PageSize;
+                int nextPageStartIndex = pageStartIndex + PageSize;
+
+                if (IsGrouping)
+                {
+                    int leafIndex = -1;
+                    if (passedFilter && PageSize > 0)
+                    {
+                        _temporaryGroup.AddToSubgroups(newItem, false /*loading*/);
+                        leafIndex = _temporaryGroup.LeafIndexOf(newItem);
+                    }
+
+                    // if we are not paging, we should just be able to add the item.
+                    // otherwise, we need to validate that it is within the current page.
+                    if (passedFilter && (PageSize == 0 ||
+                       (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex)))
+                    {
+                        _group.AddToSubgroups(newItem, false /*loading*/);
+                        int addIndex = IndexOf(newItem);
+
+                        // adjust currency to either the previous current item if possible
+                        // or to the item at the end of the list where the new item was.
+                        if (previousCurrentItem != null)
+                        {
+                            if (Contains(previousCurrentItem))
+                            {
+                                AdjustCurrencyForAdd(previousCurrentItem, addIndex);
+                            }
+                            else
+                            {
+                                AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex);
+                            }
+                        }
+
+                        OnCollectionChanged(
+                            new NotifyCollectionChangedEventArgs(
+                                NotifyCollectionChangedAction.Add,
+                                newItem,
+                                addIndex));
+                    }
+                    else
+                    {
+                        if (!passedFilter && (PageSize == 0 || OnLastLocalPage))
+                        {
+                            AdjustCurrencyForRemove(removeIndex);
+                        }
+                        else if (PageSize > 0)
+                        {
+                            int addIndex = -1;
+                            if (passedFilter && leafIndex < pageStartIndex)
+                            {
+                                // if the item was added to an earlier page, then we need to bring
+                                // in the item that would have been pushed down to this page
+                                addIndex = pageStartIndex;
+                            }
+                            else if (!OnLastLocalPage)
+                            {
+                                // if the item was added to a later page, then we need to bring in the
+                                // first item from the next page
+                                addIndex = nextPageStartIndex - 1;
+                            }
+
+                            object addItem = _temporaryGroup.LeafAt(addIndex);
+                            if (addItem != null)
+                            {
+                                _group.AddToSubgroups(addItem, false /*loading*/);
+                                addIndex = IndexOf(addItem);
+
+                                // adjust currency to either the previous current item if possible
+                                // or to the item at the end of the list where the new item was.
+                                if (previousCurrentItem != null)
+                                {
+                                    if (Contains(previousCurrentItem))
+                                    {
+                                        AdjustCurrencyForAdd(previousCurrentItem, addIndex);
+                                    }
+                                    else
+                                    {
+                                        AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex);
+                                    }
+                                }
+
+                                OnCollectionChanged(
+                                    new NotifyCollectionChangedEventArgs(
+                                        NotifyCollectionChangedAction.Add,
+                                        addItem,
+                                        addIndex));
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // if we are still within the view
+                    int addIndex = IndexOf(newItem);
+                    if (addIndex >= 0)
+                    {
+                        AdjustCurrencyForAdd(newItem, addIndex);
+                        OnCollectionChanged(
+                            new NotifyCollectionChangedEventArgs(
+                                NotifyCollectionChangedAction.Add,
+                                newItem,
+                                addIndex));
+                    }
+                    else
+                    {
+                        if (!passedFilter && (PageSize == 0 || OnLastLocalPage))
+                        {
+                            AdjustCurrencyForRemove(removeIndex);
+                        }
+                        else if (PageSize > 0)
+                        {
+                            bool insertedToPreviousPage = InternalIndexOf(newItem) < ConvertToInternalIndex(0);
+                            addIndex = insertedToPreviousPage ? 0 : Count - 1;
+
+                            // don't fire the event if we are on the last page
+                            // and we don't have any items to bring in.
+                            if (insertedToPreviousPage || !OnLastLocalPage)
+                            {
+                                AdjustCurrencyForAdd(null, addIndex);
+                                OnCollectionChanged(
+                                    new NotifyCollectionChangedEventArgs(
+                                        NotifyCollectionChangedAction.Add,
+                                        GetItemAt(addIndex),
+                                        addIndex));
+                            }
+                        }
+                    }
+                }
+
+                // we want to fire the current changed event, even if we kept
+                // the same current item and position, since the item was
+                // removed/added back to the collection
+                RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+            }
+        }
+
+        /// <summary>
+        /// Return true if the item belongs to this view.  No assumptions are
+        /// made about the item. This method will behave similarly to IList.Contains().
+        /// If the caller knows that the item belongs to the
+        /// underlying collection, it is more efficient to call PassesFilter.
+        /// </summary>
+        /// <param name="item">The item we are checking to see whether it is within the collection</param>
+        /// <returns>Boolean value of whether or not the collection contains the item</returns>
+        public bool Contains(object item)
+        {
+            EnsureCollectionInSync();
+            VerifyRefreshNotDeferred();
+            return IndexOf(item) >= 0;
+        }
+
+        /// <summary>
+        /// Enter a Defer Cycle.
+        /// Defer cycles are used to coalesce changes to the ICollectionView.
+        /// </summary>
+        /// <returns>IDisposable used to notify that we no longer need to defer, when we dispose</returns>
+        public IDisposable DeferRefresh()
+        {
+            if (IsAddingNew || IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(DeferRefresh)));
+            }
+
+            ++_deferLevel;
+            return new DeferHelper(this);
+        }
+
+        /// <summary>
+        /// Begins an editing transaction on the given item.  The transaction is
+        /// completed by calling either CommitEdit or CancelEdit.  Any changes made 
+        /// to the item during the transaction are considered "pending", provided 
+        /// that the view supports the notion of "pending changes" for the given item.
+        /// </summary>
+        /// <param name="item">Item we want to edit</param>
+        public void EditItem(object item)
+        {
+            VerifyRefreshNotDeferred();
+
+            if (IsAddingNew)
+            {
+                if (Object.Equals(item, CurrentAddItem))
+                {
+                    // EditItem(newItem) is a no-op
+                    return;
+                }
+
+                // implicitly close a previous AddNew
+                CommitNew();
+            }
+
+            // implicitly close a previous EditItem transaction
+            CommitEdit();
+
+            CurrentEditItem = item;
+
+            if (item is IEditableObject ieo)
+            {
+                ieo.BeginEdit();
+            }
+        }
+
+        /// <summary> 
+        /// Implementation of IEnumerable.GetEnumerator().
+        /// This provides a way to enumerate the members of the collection
+        /// without changing the currency.
+        /// </summary>
+        /// <returns>IEnumerator for the collection</returns>
+        //TODO Paging
+        public IEnumerator GetEnumerator()
+        {
+            EnsureCollectionInSync();
+            VerifyRefreshNotDeferred();
+
+            if (IsGrouping)
+            {
+                return RootGroup?.GetLeafEnumerator();
+            }
+
+            // if we are paging
+            if (PageSize > 0)
+            {
+                List<object> list = new List<object>();
+
+                // if we are in the middle of asynchronous load
+                if (PageIndex < 0)
+                {
+                    return list.GetEnumerator();
+                }
+
+                for (int index = _pageSize * PageIndex;
+                    index < (int)Math.Min(_pageSize * (PageIndex + 1), InternalList.Count);
+                    index++)
+                {
+                    list.Add(InternalList[index]);
+                }
+
+                return new NewItemAwareEnumerator(this, list.GetEnumerator(), CurrentAddItem);
+            }
+            else
+            {
+                return new NewItemAwareEnumerator(this, InternalList.GetEnumerator(), CurrentAddItem);
+            }
+        }
+
+        /// <summary>
+        /// Interface Implementation for GetEnumerator()
+        /// </summary>
+        /// <returns>IEnumerator that we get from our internal collection</returns>
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        /// <summary>
+        /// Retrieve item at the given zero-based index in this DataGridCollectionView, after the source collection
+        /// is filtered, sorted, and paged.
+        /// </summary>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// Thrown if index is out of range
+        /// </exception>
+        /// <param name="index">Index of the item we want to retrieve</param>
+        /// <returns>Item at specified index</returns>
+        public object GetItemAt(int index)
+        {
+            EnsureCollectionInSync();
+            VerifyRefreshNotDeferred();
+
+            // for indicies larger than the count
+            if (index >= Count || index < 0)
+            {
+                throw new ArgumentOutOfRangeException("index");
+            }
+
+            if (IsGrouping)
+            {
+                return RootGroup?.LeafAt(_isUsingTemporaryGroup ? ConvertToInternalIndex(index) : index);
+            }
+
+            if (IsAddingNew && UsesLocalArray && index == Count - 1)
+            {
+                return CurrentAddItem;
+            }
+
+            return InternalItemAt(ConvertToInternalIndex(index));
+        }
+
+        /// <summary> 
+        /// Return the index where the given item appears, or -1 if doesn't appear.
+        /// </summary>
+        /// <param name="item">Item we are searching for</param>
+        /// <returns>Index of specified item</returns>
+        //TODO Paging
+        public int IndexOf(object item)
+        {
+            EnsureCollectionInSync();
+            VerifyRefreshNotDeferred();
+
+            if (IsGrouping)
+            {
+                return RootGroup?.LeafIndexOf(item) ?? -1;
+            }
+            if (IsAddingNew && Object.Equals(item, CurrentAddItem) && UsesLocalArray)
+            {
+                return Count - 1;
+            }
+
+            int internalIndex = InternalIndexOf(item);
+
+            if (PageSize > 0 && internalIndex != -1)
+            {
+                if ((internalIndex >= (PageIndex * _pageSize)) &&
+                    (internalIndex < ((PageIndex + 1) * _pageSize)))
+                {
+                    return internalIndex - (PageIndex * _pageSize);
+                }
+                else
+                {
+                    return -1;
+                }
+            }
+            else
+            {
+                return internalIndex;
+            }
+        }
+
+        /// <summary> 
+        /// Move to the given item. 
+        /// </summary>
+        /// <param name="item">Item we want to move the currency to</param>
+        /// <returns>Whether the operation was successful</returns>
+        public bool MoveCurrentTo(object item)
+        {
+            VerifyRefreshNotDeferred();
+
+            // if already on item, don't do anything
+            if (Object.Equals(CurrentItem, item))
+            {
+                // also check that we're not fooled by a false null currentItem
+                if (item != null || IsCurrentInView)
+                {
+                    return IsCurrentInView;
+                }
+            }
+
+            // if the item is not found IndexOf() will return -1, and
+            // the MoveCurrentToPosition() below will move current to BeforeFirst
+            // The IndexOf function takes into account paging, filtering, and sorting
+            return MoveCurrentToPosition(IndexOf(item));
+        }
+
+        /// <summary> 
+        /// Move to the first item. 
+        /// </summary>
+        /// <returns>Whether the operation was successful</returns>
+        public bool MoveCurrentToFirst()
+        {
+            VerifyRefreshNotDeferred();
+
+            return MoveCurrentToPosition(0);
+        }
+
+        /// <summary> 
+        /// Move to the last item. 
+        /// </summary>
+        /// <returns>Whether the operation was successful</returns>
+        public bool MoveCurrentToLast()
+        {
+            VerifyRefreshNotDeferred();
+
+            int index = Count - 1;
+
+            return MoveCurrentToPosition(index);
+        }
+
+        /// <summary> 
+        /// Move to the next item. 
+        /// </summary>
+        /// <returns>Whether the operation was successful</returns>
+        public bool MoveCurrentToNext()
+        {
+            VerifyRefreshNotDeferred();
+
+            int index = CurrentPosition + 1;
+
+            if (index <= Count)
+            {
+                return MoveCurrentToPosition(index);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Move CurrentItem to this index
+        /// </summary>
+        /// <param name="position">Position we want to move the currency to</param>
+        /// <returns>True if the resulting CurrentItem is an item within the view; otherwise False</returns>
+        public bool MoveCurrentToPosition(int position)
+        {
+            VerifyRefreshNotDeferred();
+
+            // We want to allow the user to set the currency to just
+            // beyond the last item. EnumerableCollectionView in WPF
+            // also checks (position > Count) though the ListCollectionView
+            // looks for (position >= Count).
+            if (position < -1 || position > Count)
+            {
+                throw new ArgumentOutOfRangeException(nameof(position));
+            }
+
+            if ((position != CurrentPosition || !IsCurrentInSync)
+                && OkToChangeCurrent())
+            {
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                SetCurrentToPosition(position);
+                OnCurrentChanged();
+
+                if (IsCurrentAfterLast != oldIsCurrentAfterLast)
+                {
+                    OnPropertyChanged(nameof(IsCurrentAfterLast));
+                }
+
+                if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst)
+                {
+                    OnPropertyChanged(nameof(IsCurrentBeforeFirst));
+                }
+
+                OnPropertyChanged(nameof(CurrentPosition));
+                OnPropertyChanged(nameof(CurrentItem));
+            }
+
+            return IsCurrentInView;
+        }
+
+        /// <summary> 
+        /// Move to the previous item. 
+        /// </summary>
+        /// <returns>Whether the operation was successful</returns>
+        public bool MoveCurrentToPrevious()
+        {
+            VerifyRefreshNotDeferred();
+
+            int index = CurrentPosition - 1;
+
+            if (index >= -1)
+            {
+                return MoveCurrentToPosition(index);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Moves to the first page.
+        /// </summary>
+        /// <returns>Whether or not the move was successful.</returns>
+        //TODO Paging
+        public bool MoveToFirstPage()
+        {
+            return MoveToPage(0);
+        }
+
+        /// <summary>
+        /// Moves to the last page.
+        /// The move is only attempted when TotalItemCount is known.
+        /// </summary>
+        /// <returns>Whether or not the move was successful.</returns>
+        //TODO Paging
+        public bool MoveToLastPage()
+        {
+            if (TotalItemCount != -1 && PageSize > 0)
+            {
+                return MoveToPage(PageCount - 1);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Moves to the page after the current page we are on.
+        /// </summary>
+        /// <returns>Whether or not the move was successful.</returns>
+        //TODO Paging
+        public bool MoveToNextPage()
+        {
+            return MoveToPage(_pageIndex + 1);
+        }
+
+        /// <summary>
+        /// Requests a page move to page <paramref name="pageIndex"/>.
+        /// </summary>
+        /// <param name="pageIndex">Index of the target page</param>
+        /// <returns>Whether or not the move was successfully initiated.</returns>
+        //TODO Paging
+        public bool MoveToPage(int pageIndex)
+        {
+            // Boundary checks for negative pageIndex
+            if (pageIndex < -1)
+            {
+                return false;
+            }
+
+            // if the Refresh is deferred, cache the requested PageIndex so that we
+            // can move to the desired page when EndDefer is called.
+            if (IsRefreshDeferred)
+            {
+                // set cached value and flag so that we move to the page on EndDefer
+                _cachedPageIndex = pageIndex;
+                SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true);
+                return false;
+            }
+
+            // check for invalid pageIndex
+            if (pageIndex == -1 && PageSize > 0)
+            {
+                return false;
+            }
+
+            // Check if the target page is out of bound, or equal to the current page
+            if (pageIndex >= PageCount || _pageIndex == pageIndex)
+            {
+                return false;
+            }
+
+            // Check with the ICollectionView.CurrentChanging listeners if it's OK to move
+            // on to another page
+            if (!OkToChangeCurrent())
+            {
+                return false;
+            }
+
+            if (RaisePageChanging(pageIndex) && pageIndex != -1)
+            {
+                // Page move was cancelled. Abort the move, but only if the target index isn't -1.
+                return false;
+            }
+
+            // Check if there is a current edited or new item so changes can be committed first.
+            if (CurrentAddItem != null || CurrentEditItem != null)
+            {
+                // Remember current currency values for upcoming OnPropertyChanged notifications
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or 
+                // cancelling an item that is no longer on the current page. That's acceptable and means that
+                // the potential _newItem or _editItem needs to be committed before this page move.
+                // The reason why we temporarily reset currency here is to give a chance to the bound
+                // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit()
+                // for example as a result of changing currency.
+                SetCurrentToPosition(-1);
+                RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+                // If the bound controls did not successfully end their potential item editing/addition, the 
+                // page move needs to be aborted. 
+                if (CurrentAddItem != null || CurrentEditItem != null)
+                {
+                    // Since PageChanging was raised and not cancelled, a PageChanged notification needs to be raised
+                    // even though the PageIndex actually did not change.
+                    RaisePageChanged();
+
+                    // Restore original currency
+                    Debug.Assert(CurrentItem == null, "Unexpected CurrentItem != null");
+                    Debug.Assert(CurrentPosition == -1, "Unexpected CurrentPosition != -1");
+                    Debug.Assert(IsCurrentBeforeFirst, "Unexpected IsCurrentBeforeFirst == false");
+                    Debug.Assert(!IsCurrentAfterLast, "Unexpected IsCurrentAfterLast == true");
+
+                    SetCurrentToPosition(oldCurrentPosition);
+                    RaiseCurrencyChanges(false /*fireChangedEvent*/, null /*oldCurrentItem*/, -1 /*oldCurrentPosition*/,
+                        true /*oldIsCurrentBeforeFirst*/, false /*oldIsCurrentAfterLast*/);
+
+                    return false;
+                }
+
+                // Finally raise a CurrentChanging notification for the upcoming currency change
+                // that will occur in CompletePageMove(pageIndex).
+                OnCurrentChanging();
+            }
+
+            IsPageChanging = true;
+            CompletePageMove(pageIndex);
+
+            return true;
+        }
+
+        /// <summary>
+        /// Moves to the page before the current page we are on.
+        /// </summary>
+        /// <returns>Whether or not the move was successful.</returns>
+        //TODO Paging
+        public bool MoveToPreviousPage()
+        {
+            return MoveToPage(_pageIndex - 1);
+        }
+
+        /// <summary>
+        /// Return true if the item belongs to this view.  The item is assumed to belong to the
+        /// underlying DataCollection;  this method merely takes filters into account.
+        /// It is commonly used during collection-changed notifications to determine if the added/removed
+        /// item requires processing.
+        /// Returns true if no filter is set on collection view.
+        /// </summary>
+        /// <param name="item">The item to compare against the Filter</param>
+        /// <returns>Whether the item passes the filter</returns>
+        public bool PassesFilter(object item)
+        {
+            if (Filter != null)
+            {
+                return Filter(item);
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Re-create the view, using any SortDescriptions and/or Filters.
+        /// </summary>
+        public void Refresh()
+        {
+            if (this is IDataGridEditableCollectionView ecv && (ecv.IsAddingNew || ecv.IsEditingItem))
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Refresh)));
+            }
+
+            RefreshInternal();
+        }
+
+        /// <summary>
+        /// Remove the given item from the underlying collection. It
+        /// needs to be in the current filtered, sorted, and paged view
+        /// to call 
+        /// </summary>
+        /// <param name="item">Item we want to remove</param>
+        public void Remove(object item)
+        {
+            int index = IndexOf(item);
+            if (index >= 0)
+            {
+                RemoveAt(index);
+            }
+        }
+
+        /// <summary>
+        /// Remove the item at the given index from the underlying collection.
+        /// The index is interpreted with respect to the view (filtered, sorted,
+        /// and paged list).
+        /// </summary>
+        /// <param name="index">Index of the item we want to remove</param>
+        //TODO Paging
+        public void RemoveAt(int index)
+        {
+            if (index < 0 || index >= Count)
+            {
+                throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the collection.");
+            }
+
+            if (IsEditingItem || IsAddingNew)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(RemoveAt)));
+            }
+            else if (!CanRemove)
+            {
+                throw new InvalidOperationException("Remove/RemoveAt is not supported.");
+            }
+
+            VerifyRefreshNotDeferred();
+
+            // convert the index from "view-relative" to "list-relative"
+            object item = GetItemAt(index);
+
+            // before we remove the item, see if we are not on the last page
+            // and will have to bring in a new item to replace it
+            bool replaceItem = PageSize > 0 && !OnLastLocalPage;
+
+            try
+            {
+                // temporarily disable the CollectionChanged event
+                // handler so filtering, sorting, or grouping
+                // doesn't get applied yet
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false);
+
+                if (SourceList != null)
+                {
+                    SourceList.Remove(item);
+                }
+            }
+            finally
+            {
+                SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true);
+            }
+
+            // Modify our _trackingEnumerator so that it shows that our collection is "up to date" 
+            // and will not refresh for now.
+            _trackingEnumerator = _sourceCollection.GetEnumerator();
+
+            Debug.Assert(index == IndexOf(item), "IndexOf returned unexpected value");
+
+            // remove the item from the internal list
+            _internalList.Remove(item);
+
+            if (IsGrouping)
+            {
+                if (PageSize > 0)
+                {
+                    _temporaryGroup.RemoveFromSubgroups(item);
+                }
+                _group.RemoveFromSubgroups(item);
+            }
+
+            object oldCurrentItem = CurrentItem;
+            int oldCurrentPosition = CurrentPosition;
+            bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+            bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+            AdjustCurrencyForRemove(index);
+
+            // fire remove notification
+            OnCollectionChanged(
+                new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Remove,
+                    item,
+                    index));
+
+            RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+            // if we removed all items from the current page,
+            // move to the previous page. we do not need to 
+            // fire additional notifications, as moving the page will
+            // trigger a reset.
+            if (NeedToMoveToPreviousPage)
+            {
+                MoveToPreviousPage();
+                return;
+            }
+
+            // if we are paging, we may have to fire another notification for the item
+            // that needs to replace the one we removed on this page.
+            if (replaceItem)
+            {
+                // we first need to add the item into the current group
+                if (IsGrouping)
+                {
+                    object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1);
+                    if (newItem != null)
+                    {
+                        _group.AddToSubgroups(newItem, loading: false);
+                    }
+                }
+
+                // fire the add notification
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Add,
+                        GetItemAt(PageSize - 1),
+                        PageSize - 1));
+            }
+        }
+
+        /// <summary>
+        /// Helper for SortList to handle nested properties (e.g. Address.Street)
+        /// </summary>
+        /// <param name="item">parent object</param>
+        /// <param name="propertyPath">property names path</param>
+        /// <param name="propertyType">property type that we want to check for</param>
+        /// <returns>child object</returns>
+        private static object InvokePath(object item, string propertyPath, Type propertyType)
+        {
+            object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception);
+            if (exception != null)
+            {
+                throw exception;
+            }
+            return propertyValue;
+        }
+
+        /// <summary>
+        /// Fix up CurrentPosition and CurrentItem after a collection change
+        /// </summary>
+        /// <param name="newCurrentItem">Item that we want to set currency to</param>
+        /// <param name="index">Index of item involved in the collection change</param>
+        private void AdjustCurrencyForAdd(object newCurrentItem, int index)
+        {
+            if (newCurrentItem != null)
+            {
+                int newItemIndex = IndexOf(newCurrentItem);
+
+                // if we already have the correct currency set, we don't 
+                // want to unnecessarily fire events
+                if (newItemIndex >= 0 && (newItemIndex != CurrentPosition || !IsCurrentInSync))
+                {
+                    OnCurrentChanging();
+                    SetCurrent(newCurrentItem, newItemIndex);
+                }
+                return;
+            }
+
+            if (Count == 1)
+            {
+                if (CurrentItem != null || CurrentPosition != -1)
+                {
+                    // fire current changing notification
+                    OnCurrentChanging();
+                }
+
+                // added first item; set current at BeforeFirst
+                SetCurrent(null, -1);
+            }
+            else if (index <= CurrentPosition)
+            {
+                // fire current changing notification
+                OnCurrentChanging();
+
+                // adjust current index if insertion is earlier
+                int newPosition = CurrentPosition + 1;
+                if (newPosition >= Count)
+                {
+                    // if currency was on last item and it got shifted up,
+                    // keep currency on last item.
+                    newPosition = Count - 1;
+                }
+                SetCurrent(GetItemAt(newPosition), newPosition);
+            }
+        }
+
+        /// <summary>
+        /// Fix up CurrentPosition and CurrentItem after a collection change
+        /// </summary>
+        /// <param name="newCurrentItem">Item that we want to set currency to</param>
+        /// <param name="index">Index of item involved in the collection change</param>
+        private void AdjustCurrencyForEdit(object newCurrentItem, int index)
+        {
+            if (newCurrentItem != null && IndexOf(newCurrentItem) >= 0)
+            {
+                OnCurrentChanging();
+                SetCurrent(newCurrentItem, IndexOf(newCurrentItem));
+                return;
+            }
+
+            if (index <= CurrentPosition)
+            {
+                // fire current changing notification
+                OnCurrentChanging();
+
+                // adjust current index if insertion is earlier
+                int newPosition = CurrentPosition + 1;
+                if (newPosition < Count)
+                {
+                    // CurrentItem might be out of sync if underlying list is not INCC
+                    // or if this Add is the result of a Replace (Rem + Add)
+                    SetCurrent(GetItemAt(newPosition), newPosition);
+                }
+                else
+                {
+                    SetCurrent(null, Count);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Fix up CurrentPosition and CurrentItem after a collection change
+        /// The index can be -1 if the item was removed from a previous page
+        /// </summary>
+        /// <param name="index">Index of item involved in the collection change</param>
+        private void AdjustCurrencyForRemove(int index)
+        {
+            // adjust current index if deletion is earlier
+            if (index < CurrentPosition)
+            {
+                // fire current changing notification
+                OnCurrentChanging();
+
+                SetCurrent(CurrentItem, CurrentPosition - 1);
+            }
+
+            // adjust current index if > Count
+            if (CurrentPosition >= Count)
+            {
+                // fire current changing notification
+                OnCurrentChanging();
+
+                SetCurrentToPosition(Count - 1);
+            }
+
+            // make sure that current position and item are in sync
+            if (!IsCurrentInSync)
+            {
+                // fire current changing notification
+                OnCurrentChanging();
+
+                SetCurrentToPosition(CurrentPosition);
+            }
+        }
+
+        /// <summary>
+        /// Returns true if specified flag in flags is set.
+        /// </summary>
+        /// <param name="flags">Flag we are checking for</param>
+        /// <returns>Whether the specified flag is set</returns>
+        private bool CheckFlag(CollectionViewFlags flags)
+        {
+            return (_flags & flags) != 0;
+        }
+
+        /// <summary>
+        /// Called to complete the page move operation to set the
+        /// current page index.
+        /// </summary>
+        /// <param name="pageIndex">Final page index</param>
+        //TODO Paging
+        private void CompletePageMove(int pageIndex)
+        {
+            Debug.Assert(_pageIndex != pageIndex, "Unexpected _pageIndex == pageIndex");
+
+            // to see whether or not to fire an OnPropertyChanged
+            int oldCount = Count;
+            object oldCurrentItem = CurrentItem;
+            int oldCurrentPosition = CurrentPosition;
+            bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+            bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+            _pageIndex = pageIndex;
+
+            // update the groups
+            if (IsGrouping && PageSize > 0)
+            {
+                PrepareGroupsForCurrentPage();
+            }
+
+            // update currency
+            if (Count >= 1)
+            {
+                SetCurrent(GetItemAt(0), 0);
+            }
+            else
+            {
+                SetCurrent(null, -1);
+            }
+
+            IsPageChanging = false;
+            OnPropertyChanged(nameof(PageIndex));
+            RaisePageChanged();
+
+            // if the count has changed
+            if (Count != oldCount)
+            {
+                OnPropertyChanged(nameof(Count));
+            }
+
+            OnCollectionChanged(
+                new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Reset));
+
+            // Always raise CurrentChanged since the calling method MoveToPage(pageIndex) raised CurrentChanging.
+            RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+        }
+
+        /// <summary>
+        /// Convert a value for the index passed in to the index it would be
+        /// relative to the InternalIndex property.
+        /// </summary>
+        /// <param name="index">Index to convert</param>
+        /// <returns>Value for the InternalIndex</returns>
+        //TODO Paging
+        private int ConvertToInternalIndex(int index)
+        {
+            Debug.Assert(index > -1, "Unexpected index == -1");
+            if (PageSize > 0)
+            {
+                return (_pageSize * PageIndex) + index;
+            }
+            else
+            {
+                return index;
+            }
+        }
+
+        /// <summary>
+        /// Copy all items from the source collection to the internal list for processing.
+        /// </summary>
+        private void CopySourceToInternalList()
+        {
+            _internalList = new List<object>();
+
+            IEnumerator enumerator = SourceCollection.GetEnumerator();
+
+            while (enumerator.MoveNext())
+            {
+                _internalList.Add(enumerator.Current);
+            }
+        }
+
+        /// <summary>
+        /// Common functionality used by CommitNew, CancelNew, and when the
+        /// new item is removed by Remove or Refresh.
+        /// </summary>
+        /// <param name="cancel">Whether we canceled the add</param>
+        /// <returns>The new item we ended adding</returns>
+        private object EndAddNew(bool cancel)
+        {
+            object newItem = CurrentAddItem;
+
+            CurrentAddItem = null;    // leave "adding-new" mode
+
+            if (newItem is IEditableObject ieo)
+            {
+                if (cancel)
+                {
+                    ieo.CancelEdit();
+                }
+                else
+                {
+                    ieo.EndEdit();
+                }
+            }
+
+            return newItem;
+        }
+
+        /// <summary>
+        /// Subtracts from the deferLevel counter and calls Refresh() if there are no other defers
+        /// </summary>
+        private void EndDefer()
+        {
+            --_deferLevel;
+
+            if (_deferLevel == 0)
+            {
+                if (CheckFlag(CollectionViewFlags.IsUpdatePageSizeDeferred))
+                {
+                    SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, false);
+                    PageSize = _cachedPageSize;
+                }
+
+                if (CheckFlag(CollectionViewFlags.IsMoveToPageDeferred))
+                {
+                    SetFlag(CollectionViewFlags.IsMoveToPageDeferred, false);
+                    MoveToPage(_cachedPageIndex);
+                    _cachedPageIndex = -1;
+                }
+
+                if (CheckFlag(CollectionViewFlags.NeedsRefresh))
+                {
+                    Refresh();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Makes sure that the ItemConstructor is set for the correct type
+        /// </summary>
+        private void EnsureItemConstructor()
+        {
+            if (!_itemConstructorIsValid)
+            {
+                Type itemType = ItemType;
+                if (itemType != null)
+                {
+                    _itemConstructor = itemType.GetConstructor(Type.EmptyTypes);
+                    _itemConstructorIsValid = true;
+                }
+            }
+        }
+
+        /// <summary>
+        ///  If the IEnumerable has changed, bring the collection up to date.
+        ///  (This isn't necessary if the IEnumerable is also INotifyCollectionChanged
+        ///  because we keep the collection in sync incrementally.)
+        /// </summary>
+        private void EnsureCollectionInSync()
+        {
+            // if the IEnumerable is not a INotifyCollectionChanged
+            if (_pollForChanges)
+            {
+                try
+                {
+                    _trackingEnumerator.MoveNext();
+                }
+                catch (InvalidOperationException)
+                {
+                    // When the collection has been modified, calling MoveNext()
+                    // on the enumerator throws an InvalidOperationException, stating
+                    // that the collection has been modified. Therefore, we know when
+                    // to update our internal collection.
+                    _trackingEnumerator = SourceCollection.GetEnumerator();
+                    RefreshOrDefer();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Helper function used to determine the type of an item
+        /// </summary>
+        /// <param name="useRepresentativeItem">Whether we should use a representative item</param>
+        /// <returns>The type of the items in the collection</returns>
+        private Type GetItemType(bool useRepresentativeItem)
+        {
+            Type collectionType = SourceCollection.GetType();
+            Type[] interfaces = collectionType.GetInterfaces();
+
+            // Look for IEnumerable<T>.  All generic collections should implement
+            //   We loop through the interface list, rather than call
+            // GetInterface(IEnumerableT), so that we handle an ambiguous match
+            // (by using the first match) without an exception.
+            for (int i = 0; i < interfaces.Length; ++i)
+            {
+                Type interfaceType = interfaces[i];
+                if (interfaceType.Name == typeof(IEnumerable<>).Name)
+                {
+                    // found IEnumerable<>, extract T
+                    Type[] typeParameters = interfaceType.GetGenericArguments();
+                    if (typeParameters.Length == 1)
+                    {
+                        return typeParameters[0];
+                    }
+                }
+            }
+
+            // No generic information found.  Use a representative item instead.
+            if (useRepresentativeItem)
+            {
+                // get type of a representative item
+                object item = GetRepresentativeItem();
+                if (item != null)
+                {
+                    return item.GetType();
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets a representative item from the collection
+        /// </summary>
+        /// <returns>An item that can represent the collection</returns>
+        private object GetRepresentativeItem()
+        {
+            if (IsEmpty)
+            {
+                return null;
+            }
+
+            IEnumerator enumerator = GetEnumerator();
+            while (enumerator.MoveNext())
+            {
+                object item = enumerator.Current;
+                // Since this collection view does not support a NewItemPlaceholder, 
+                // simply return the first non-null item.
+                if (item != null)
+                {
+                    return item;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Return index of item in the internal list.
+        /// </summary>
+        /// <param name="item">The item we are checking</param>
+        /// <returns>Integer value on where in the InternalList the object is located</returns>
+        private int InternalIndexOf(object item)
+        {
+            return InternalList.IndexOf(item);
+        }
+
+        /// <summary>
+        /// Return item at the given index in the internal list.
+        /// </summary>
+        /// <param name="index">The index we are checking</param>
+        /// <returns>The item at the specified index</returns>
+        private object InternalItemAt(int index)
+        {
+            if (index >= 0 && index < InternalList.Count)
+            {
+                return InternalList[index];
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Ask listeners (via ICollectionView.CurrentChanging event) if it's OK to change currency
+        /// </summary>
+        /// <returns>False if a listener cancels the change, True otherwise</returns>
+        private bool OkToChangeCurrent()
+        {
+            DataGridCurrentChangingEventArgs args = new DataGridCurrentChangingEventArgs();
+            OnCurrentChanging(args);
+            return !args.Cancel;
+        }
+
+        /// <summary>
+        ///     Notify listeners that this View has changed
+        /// </summary>
+        /// <remarks>
+        ///     CollectionViews (and sub-classes) should take their filter/sort/grouping/paging
+        ///     into account before calling this method to forward CollectionChanged events.
+        /// </remarks>
+        /// <param name="args">
+        ///     The NotifyCollectionChangedEventArgs to be passed to the EventHandler
+        /// </param>
+        //TODO Paging
+        private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
+        {
+            if (args == null)
+            {
+                throw new ArgumentNullException(nameof(args));
+            }
+
+            unchecked
+            {
+                // invalidate enumerators because of a change
+                ++_timestamp;
+            }
+
+            if (CollectionChanged != null)
+            {
+                if (args.Action != NotifyCollectionChangedAction.Add || PageSize == 0 || args.NewStartingIndex < Count)
+                {
+                    CollectionChanged(this, args);
+                }
+            }
+
+            // Collection changes change the count unless an item is being
+            // replaced within the collection.
+            if (args.Action != NotifyCollectionChangedAction.Replace)
+            {
+                OnPropertyChanged(nameof(Count));
+            }
+
+            bool listIsEmpty = IsEmpty;
+            if (listIsEmpty != CheckFlag(CollectionViewFlags.CachedIsEmpty))
+            {
+                SetFlag(CollectionViewFlags.CachedIsEmpty, listIsEmpty);
+                OnPropertyChanged(nameof(IsEmpty));
+            }
+        }
+
+        /// <summary>
+        /// Raises the CurrentChanged event
+        /// </summary>
+        private void OnCurrentChanged()
+        {
+            if (CurrentChanged != null && _currentChangedMonitor.Enter())
+            {
+                using (_currentChangedMonitor)
+                {
+                    CurrentChanged(this, EventArgs.Empty);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Raise a CurrentChanging event that is not cancelable.
+        /// This is called by CollectionChanges (Add, Remove, and Refresh) that 
+        /// affect the CurrentItem.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// This CurrentChanging event cannot be canceled.
+        /// </exception>
+        private void OnCurrentChanging()
+        {
+            OnCurrentChanging(uncancelableCurrentChangingEventArgs);
+        }
+
+        /// <summary>
+        /// Raises the CurrentChanging event
+        /// </summary>
+        /// <param name="args">
+        ///     CancelEventArgs used by the consumer of the event.  args.Cancel will
+        ///     be true after this call if the CurrentItem should not be changed for
+        ///     any reason.
+        /// </param>
+        /// <exception cref="InvalidOperationException">
+        ///     This CurrentChanging event cannot be canceled.
+        /// </exception>
+        private void OnCurrentChanging(DataGridCurrentChangingEventArgs args)
+        {
+            if (args == null)
+            {
+                throw new ArgumentNullException(nameof(args));
+            }
+
+            if (_currentChangedMonitor.Busy)
+            {
+                if (args.IsCancelable)
+                {
+                    args.Cancel = true;
+                }
+
+                return;
+            }
+
+            CurrentChanging?.Invoke(this, args);
+        }
+
+        /// <summary>
+        /// GroupBy changed handler
+        /// </summary>
+        /// <param name="sender">CollectionViewGroup whose GroupBy has changed</param>
+        /// <param name="e">Arguments for the NotifyCollectionChanged event</param>
+        private void OnGroupByChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (IsAddingNew || IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping"));
+            }
+
+            RefreshOrDefer();
+        }
+
+        /// <summary>
+        /// GroupDescription changed handler
+        /// </summary>
+        /// <param name="sender">CollectionViewGroup whose GroupDescription has changed</param>
+        /// <param name="e">Arguments for the GroupDescriptionChanged event</param>
+        //TODO Paging
+        private void OnGroupDescriptionChanged(object sender, EventArgs e)
+        {
+            if (IsAddingNew || IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping"));
+            }
+
+            // we want to make sure that the data is refreshed before we try to move to a page
+            // since the refresh would take care of the filtering, sorting, and grouping.
+            RefreshOrDefer();
+
+            if (PageSize > 0)
+            {
+                if (IsRefreshDeferred)
+                {
+                    // set cached value and flag so that we move to first page on EndDefer
+                    _cachedPageIndex = 0;
+                    SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true);
+                }
+                else
+                {
+                    MoveToFirstPage();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Raises a PropertyChanged event.
+        /// </summary>
+        /// <param name="e">PropertyChangedEventArgs for this change</param>
+        private void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            PropertyChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Helper to raise a PropertyChanged event.
+        /// </summary>
+        /// <param name="propertyName">Property name for the property that changed</param>
+        private void OnPropertyChanged(string propertyName)
+        {
+            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+        }
+
+        /// <summary>
+        /// Sets up the ActiveComparer for the CollectionViewGroupRoot specified
+        /// </summary>
+        /// <param name="groupRoot">The CollectionViewGroupRoot</param>
+        private void PrepareGroupingComparer(CollectionViewGroupRoot groupRoot)
+        {
+            if (groupRoot == _temporaryGroup || PageSize == 0)
+            {
+                if (groupRoot.ActiveComparer is DataGridCollectionViewGroupInternal.ListComparer listComparer)
+                {
+                    listComparer.ResetList(InternalList);
+                }
+                else
+                {
+                    groupRoot.ActiveComparer = new DataGridCollectionViewGroupInternal.ListComparer(InternalList);
+                }
+            }
+            else if (groupRoot == _group)
+            {
+                // create the new comparer based on the current _temporaryGroup
+                groupRoot.ActiveComparer = new DataGridCollectionViewGroupInternal.CollectionViewGroupComparer(_temporaryGroup);
+            }
+        }
+
+        /// <summary>
+        /// Use the GroupDescriptions to place items into their respective groups.
+        /// This assumes that there is no paging, so we just group the entire collection
+        /// of items that the CollectionView holds.
+        /// </summary>
+        private void PrepareGroups()
+        {
+            // we should only use this method if we aren't paging
+            Debug.Assert(PageSize == 0, "Unexpected PageSize != 0");
+
+            _group.Clear();
+            _group.Initialize();
+
+            _group.IsDataInGroupOrder = CheckFlag(CollectionViewFlags.IsDataInGroupOrder);
+
+            // set to false so that we access internal collection items
+            // instead of the group items, as they have been cleared
+            _isGrouping = false;
+
+            if (_group.GroupDescriptions.Count > 0)
+            {
+                for (int num = 0, count = _internalList.Count; num < count; ++num)
+                {
+                    object item = _internalList[num];
+                    if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item)))
+                    {
+                        _group.AddToSubgroups(item, loading: true);
+                    }
+                }
+                if (IsAddingNew)
+                {
+                    _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true);
+                }
+            }
+
+            _isGrouping = _group.GroupBy != null;
+
+            // now we set the value to false, so that subsequent adds will insert
+            // into the correct groups.
+            _group.IsDataInGroupOrder = false;
+
+            // reset the grouping comparer
+            PrepareGroupingComparer(_group);
+        }
+
+        /// <summary>
+        /// Use the GroupDescriptions to place items into their respective groups.
+        /// Because of the fact that we have paging, it is possible that we are only
+        /// going to need a subset of the items to be displayed. However, before we 
+        /// actually group the entire collection, we can't display the items in the
+        /// correct order. We therefore want to just create a temporary group with
+        /// the entire collection, and then using this data we can create the group
+        /// that is exposed with just the items we need.
+        /// </summary>
+        private void PrepareTemporaryGroups()
+        {
+            _temporaryGroup = new CollectionViewGroupRoot(this, CheckFlag(CollectionViewFlags.IsDataInGroupOrder));
+
+            foreach (var gd in _group.GroupDescriptions)
+            {
+                _temporaryGroup.GroupDescriptions.Add(gd);
+            }
+
+            _temporaryGroup.Initialize();
+
+            // set to false so that we access internal collection items
+            // instead of the group items, as they have been cleared
+            _isGrouping = false;
+
+            if (_temporaryGroup.GroupDescriptions.Count > 0)
+            {
+                for (int num = 0, count = _internalList.Count; num < count; ++num)
+                {
+                    object item = _internalList[num];
+                    if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item)))
+                    {
+                        _temporaryGroup.AddToSubgroups(item, loading: true);
+                    }
+                }
+                if (IsAddingNew)
+                {
+                    _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, CurrentAddItem, true);
+                }
+            }
+
+            _isGrouping = _temporaryGroup.GroupBy != null;
+
+            // reset the grouping comparer
+            PrepareGroupingComparer(_temporaryGroup);
+        }
+
+        /// <summary>
+        /// Update our Groups private accessor to point to the subset of data
+        /// covered by the current page, or to display the entire group if paging is not
+        /// being used.
+        /// </summary>
+        //TODO Paging
+        private void PrepareGroupsForCurrentPage()
+        {
+            _group.Clear();
+            _group.Initialize();
+
+            // set to indicate that we will be pulling data from the temporary group data
+            _isUsingTemporaryGroup = true;
+
+            // since we are getting our data from the temporary group, it should
+            // already be in group order
+            _group.IsDataInGroupOrder = true;
+            _group.ActiveComparer = null;
+
+            if (GroupDescriptions.Count > 0)
+            {
+                for (int num = 0, count = Count; num < count; ++num)
+                {
+                    object item = GetItemAt(num);
+                    if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item)))
+                    {
+                        _group.AddToSubgroups(item, loading: true);
+                    }
+                }
+                if (IsAddingNew)
+                {
+                    _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true);
+                }
+            }
+
+            // set flag to indicate that we do not need to access the temporary data any longer
+            _isUsingTemporaryGroup = false;
+
+            // now we set the value to false, so that subsequent adds will insert
+            // into the correct groups.
+            _group.IsDataInGroupOrder = false;
+
+            // reset the grouping comparer
+            PrepareGroupingComparer(_group);
+
+            _isGrouping = _group.GroupBy != null;
+        }
+
+        /// <summary>
+        /// Create, filter and sort the local index array.
+        /// called from Refresh(), override in derived classes as needed.
+        /// </summary>
+        /// <param name="enumerable">new IEnumerable to associate this view with</param>
+        /// <returns>new local array to use for this view</returns>
+        private IList PrepareLocalArray(IEnumerable enumerable)
+        {
+            Debug.Assert(enumerable != null, "Input list to filter/sort should not be null");
+
+            // filter the collection's array into the local array
+            List<object> localList = new List<object>();
+
+            foreach (object item in enumerable)
+            {
+                if (Filter == null || PassesFilter(item))
+                {
+                    localList.Add(item);
+                }
+            }
+
+            // sort the local array
+            if (!CheckFlag(CollectionViewFlags.IsDataSorted) && SortDescriptions.Count > 0)
+            {
+                localList = SortList(localList);
+            }
+
+            return localList;
+        }
+
+        /// <summary>
+        /// Process an Add operation from an INotifyCollectionChanged event handler.
+        /// </summary>
+        /// <param name="addedItem">Item added to the source collection</param>
+        /// <param name="addIndex">Index item was added into</param>
+        //TODO Paging
+        private void ProcessAddEvent(object addedItem, int addIndex)
+        {
+            // item to fire remove notification for if necessary
+            object removeNotificationItem = null;
+            if (PageSize > 0 && !IsGrouping)
+            {
+                removeNotificationItem = (Count == PageSize) ?
+                    GetItemAt(PageSize - 1) : null;
+            }
+
+            // process the add by filtering and sorting the item
+            ProcessInsertToCollection(
+                addedItem,
+                addIndex);
+
+            // next check if we need to add an item into the current group
+            // bool needsGrouping = false;
+            if (Count == 1 && GroupDescriptions.Count > 0)
+            {
+                // if this is the first item being added
+                // we want to setup the groups with the
+                // correct element type comparer
+                if (PageSize > 0)
+                {
+                    PrepareGroupingComparer(_temporaryGroup);
+                }
+                PrepareGroupingComparer(_group);
+            }
+
+            if (IsGrouping)
+            {
+                int leafIndex = -1;
+
+                if (PageSize > 0)
+                {
+                    _temporaryGroup.AddToSubgroups(addedItem, false /*loading*/);
+                    leafIndex = _temporaryGroup.LeafIndexOf(addedItem);
+                }
+
+                // if we are not paging, we should just be able to add the item.
+                // otherwise, we need to validate that it is within the current page.
+                if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
+                {
+                    //needsGrouping = true;
+
+                    int pageStartIndex = PageIndex * PageSize;
+
+                    // if the item was inserted on a previous page
+                    if (pageStartIndex > leafIndex && PageSize > 0)
+                    {
+                        addedItem = _temporaryGroup.LeafAt(pageStartIndex);
+                    }
+
+                    // if we're grouping and have more items than the 
+                    // PageSize will allow, remove the last item
+                    if (PageSize > 0 && _group.ItemCount == PageSize)
+                    {
+                        removeNotificationItem = _group.LeafAt(PageSize - 1);
+                        _group.RemoveFromSubgroups(removeNotificationItem);
+                    }
+                }
+            }
+
+            // if we are paging, we may have to fire another notification for the item
+            // that needs to be removed for the one we added on this page.
+            if (PageSize > 0 && !OnLastLocalPage &&
+               (((IsGrouping && removeNotificationItem != null) ||
+               (!IsGrouping && (PageIndex + 1) * PageSize > InternalIndexOf(addedItem)))))
+            {
+                if (removeNotificationItem != null && removeNotificationItem != addedItem)
+                {
+                    AdjustCurrencyForRemove(PageSize - 1);
+
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Remove,
+                            removeNotificationItem,
+                            PageSize - 1));
+                }
+            }
+
+            int addedIndex = IndexOf(addedItem);
+
+            // if the item is within the current page
+            if (addedIndex >= 0)
+            {
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                AdjustCurrencyForAdd(null, addedIndex);
+
+                // fire add notification
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Add,
+                        addedItem,
+                        addedIndex));
+
+                RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+            }
+            else if (PageSize > 0)
+            {
+                // otherwise if the item was added into a previous page
+                int internalIndex = InternalIndexOf(addedItem);
+
+                if (internalIndex < ConvertToInternalIndex(0))
+                {
+                    // fire add notification for item pushed in
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Add,
+                            GetItemAt(0),
+                            0));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Process CollectionChanged event on source collection 
+        /// that implements INotifyCollectionChanged.
+        /// </summary>
+        /// <param name="args">
+        /// The NotifyCollectionChangedEventArgs to be processed.
+        /// </param>
+        private void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args)
+        {
+            // if we do not want to handle the CollectionChanged event, return
+            if (!CheckFlag(CollectionViewFlags.ShouldProcessCollectionChanged))
+            {
+                return;
+            }
+
+            if (args.Action == NotifyCollectionChangedAction.Reset)
+            {
+                // if we have no items now, clear our own internal list
+                if (!SourceCollection.GetEnumerator().MoveNext())
+                {
+                    _internalList.Clear();
+                }
+
+                // calling Refresh, will fire the collectionchanged event
+                RefreshOrDefer();
+                return;
+            }
+
+            object addedItem = args.NewItems?[0];
+            object removedItem = args.OldItems?[0];
+
+            // fire notifications for removes
+            if (args.Action == NotifyCollectionChangedAction.Remove ||
+                args.Action == NotifyCollectionChangedAction.Replace)
+            {
+                ProcessRemoveEvent(removedItem, args.Action == NotifyCollectionChangedAction.Replace);
+            }
+
+            // fire notifications for adds
+            if ((args.Action == NotifyCollectionChangedAction.Add ||
+                args.Action == NotifyCollectionChangedAction.Replace) &&
+                (Filter == null || PassesFilter(addedItem)))
+            {
+                ProcessAddEvent(addedItem, args.NewStartingIndex);
+            }
+            if (args.Action != NotifyCollectionChangedAction.Replace)
+            {
+                OnPropertyChanged(nameof(ItemCount));
+            }
+        }
+
+        /// <summary>
+        /// Process a Remove operation from an INotifyCollectionChanged event handler.
+        /// </summary>
+        /// <param name="removedItem">Item removed from the source collection</param>
+        /// <param name="isReplace">Whether this was part of a Replace operation</param>
+        //TODO Paging
+        private void ProcessRemoveEvent(object removedItem, bool isReplace)
+        {
+            int internalRemoveIndex = -1;
+
+            if (IsGrouping)
+            {
+                internalRemoveIndex = PageSize > 0 ? _temporaryGroup.LeafIndexOf(removedItem) :
+                    _group.LeafIndexOf(removedItem);
+            }
+            else
+            {
+                internalRemoveIndex = InternalIndexOf(removedItem);
+            }
+
+            int removeIndex = IndexOf(removedItem);
+
+            // remove the item from the collection
+            _internalList.Remove(removedItem);
+
+            // only fire the remove if it was removed from either the current page, or a previous page
+            bool needToRemove = (PageSize == 0 && removeIndex >= 0) || (internalRemoveIndex < (PageIndex + 1) * PageSize);
+
+            if (IsGrouping)
+            {
+                if (PageSize > 0)
+                {
+                    _temporaryGroup.RemoveFromSubgroups(removedItem);
+                }
+
+                if (needToRemove)
+                {
+                    _group.RemoveFromSubgroups(removeIndex >= 0 ? removedItem : _group.LeafAt(0));
+                }
+            }
+
+            if (needToRemove)
+            {
+                object oldCurrentItem = CurrentItem;
+                int oldCurrentPosition = CurrentPosition;
+                bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+                bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+                AdjustCurrencyForRemove(removeIndex);
+
+                // fire remove notification 
+                // if we removed from current page, remove from removeIndex,
+                // if we removed from previous page, remove first item (index=0)
+                OnCollectionChanged(
+                    new NotifyCollectionChangedEventArgs(
+                        NotifyCollectionChangedAction.Remove,
+                        removedItem,
+                        Math.Max(0, removeIndex)));
+
+                RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+                // if we removed all items from the current page,
+                // move to the previous page. we do not need to 
+                // fire additional notifications, as moving the page will
+                // trigger a reset.
+                if (NeedToMoveToPreviousPage && !isReplace)
+                {
+                    MoveToPreviousPage();
+                    return;
+                }
+
+                // if we are paging, we may have to fire another notification for the item
+                // that needs to replace the one we removed on this page.
+                if (PageSize > 0 && Count == PageSize)
+                {
+                    // we first need to add the item into the current group
+                    if (IsGrouping)
+                    {
+                        object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1);
+                        if (newItem != null)
+                        {
+                            _group.AddToSubgroups(newItem, false /*loading*/);
+                        }
+                    }
+
+                    // fire the add notification
+                    OnCollectionChanged(
+                        new NotifyCollectionChangedEventArgs(
+                            NotifyCollectionChangedAction.Add,
+                            GetItemAt(PageSize - 1),
+                            PageSize - 1));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Handles adding an item into the collection, and applying sorting, filtering, grouping, paging.
+        /// </summary>
+        /// <param name="item">Item to insert in the collection</param>
+        /// <param name="index">Index to insert item into</param>
+        private void ProcessInsertToCollection(object item, int index)
+        {
+            // first check to see if it passes the filter
+            if (Filter == null || PassesFilter(item))
+            {
+                if (SortDescriptions.Count > 0)
+                {
+                    var itemType = ItemType;
+                    foreach (var sort in SortDescriptions)
+                        sort.Initialize(itemType);
+
+                    // create the SortFieldComparer to use
+                    var sortFieldComparer = new MergedComparer(this);
+
+                    // check if the item would be in sorted order if inserted into the specified index
+                    // otherwise, calculate the correct sorted index
+                    if (index < 0 || /* if item was not originally part of list */
+                        (index > 0 && (sortFieldComparer.Compare(item, InternalItemAt(index - 1)) < 0)) || /* item has moved up in the list */
+                        ((index < InternalList.Count - 1) && (sortFieldComparer.Compare(item, InternalItemAt(index)) > 0))) /* item has moved down in the list */
+                    {
+                        index = sortFieldComparer.FindInsertIndex(item, _internalList);
+                    }
+                }
+
+                // make sure that the specified insert index is within the valid range
+                // otherwise, just add it to the end. the index can be set to an invalid
+                // value if the item was originally not in the collection, on a different
+                // page, or if it had been previously filtered out.
+                if (index < 0 || index > _internalList.Count)
+                {
+                    index = _internalList.Count;
+                }
+
+                _internalList.Insert(index, item);
+            }
+        }
+
+        /// <summary>
+        /// Raises Currency Change events
+        /// </summary>
+        /// <param name="fireChangedEvent">Whether to fire the CurrentChanged event even if the parameters have not changed</param>
+        /// <param name="oldCurrentItem">CurrentItem before processing changes</param>
+        /// <param name="oldCurrentPosition">CurrentPosition before processing changes</param>
+        /// <param name="oldIsCurrentBeforeFirst">IsCurrentBeforeFirst before processing changes</param>
+        /// <param name="oldIsCurrentAfterLast">IsCurrentAfterLast before processing changes</param>
+        private void RaiseCurrencyChanges(bool fireChangedEvent, object oldCurrentItem, int oldCurrentPosition, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast)
+        {
+            // fire events for currency changes
+            if (fireChangedEvent || CurrentItem != oldCurrentItem || CurrentPosition != oldCurrentPosition)
+            {
+                OnCurrentChanged();
+            }
+            if (CurrentItem != oldCurrentItem)
+            {
+                OnPropertyChanged(nameof(CurrentItem));
+            }
+            if (CurrentPosition != oldCurrentPosition)
+            {
+                OnPropertyChanged(nameof(CurrentPosition));
+            }
+            if (IsCurrentAfterLast != oldIsCurrentAfterLast)
+            {
+                OnPropertyChanged(nameof(IsCurrentAfterLast));
+            }
+            if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst)
+            {
+                OnPropertyChanged(nameof(IsCurrentBeforeFirst));
+            }
+        }
+
+        /// <summary>
+        /// Raises the PageChanged event
+        /// </summary>
+        private void RaisePageChanged()
+        {
+            PageChanged?.Invoke(this, EventArgs.Empty);
+        }
+
+        /// <summary>
+        /// Raises the PageChanging event
+        /// </summary>
+        /// <param name="newPageIndex">Index of the requested page</param>
+        /// <returns>True if the event is cancelled (e.Cancel was set to True), False otherwise</returns>
+        private bool RaisePageChanging(int newPageIndex)
+        {
+            EventHandler<PageChangingEventArgs> handler = PageChanging;
+            if (handler != null)
+            {
+                PageChangingEventArgs pageChangingEventArgs = new PageChangingEventArgs(newPageIndex);
+                handler(this, pageChangingEventArgs);
+                return pageChangingEventArgs.Cancel;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Will call RefreshOverride and clear the NeedsRefresh flag
+        /// </summary>
+        private void RefreshInternal()
+        {
+            RefreshOverride();
+            SetFlag(CollectionViewFlags.NeedsRefresh, false);
+        }
+
+        /// <summary>
+        /// Refresh, or mark that refresh is needed when defer cycle completes.
+        /// </summary>
+        private void RefreshOrDefer()
+        {
+            if (IsRefreshDeferred)
+            {
+                SetFlag(CollectionViewFlags.NeedsRefresh, true);
+            }
+            else
+            {
+                RefreshInternal();
+            }
+        }
+
+        /// <summary>
+        /// Re-create the view, using any SortDescriptions. 
+        /// Also updates currency information.
+        /// </summary>
+        //TODO Paging
+        private void RefreshOverride()
+        {
+            object oldCurrentItem = CurrentItem;
+            int oldCurrentPosition = CurrentPosition;
+            bool oldIsCurrentAfterLast = IsCurrentAfterLast;
+            bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst;
+
+            // set IsGrouping to false
+            _isGrouping = false;
+
+            // force currency off the collection (gives user a chance to save dirty information)
+            OnCurrentChanging();
+
+            // if there's no sort/filter/paging/grouping, just use the collection's array
+            if (UsesLocalArray)
+            {
+                try
+                {
+                    // apply filtering/sorting through the PrepareLocalArray method
+                    _internalList = PrepareLocalArray(_sourceCollection);
+
+                    // apply grouping
+                    if (PageSize == 0)
+                    {
+                        PrepareGroups();
+                    }
+                    else
+                    {
+                        PrepareTemporaryGroups();
+                        PrepareGroupsForCurrentPage();
+                    }
+                }
+                catch (TargetInvocationException e)
+                {
+                    // If there's an exception while invoking PrepareLocalArray,
+                    // we want to unwrap it and throw its inner exception
+                    if (e.InnerException != null)
+                    {
+                        throw e.InnerException;
+                    }
+                    else
+                    {
+                        throw;
+                    }
+                }
+            }
+            else
+            {
+                CopySourceToInternalList();
+            }
+
+            // check if PageIndex is still valid after filter/sort
+            if (PageSize > 0 &&
+                PageIndex > 0 &&
+                PageIndex >= PageCount)
+            {
+                MoveToPage(PageCount - 1);
+            }
+
+            // reset currency values
+            ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+
+            OnCollectionChanged(
+                new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Reset));
+
+            // now raise currency changes at the end
+            RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast);
+        }
+
+        /// <summary>
+        /// Set currency back to the previous value it had if possible. If the item is no longer in view
+        /// then either use the first item in the view, or if the list is empty, use null.
+        /// </summary>
+        /// <param name="oldCurrentItem">CurrentItem before processing changes</param>
+        /// <param name="oldIsCurrentBeforeFirst">IsCurrentBeforeFirst before processing changes</param>
+        /// <param name="oldIsCurrentAfterLast">IsCurrentAfterLast before processing changes</param>
+        private void ResetCurrencyValues(object oldCurrentItem, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast)
+        {
+            if (oldIsCurrentBeforeFirst || IsEmpty)
+            {
+                SetCurrent(null, -1);
+            }
+            else if (oldIsCurrentAfterLast)
+            {
+                SetCurrent(null, Count);
+            }
+            else
+            {
+                // try to set currency back to old current item
+                // if there are duplicates, use the position of the first matching item
+                int newPosition = IndexOf(oldCurrentItem);
+
+                // if the old current item is no longer in view
+                if (newPosition < 0)
+                {
+                    // if we are adding a new item, set it as the current item, otherwise, set it to null
+                    newPosition = 0;
+
+                    if (newPosition < Count)
+                    {
+                        SetCurrent(GetItemAt(newPosition), newPosition);
+                    }
+                    else if (!IsEmpty)
+                    {
+                        SetCurrent(GetItemAt(0), 0);
+                    }
+                    else
+                    {
+                        SetCurrent(null, -1);
+                    }
+                }
+                else
+                {
+                    SetCurrent(oldCurrentItem, newPosition);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Set CurrentItem and CurrentPosition, no questions asked!
+        /// </summary>
+        /// <remarks>
+        /// CollectionViews (and sub-classes) should use this method to update
+        /// the Current values.
+        /// </remarks>
+        /// <param name="newItem">New CurrentItem</param>
+        /// <param name="newPosition">New CurrentPosition</param>
+        private void SetCurrent(object newItem, int newPosition)
+        {
+            int count = (newItem != null) ? 0 : (IsEmpty ? 0 : Count);
+            SetCurrent(newItem, newPosition, count);
+        }
+
+        /// <summary>
+        /// Set CurrentItem and CurrentPosition, no questions asked!
+        /// </summary>
+        /// <remarks>
+        /// This method can be called from a constructor - it does not call
+        /// any virtuals.  The 'count' parameter is substitute for the real Count,
+        /// used only when newItem is null.
+        /// In that case, this method sets IsCurrentAfterLast to true if and only
+        /// if newPosition >= count.  This distinguishes between a null belonging
+        /// to the view and the dummy null when CurrentPosition is past the end.
+        /// </remarks>
+        /// <param name="newItem">New CurrentItem</param>
+        /// <param name="newPosition">New CurrentPosition</param>
+        /// <param name="count">Numbers of items in the collection</param>
+        private void SetCurrent(object newItem, int newPosition, int count)
+        {
+            if (newItem != null)
+            {
+                // non-null item implies position is within range.
+                // We ignore count - it's just a placeholder
+                SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, false);
+                SetFlag(CollectionViewFlags.IsCurrentAfterLast, false);
+            }
+            else if (count == 0)
+            {
+                // empty collection - by convention both flags are true and position is -1
+                SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true);
+                SetFlag(CollectionViewFlags.IsCurrentAfterLast, true);
+                newPosition = -1;
+            }
+            else
+            {
+                // null item, possibly within range.
+                SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, newPosition < 0);
+                SetFlag(CollectionViewFlags.IsCurrentAfterLast, newPosition >= count);
+            }
+
+            _currentItem = newItem;
+            _currentPosition = newPosition;
+        }
+
+        /// <summary>
+        /// Just move it. No argument check, no events, just move current to position.
+        /// </summary>
+        /// <param name="position">Position to move the current item to</param>
+        private void SetCurrentToPosition(int position)
+        {
+            if (position < 0)
+            {
+                SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true);
+                SetCurrent(null, -1);
+            }
+            else if (position >= Count)
+            {
+                SetFlag(CollectionViewFlags.IsCurrentAfterLast, true);
+                SetCurrent(null, Count);
+            }
+            else
+            {
+                SetFlag(CollectionViewFlags.IsCurrentBeforeFirst | CollectionViewFlags.IsCurrentAfterLast, false);
+                SetCurrent(GetItemAt(position), position);
+            }
+        }
+
+        /// <summary>
+        /// Sets the specified Flag(s)
+        /// </summary>
+        /// <param name="flags">Flags we want to set</param>
+        /// <param name="value">Value we want to set these flags to</param>
+        private void SetFlag(CollectionViewFlags flags, bool value)
+        {
+            if (value)
+            {
+                _flags = _flags | flags;
+            }
+            else
+            {
+                _flags = _flags & ~flags;
+            }
+        }
+
+        /// <summary>
+        /// Set new SortDescription collection; re-hook collection change notification handler
+        /// </summary>
+        /// <param name="descriptions">SortDescriptionCollection to set the property value to</param>
+        private void SetSortDescriptions(DataGridSortDescriptionCollection descriptions)
+        {
+            if (_sortDescriptions != null)
+            {
+                _sortDescriptions.CollectionChanged -= SortDescriptionsChanged;
+            }
+
+            _sortDescriptions = descriptions;
+
+            if (_sortDescriptions != null)
+            {
+                Debug.Assert(_sortDescriptions.Count == 0, "must be empty SortDescription collection");
+                _sortDescriptions.CollectionChanged += SortDescriptionsChanged;
+            }
+        }
+
+        /// <summary>
+        /// SortDescription was added/removed, refresh DataGridCollectionView
+        /// </summary>
+        /// <param name="sender">Sender that triggered this handler</param>
+        /// <param name="e">NotifyCollectionChangedEventArgs for this change</param>
+        private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (IsAddingNew || IsEditingItem)
+            {
+                throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Sorting"));
+            }
+
+            // we want to make sure that the data is refreshed before we try to move to a page
+            // since the refresh would take care of the filtering, sorting, and grouping.
+            RefreshOrDefer();
+
+            if (PageSize > 0)
+            {
+                if (IsRefreshDeferred)
+                {
+                    // set cached value and flag so that we move to first page on EndDefer
+                    _cachedPageIndex = 0;
+                    SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true);
+                }
+                else
+                {
+                    MoveToFirstPage();
+                }
+            }
+
+            OnPropertyChanged("SortDescriptions");
+        }
+
+        /// <summary>
+        /// Sort the List based on the SortDescriptions property.
+        /// </summary>
+        /// <param name="list">List of objects to sort</param>
+        /// <returns>The sorted list</returns>
+        private List<object> SortList(List<object> list)
+        {
+            Debug.Assert(list != null, "Input list to sort should not be null");
+
+            IEnumerable<object> seq = (IEnumerable<object>)list;
+            IComparer<object> comparer = new CultureSensitiveComparer(Culture);
+            var itemType = ItemType;
+
+            foreach (DataGridSortDescription sort in SortDescriptions)
+            {
+                sort.Initialize(itemType); 
+
+                if(seq is IOrderedEnumerable<object> orderedEnum)
+                {
+                    seq = sort.ThenBy(orderedEnum);
+                }
+                else
+                {
+                    seq = sort.OrderBy(seq);
+                }
+            }
+
+            return seq.ToList();
+        }
+
+        /// <summary>
+        /// Helper to validate that we are not in the middle of a DeferRefresh
+        /// and throw if that is the case.
+        /// </summary>
+        private void VerifyRefreshNotDeferred()
+        {
+            // If the Refresh is being deferred to change filtering or sorting of the
+            // data by this DataGridCollectionView, then DataGridCollectionView will not reflect the correct
+            // state of the underlying data.
+            if (IsRefreshDeferred)
+            {
+                throw new InvalidOperationException("Cannot change or check the contents or current position of the CollectionView while Refresh is being deferred.");
+            }
+        }
+
+        /// <summary>
+        /// Creates a comparer class that takes in a CultureInfo as a parameter,
+        /// which it will use when comparing strings.
+        /// </summary>
+        private class CultureSensitiveComparer : IComparer<object>
+        {
+            /// <summary>
+            /// Private accessor for the CultureInfo of our comparer
+            /// </summary>
+            private CultureInfo _culture;
+
+            /// <summary>
+            /// Creates a comparer which will respect the CultureInfo
+            /// that is passed in when comparing strings.
+            /// </summary>
+            /// <param name="culture">The CultureInfo to use in string comparisons</param>
+            public CultureSensitiveComparer(CultureInfo culture)
+                : base()
+            {
+                _culture = culture ?? CultureInfo.InvariantCulture;
+            }
+
+            /// <summary>
+            /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other.
+            /// </summary>
+            /// <param name="x">first item to compare</param>
+            /// <param name="y">second item to compare</param>
+            /// <returns>Negative number if x is less than y, zero if equal, and a positive number if x is greater than y</returns>
+            /// <remarks>
+            /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects.
+            /// </remarks>
+            public int Compare(object x, object y)
+            {
+                if (x == null)
+                {
+                    if (y != null)
+                    {
+                        return -1;
+                    }
+                    return 0;
+                }
+                if (y == null)
+                {
+                    return 1;
+                }
+
+                // at this point x and y are not null
+                if (x.GetType() == typeof(string) && y.GetType() == typeof(string))
+                {
+                    return _culture.CompareInfo.Compare((string)x, (string)y);
+                }
+                else
+                {
+                    return Comparer<object>.Default.Compare(x, y);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Used to keep track of Defer calls on the DataGridCollectionView, which
+        /// will prevent the user from calling Refresh() on the view. In order
+        /// to allow refreshes again, the user will have to call IDisposable.Dispose,
+        /// to end the Defer operation.
+        /// </summary>
+        private class DeferHelper : IDisposable
+        {
+            /// <summary>
+            /// Private reference to the CollectionView that created this DeferHelper
+            /// </summary>
+            private DataGridCollectionView collectionView;
+
+            /// <summary>
+            /// Initializes a new instance of the DeferHelper class
+            /// </summary>
+            /// <param name="collectionView">CollectionView that created this DeferHelper</param>
+            public DeferHelper(DataGridCollectionView collectionView)
+            {
+                this.collectionView = collectionView;
+            }
+
+            /// <summary>
+            /// Cleanup method called when done using this class
+            /// </summary>
+            public void Dispose()
+            {
+                if (collectionView != null)
+                {
+                    collectionView.EndDefer();
+                    collectionView = null;
+                }
+                GC.SuppressFinalize(this);
+            }
+        }
+
+        /// <summary>
+        /// A simple monitor class to help prevent re-entrant calls
+        /// </summary>
+        private class SimpleMonitor : IDisposable
+        {
+            /// <summary>
+            /// Whether the monitor is entered
+            /// </summary>
+            private bool entered;
+
+            /// <summary>
+            /// Gets a value indicating whether we have been entered or not
+            /// </summary>
+            public bool Busy
+            {
+                get { return entered; }
+            }
+
+            /// <summary>
+            /// Sets a value indicating that we have been entered
+            /// </summary>
+            /// <returns>Boolean value indicating whether we were already entered</returns>
+            public bool Enter()
+            {
+                if (entered)
+                {
+                    return false;
+                }
+
+                entered = true;
+                return true;
+            }
+
+            /// <summary>
+            /// Cleanup method called when done using this class
+            /// </summary>
+            public void Dispose()
+            {
+                entered = false;
+                GC.SuppressFinalize(this);
+            }
+        }
+
+        /// <summary>
+        /// IEnumerator generated using the new item taken into account
+        /// </summary>
+        private class NewItemAwareEnumerator : IEnumerator
+        {
+            private enum Position
+            {
+                /// <summary>
+                /// Whether the position is before the new item
+                /// </summary>
+                BeforeNewItem,
+
+                /// <summary>
+                /// Whether the position is on the new item that is being created
+                /// </summary>
+                OnNewItem,
+
+                /// <summary>
+                /// Whether the position is after the new item
+                /// </summary>
+                AfterNewItem
+            }
+
+            /// <summary>
+            /// Initializes a new instance of the NewItemAwareEnumerator class.
+            /// </summary>
+            /// <param name="collectionView">The DataGridCollectionView we are creating the enumerator for</param>
+            /// <param name="baseEnumerator">The baseEnumerator that we pass in</param>
+            /// <param name="newItem">The new item we are adding to the collection</param>
+            public NewItemAwareEnumerator(DataGridCollectionView collectionView, IEnumerator baseEnumerator, object newItem)
+            {
+                _collectionView = collectionView;
+                _timestamp = collectionView.Timestamp;
+                _baseEnumerator = baseEnumerator;
+                _newItem = newItem;
+            }
+
+            /// <summary>
+            /// Implements the MoveNext function for IEnumerable
+            /// </summary>
+            /// <returns>Whether we can move to the next item</returns>
+            public bool MoveNext()
+            {
+                if (_timestamp != _collectionView.Timestamp)
+                {
+                    throw new InvalidOperationException("Collection was modified; enumeration operation cannot execute.");
+                }
+
+                switch (_position)
+                {
+                    case Position.BeforeNewItem:
+                        if (_baseEnumerator.MoveNext() &&
+                                    (_newItem == null || _baseEnumerator.Current != _newItem
+                                            || _baseEnumerator.MoveNext()))
+                        {
+                            // advance base, skipping the new item
+                        }
+                        else if (_newItem != null)
+                        {
+                            // if base has reached the end, move to new item
+                            _position = Position.OnNewItem;
+                        }
+                        else
+                        {
+                            return false;
+                        }
+                        return true;
+                }
+
+                // in all other cases, simply advance base, skipping the new item
+                _position = Position.AfterNewItem;
+                return _baseEnumerator.MoveNext() &&
+                    (_newItem == null
+                        || _baseEnumerator.Current != _newItem
+                        || _baseEnumerator.MoveNext());
+            }
+
+            /// <summary>
+            /// Gets the Current value for IEnumerable
+            /// </summary>
+            public object Current
+            {
+                get
+                {
+                    return (_position == Position.OnNewItem) ? _newItem : _baseEnumerator.Current;
+                }
+            }
+
+            /// <summary>
+            /// Implements the Reset function for IEnumerable
+            /// </summary>
+            public void Reset()
+            {
+                _position = Position.BeforeNewItem;
+                _baseEnumerator.Reset();
+            }
+
+            /// <summary>
+            /// CollectionView that we are creating the enumerator for
+            /// </summary>
+            private DataGridCollectionView _collectionView;
+
+            /// <summary>
+            /// The Base Enumerator that we are passing in
+            /// </summary>
+            private IEnumerator _baseEnumerator;
+
+            /// <summary>
+            /// The position we are appending items to the enumerator
+            /// </summary>
+            private Position _position;
+
+            /// <summary>
+            /// Reference to any new item that we want to add to the collection
+            /// </summary>
+            private object _newItem;
+
+            /// <summary>
+            /// Timestamp to let us know whether there have been updates to the collection
+            /// </summary>
+            private int _timestamp;
+        }
+
+        internal class MergedComparer
+        {
+            private readonly IComparer<object>[] _comparers;
+
+            public MergedComparer(DataGridSortDescriptionCollection coll)
+            {
+                _comparers = MakeComparerArray(coll);
+            }
+            public MergedComparer(DataGridCollectionView collectionView)
+                : this(collectionView.SortDescriptions)
+            { }
+
+            private static IComparer<object>[] MakeComparerArray(DataGridSortDescriptionCollection coll)
+            {
+                return 
+                    coll.Select(c => c.Comparer)
+                        .ToArray();
+            }
+
+            /// <summary>
+            /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other.
+            /// </summary>
+            /// <param name="x">first item to compare</param>
+            /// <param name="y">second item to compare</param>
+            /// <returns>Negative number if x is less than y, zero if equal, and a positive number if x is greater than y</returns>
+            /// <remarks>
+            /// Compares the 2 items using the list of property names and directions.
+            /// </remarks>
+            public int Compare(object x, object y)
+            {
+                int result = 0;
+
+                // compare both objects by each of the properties until property values don't match
+                for (int k = 0; k < _comparers.Length; ++k)
+                {
+                    var comparer = _comparers[k];
+                    result = comparer.Compare(x, y);
+
+                    if (result != 0)
+                    {
+                        break;
+                    }
+                }
+
+                return result;
+            }
+
+            /// <summary>
+            /// Steps through the given list using the comparer to find where
+            /// to insert the specified item to maintain sorted order
+            /// </summary>
+            /// <param name="x">Item to insert into the list</param>
+            /// <param name="list">List where we want to insert the item</param>
+            /// <returns>Index where we should insert into</returns>
+            public int FindInsertIndex(object x, IList list)
+            {
+                int min = 0;
+                int max = list.Count - 1;
+                int index;
+
+                // run a binary search to find the right index
+                // to insert into.
+                while (min <= max)
+                {
+                    index = (min + max) / 2;
+
+                    int result = Compare(x, list[index]);
+                    if (result == 0)
+                    {
+                        return index;
+                    }
+                    else if (result > 0)
+                    {
+                        min = index + 1;
+                    }
+                    else
+                    {
+                        max = index - 1;
+                    }
+                }
+
+                return min;
+            }
+        }       
+    }
+}

+ 1366 - 0
src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs

@@ -0,0 +1,1366 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+using Avalonia.Controls;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Utilities;
+
+namespace Avalonia.Collections
+{
+    public abstract class DataGridGroupDescription : INotifyPropertyChanged
+    {
+        public AvaloniaList<object> GroupKeys { get; }
+
+        public DataGridGroupDescription()
+        {
+            GroupKeys = new AvaloniaList<object>();
+            GroupKeys.CollectionChanged += (sender, e) => OnPropertyChanged(new PropertyChangedEventArgs(nameof(GroupKeys)));
+        }
+
+        protected virtual event PropertyChangedEventHandler PropertyChanged;
+        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+        {
+            add
+            {
+                PropertyChanged += value;
+            }
+
+            remove
+            {
+                PropertyChanged -= value;
+            }
+        }
+        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            PropertyChanged?.Invoke(this, e);
+        }
+
+        public virtual string PropertyName => String.Empty;
+        public abstract object GroupKeyFromItem(object item, int level, CultureInfo culture);
+        public virtual bool KeysMatch(object groupKey, object itemKey)
+        {
+            return object.Equals(groupKey, itemKey);
+        }
+    }
+    public class DataGridPathGroupDescription : DataGridGroupDescription
+    {
+        private string _propertyPath;
+        private Type _propertyType;
+        private IValueConverter _valueConverter;
+        private StringComparison _stringComparison = StringComparison.Ordinal;
+
+        public DataGridPathGroupDescription(string propertyPath)
+        {
+            _propertyPath = propertyPath;
+        }
+
+        public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
+        {
+            object GetKey(object o)
+            {
+                if(o == null)
+                    return null;
+
+                if (_propertyType == null)
+                    _propertyType = GetPropertyType(o);
+
+                return InvokePath(o, _propertyPath, _propertyType);
+            }
+
+            var key = GetKey(item);
+            if (key == null)
+                key = item;
+
+            if (_valueConverter != null)
+                key = _valueConverter.Convert(key, typeof(object), level, culture);
+
+            return key;
+        }
+        public override bool KeysMatch(object groupKey, object itemKey)
+        {
+            if(groupKey is string k1 && itemKey is string k2)
+            {
+                return String.Equals(k1, k2, _stringComparison);
+            }
+            else
+                return base.KeysMatch(groupKey, itemKey);
+        }
+        public override string PropertyName => _propertyPath;
+
+        private Type GetPropertyType(object o)
+        {
+            return o.GetType().GetNestedPropertyType(_propertyPath);
+        }
+        private static object InvokePath(object item, string propertyPath, Type propertyType)
+        {
+            object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception);
+            if (exception != null)
+            {
+                throw exception;
+            }
+            return propertyValue;
+        }
+    }
+
+    public abstract class DataGridCollectionViewGroup : INotifyPropertyChanged
+    {
+        private int _itemCount;
+
+        public object Key { get; }
+        public int ItemCount => _itemCount;
+        public IAvaloniaReadOnlyList<object> Items => ProtectedItems;
+
+        protected AvaloniaList<object> ProtectedItems { get; }
+        protected int ProtectedItemCount
+        {
+            get { return _itemCount; }
+            set
+            {
+                _itemCount = value;
+                OnPropertyChanged(new PropertyChangedEventArgs(nameof(ItemCount)));
+            }
+        }
+
+        protected DataGridCollectionViewGroup(object key)
+        {
+            Key = key;
+            ProtectedItems = new AvaloniaList<object>();
+        }
+
+        public abstract bool IsBottomLevel { get; }
+
+        protected virtual event PropertyChangedEventHandler PropertyChanged;
+        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+        {
+            add
+            {
+                PropertyChanged += value;
+            }
+
+            remove
+            {
+                PropertyChanged -= value;
+            }
+        }
+        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            PropertyChanged?.Invoke(this, e);
+        }
+    }
+    internal class DataGridCollectionViewGroupInternal : DataGridCollectionViewGroup
+    {
+        /// <summary>
+        /// GroupDescription used to define how to group the items
+        /// </summary>
+        private DataGridGroupDescription _groupBy;
+
+        /// <summary>
+        /// Parent group of this CollectionViewGroupInternal
+        /// </summary>
+        private readonly DataGridCollectionViewGroupInternal _parentGroup;
+
+        /// <summary>
+        /// Used for detecting stale enumerators
+        /// </summary>
+        private int _version;
+
+        public DataGridCollectionViewGroupInternal(object key, DataGridCollectionViewGroupInternal parent)
+            : base(key)
+        {
+            _parentGroup = parent;
+        }
+
+        public override bool IsBottomLevel => _groupBy == null;
+
+        internal int FullCount { get; set; }
+
+        internal DataGridGroupDescription GroupBy
+        {
+            get { return _groupBy; }
+            set
+            {
+                bool oldIsBottomLevel = IsBottomLevel;
+
+                if (_groupBy != null)
+                {
+                    ((INotifyPropertyChanged)_groupBy).PropertyChanged -= OnGroupByChanged;
+                }
+
+                _groupBy = value;
+
+                if (_groupBy != null)
+                {
+                    ((INotifyPropertyChanged)_groupBy).PropertyChanged += OnGroupByChanged;
+                }
+
+                if (oldIsBottomLevel != IsBottomLevel)
+                {
+                    OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBottomLevel)));
+                }
+            }
+        }
+
+        private void OnGroupByChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+        {
+            OnGroupByChanged();
+        }
+        protected virtual void OnGroupByChanged()
+        {
+            _parentGroup?.OnGroupByChanged();
+        }
+
+        /// <summary>
+        /// Gets or sets the most recent index where activity took place
+        /// </summary>
+        internal int LastIndex { get; set; }
+
+        /// <summary>
+        /// Gets the first item (leaf) added to this group.  If this can't be determined,
+        /// DependencyProperty.UnsetValue.
+        /// </summary>
+        internal object SeedItem
+        {
+            get
+            {
+                if (ItemCount > 0 && (GroupBy == null || GroupBy.GroupKeys.Count == 0))
+                {
+                    // look for first item, child by child
+                    for (int k = 0, n = Items.Count; k < n; ++k)
+                    {
+                        if (!(Items[k] is DataGridCollectionViewGroupInternal subgroup))
+                        {
+                            // child is an item - return it
+                            return Items[k];
+                        }
+                        else if (subgroup.ItemCount > 0)
+                        {
+                            // child is a nonempty subgroup - ask it
+                            return subgroup.SeedItem;
+                        }
+                        //// otherwise child is an empty subgroup - go to next child
+                    }
+
+                    // we shouldn't get here, but just in case...
+
+                    return AvaloniaProperty.UnsetValue;
+                }
+                else
+                {
+                    // the group is empty, or it has explicit subgroups.
+                    // In either case, we cannot determine the first item -
+                    // it could have gone into any of the subgroups.
+                    return AvaloniaProperty.UnsetValue;
+                }
+            }
+        }
+
+        private DataGridCollectionViewGroupInternal Parent => _parentGroup;
+
+        /// <summary>
+        /// Adds the specified item to the collection
+        /// </summary>
+        /// <param name="item">Item to add</param>
+        internal void Add(object item)
+        {
+            ChangeCounts(item, +1);
+            ProtectedItems.Add(item);
+        }
+
+        /// <summary>
+        /// Clears the collection of items
+        /// </summary>
+        internal void Clear()
+        {
+            ProtectedItems.Clear();
+            FullCount = 1;
+            ProtectedItemCount = 0;
+        }
+
+        /// <summary>
+        /// Finds the index of the specified item
+        /// </summary>
+        /// <param name="item">Item we are looking for</param>
+        /// <param name="seed">Seed of the item we are looking for</param>
+        /// <param name="comparer">Comparer used to find the item</param>
+        /// <param name="low">Low range of item index</param>
+        /// <param name="high">High range of item index</param>
+        /// <returns>Index of the specified item</returns>
+        protected virtual int FindIndex(object item, object seed, IComparer comparer, int low, int high)
+        {
+            int index;
+
+            if (comparer != null)
+            {
+                if (comparer is ListComparer listComparer)
+                {
+                    // reset the IListComparer before each search. This cannot be done
+                    // any less frequently (e.g. in Root.AddToSubgroups), due to the
+                    // possibility that the item may appear in more than one subgroup.
+                    listComparer.Reset();
+                }
+
+                if (comparer is CollectionViewGroupComparer groupComparer)
+                {
+                    // reset the CollectionViewGroupComparer before each search. This cannot be done
+                    // any less frequently (e.g. in Root.AddToSubgroups), due to the
+                    // possibility that the item may appear in more than one subgroup.
+                    groupComparer.Reset();
+                }
+
+                for (index = low; index < high; ++index)
+                {
+                    object seed1 = (ProtectedItems[index] is DataGridCollectionViewGroupInternal subgroup) ? subgroup.SeedItem : ProtectedItems[index];
+                    if (seed1 == AvaloniaProperty.UnsetValue)
+                    {
+                        continue;
+                    }
+                    if (comparer.Compare(seed, seed1) < 0)
+                    {
+                        break;
+                    }
+                }
+            }
+            else
+            {
+                index = high;
+            }
+
+            return index;
+        }
+
+        /// <summary>
+        /// Returns an enumerator over the leaves governed by this group
+        /// </summary>
+        /// <returns>Enumerator of leaves</returns>
+        internal IEnumerator GetLeafEnumerator()
+        {
+            return new LeafEnumerator(this);
+        }
+
+        /// <summary>
+        /// Insert a new item or subgroup and return its index.  Seed is a
+        /// representative from the subgroup (or the item itself) that
+        /// is used to position the new item/subgroup w.r.t. the order given
+        /// by the comparer. (If comparer is null, just add at the end).
+        /// </summary>
+        /// <param name="item">Item we are looking for</param>
+        /// <param name="seed">Seed of the item we are looking for</param>
+        /// <param name="comparer">Comparer used to find the item</param>
+        /// <returns>The index where the item was inserted</returns>
+        internal int Insert(object item, object seed, IComparer comparer)
+        {
+            // never insert the new item/group before the explicit subgroups
+            int low = (GroupBy == null) ? 0 : GroupBy.GroupKeys.Count;
+            int index = FindIndex(item, seed, comparer, low, ProtectedItems.Count);
+
+            // now insert the item
+            ChangeCounts(item, +1);
+            ProtectedItems.Insert(index, item);
+
+            return index;
+        }
+
+        /// <summary>
+        /// Return the item at the given index within the list of leaves governed
+        /// by this group
+        /// </summary>
+        /// <param name="index">Index of the leaf</param>
+        /// <returns>Item at given index</returns>
+        internal object LeafAt(int index)
+        {
+            for (int k = 0, n = Items.Count; k < n; ++k)
+            {
+                if (Items[k] is DataGridCollectionViewGroupInternal subgroup)
+                {
+                    // current item is a group - either drill in, or skip over
+                    if (index < subgroup.ItemCount)
+                    {
+                        return subgroup.LeafAt(index);
+                    }
+                    else
+                    {
+                        index -= subgroup.ItemCount;
+                    }
+                }
+                else
+                {
+                    // current item is a leaf - see if we're done
+                    if (index == 0)
+                    {
+                        return Items[k];
+                    }
+                    else
+                    {
+                        index -= 1;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Returns the index of the given item within the list of leaves governed
+        /// by the full group structure.  The item must be a (direct) child of this
+        /// group.  The caller provides the index of the item within this group,
+        /// if known, or -1 if not.
+        /// </summary>
+        /// <param name="item">Item we are looking for</param>
+        /// <param name="index">Index of the leaf</param>
+        /// <returns>Number of items under that leaf</returns>
+        internal int LeafIndexFromItem(object item, int index)
+        {
+            int result = 0;
+
+            // accumulate the number of predecessors at each level
+            for (DataGridCollectionViewGroupInternal group = this;
+                    group != null;
+                    item = group, group = group.Parent, index = -1)
+            {
+                // accumulate the number of predecessors at the level of item
+                for (int k = 0, n = group.Items.Count; k < n; ++k)
+                {
+                    // if we've reached the item, move up to the next level
+                    if ((index < 0 && Object.Equals(item, group.Items[k])) ||
+                        index == k)
+                    {
+                        break;
+                    }
+
+                    // accumulate leaf count
+                    DataGridCollectionViewGroupInternal subgroup = group.Items[k] as DataGridCollectionViewGroupInternal;
+                    result += subgroup?.ItemCount ?? 1;
+                }
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Returns the index of the given item within the list of leaves governed
+        /// by this group
+        /// </summary>
+        /// <param name="item">Item we are looking for</param>
+        /// <returns>Number of items under that leaf</returns>
+        internal int LeafIndexOf(object item)
+        {
+            int leaves = 0;         // number of leaves we've passed over so far
+            for (int k = 0, n = Items.Count; k < n; ++k)
+            {
+                if (Items[k] is DataGridCollectionViewGroupInternal subgroup)
+                {
+                    int subgroupIndex = subgroup.LeafIndexOf(item);
+                    if (subgroupIndex < 0)
+                    {
+                        leaves += subgroup.ItemCount;       // item not in this subgroup
+                    }
+                    else
+                    {
+                        return leaves + subgroupIndex;    // item is in this subgroup
+                    }
+                }
+                else
+                {
+                    // current item is a leaf - compare it directly
+                    if (Object.Equals(item, Items[k]))
+                    {
+                        return leaves;
+                    }
+                    else
+                    {
+                        leaves += 1;
+                    }
+                }
+            }
+
+            // item not found
+            return -1;
+        }
+
+        /// <summary>
+        /// Removes the specified item from the collection
+        /// </summary>
+        /// <param name="item">Item to remove</param>
+        /// <param name="returnLeafIndex">Whether we want to return the leaf index</param>
+        /// <returns>Leaf index where item was removed, if value was specified. Otherwise '-1'</returns>
+        internal int Remove(object item, bool returnLeafIndex)
+        {
+            int index = -1;
+            int localIndex = ProtectedItems.IndexOf(item);
+
+            if (localIndex >= 0)
+            {
+                if (returnLeafIndex)
+                {
+                    index = LeafIndexFromItem(null, localIndex);
+                }
+
+                ChangeCounts(item, -1);
+                ProtectedItems.RemoveAt(localIndex);
+            }
+
+            return index;
+        }
+
+        /// <summary>
+        /// Removes an empty group from the PagedCollectionView grouping
+        /// </summary>
+        /// <param name="group">Empty subgroup to remove</param>
+        private static void RemoveEmptyGroup(DataGridCollectionViewGroupInternal group)
+        {
+            DataGridCollectionViewGroupInternal parent = group.Parent;
+
+            if (parent != null)
+            {
+                DataGridGroupDescription groupBy = parent.GroupBy;
+                int index = parent.ProtectedItems.IndexOf(group);
+
+                // remove the subgroup unless it is one of the explicit groups
+                if (index >= groupBy.GroupKeys.Count)
+                {
+                    parent.Remove(group, false);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Update the item count of the CollectionViewGroup
+        /// </summary>
+        /// <param name="item">CollectionViewGroup to update</param>
+        /// <param name="delta">Delta to change count by</param>
+        protected void ChangeCounts(object item, int delta)
+        {
+            bool changeLeafCount = !(item is DataGridCollectionViewGroup);
+
+            for (DataGridCollectionViewGroupInternal group = this;
+                    group != null;
+                    group = group._parentGroup)
+            {
+                group.FullCount += delta;
+                if (changeLeafCount)
+                {
+                    group.ProtectedItemCount += delta;
+
+                    if (group.ProtectedItemCount == 0)
+                    {
+                        RemoveEmptyGroup(group);
+                    }
+                }
+            }
+
+            unchecked
+            {
+                // this invalidates enumerators
+                ++_version;
+            }
+        }
+
+        /// <summary>
+        /// Enumerator for the leaves in the CollectionViewGroupInternal class.
+        /// </summary>
+        private class LeafEnumerator : IEnumerator
+        {
+            private object _current;   // current item
+            private DataGridCollectionViewGroupInternal _group; // parent group
+            private int _index;     // current index into Items
+            private IEnumerator _subEnum;   // enumerator over current subgroup
+            private int _version;   // parent group's version at ctor
+
+            /// <summary>
+            /// Initializes a new instance of the LeafEnumerator class.
+            /// </summary>
+            /// <param name="group">CollectionViewGroupInternal that uses the enumerator</param>
+            public LeafEnumerator(DataGridCollectionViewGroupInternal group)
+            {
+                _group = group;
+                DoReset();  // don't call virtual Reset in ctor
+            }
+
+            /// <summary>
+            /// Private helper to reset the enumerator
+            /// </summary>
+            private void DoReset()
+            {
+                Debug.Assert(_group != null, "_group should have been initialized in constructor");
+                _version = _group._version;
+                _index = -1;
+                _subEnum = null;
+            }
+
+            /// <summary>
+            /// Reset implementation for IEnumerator
+            /// </summary>
+            void IEnumerator.Reset()
+            {
+                DoReset();
+            }
+
+            /// <summary>
+            /// MoveNext implementation for IEnumerator
+            /// </summary>
+            /// <returns>Returns whether the MoveNext operation was successful</returns>
+            bool IEnumerator.MoveNext()
+            {
+                Debug.Assert(_group != null, "_group should have been initialized in constructor");
+
+                // check for invalidated enumerator
+                if (_group._version != _version)
+                {
+                    throw new InvalidOperationException();
+                }
+
+                // move forward to the next leaf
+                while (_subEnum == null || !_subEnum.MoveNext())
+                {
+                    // done with the current top-level item.  Move to the next one.
+                    ++_index;
+                    if (_index >= _group.Items.Count)
+                    {
+                        return false;
+                    }
+
+                    DataGridCollectionViewGroupInternal subgroup = _group.Items[_index] as DataGridCollectionViewGroupInternal;
+                    if (subgroup == null)
+                    {
+                        // current item is a leaf - it's the new Current
+                        _current = _group.Items[_index];
+                        _subEnum = null;
+                        return true;
+                    }
+                    else
+                    {
+                        // current item is a subgroup - get its enumerator
+                        _subEnum = subgroup.GetLeafEnumerator();
+                    }
+                }
+
+                // the loop terminates only when we have a subgroup enumerator
+                // positioned at the new Current item
+                _current = _subEnum.Current;
+                return true;
+            }
+
+            /// <summary>
+            /// Gets the current implementation for IEnumerator
+            /// </summary>
+            object IEnumerator.Current
+            {
+                get
+                {
+                    Debug.Assert(_group != null, "_group should have been initialized in constructor");
+
+                    if (_index < 0 || _index >= _group.Items.Count)
+                    {
+                        throw new InvalidOperationException();
+                    }
+
+                    return _current;
+                }
+            }
+
+        }
+
+        // / <summary>
+        // / This comparer is used to insert an item into a group in a position consistent
+        // / with a given IList.  It only works when used in the pattern that FindIndex
+        // / uses, namely first call Reset(), then call Compare(item, itemSequence) any number of
+        // / times with the same item (the new item) as the first argument, and a sequence
+        // / of items as the second argument that appear in the IList in the same sequence.
+        // / This makes the total search time linear in the size of the IList.  (To give
+        // / the correct answer regardless of the sequence of arguments would involve
+        // / calling IndexOf and leads to O(N^2) total search time.) 
+        // / </summary>
+        internal class ListComparer : IComparer
+        {
+            /// <summary>
+            /// Constructor for the ListComparer that takes
+            /// in an IList.
+            /// </summary>
+            /// <param name="list">IList used to compare on</param>
+            internal ListComparer(IList list)
+            {
+                ResetList(list);
+            }
+
+            /// <summary>
+            /// Sets the index that we start comparing
+            /// from to 0.
+            /// </summary>
+            internal void Reset()
+            {
+                _index = 0;
+            }
+
+            /// <summary>
+            /// Sets our IList to a new instance
+            /// of a list being passed in and resets
+            /// the index.
+            /// </summary>
+            /// <param name="list">IList used to compare on</param>
+            internal void ResetList(IList list)
+            {
+                _list = list;
+                _index = 0;
+            }
+
+            /// <summary>
+            /// Compares objects x and y to see which one
+            /// should appear first.
+            /// </summary>
+            /// <param name="x">The first object</param>
+            /// <param name="y">The second object</param>
+            /// <returns>-1 if x is less than y, +1 otherwise</returns>
+            public int Compare(object x, object y)
+            {
+                if (Object.Equals(x, y))
+                {
+                    return 0;
+                }
+
+                // advance the index until seeing one x or y
+                int n = (_list != null) ? _list.Count : 0;
+                for (; _index < n; ++_index)
+                {
+                    object z = _list[_index];
+                    if (Object.Equals(x, z))
+                    {
+                        return -1;  // x occurs first, so x < y
+                    }
+                    else if (Object.Equals(y, z))
+                    {
+                        return +1;  // y occurs first, so x > y
+                    }
+                }
+
+                // if we don't see either x or y, declare x > y.
+                // This has the effect of putting x at the end of the list.
+                return +1;
+            }
+
+            private int _index;
+            private IList _list;
+        }
+
+        // / <summary>
+        // / This comparer is used to insert an item into a group in a position consistent
+        // / with a given CollectionViewGroupRoot. We will only use this when dealing with
+        // / a temporary CollectionViewGroupRoot that points to the correct grouping of the
+        // / entire collection, and we have paging that requires us to keep the paged group
+        // / consistent with the order of items in the temporary group.
+        // / </summary>
+        internal class CollectionViewGroupComparer : IComparer
+        {
+            /// <summary>
+            /// Constructor for the CollectionViewGroupComparer that takes
+            /// in an CollectionViewGroupRoot.
+            /// </summary>
+            /// <param name="group">CollectionViewGroupRoot used to compare on</param>
+            internal CollectionViewGroupComparer(CollectionViewGroupRoot group)
+            {
+                ResetGroup(group);
+            }
+
+            /// <summary>
+            /// Sets the index that we start comparing
+            /// from to 0.
+            /// </summary>
+            internal void Reset()
+            {
+                _index = 0;
+            }
+
+            /// <summary>
+            /// Sets our group to a new instance of a
+            /// CollectionViewGroupRoot being passed in
+            /// and resets the index.
+            /// </summary>
+            /// <param name="group">CollectionViewGroupRoot used to compare on</param>
+            internal void ResetGroup(CollectionViewGroupRoot group)
+            {
+                _group = group;
+                _index = 0;
+            }
+
+            /// <summary>
+            /// Compares objects x and y to see which one
+            /// should appear first.
+            /// </summary>
+            /// <param name="x">The first object</param>
+            /// <param name="y">The second object</param>
+            /// <returns>-1 if x is less than y, +1 otherwise</returns>
+            public int Compare(object x, object y)
+            {
+                if (Object.Equals(x, y))
+                {
+                    return 0;
+                }
+
+                // advance the index until seeing one x or y
+                int n = (_group != null) ? _group.ItemCount : 0;
+                for (; _index < n; ++_index)
+                {
+                    object z = _group.LeafAt(_index);
+                    if (Object.Equals(x, z))
+                    {
+                        return -1;  // x occurs first, so x < y
+                    }
+                    else if (Object.Equals(y, z))
+                    {
+                        return +1;  // y occurs first, so x > y
+                    }
+                }
+
+                // if we don't see either x or y, declare x > y.
+                // This has the effect of putting x at the end of the list.
+                return +1;
+            }
+
+            private int _index;
+            private CollectionViewGroupRoot _group;
+        }
+
+    }
+
+    internal class CollectionViewGroupRoot : DataGridCollectionViewGroupInternal, INotifyCollectionChanged
+    {
+        /// <summary>
+        /// String constant used for the Root Name
+        /// </summary>
+        private const string RootName = "Root";
+
+        /// <summary>
+        /// Private accessor for empty object instance
+        /// </summary>
+        private static readonly object UseAsItemDirectly = new object();
+
+        /// <summary>
+        /// Private accessor for the top level GroupDescription
+        /// </summary>
+        private static DataGridGroupDescription topLevelGroupDescription;
+
+        /// <summary>
+        /// Private accessor for an ObservableCollection containing group descriptions
+        /// </summary>
+        private readonly AvaloniaList<DataGridGroupDescription> _groupBy = new AvaloniaList<DataGridGroupDescription>();
+
+        /// <summary>
+        /// Indicates whether the list of items (after applying the sort and filters, if any) 
+        /// is already in the correct order for grouping.
+        /// </summary>
+        private bool _isDataInGroupOrder;
+
+        /// <summary>
+        /// Private accessor for the owning ICollectionView
+        /// </summary>
+        private readonly IDataGridCollectionView _view;
+
+        /// <summary>
+        /// Raise this event when the (grouped) view changes
+        /// </summary>
+        public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+        /// <summary>
+        /// Raise this event when the GroupDescriptions change
+        /// </summary>
+        internal event EventHandler GroupDescriptionChanged;
+
+        /// <summary>
+        /// Initializes a new instance of the CollectionViewGroupRoot class.
+        /// </summary>
+        /// <param name="view">CollectionView that contains this grouping</param>
+        /// <param name="isDataInGroupOrder">True if items are already in correct order for grouping</param>
+        internal CollectionViewGroupRoot(IDataGridCollectionView view, bool isDataInGroupOrder)
+            : base(RootName, null)
+        {
+            _view = view;
+            _isDataInGroupOrder = isDataInGroupOrder;
+        }
+
+        /// <summary>
+        /// Gets the description of grouping, indexed by level.
+        /// </summary>
+        public virtual AvaloniaList<DataGridGroupDescription> GroupDescriptions => _groupBy;
+
+        /// <summary>
+        /// Gets or sets the current IComparer being used
+        /// </summary>
+        internal IComparer ActiveComparer { get; set; }
+
+        /// <summary>
+        /// Gets the culture to use during sorting.
+        /// </summary>
+        internal CultureInfo Culture
+        {
+            get
+            {
+                Debug.Assert(_view != null, "this._view should have been set from the constructor");
+                return _view.Culture;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the data is in group order
+        /// </summary>
+        internal bool IsDataInGroupOrder
+        {
+            get { return _isDataInGroupOrder; }
+            set { _isDataInGroupOrder = value; }
+        }
+
+        /// <summary>
+        /// Finds the index of the specified item
+        /// </summary>
+        /// <param name="item">Item we are looking for</param>
+        /// <param name="seed">Seed of the item we are looking for</param>
+        /// <param name="comparer">Comparer used to find the item</param>
+        /// <param name="low">Low range of item index</param>
+        /// <param name="high">High range of item index</param>
+        /// <returns>Index of the specified item</returns>
+        protected override int FindIndex(object item, object seed, IComparer comparer, int low, int high)
+        {
+            // root group needs to adjust the bounds of the search to exclude the new item (if any)
+            if (_view is IDataGridEditableCollectionView iecv && iecv.IsAddingNew)
+            {
+                --high;
+            }
+
+            return base.FindIndex(item, seed, comparer, low, high);
+        }
+
+        /// <summary>
+        /// Initializes the group descriptions
+        /// </summary>
+        internal void Initialize()
+        {
+            if (topLevelGroupDescription == null)
+            {
+                topLevelGroupDescription = new TopLevelGroupDescription();
+            }
+
+            InitializeGroup(this, 0, null);
+        }
+
+        /// <summary>
+        /// Inserts specified item into the collection
+        /// </summary>
+        /// <param name="index">Index to insert into</param>
+        /// <param name="item">Item to insert</param>
+        /// <param name="loading">Whether we are currently loading</param>
+        internal void InsertSpecialItem(int index, object item, bool loading)
+        {
+            ChangeCounts(item, +1);
+            ProtectedItems.Insert(index, item);
+
+            if (!loading)
+            {
+                int globalIndex = LeafIndexFromItem(item, index);
+                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, globalIndex));
+            }
+        }
+
+        /// <summary>
+        /// Notify listeners that this View has changed
+        /// </summary>
+        /// <remarks>
+        /// CollectionViews (and sub-classes) should take their filter/sort/grouping
+        /// into account before calling this method to forward CollectionChanged events.
+        /// </remarks>
+        /// <param name="args">The NotifyCollectionChangedEventArgs to be passed to the EventHandler</param>
+        public void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
+        {
+            Debug.Assert(args != null, "Arguments passed in should not be null");
+            CollectionChanged?.Invoke(this, args);
+        }
+
+        /// <summary>
+        /// Notify host that a group description has changed somewhere in the tree
+        /// </summary>
+        protected override void OnGroupByChanged()
+        {
+            GroupDescriptionChanged?.Invoke(this, EventArgs.Empty);
+        }
+
+        /// <summary>
+        /// Remove specified item from subgroups
+        /// </summary>
+        /// <param name="item">Item to remove</param>
+        /// <returns>Whether the operation was successful</returns>
+        internal bool RemoveFromSubgroups(object item)
+        {
+            return RemoveFromSubgroups(item, this, 0);
+        }
+
+        /// <summary>
+        /// Remove specified item from subgroups using an exhaustive search
+        /// </summary>
+        /// <param name="item">Item to remove</param>
+        internal void RemoveItemFromSubgroupsByExhaustiveSearch(object item)
+        {
+            RemoveItemFromSubgroupsByExhaustiveSearch(this, item);
+        }
+
+        /// <summary>
+        /// Removes specified item into the collection
+        /// </summary>
+        /// <param name="index">Index to remove from</param>
+        /// <param name="item">Item to remove</param>
+        /// <param name="loading">Whether we are currently loading</param>
+        internal void RemoveSpecialItem(int index, object item, bool loading)
+        {
+            Debug.Assert(Object.Equals(item, ProtectedItems[index]), "RemoveSpecialItem finds inconsistent data");
+            int globalIndex = -1;
+
+            if (!loading)
+            {
+                globalIndex = LeafIndexFromItem(item, index);
+            }
+
+            ChangeCounts(item, -1);
+            ProtectedItems.RemoveAt(index);
+
+            if (!loading)
+            {
+                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, globalIndex));
+            }
+        }
+
+        /// <summary>
+        /// Adds specified item to subgroups
+        /// </summary>
+        /// <param name="item">Item to add</param>
+        /// <param name="loading">Whether we are currently loading</param>
+        internal void AddToSubgroups(object item, bool loading)
+        {
+            AddToSubgroups(item, this, 0, loading);
+        }
+
+        /// <summary>
+        /// Add an item to the subgroup with the given name
+        /// </summary>
+        /// <param name="item">Item to add</param>
+        /// <param name="group">Group to add item to</param>
+        /// <param name="level">The level of grouping.</param>
+        /// <param name="key">Name of subgroup to add to</param>
+        /// <param name="loading">Whether we are currently loading</param>
+        private void AddToSubgroup(object item, DataGridCollectionViewGroupInternal group, int level, object key, bool loading)
+        {
+            DataGridCollectionViewGroupInternal subgroup;
+            int index = (_isDataInGroupOrder) ? group.LastIndex : 0;
+
+            // find the desired subgroup
+            for (int n = group.Items.Count; index < n; ++index)
+            {
+                subgroup = group.Items[index] as DataGridCollectionViewGroupInternal;
+                if (subgroup == null)
+                {
+                    continue;           // skip children that are not groups
+                }
+
+                if (group.GroupBy.KeysMatch(subgroup.Key, key))
+                {
+                    group.LastIndex = index;
+                    AddToSubgroups(item, subgroup, level + 1, loading);
+                    return;
+                }
+            }
+
+            // the item didn't match any subgroups.  Create a new subgroup and add the item.
+            subgroup = new DataGridCollectionViewGroupInternal(key, group);
+            InitializeGroup(subgroup, level + 1, item);
+
+            if (loading)
+            {
+                group.Add(subgroup);
+                group.LastIndex = index;
+            }
+            else
+            {
+                // using insert will find the correct sort index to
+                // place the subgroup, and will default to the last
+                // position if no ActiveComparer is specified
+                group.Insert(subgroup, item, ActiveComparer);
+            }
+
+            AddToSubgroups(item, subgroup, level + 1, loading);
+        }
+
+        /// <summary>
+        /// Add an item to the desired subgroup(s) of the given group
+        /// </summary>
+        /// <param name="item">Item to add</param>
+        /// <param name="group">Group to add item to</param>
+        /// <param name="level">The level of grouping</param>
+        /// <param name="loading">Whether we are currently loading</param>
+        private void AddToSubgroups(object item, DataGridCollectionViewGroupInternal group, int level, bool loading)
+        {
+            object key = GetGroupKey(item, group.GroupBy, level);
+
+            if (key == UseAsItemDirectly)
+            {
+                // the item belongs to the group itself (not to any subgroups)
+                if (loading)
+                {
+                    group.Add(item);
+                }
+                else
+                {
+                    int localIndex = group.Insert(item, item, ActiveComparer);
+                    int index = group.LeafIndexFromItem(item, localIndex);
+                    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
+                }
+            }
+            else if(key is ICollection keyList)
+            {
+                // the item belongs to multiple subgroups
+                foreach (object o in keyList)
+                {
+                    AddToSubgroup(item, group, level, o, loading);
+                }
+            }
+            else
+            {
+                // the item belongs to one subgroup
+                AddToSubgroup(item, group, level, key, loading);
+            }
+        }
+
+        public virtual Func<DataGridCollectionViewGroup, int, DataGridGroupDescription> GroupBySelector { get; set; }
+
+        /// <summary>
+        /// Returns the description of how to divide the given group into subgroups
+        /// </summary>
+        /// <param name="group">CollectionViewGroup to get group description from</param>
+        /// <param name="level">The level of grouping</param>
+        /// <returns>GroupDescription of how to divide the given group</returns>
+        private DataGridGroupDescription GetGroupDescription(DataGridCollectionViewGroup group, int level)
+        {
+            DataGridGroupDescription result = null;
+            if (group == this)
+            {
+                group = null;
+            }
+
+            if (result == null && GroupBySelector != null)
+            {
+                result = GroupBySelector?.Invoke(group, level);
+            }
+
+            if (result == null && level < GroupDescriptions.Count)
+            {
+                result = GroupDescriptions[level];
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Get the group name(s) for the given item
+        /// </summary>
+        /// <param name="item">Item to get group name for</param>
+        /// <param name="groupDescription">GroupDescription for the group</param>
+        /// <param name="level">The level of grouping</param>
+        /// <returns>Group names for the specified item</returns>
+        private object GetGroupKey(object item, DataGridGroupDescription groupDescription, int level)
+        {
+            if (groupDescription != null)
+            {
+                return groupDescription.GroupKeyFromItem(item, level, Culture);
+            }
+            else
+            {
+                return UseAsItemDirectly;
+            }
+        }
+
+        /// <summary>
+        /// Initialize the given group
+        /// </summary>
+        /// <param name="group">Group to initialize</param>
+        /// <param name="level">The level of grouping</param>
+        /// <param name="seedItem">The seed item to compare with to see where to insert</param>
+        private void InitializeGroup(DataGridCollectionViewGroupInternal group, int level, object seedItem)
+        {
+            // set the group description for dividing the group into subgroups
+            DataGridGroupDescription groupDescription = GetGroupDescription(group, level);
+            group.GroupBy = groupDescription;
+
+            // create subgroups for each of the explicit names
+            var keys = groupDescription?.GroupKeys;
+            if (keys != null)
+            {
+                for (int k = 0, n = keys.Count; k < n; ++k)
+                {
+                    DataGridCollectionViewGroupInternal subgroup = new DataGridCollectionViewGroupInternal(keys[k], group);
+                    InitializeGroup(subgroup, level + 1, seedItem);
+                    group.Add(subgroup);
+                }
+            }
+
+            group.LastIndex = 0;
+        }
+
+        /// <summary>
+        /// Remove an item from the direct children of a group.
+        /// </summary>
+        /// <param name="group">Group to remove item from</param>
+        /// <param name="item">Item to remove</param>
+        /// <returns>True if item could not be removed</returns>
+        private bool RemoveFromGroupDirectly(DataGridCollectionViewGroupInternal group, object item)
+        {
+            int leafIndex = group.Remove(item, true);
+            if (leafIndex >= 0)
+            {
+                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, leafIndex));
+                return false;
+            }
+            else
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Remove an item from the subgroup with the given name.
+        /// </summary>
+        /// <param name="item">Item to remove</param>
+        /// <param name="group">Group to remove item from</param>
+        /// <param name="level">The level of grouping</param>
+        /// <param name="key">Name of item to remove</param>
+        /// <returns>Return true if the item was not in one of the subgroups it was supposed to be.</returns>
+        private bool RemoveFromSubgroup(object item, DataGridCollectionViewGroupInternal group, int level, object key)
+        {
+            bool itemIsMissing = false;
+            DataGridCollectionViewGroupInternal subgroup;
+
+            // find the desired subgroup
+            for (int index = 0, n = group.Items.Count; index < n; ++index)
+            {
+                subgroup = group.Items[index] as DataGridCollectionViewGroupInternal;
+                if (subgroup == null)
+                {
+                    continue;           // skip children that are not groups
+                }
+
+                if (group.GroupBy.KeysMatch(subgroup.Key, key))
+                {
+                    if (RemoveFromSubgroups(item, subgroup, level + 1))
+                    {
+                        itemIsMissing = true;
+                    }
+
+                    return itemIsMissing;
+                }
+            }
+
+            // the item didn't match any subgroups.  It should have.
+            return true;
+        }
+
+        /// <summary>
+        /// Remove an item from the desired subgroup(s) of the given group.
+        /// </summary>
+        /// <param name="item">Item to remove</param>
+        /// <param name="group">Group to remove item from</param>
+        /// <param name="level">The level of grouping</param>
+        /// <returns>Return true if the item was not in one of the subgroups it was supposed to be.</returns>
+        private bool RemoveFromSubgroups(object item, DataGridCollectionViewGroupInternal group, int level)
+        {
+            bool itemIsMissing = false;
+            object key = GetGroupKey(item, group.GroupBy, level);
+
+            if (key == UseAsItemDirectly)
+            {
+                // the item belongs to the group itself (not to any subgroups)
+                itemIsMissing = RemoveFromGroupDirectly(group, item);
+            }
+            else if (key is ICollection keyList)
+            {
+                // the item belongs to multiple subgroups
+                foreach (object o in keyList)
+                {
+                    if (RemoveFromSubgroup(item, group, level, o))
+                    {
+                        itemIsMissing = true;
+                    }
+                }
+            }
+            else
+            {
+                // the item belongs to one subgroup
+                if (RemoveFromSubgroup(item, group, level, key))
+                {
+                    itemIsMissing = true;
+                }
+            }
+
+            return itemIsMissing;
+        }
+
+        /// <summary>
+        /// The item did not appear in one or more of the subgroups it
+        /// was supposed to.  This can happen if the item's properties
+        /// change so that the group names we used to insert it are
+        /// different from the names used to remove it. If this happens,
+        /// remove the item the hard way.
+        /// </summary>
+        /// <param name="group">Group to remove item from</param>
+        /// <param name="item">Item to remove</param>
+        private void RemoveItemFromSubgroupsByExhaustiveSearch(DataGridCollectionViewGroupInternal group, object item)
+        {
+            // try to remove the item from the direct children 
+            // this function only returns true if it failed to remove from group directly
+            // in which case we will step through and search exhaustively
+            if (RemoveFromGroupDirectly(group, item))
+            {
+                // if that didn't work, recurse into each subgroup
+                // (loop runs backwards in case an entire group is deleted)
+                for (int k = group.Items.Count - 1; k >= 0; --k)
+                {
+                    if (group.Items[k] is DataGridCollectionViewGroupInternal subgroup)
+                    {
+                        RemoveItemFromSubgroupsByExhaustiveSearch(subgroup, item);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// TopLevelGroupDescription class
+        /// </summary>
+        private class TopLevelGroupDescription : DataGridGroupDescription
+        {
+            /// <summary>
+            /// Initializes a new instance of the TopLevelGroupDescription class.
+            /// </summary>
+            public TopLevelGroupDescription()
+            {
+            }
+
+            /// <summary>
+            /// We have to implement this abstract method, but it should never be called
+            /// </summary>
+            /// <param name="item">Item to get group name from</param>
+            /// <param name="level">The level of grouping</param>
+            /// <param name="culture">Culture used for sorting</param>
+            /// <returns>We do not return a value here</returns>
+            public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
+            {
+                Debug.Assert(true, "We have to implement this abstract method, but it should never be called");
+                return null;
+            }
+        }
+    }
+
+}

+ 259 - 0
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@@ -0,0 +1,259 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.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;
+        public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath);
+        public abstract IComparer<object> Comparer { get; }
+
+        public virtual IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq)
+        {
+            return seq.OrderBy(o => o, Comparer);
+        }
+        public virtual IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq)
+        {
+            return seq.ThenBy(o => o, Comparer);
+        }
+
+        internal virtual DataGridSortDescription SwitchSortDirection()
+        {
+            return this;
+        }
+
+        internal virtual void Initialize(Type itemType)
+        { }
+
+        private static object InvokePath(object item, string propertyPath, Type propertyType)
+        {
+            object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception);
+            if (exception != null)
+            {
+                throw exception;
+            }
+            return propertyValue;
+        }
+
+        /// <summary>
+        /// Creates a comparer class that takes in a CultureInfo as a parameter,
+        /// which it will use when comparing strings.
+        /// </summary>
+        private class CultureSensitiveComparer : Comparer<object>
+        {
+            /// <summary>
+            /// Private accessor for the CultureInfo of our comparer
+            /// </summary>
+            private CultureInfo _culture;
+
+            /// <summary>
+            /// Creates a comparer which will respect the CultureInfo
+            /// that is passed in when comparing strings.
+            /// </summary>
+            /// <param name="culture">The CultureInfo to use in string comparisons</param>
+            public CultureSensitiveComparer(CultureInfo culture)
+                : base()
+            {
+                _culture = culture ?? CultureInfo.InvariantCulture;
+            }
+
+            /// <summary>
+            /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other.
+            /// </summary>
+            /// <param name="x">first item to compare</param>
+            /// <param name="y">second item to compare</param>
+            /// <returns>Negative number if x is less than y, zero if equal, and a positive number if x is greater than y</returns>
+            /// <remarks>
+            /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects.
+            /// </remarks>
+            public override int Compare(object x, object y)
+            {
+                if (x == null)
+                {
+                    if (y != null)
+                    {
+                        return -1;
+                    }
+                    return 0;
+                }
+                if (y == null)
+                {
+                    return 1;
+                }
+
+                // at this point x and y are not null
+                if (x.GetType() == typeof(string) && y.GetType() == typeof(string))
+                {
+                    return _culture.CompareInfo.Compare((string)x, (string)y);
+                }
+                else
+                {
+                    return Comparer<object>.Default.Compare(x, y);
+                }
+            }
+
+        }
+
+        private class DataGridPathSortDescription : DataGridSortDescription
+        {
+            private readonly bool _descending;
+            private readonly string _propertyPath;
+            private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer;
+            private readonly Lazy<IComparer<object>> _comparer;
+            private Type _propertyType;
+            private IComparer _internalComparer;
+            private IComparer<object> _internalComparerTyped;
+            private IComparer<object> InternalComparer
+            {
+                get
+                {
+                    if (_internalComparerTyped == null && _internalComparer != null)
+                    {
+                        if (_internalComparerTyped is IComparer<object> c)
+                            _internalComparerTyped = c;
+                        else
+                            _internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y));
+                    }
+
+                    return _internalComparerTyped;
+                }
+            }
+
+            public override string PropertyPath => _propertyPath;
+            public override IComparer<object> Comparer => _comparer.Value;
+            public override bool Descending => _descending;
+
+            public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture)
+            {
+                _propertyPath = propertyPath;
+                _descending = descending;
+                _cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture));
+                _comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y)));
+            }
+            private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending)
+            {
+                _propertyPath = inner._propertyPath;
+                _descending = descending;
+                _propertyType = inner._propertyType;
+                _cultureSensitiveComparer = inner._cultureSensitiveComparer;
+                _internalComparer = inner._internalComparer;
+                _internalComparerTyped = inner._internalComparerTyped;
+
+                _comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y)));
+            }
+
+            private object GetValue(object o)
+            {
+                if (o == null)
+                    return null;
+
+                if (HasPropertyPath)
+                    return InvokePath(o, _propertyPath, _propertyType);
+
+                if (_propertyType == o.GetType())
+                    return o;
+                else
+                    return null;
+            }
+
+            private IComparer GetComparerForType(Type type)
+            {
+                if (type == typeof(string))
+                    return _cultureSensitiveComparer.Value;
+                else
+                    return (typeof(Comparer<>).MakeGenericType(type).GetProperty("Default")).GetValue(null, null) as IComparer;
+            }
+            private Type GetPropertyType(object o)
+            {
+                return o.GetType().GetNestedPropertyType(_propertyPath);
+            }
+
+            private int Compare(object x, object y)
+            {
+                int result = 0;
+
+                if(_propertyType == null)
+                {
+                    if(x != null)
+                    {
+                        _propertyType = GetPropertyType(x);
+                    }
+                    if(_propertyType == null && y != null)
+                    {
+                        _propertyType = GetPropertyType(y);
+                    }
+                }
+
+                object v1 = GetValue(x);
+                object v2 = GetValue(y);
+
+                if (_propertyType != null && _internalComparer == null)
+                    _internalComparer = GetComparerForType(_propertyType);
+
+                result = _internalComparer?.Compare(v1, v2) ?? 0;
+
+                if (_descending)
+                    return -result;
+                else
+                    return result;
+            }
+
+            internal override void Initialize(Type itemType)
+            {
+                base.Initialize(itemType);
+
+                if(_propertyType == null)
+                    _propertyType = itemType.GetNestedPropertyType(_propertyPath);
+                if (_internalComparer == null && _propertyType != null)
+                    _internalComparer = GetComparerForType(_propertyType);
+            }
+            public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq)
+            {
+                if(_descending)
+                {
+                    return seq.OrderByDescending(o => GetValue(o), InternalComparer);
+                }
+                else
+                {
+                    return seq.OrderBy(o => GetValue(o), InternalComparer);
+                }
+            }
+            public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq)
+            {
+                if (_descending)
+                {
+                    return seq.ThenByDescending(o => GetValue(o), InternalComparer);
+                }
+                else
+                {
+                    return seq.ThenByDescending(o => GetValue(o), InternalComparer);
+                }
+            }
+
+            internal override DataGridSortDescription SwitchSortDirection()
+            {
+                return new DataGridPathSortDescription(this, !_descending);
+            }
+        }
+
+        public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null)
+        {
+            return new DataGridPathSortDescription(propertyPath, descending, culture);
+        }
+    }
+
+    public class DataGridSortDescriptionCollection : AvaloniaList<DataGridSortDescription>
+    { }
+}

+ 233 - 0
src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs

@@ -0,0 +1,233 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text;
+
+namespace Avalonia.Collections
+{
+    /// <summary>Provides data for the <see cref="E:Avalonia.Collections.ICollectionView.CurrentChanging" /> event.</summary>
+    public class DataGridCurrentChangingEventArgs : EventArgs
+    {
+        private bool _cancel;
+        private bool _isCancelable;
+
+        /// <summary>Initializes a new instance of the <see cref="T:System.ComponentModel.CurrentChangingEventArgs" /> class and sets the <see cref="P:System.ComponentModel.CurrentChangingEventArgs.IsCancelable" /> property to true.</summary>
+        public DataGridCurrentChangingEventArgs()
+        {
+            Initialize(true);
+        }
+
+        /// <summary>Initializes a new instance of the <see cref="T:System.ComponentModel.CurrentChangingEventArgs" /> class and sets the <see cref="P:System.ComponentModel.CurrentChangingEventArgs.IsCancelable" /> property to the specified value.</summary>
+        /// <param name="isCancelable">true to disable the ability to cancel a <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> change; false to enable cancellation.</param>
+        public DataGridCurrentChangingEventArgs(bool isCancelable)
+        {
+            Initialize(isCancelable);
+        }
+
+        private void Initialize(bool isCancelable)
+        {
+            _isCancelable = isCancelable;
+        }
+
+        /// <summary>Gets a value that indicates whether the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> change can be canceled. </summary>
+        /// <returns>true if the event can be canceled; false if the event cannot be canceled.</returns>
+        public bool IsCancelable
+        {
+            get
+            {
+                return _isCancelable;
+            }
+        }
+
+        /// <summary>Gets or sets a value that indicates whether the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> change should be canceled. </summary>
+        /// <returns>true if the event should be canceled; otherwise, false. The default is false.</returns>
+        /// <exception cref="T:System.InvalidOperationException">The <see cref="P:System.ComponentModel.CurrentChangingEventArgs.IsCancelable" /> property value is false.</exception>
+        public bool Cancel
+        {
+            get
+            {
+                return _cancel;
+            }
+            set
+            {
+                if (IsCancelable)
+                    _cancel = value;
+                else if (value)
+                    throw new InvalidOperationException("CurrentChanging Cannot Be Canceled");
+            }
+        }
+    }
+
+    /// <summary>Enables collections to have the functionalities of current record management, custom sorting, filtering, and grouping.</summary>
+    internal interface IDataGridCollectionView : IEnumerable, INotifyCollectionChanged
+    {
+        /// <summary>Gets or sets the cultural information for any operations of the view that may differ by culture, such as sorting.</summary>
+        /// <returns>The culture information to use during culture-sensitive operations. </returns>
+        CultureInfo Culture { get; set; }
+
+        /// <summary>Indicates whether the specified item belongs to this collection view. </summary>
+        /// <returns>true if the item belongs to this collection view; otherwise, false.</returns>
+        /// <param name="item">The object to check. </param>
+        bool Contains(object item);
+
+        /// <summary>Gets the underlying collection.</summary>
+        /// <returns>The underlying collection.</returns>
+        IEnumerable SourceCollection { get; }
+
+        /// <summary>Gets or sets a callback that is used to determine whether an item is appropriate for inclusion in the view. </summary>
+        /// <returns>A method that is used to determine whether an item is appropriate for inclusion in the view.</returns>
+        Func<object, bool> Filter { get; set; }
+
+        /// <summary>Gets a value that indicates whether this view supports filtering by way of the <see cref="P:System.ComponentModel.ICollectionView.Filter" /> property.</summary>
+        /// <returns>true if this view supports filtering; otherwise, false.</returns>
+        bool CanFilter { get; }
+
+        /// <summary>Gets a collection of <see cref="T:System.ComponentModel.SortDescription" /> instances that describe how the items in the collection are sorted in the view.</summary>
+        /// <returns>A collection of values that describe how the items in the collection are sorted in the view.</returns>
+        DataGridSortDescriptionCollection SortDescriptions { get; }
+
+        /// <summary>Gets a value that indicates whether this view supports sorting by way of the <see cref="P:System.ComponentModel.ICollectionView.SortDescriptions" /> property.</summary>
+        /// <returns>true if this view supports sorting; otherwise, false.</returns>
+        bool CanSort { get; }
+
+        /// <summary>Gets a value that indicates whether this view supports grouping by way of the <see cref="P:System.ComponentModel.ICollectionView.GroupDescriptions" /> property.</summary>
+        /// <returns>true if this view supports grouping; otherwise, false.</returns>
+        bool CanGroup { get; }
+
+        /// <summary>Gets a collection of <see cref="T:System.ComponentModel.GroupDescription" /> objects that describe how the items in the collection are grouped in the view. </summary>
+        /// <returns>A collection of objects that describe how the items in the collection are grouped in the view. </returns>
+        //ObservableCollection<GroupDescription> GroupDescriptions { get; }
+
+        bool IsGrouping { get; }
+        int GroupingDepth { get; }
+        string GetGroupingPropertyNameAtDepth(int level);
+
+        /// <summary>Gets the top-level groups.</summary>
+        /// <returns>A read-only collection of the top-level groups or null if there are no groups.</returns>
+        IAvaloniaReadOnlyList<object> Groups { get; }
+
+        /// <summary>Gets a value that indicates whether the view is empty.</summary>
+        /// <returns>true if the view is empty; otherwise, false.</returns>
+        bool IsEmpty { get; }
+
+        /// <summary>Recreates the view.</summary>
+        void Refresh();
+
+        /// <summary>Enters a defer cycle that you can use to merge changes to the view and delay automatic refresh. </summary>
+        /// <returns>The typical usage is to create a using scope with an implementation of this method and then include multiple view-changing calls within the scope. The implementation should delay automatic refresh until after the using scope exits. </returns>
+        IDisposable DeferRefresh();
+
+        /// <summary>Gets the current item in the view.</summary>
+        /// <returns>The current item in the view or null if there is no current item.</returns>
+        object CurrentItem { get; }
+
+        /// <summary>Gets the ordinal position of the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> in the view.</summary>
+        /// <returns>The ordinal position of the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> in the view.</returns>
+        int CurrentPosition { get; }
+
+        /// <summary>Gets a value that indicates whether the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> of the view is beyond the end of the collection.</summary>
+        /// <returns>true if the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> of the view is beyond the end of the collection; otherwise, false.</returns>
+        bool IsCurrentAfterLast { get; }
+
+        /// <summary>Gets a value that indicates whether the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> of the view is beyond the start of the collection.</summary>
+        /// <returns>true if the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> of the view is beyond the start of the collection; otherwise, false.</returns>
+        bool IsCurrentBeforeFirst { get; }
+
+        /// <summary>Sets the first item in the view as the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" />.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        bool MoveCurrentToFirst();
+
+        /// <summary>Sets the last item in the view as the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" />.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        bool MoveCurrentToLast();
+
+        /// <summary>Sets the item after the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> in the view as the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" />.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        bool MoveCurrentToNext();
+
+        /// <summary>Sets the item before the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> in the view to the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" />.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        bool MoveCurrentToPrevious();
+
+        /// <summary>Sets the specified item in the view as the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" />.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        /// <param name="item">The item to set as the current item.</param>
+        bool MoveCurrentTo(object item);
+
+        /// <summary>Sets the item at the specified index to be the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> in the view.</summary>
+        /// <returns>true if the resulting <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> is an item in the view; otherwise, false.</returns>
+        /// <param name="position">The index to set the <see cref="P:System.ComponentModel.ICollectionView.CurrentItem" /> to.</param>
+        bool MoveCurrentToPosition(int position);
+
+        /// <summary>Occurs before the current item changes.</summary>
+        event EventHandler<DataGridCurrentChangingEventArgs> CurrentChanging;
+
+        /// <summary>Occurs after the current item has been changed.</summary>
+        event EventHandler CurrentChanged;
+    }
+    internal interface IDataGridEditableCollectionView
+    {
+        /// <summary>Gets a value that indicates whether a new item can be added to the collection.</summary>
+        /// <returns>true if a new item can be added to the collection; otherwise, false.</returns>
+        bool CanAddNew { get; }
+
+        /// <summary>Adds a new item to the underlying collection.</summary>
+        /// <returns>The new item that is added to the collection.</returns>
+        object AddNew();
+
+        /// <summary>Ends the add transaction and saves the pending new item.</summary>
+        void CommitNew();
+
+        /// <summary>Ends the add transaction and discards the pending new item.</summary>
+        void CancelNew();
+
+        /// <summary>Gets a value that indicates whether an add transaction is in progress.</summary>
+        /// <returns>true if an add transaction is in progress; otherwise, false.</returns>
+        bool IsAddingNew { get; }
+
+        /// <summary>Gets the item that is being added during the current add transaction.</summary>
+        /// <returns>The item that is being added if <see cref="P:System.ComponentModel.IEditableCollectionView.IsAddingNew" /> is true; otherwise, null.</returns>
+        object CurrentAddItem { get; }
+
+        /// <summary>Gets a value that indicates whether an item can be removed from the collection.</summary>
+        /// <returns>true if an item can be removed from the collection; otherwise, false.</returns>
+        bool CanRemove { get; }
+
+        /// <summary>Removes the item at the specified position from the collection.</summary>
+        /// <param name="index">Index of item to remove.</param>
+        void RemoveAt(int index);
+
+        /// <summary>Removes the specified item from the collection.</summary>
+        /// <param name="item">The item to remove.</param>
+        void Remove(object item);
+
+        /// <summary>Begins an edit transaction on the specified item.</summary>
+        /// <param name="item">The item to edit.</param>
+        void EditItem(object item);
+
+        /// <summary>Ends the edit transaction and saves the pending changes.</summary>
+        void CommitEdit();
+
+        /// <summary>Ends the edit transaction and, if possible, restores the original value of the item.</summary>
+        void CancelEdit();
+
+        /// <summary>Gets a value that indicates whether the collection view can discard pending changes and restore the original values of an edited object.</summary>
+        /// <returns>true if the collection view can discard pending changes and restore the original values of an edited object; otherwise, false.</returns>
+        bool CanCancelEdit { get; }
+
+        /// <summary>Gets a value that indicates whether an edit transaction is in progress.</summary>
+        /// <returns>true if an edit transaction is in progress; otherwise, false.</returns>
+        bool IsEditingItem { get; }
+
+        /// <summary>Gets the item in the collection that is being edited.</summary>
+        /// <returns>The item that is being edited if <see cref="P:System.ComponentModel.IEditableCollectionView.IsEditingItem" /> is true; otherwise, null.</returns>
+        object CurrentEditItem { get; }
+    }
+}

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

@@ -0,0 +1,5953 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved. 
+
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+using Avalonia.Utilities;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Text;
+using System.Linq;
+using Avalonia.Input.Platform;
+using System.ComponentModel.DataAnnotations;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Displays data in a customizable grid.
+    /// </summary>
+    public partial class DataGrid : TemplatedControl
+    {
+        private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter";
+        private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter";
+        private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer";
+        private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar";
+        private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter";
+        private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader";
+        private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader";
+        private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary";
+        private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar";
+
+        private const bool DATAGRID_defaultAutoGenerateColumns = true;
+        internal const bool DATAGRID_defaultCanUserReorderColumns = true;
+        internal const bool DATAGRID_defaultCanUserResizeColumns = true;
+        internal const bool DATAGRID_defaultCanUserSortColumns = true;
+        private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected;
+        private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended;
+
+        /// <summary>
+        /// The default order to use for columns when there is no <see cref="DisplayAttribute.Order"/>
+        /// value available for the property.
+        /// </summary>
+        /// <remarks>
+        /// The value of 10,000 comes from the DataAnnotations spec, allowing
+        /// some properties to be ordered at the beginning and some at the end.
+        /// </remarks>
+        private const int DATAGRID_defaultColumnDisplayOrder = 10000;
+
+        private const double DATAGRID_horizontalGridLinesThickness = 1;
+        private const double DATAGRID_minimumRowHeaderWidth = 4;
+        private const double DATAGRID_minimumColumnHeaderHeight = 4;
+        internal const double DATAGRID_maximumStarColumnWidth = 10000;
+        internal const double DATAGRID_minimumStarColumnWidth = 0.001;
+        private const double DATAGRID_mouseWheelDelta = 48.0;
+        private const double DATAGRID_maxHeadersThickness = 32768;
+
+        private const double DATAGRID_defaultRowHeight = 22;
+        internal const double DATAGRID_defaultRowGroupSublevelIndent = 20;
+        private const double DATAGRID_defaultMinColumnWidth = 20;
+        private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity;
+
+        private List<Exception> _validationErrors;
+        private List<Exception> _bindingValidationErrors;
+        private IDisposable _validationSubscription;
+
+        private INotifyCollectionChanged _topLevelGroup;
+        private ContentControl _clipboardContentControl;
+
+        private DataGridColumnHeadersPresenter _columnHeadersPresenter;
+        private DataGridRowsPresenter _rowsPresenter;
+        private ScrollBar _vScrollBar;
+        private ScrollBar _hScrollBar;
+
+        private ContentControl _topLeftCornerHeader;
+        private ContentControl _topRightCornerHeader;
+        private Control _frozenColumnScrollBarSpacer;
+
+        // the sum of the widths in pixels of the scrolling columns preceding 
+        // the first displayed scrolling column
+        private double _horizontalOffset;
+
+        // the number of pixels of the firstDisplayedScrollingCol which are not displayed
+        private double _negHorizontalOffset;
+        private byte _autoGeneratingColumnOperationCount;
+        private bool _areHandlersSuspended;
+        private bool _autoSizingColumns;
+        private IndexToValueTable<bool> _collapsedSlotsTable;
+        private DataGridCellCoordinates _currentCellCoordinates;
+        private Control _clickedElement;
+
+        // used to store the current column during a Reset
+        private int _desiredCurrentColumnIndex;
+        private int _editingColumnIndex;
+
+        // this is a workaround only for the scenarios where we need it, it is not all encompassing nor always updated
+        private RoutedEventArgs _editingEventArgs;
+        private bool _executingLostFocusActions;
+        private bool _flushCurrentCellChanged;
+        private bool _focusEditingControl;
+        private IVisual _focusedObject;
+        private byte _horizontalScrollChangesIgnored;
+        private DataGridRow _focusedRow;
+        private bool _ignoreNextScrollBarsLayout;
+
+        // Nth row of rows 0..N that make up the RowHeightEstimate
+        private int _lastEstimatedRow;
+        private List<DataGridRow> _loadedRows;
+
+        // prevents reentry into the VerticalScroll event handler
+        private Queue<Action> _lostFocusActions;
+        private int _noSelectionChangeCount;
+        private int _noCurrentCellChangeCount;
+        private bool _makeFirstDisplayedCellCurrentCellPending;
+        private bool _measured;
+        private int? _mouseOverRowIndex;    // -1 is used for the 'new row'
+        private DataGridColumn _previousCurrentColumn;
+        private object _previousCurrentItem;
+        private double[] _rowGroupHeightsByLevel;
+        private double _rowHeaderDesiredWidth;
+        private Size? _rowsPresenterAvailableSize;
+        private bool _scrollingByHeight;
+        private IndexToValueTable<bool> _showDetailsTable;
+        private bool _successfullyUpdatedSelection;
+        private DataGridSelectedItemsCollection _selectedItems;
+        private bool _temporarilyResetCurrentCell;
+        private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
+        private ICellEditBinding _currentCellEditBinding;
+
+        // An approximation of the sum of the heights in pixels of the scrolling rows preceding 
+        // the first displayed scrolling row.  Since the scrolled off rows are discarded, the grid
+        // does not know their actual height. The heights used for the approximation are the ones
+        // set as the rows were scrolled off.
+        private double _verticalOffset;
+        private byte _verticalScrollChangesIgnored;
+
+        private IEnumerable _items;
+
+        /// <summary>
+        /// Identifies the CanUserReorderColumns dependency property.
+        /// </summary>
+        public static readonly StyledProperty<bool> CanUserReorderColumnsProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserReorderColumns));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can change 
+        /// the column display order by dragging column headers with the mouse.
+        /// </summary>
+        public bool CanUserReorderColumns
+        {
+            get { return GetValue(CanUserReorderColumnsProperty); }
+            set { SetValue(CanUserReorderColumnsProperty, value); }
+        }
+
+        /// <summary>
+        /// Identifies the CanUserResizeColumns dependency property.
+        /// </summary>
+        public static readonly StyledProperty<bool> CanUserResizeColumnsProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserResizeColumns));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can adjust column widths using the mouse.
+        /// </summary>
+        public bool CanUserResizeColumns
+        {
+            get { return GetValue(CanUserResizeColumnsProperty); }
+            set { SetValue(CanUserResizeColumnsProperty, value); }
+        }
+
+        /// <summary>
+        /// Identifies the CanUserSortColumns dependency property.
+        /// </summary>
+        public static readonly StyledProperty<bool> CanUserSortColumnsProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserSortColumns), true);
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can sort columns by clicking the column header.
+        /// </summary>
+        public bool CanUserSortColumns
+        {
+            get { return GetValue(CanUserSortColumnsProperty); }
+            set { SetValue(CanUserSortColumnsProperty, value); }
+        }
+
+        /// <summary>
+        /// Identifies the ColumnHeaderHeight dependency property.
+        /// </summary>
+        public static readonly StyledProperty<double> ColumnHeaderHeightProperty =
+            AvaloniaProperty.Register<DataGrid, double>(
+                nameof(ColumnHeaderHeight),
+                defaultValue: double.NaN,
+                validate: ValidateColumnHeaderHeight);
+
+        private static double ValidateColumnHeaderHeight(DataGrid grid, double value)
+        {
+            if (value < DATAGRID_minimumColumnHeaderHeight)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight);
+            }
+            if (value > DATAGRID_maxHeadersThickness)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the height of the column headers row.
+        /// </summary>
+        public double ColumnHeaderHeight
+        {
+            get { return GetValue(ColumnHeaderHeightProperty); }
+            set { SetValue(ColumnHeaderHeightProperty, value); }
+        }
+
+        /// <summary>
+        /// Identifies the ColumnWidth dependency property.
+        /// </summary>
+        public static readonly StyledProperty<DataGridLength> ColumnWidthProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridLength>(nameof(ColumnWidth), defaultValue: DataGridLength.Auto);
+
+        /// <summary>
+        /// Gets or sets the standard width or automatic sizing mode of columns in the control.
+        /// </summary>
+        public DataGridLength ColumnWidth
+        {
+            get { return GetValue(ColumnWidthProperty); }
+            set { SetValue(ColumnWidthProperty, value); }
+        }
+
+        public static readonly StyledProperty<IBrush> AlternatingRowBackgroundProperty =
+            AvaloniaProperty.Register<DataGrid, IBrush>(nameof(AlternatingRowBackground));
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint the background of odd-numbered rows.
+        /// </summary>
+        /// <returns>
+        /// The brush that is used to paint the background of odd-numbered rows. The default is a 
+        /// <see cref="T:System.Windows.Media.SolidColorBrush" /> with a 
+        /// <see cref="P:System.Windows.Media.SolidColorBrush.Color" /> value of white (ARGB value #00FFFFFF).
+        /// </returns>
+        public IBrush AlternatingRowBackground
+        {
+            get { return GetValue(AlternatingRowBackgroundProperty); }
+            set { SetValue(AlternatingRowBackgroundProperty, value); }
+        }
+
+        public static readonly StyledProperty<int> FrozenColumnCountProperty =
+            AvaloniaProperty.Register<DataGrid, int>(
+                nameof(FrozenColumnCount),
+                validate: ValidateFrozenColumnCount);
+
+        /// <summary>
+        /// Gets or sets the number of columns that the user cannot scroll horizontally.
+        /// </summary>
+        public int FrozenColumnCount
+        {
+            get { return GetValue(FrozenColumnCountProperty); }
+            set { SetValue(FrozenColumnCountProperty, value); }
+        }
+
+        private static int ValidateFrozenColumnCount(DataGrid grid, int value)
+        {
+            if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
+            }
+
+            return value;
+        }
+
+        public static readonly StyledProperty<DataGridGridLinesVisibility> GridLinesVisibilityProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridGridLinesVisibility>(nameof(GridLinesVisibility));
+
+        /// <summary>
+        /// Gets or sets a value that indicates which grid lines separating inner cells are shown.
+        /// </summary>
+        public DataGridGridLinesVisibility GridLinesVisibility
+        {
+            get { return GetValue(GridLinesVisibilityProperty); }
+            set { SetValue(GridLinesVisibilityProperty, value); }
+        }
+
+        public static readonly StyledProperty<DataGridHeadersVisibility> HeadersVisibilityProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridHeadersVisibility>(nameof(HeadersVisibility));
+
+        /// <summary>
+        /// Gets or sets a value that indicates the visibility of row and column headers.
+        /// </summary>
+        public DataGridHeadersVisibility HeadersVisibility
+        {
+            get { return GetValue(HeadersVisibilityProperty); }
+            set { SetValue(HeadersVisibilityProperty, value); }
+        }
+
+        public static readonly StyledProperty<IBrush> HorizontalGridLinesBrushProperty =
+            AvaloniaProperty.Register<DataGrid, IBrush>(nameof(HorizontalGridLinesBrush));
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint grid lines separating rows.
+        /// </summary>
+        public IBrush HorizontalGridLinesBrush
+        {
+            get { return GetValue(HorizontalGridLinesBrushProperty); }
+            set { SetValue(HorizontalGridLinesBrushProperty, value); }
+        }
+
+        public static readonly StyledProperty<ScrollBarVisibility> HorizontalScrollBarVisibilityProperty =
+            AvaloniaProperty.Register<DataGrid, ScrollBarVisibility>(nameof(HorizontalScrollBarVisibility));
+
+        /// <summary>
+        /// Gets or sets a value that indicates how the horizontal scroll bar is displayed.
+        /// </summary>
+        public ScrollBarVisibility HorizontalScrollBarVisibility
+        {
+            get { return GetValue(HorizontalScrollBarVisibilityProperty); }
+            set { SetValue(HorizontalScrollBarVisibilityProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> IsReadOnlyProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(IsReadOnly));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can edit the values in the control.
+        /// </summary>
+        public bool IsReadOnly
+        {
+            get { return GetValue(IsReadOnlyProperty); }
+            set { SetValue(IsReadOnlyProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> AreRowGroupHeadersFrozenProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(
+                nameof(AreRowGroupHeadersFrozen),
+                defaultValue: true);
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the row group header sections
+        /// remain fixed at the width of the display area or can scroll horizontally.
+        /// </summary>
+        public bool AreRowGroupHeadersFrozen
+        {
+            get { return GetValue(AreRowGroupHeadersFrozenProperty); }
+            set { SetValue(AreRowGroupHeadersFrozenProperty, value); }
+        }
+
+        private void OnAreRowGroupHeadersFrozenChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var value = (bool)e.NewValue;
+            ProcessFrozenColumnCount();
+
+            // Update elements in the RowGroupHeader that were previously frozen
+            if (value)
+            {
+                if (_rowsPresenter != null)
+                {
+                    foreach (Control element in _rowsPresenter.Children)
+                    {
+                        if (element is DataGridRowGroupHeader groupHeader)
+                        {
+                            groupHeader.ClearFrozenStates();
+                        }
+                    }
+                }
+            }
+        }
+
+        private bool _isValid = true;
+
+        public static readonly DirectProperty<DataGrid, bool> IsValidProperty =
+            AvaloniaProperty.RegisterDirect<DataGrid, bool>(
+                nameof(IsValid),
+                o => o.IsValid);
+
+        public bool IsValid
+        {
+            get { return _isValid; }
+            internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
+        }
+
+        public static readonly StyledProperty<double> MaxColumnWidthProperty =
+            AvaloniaProperty.Register<DataGrid, double>(
+                nameof(MaxColumnWidth),
+                defaultValue: DATAGRID_defaultMaxColumnWidth,
+                validate: ValidateMaxColumnWidth);
+
+        private static double ValidateMaxColumnWidth(DataGrid grid, double value)
+        {
+            if (double.IsNaN(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth));
+            }
+            if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0);
+            }
+            if (grid.MinColumnWidth > value)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth));
+            }
+
+            if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum width of columns in the <see cref="T:Avalonia.Controls.DataGrid" /> . 
+        /// </summary>
+        public double MaxColumnWidth
+        {
+            get { return GetValue(MaxColumnWidthProperty); }
+            set { SetValue(MaxColumnWidthProperty, value); }
+        }
+
+        public static readonly StyledProperty<double> MinColumnWidthProperty =
+            AvaloniaProperty.Register<DataGrid, double>(
+                nameof(MinColumnWidth),
+                defaultValue: DATAGRID_defaultMinColumnWidth,
+                validate: ValidateMinColumnWidth);
+
+        private static double ValidateMinColumnWidth(DataGrid grid, double value)
+        {
+            if (double.IsNaN(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth));
+            }
+            if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0);
+            }
+            if (double.IsPositiveInfinity(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth));
+            }
+            if (grid.MaxColumnWidth < value)
+            {
+                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth));
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum width of columns in the <see cref="T:Avalonia.Controls.DataGrid" />. 
+        /// </summary>
+        public double MinColumnWidth
+        {
+            get { return GetValue(MinColumnWidthProperty); }
+            set { SetValue(MinColumnWidthProperty, value); }
+        }
+
+        public static readonly StyledProperty<IBrush> RowBackgroundProperty =
+            AvaloniaProperty.Register<DataGrid, IBrush>(nameof(RowBackground));
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint row backgrounds.
+        /// </summary>
+        public IBrush RowBackground
+        {
+            get { return GetValue(RowBackgroundProperty); }
+            set { SetValue(RowBackgroundProperty, value); }
+        }
+
+        public static readonly StyledProperty<double> RowHeightProperty =
+            AvaloniaProperty.Register<DataGrid, double>(
+                nameof(RowHeight),
+                defaultValue: double.NaN,
+                validate: ValidateRowHeight);
+        private static double ValidateRowHeight(DataGrid grid, double value)
+        {
+            if (value < DataGridRow.DATAGRIDROW_minimumHeight)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0);
+            }
+            if (value > DataGridRow.DATAGRIDROW_maximumHeight)
+            {
+                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the standard height of rows in the control.
+        /// </summary>
+        public double RowHeight
+        {
+            get { return GetValue(RowHeightProperty); }
+            set { SetValue(RowHeightProperty, value); }
+        }
+
+        public static readonly StyledProperty<double> RowHeaderWidthProperty =
+            AvaloniaProperty.Register<DataGrid, double>(
+                nameof(RowHeaderWidth),
+                defaultValue: double.NaN,
+                validate: ValidateRowHeaderWidth);
+        private static double ValidateRowHeaderWidth(DataGrid grid, double value)
+        {
+            if (value < DATAGRID_minimumRowHeaderWidth)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth);
+            }
+            if (value > DATAGRID_maxHeadersThickness)
+            {
+                throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets the width of the row header column.
+        /// </summary>
+        public double RowHeaderWidth
+        {
+            get { return GetValue(RowHeaderWidthProperty); }
+            set { SetValue(RowHeaderWidthProperty, value); }
+        }
+
+        public static readonly StyledProperty<DataGridSelectionMode> SelectionModeProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridSelectionMode>(nameof(SelectionMode));
+
+        /// <summary>
+        /// Gets or sets the selection behavior of the data grid.
+        /// </summary>
+        public DataGridSelectionMode SelectionMode
+        {
+            get { return GetValue(SelectionModeProperty); }
+            set { SetValue(SelectionModeProperty, value); }
+        }
+
+        public static readonly StyledProperty<IBrush> VerticalGridLinesBrushProperty =
+            AvaloniaProperty.Register<DataGrid, IBrush>(nameof(VerticalGridLinesBrush));
+
+        /// <summary>
+        /// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint grid lines separating columns. 
+        /// </summary>
+        public IBrush VerticalGridLinesBrush
+        {
+            get { return GetValue(VerticalGridLinesBrushProperty); }
+            set { SetValue(VerticalGridLinesBrushProperty, value); }
+        }
+
+        public static readonly StyledProperty<ScrollBarVisibility> VerticalScrollBarVisibilityProperty =
+            AvaloniaProperty.Register<DataGrid, ScrollBarVisibility>(nameof(VerticalScrollBarVisibility));
+
+        /// <summary>
+        /// Gets or sets a value that indicates how the vertical scroll bar is displayed.
+        /// </summary>
+        public ScrollBarVisibility VerticalScrollBarVisibility
+        {
+            get { return GetValue(VerticalScrollBarVisibilityProperty); }
+            set { SetValue(VerticalScrollBarVisibilityProperty, value); }
+        }
+
+        public static readonly StyledProperty<ITemplate<IControl>> DropLocationIndicatorTemplateProperty =
+            AvaloniaProperty.Register<DataGrid, ITemplate<IControl>>(nameof(DropLocationIndicatorTemplate));
+
+        /// <summary>
+        /// Gets or sets the template that is used when rendering the column headers.
+        /// </summary>
+        public ITemplate<IControl> DropLocationIndicatorTemplate
+        {
+            get { return GetValue(DropLocationIndicatorTemplateProperty); }
+            set { SetValue(DropLocationIndicatorTemplateProperty, value); }
+        }
+
+        private int _selectedIndex = -1;
+        private object _selectedItem;
+
+        public static readonly DirectProperty<DataGrid, int> SelectedIndexProperty =
+            AvaloniaProperty.RegisterDirect<DataGrid, int>(
+                nameof(SelectedIndex),
+                o => o.SelectedIndex,
+                (o, v) => o.SelectedIndex = v);
+
+        /// <summary>
+        /// Gets or sets the index of the current selection.
+        /// </summary>
+        /// <returns>
+        /// The index of the current selection, or -1 if the selection is empty.
+        /// </returns> 
+        public int SelectedIndex
+        {
+            get { return _selectedIndex; }
+            set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); }
+        }
+
+        public static readonly DirectProperty<DataGrid, object> SelectedItemProperty =
+            AvaloniaProperty.RegisterDirect<DataGrid, object>(
+                nameof(SelectedItem),
+                o => o.SelectedItem,
+                (o, v) => o.SelectedItem = v);
+
+        /// <summary>
+        /// Gets or sets the data item corresponding to the selected row.
+        /// </summary>
+        public object SelectedItem
+        {
+            get { return _selectedItem; }
+            set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
+        }
+
+        public static readonly StyledProperty<DataGridClipboardCopyMode> ClipboardCopyModeProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridClipboardCopyMode>(
+                nameof(ClipboardCopyMode),
+                defaultValue: DataGridClipboardCopyMode.ExcludeHeader);
+
+        /// <summary>
+        /// The property which determines how DataGrid content is copied to the Clipboard.
+        /// </summary>
+        public DataGridClipboardCopyMode ClipboardCopyMode
+        {
+            get { return GetValue(ClipboardCopyModeProperty); }
+            set { SetValue(ClipboardCopyModeProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> AutoGenerateColumnsProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(AutoGenerateColumns));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether columns are created 
+        /// automatically when the <see cref="P:Avalonia.Controls.DataGrid.ItemsSource" /> property is set.
+        /// </summary>
+        public bool AutoGenerateColumns
+        {
+            get { return GetValue(AutoGenerateColumnsProperty); }
+            set { SetValue(AutoGenerateColumnsProperty, value); }
+        }
+
+        private void OnAutoGenerateColumnsChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var value = (bool)e.NewValue;
+            if (value)
+            {
+                InitializeElements(recycleRows: false);
+            }
+            else
+            {
+                RemoveAutoGeneratedColumns();
+            }
+        }
+
+        /// <summary>
+        /// Identifies the ItemsSource dependency property.
+        /// </summary>
+        public static readonly DirectProperty<DataGrid, IEnumerable> ItemsProperty =
+            AvaloniaProperty.RegisterDirect<DataGrid, IEnumerable>(
+                nameof(Items),
+                o => o.Items,
+                (o, v) => o.Items = v);
+
+        /// <summary>
+        /// Gets or sets a collection that is used to generate the content of the control.
+        /// </summary>
+        public IEnumerable Items
+        {
+            get { return _items; }
+            set { SetAndRaise(ItemsProperty, ref _items, value); }
+        }
+
+        public static readonly StyledProperty<bool> AreRowDetailsFrozenProperty =
+            AvaloniaProperty.Register<DataGrid, bool>(nameof(AreRowDetailsFrozen));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the row details sections remain 
+        /// fixed at the width of the display area or can scroll horizontally.
+        /// </summary>
+        public bool AreRowDetailsFrozen
+        {
+            get { return GetValue(AreRowDetailsFrozenProperty); }
+            set { SetValue(AreRowDetailsFrozenProperty, value); }
+        }
+
+        public static readonly StyledProperty<IDataTemplate> RowDetailsTemplateProperty =
+            AvaloniaProperty.Register<DataGrid, IDataTemplate>(nameof(RowDetailsTemplate));
+
+        /// <summary>
+        /// Gets or sets the template that is used to display the content of the details section of rows.
+        /// </summary>
+        public IDataTemplate RowDetailsTemplate
+        {
+            get { return GetValue(RowDetailsTemplateProperty); }
+            set { SetValue(RowDetailsTemplateProperty, value); }
+        }
+
+        public static readonly StyledProperty<DataGridRowDetailsVisibilityMode> RowDetailsVisibilityModeProperty =
+            AvaloniaProperty.Register<DataGrid, DataGridRowDetailsVisibilityMode>(nameof(RowDetailsVisibilityMode));
+
+        /// <summary>
+        /// Gets or sets a value that indicates when the details sections of rows are displayed.
+        /// </summary>
+        public DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode
+        {
+            get { return GetValue(RowDetailsVisibilityModeProperty); }
+            set { SetValue(RowDetailsVisibilityModeProperty, value); }
+        }
+
+        static DataGrid()
+        {
+            AffectsMeasure<DataGrid>(
+                ColumnHeaderHeightProperty,
+                HorizontalScrollBarVisibilityProperty,
+                VerticalScrollBarVisibilityProperty);
+
+            PseudoClass<DataGrid, bool>(IsValidProperty, x => !x, ":invalid");
+
+            ItemsProperty.Changed.AddClassHandler<DataGrid>(x => x.OnItemsPropertyChanged);
+            CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>(x => x.OnCanUserResizeColumnsChanged);
+            ColumnWidthProperty.Changed.AddClassHandler<DataGrid>(x => x.OnColumnWidthChanged);
+            RowBackgroundProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowBackgroundChanged);
+            AlternatingRowBackgroundProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowBackgroundChanged);
+            FrozenColumnCountProperty.Changed.AddClassHandler<DataGrid>(x => x.OnFrozenColumnCountChanged);
+            GridLinesVisibilityProperty.Changed.AddClassHandler<DataGrid>(x => x.OnGridLinesVisibilityChanged);
+            HeadersVisibilityProperty.Changed.AddClassHandler<DataGrid>(x => x.OnHeadersVisibilityChanged);
+            HorizontalGridLinesBrushProperty.Changed.AddClassHandler<DataGrid>(x => x.OnHorizontalGridLinesBrushChanged);
+            IsReadOnlyProperty.Changed.AddClassHandler<DataGrid>(x => x.OnIsReadOnlyChanged);
+            MaxColumnWidthProperty.Changed.AddClassHandler<DataGrid>(x => x.OnMaxColumnWidthChanged);
+            MinColumnWidthProperty.Changed.AddClassHandler<DataGrid>(x => x.OnMinColumnWidthChanged);
+            RowHeightProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowHeightChanged);
+            RowHeaderWidthProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowHeaderWidthChanged);
+            SelectionModeProperty.Changed.AddClassHandler<DataGrid>(x => x.OnSelectionModeChanged);
+            VerticalGridLinesBrushProperty.Changed.AddClassHandler<DataGrid>(x => x.OnVerticalGridLinesBrushChanged);
+            SelectedIndexProperty.Changed.AddClassHandler<DataGrid>(x => x.OnSelectedIndexChanged);
+            SelectedItemProperty.Changed.AddClassHandler<DataGrid>(x => x.OnSelectedItemChanged);
+            IsEnabledProperty.Changed.AddClassHandler<DataGrid>(x => x.DataGrid_IsEnabledChanged);
+            AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler<DataGrid>(x => x.OnAreRowGroupHeadersFrozenChanged);
+            RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowDetailsTemplateChanged);
+            RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>(x => x.OnRowDetailsVisibilityModeChanged);
+            AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>(x => x.OnAutoGenerateColumnsChanged);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGrid" /> class.
+        /// </summary>
+        public DataGrid()
+        {
+            KeyDown += DataGrid_KeyDown;
+            KeyUp += DataGrid_KeyUp;
+
+            //TODO: Check if override works
+            GotFocus += DataGrid_GotFocus;
+            LostFocus += DataGrid_LostFocus;
+
+            _loadedRows = new List<DataGridRow>();
+            _lostFocusActions = new Queue<Action>();
+            _selectedItems = new DataGridSelectedItemsCollection(this);
+            RowGroupHeadersTable = new IndexToValueTable<DataGridRowGroupInfo>();
+            _bindingValidationErrors = new List<Exception>();
+
+            DisplayData = new DataGridDisplayData(this);
+            ColumnsInternal = CreateColumnsInstance();
+
+            RowHeightEstimate = DATAGRID_defaultRowHeight;
+            RowDetailsHeightEstimate = 0;
+            _rowHeaderDesiredWidth = 0;
+
+            DataConnection = new DataGridDataConnection(this);
+            _showDetailsTable = new IndexToValueTable<bool>();
+            _collapsedSlotsTable = new IndexToValueTable<bool>();
+
+            AnchorSlot = -1;
+            _lastEstimatedRow = -1;
+            _editingColumnIndex = -1;
+            _mouseOverRowIndex = null;
+            CurrentCellCoordinates = new DataGridCellCoordinates(-1, -1);
+
+            RowGroupHeaderHeightEstimate = DATAGRID_defaultRowHeight;
+        }
+
+        private void SetValueNoCallback<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
+        {
+            _areHandlersSuspended = true;
+            try
+            {
+                SetValue(property, value, priority);
+            }
+            finally
+            {
+                _areHandlersSuspended = false;
+            }
+        }
+
+        private void OnRowDetailsVisibilityModeChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UpdateRowDetailsVisibilityMode((DataGridRowDetailsVisibilityMode)e.NewValue);
+        }
+
+        private void OnRowDetailsTemplateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+
+            // Update the RowDetails templates if necessary
+            if (_rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    if (GetRowDetailsVisibility(row.Index))
+                    {
+                        // DetailsPreferredHeight is initialized when the DetailsElement's size changes.
+                        row.ApplyDetailsTemplate(initializeDetailsPreferredHeight: false);
+                    }
+                }
+            }
+
+            UpdateRowDetailsHeightEstimate();
+            InvalidateMeasure();
+        }
+
+        /// <summary>
+        /// ItemsProperty property changed handler.
+        /// </summary>
+        /// <param name="e">AvaloniaPropertyChangedEventArgs.</param>
+        private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                Debug.Assert(DataConnection != null);
+
+                var oldValue = (IEnumerable)e.OldValue;
+                var newItemsSource = (IEnumerable)e.NewValue;
+
+                if (LoadingOrUnloadingRow)
+                {
+                    SetValueNoCallback(ItemsProperty, oldValue);
+                    throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows();
+                }
+
+                // Try to commit edit on the old DataSource, but force a cancel if it fails
+                if (!CommitEdit())
+                {
+                    CancelEdit(DataGridEditingUnit.Row, false);
+                }
+
+                DataConnection.UnWireEvents(DataConnection.DataSource);
+                DataConnection.ClearDataProperties();
+                ClearRowGroupHeadersTable();
+
+                // The old selected indexes are no longer relevant. There's a perf benefit from
+                // updating the selected indexes with a null DataSource, because we know that all
+                // of the previously selected indexes have been removed from selection
+                DataConnection.DataSource = null;
+                _selectedItems.UpdateIndexes();
+                CoerceSelectedItem();
+
+                // Wrap an IEnumerable in an ICollectionView if it's not already one
+                bool setDefaultSelection = false;
+                if (newItemsSource != null && !(newItemsSource is IDataGridCollectionView))
+                {
+                    DataConnection.DataSource = DataGridDataConnection.CreateView(newItemsSource);
+                }
+                else
+                {
+                    DataConnection.DataSource = newItemsSource;
+                    setDefaultSelection = true;
+                }
+
+                if (DataConnection.DataSource != null)
+                {
+                    // Setup the column headers
+                    if (DataConnection.DataType != null)
+                    {
+                        foreach (var column in ColumnsInternal.GetDisplayedColumns())
+                        {
+                            if (column is DataGridBoundColumn boundColumn)
+                            {
+                                boundColumn.SetHeaderFromBinding();
+                            }
+                        }
+                    }
+                    DataConnection.WireEvents(DataConnection.DataSource);
+                }
+
+                // Wait for the current cell to be set before we raise any SelectionChanged events
+                _makeFirstDisplayedCellCurrentCellPending = true;
+
+                // Clear out the old rows and remove the generated columns
+                ClearRows(false); //recycle
+                RemoveAutoGeneratedColumns();
+
+                // Set the SlotCount (from the data count and number of row group headers) before we make the default selection
+                PopulateRowGroupHeadersTable();
+                SelectedItem = null;
+                if (DataConnection.CollectionView != null && setDefaultSelection)
+                {
+                    SelectedItem = DataConnection.CollectionView.CurrentItem;
+                }
+
+                // Treat this like the DataGrid has never been measured because all calculations at
+                // this point are invalid until the next layout cycle.  For instance, the ItemsSource
+                // can be set when the DataGrid is not part of the visual tree
+                _measured = false;
+                InvalidateMeasure();
+            }
+        }
+
+        private void OnSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                int index = (int)e.NewValue;
+
+                // GetDataItem returns null if index is >= Count, we do not check newValue 
+                // against Count here to avoid enumerating through an Enumerable twice
+                // Setting SelectedItem coerces the finally value of the SelectedIndex
+                object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index);
+                SelectedItem = newSelectedItem;
+                if (SelectedItem != newSelectedItem)
+                {
+                    SetValueNoCallback(SelectedIndexProperty, (int)e.OldValue);
+                }
+            }
+        }
+
+        private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                int rowIndex = (e.NewValue == null) ? -1 : DataConnection.IndexOf(e.NewValue);
+                if (rowIndex == -1)
+                {
+                    // If the Item is null or it's not found, clear the Selection
+                    if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
+                    {
+                        // Edited value couldn't be committed or aborted
+                        SetValueNoCallback(SelectedItemProperty, e.OldValue);
+                        return;
+                    }
+
+                    // Clear all row selections
+                    ClearRowSelection(resetAnchorSlot: true);
+                }
+                else
+                {
+                    int slot = SlotFromRowIndex(rowIndex);
+                    if (slot != CurrentSlot)
+                    {
+                        if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
+                        {
+                            // Edited value couldn't be committed or aborted
+                            SetValueNoCallback(SelectedItemProperty, e.OldValue);
+                            return;
+                        }
+                        if (slot >= SlotCount || slot < -1)
+                        {
+                            if (DataConnection.CollectionView != null)
+                            {
+                                DataConnection.CollectionView.MoveCurrentToPosition(rowIndex);
+                            }
+                        }
+                    }
+
+                    int oldSelectedIndex = SelectedIndex;
+                    SetValueNoCallback(SelectedIndexProperty, rowIndex);
+                    try
+                    {
+                        _noSelectionChangeCount++;
+                        int columnIndex = CurrentColumnIndex;
+
+                        if (columnIndex == -1)
+                        {
+                            columnIndex = FirstDisplayedNonFillerColumnIndex;
+                        }
+                        if (IsSlotOutOfSelectionBounds(slot))
+                        {
+                            ClearRowSelection(slotException: slot, setAnchorSlot: true);
+                            return;
+                        }
+
+                        UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
+                    }
+                    finally
+                    {
+                        NoSelectionChangeCount--;
+                    }
+
+                    if (!_successfullyUpdatedSelection)
+                    {
+                        SetValueNoCallback(SelectedIndexProperty, oldSelectedIndex);
+                        SetValueNoCallback(SelectedItemProperty, e.OldValue);
+                    }
+                }
+            }
+        }
+
+        private void OnVerticalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    row.EnsureGridLines();
+                }
+            }
+        }
+
+        private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                ClearRowSelection(resetAnchorSlot: true);
+            }
+        }
+
+        private void OnRowHeaderWidthChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                EnsureRowHeaderWidth();
+            }
+        }
+
+        private void OnRowHeightChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                InvalidateRowHeightEstimate();
+                // Re-measure all the rows due to the Height change
+                InvalidateRowsMeasure(invalidateIndividualElements: true);
+                // DataGrid needs to update the layout information and the ScrollBars
+                InvalidateMeasure();
+            }
+        }
+
+        private void OnMinColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                double oldValue = (double)e.OldValue;
+                foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
+                {
+                    OnColumnMinWidthChanged(column, Math.Max(column.MinWidth, oldValue));
+                }
+            }
+        }
+
+        private void OnMaxColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                var oldValue = (double)e.OldValue;
+                foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
+                {
+                    OnColumnMaxWidthChanged(column, Math.Min(column.MaxWidth, oldValue));
+                }
+            }
+        }
+
+        private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                var value = (bool)e.NewValue;
+                if (value && !CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
+                {
+                    CancelEdit(DataGridEditingUnit.Row, raiseEvents: false);
+                }
+            }
+        }
+
+        private void OnHorizontalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended && _rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    row.EnsureGridLines();
+                }
+            }
+        }
+
+        private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (DataGridHeadersVisibility)e.OldValue;
+            var newValue = (DataGridHeadersVisibility)e.NewValue;
+            bool hasFlags(DataGridHeadersVisibility value, DataGridHeadersVisibility flags) => ((value & flags) == flags);
+
+            bool newValueCols = hasFlags(newValue, DataGridHeadersVisibility.Column);
+            bool newValueRows = hasFlags(newValue, DataGridHeadersVisibility.Row);
+            bool oldValueCols = hasFlags(oldValue, DataGridHeadersVisibility.Column);
+            bool oldValueRows = hasFlags(oldValue, DataGridHeadersVisibility.Row);
+
+            // Columns
+            if (newValueCols != oldValueCols)
+            {
+                if (_columnHeadersPresenter != null)
+                {
+                    EnsureColumnHeadersVisibility();
+                    if (!newValueCols)
+                    {
+                        _columnHeadersPresenter.Measure(Size.Empty);
+                    }
+                    else
+                    {
+                        EnsureVerticalGridLines();
+                    }
+                    InvalidateMeasure();
+                }
+            }
+
+            // Rows
+            if (newValueRows != oldValueRows)
+            {
+                if (_rowsPresenter != null)
+                {
+                    foreach (Control element in _rowsPresenter.Children)
+                    {
+                        if (element is DataGridRow row)
+                        {
+                            row.EnsureHeaderStyleAndVisibility(null);
+                            if (newValueRows)
+                            {
+                                row.UpdatePseudoClasses();
+                                row.EnsureHeaderVisibility();
+                            }
+                        }
+                        else if (element is DataGridRowGroupHeader rowGroupHeader)
+                        {
+                            rowGroupHeader.EnsureHeaderVisibility();
+                        }
+                    }
+                    InvalidateRowHeightEstimate();
+                    InvalidateRowsMeasure(invalidateIndividualElements: true);
+                }
+            }
+
+            if (_topLeftCornerHeader != null)
+            {
+                _topLeftCornerHeader.IsVisible = newValueRows && newValueCols;
+                if (_topLeftCornerHeader.IsVisible)
+                {
+                    _topLeftCornerHeader.Measure(Size.Empty);
+                }
+            }
+
+        }
+
+        private void OnGridLinesVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            foreach (DataGridRow row in GetAllRows())
+            {
+                row.EnsureGridLines();
+                row.InvalidateHorizontalArrange();
+            }
+        }
+
+        private void OnFrozenColumnCountChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            ProcessFrozenColumnCount();
+        }
+
+        private void ProcessFrozenColumnCount()
+        {
+            CorrectColumnFrozenStates();
+            ComputeScrollBarsLayout();
+
+            InvalidateColumnHeadersArrange();
+            InvalidateCellsArrange();
+        }
+
+        private void OnRowBackgroundChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            foreach (DataGridRow row in GetAllRows())
+            {
+                row.EnsureBackground();
+            }
+        }
+
+        private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var value = (DataGridLength)e.NewValue;
+
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
+            {
+                if (column.InheritsWidth)
+                {
+                    column.SetWidthInternalNoCallback(value);
+                }
+            }
+
+            EnsureHorizontalLayout();
+        }
+
+        private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            EnsureHorizontalLayout();
+        }
+
+        /// <summary>
+        /// Occurs one time for each public, non-static property in the bound data type when the 
+        /// <see cref="P:Avalonia.Controls.DataGrid.ItemsSource" /> property is changed and the 
+        /// <see cref="P:Avalonia.Controls.DataGrid.AutoGenerateColumns" /> property is true.
+        /// </summary>
+        public event EventHandler<DataGridAutoGeneratingColumnEventArgs> AutoGeneratingColumn;
+
+        /// <summary>
+        /// Occurs before a cell or row enters editing mode. 
+        /// </summary>
+        public event EventHandler<DataGridBeginningEditEventArgs> BeginningEdit;
+
+        /// <summary>
+        /// Occurs after cell editing has ended.
+        /// </summary>
+        public event EventHandler<DataGridCellEditEndedEventArgs> CellEditEnded;
+
+        /// <summary>
+        /// Occurs immediately before cell editing has ended.
+        /// </summary>
+        public event EventHandler<DataGridCellEditEndingEventArgs> CellEditEnding;
+
+        /// <summary>
+        /// Occurs when cell is mouse-pressed.
+        /// </summary>
+        public event EventHandler<DataGridCellPointerPressedEventArgs> CellPointerPressed;
+
+        /// <summary>
+        /// Occurs when the <see cref="P:Avalonia.Controls.DataGridColumn.DisplayIndex" /> 
+        /// property of a column changes.
+        /// </summary>
+        public event EventHandler<DataGridColumnEventArgs> ColumnDisplayIndexChanged;
+
+        /// <summary>
+        /// Raised when column reordering ends, to allow subscribers to clean up.
+        /// </summary>
+        public event EventHandler<DataGridColumnEventArgs> ColumnReordered;
+
+        /// <summary>
+        /// Raised when starting a column reordering action.  Subscribers to this event can
+        /// set tooltip and caret UIElements, constrain tooltip position, indicate that
+        /// a preview should be shown, or cancel reordering.
+        /// </summary>
+        public event EventHandler<DataGridColumnReorderingEventArgs> ColumnReordering;
+
+        /// <summary>
+        /// Occurs when a different cell becomes the current cell.
+        /// </summary>
+        public event EventHandler<EventArgs> CurrentCellChanged;
+
+        /// <summary>
+        /// Occurs after a <see cref="T:Avalonia.Controls.DataGridRow" /> 
+        /// is instantiated, so that you can customize it before it is used.
+        /// </summary>
+        public event EventHandler<DataGridRowEventArgs> LoadingRow;
+
+        /// <summary>
+        /// Occurs when a cell in a <see cref="T:Avalonia.Controls.DataGridTemplateColumn" /> enters editing mode.
+        /// 
+        /// </summary>
+        public event EventHandler<DataGridPreparingCellForEditEventArgs> PreparingCellForEdit;
+
+        /// <summary>
+        /// Occurs when the row has been successfully committed or cancelled.
+        /// </summary>
+        public event EventHandler<DataGridRowEditEndedEventArgs> RowEditEnded;
+
+        /// <summary>
+        /// Occurs immediately before the row has been successfully committed or cancelled.
+        /// </summary>
+        public event EventHandler<DataGridRowEditEndingEventArgs> RowEditEnding;
+
+        public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
+            RoutedEvent.Register<DataGrid, SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Occurs when the <see cref="P:Avalonia.Controls.DataGrid.SelectedItem" /> or 
+        /// <see cref="P:Avalonia.Controls.DataGrid.SelectedItems" /> property value changes.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged
+        {
+            add { AddHandler(SelectionChangedEvent, value); }
+            remove { AddHandler(SelectionChangedEvent, value); }
+        }
+
+        /// <summary>
+        /// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" /> 
+        /// object becomes available for reuse.
+        /// </summary>
+        public event EventHandler<DataGridRowEventArgs> UnloadingRow;
+
+        /// <summary>
+        /// Occurs when a new row details template is applied to a row, so that you can customize 
+        /// the details section before it is used.
+        /// </summary>
+        public event EventHandler<DataGridRowDetailsEventArgs> LoadingRowDetails;
+
+        /// <summary>
+        /// Occurs when the <see cref="P:Avalonia.Controls.DataGrid.RowDetailsVisibilityMode" /> 
+        /// property value changes.
+        /// </summary>
+        public event EventHandler<DataGridRowDetailsEventArgs> RowDetailsVisibilityChanged;
+
+        /// <summary>
+        /// Occurs when a row details element becomes available for reuse.
+        /// </summary>
+        public event EventHandler<DataGridRowDetailsEventArgs> UnloadingRowDetails;
+
+        /// <summary>
+        /// Gets a collection that contains all the columns in the control.
+        /// </summary>      
+        public ObservableCollection<DataGridColumn> Columns
+        {
+            get
+            {
+                // we use a backing field here because the field's type
+                // is a subclass of the property's
+                return ColumnsInternal;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the column that contains the current cell.
+        /// </summary>
+        public DataGridColumn CurrentColumn
+        {
+            get
+            {
+                if (CurrentColumnIndex == -1)
+                {
+                    return null;
+                }
+                Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+                return ColumnsItemsInternal[CurrentColumnIndex];
+            }
+            set
+            {
+                DataGridColumn dataGridColumn = value;
+                if (dataGridColumn == null)
+                {
+                    throw DataGridError.DataGrid.ValueCannotBeSetToNull("value", "CurrentColumn");
+                }
+                if (CurrentColumn != dataGridColumn)
+                {
+                    if (dataGridColumn.OwningGrid != this)
+                    {
+                        // Provided column does not belong to this DataGrid
+                        throw DataGridError.DataGrid.ColumnNotInThisDataGrid();
+                    }
+                    if (!dataGridColumn.IsVisible)
+                    {
+                        // CurrentColumn cannot be set to an invisible column
+                        throw DataGridError.DataGrid.ColumnCannotBeCollapsed();
+                    }
+                    if (CurrentSlot == -1)
+                    {
+                        // There is no current row so the current column cannot be set
+                        throw DataGridError.DataGrid.NoCurrentRow();
+                    }
+                    bool beginEdit = _editingColumnIndex != -1;
+
+                    //exitEditingMode, keepFocus, raiseEvents
+                    if (!EndCellEdit(DataGridEditAction.Commit, true, ContainsFocus, true))
+                    {
+                        // Edited value couldn't be committed or aborted
+                        return;
+                    }
+
+                    UpdateSelectionAndCurrency(dataGridColumn.Index, CurrentSlot, DataGridSelectionAction.None, false); //scrollIntoView
+                    Debug.Assert(_successfullyUpdatedSelection);
+
+                    if (beginEdit &&
+                        _editingColumnIndex == -1 &&
+                        CurrentSlot != -1 &&
+                        CurrentColumnIndex != -1 &&
+                        CurrentColumnIndex == dataGridColumn.Index &&
+                        dataGridColumn.OwningGrid == this &&
+                        !GetColumnEffectiveReadOnlyState(dataGridColumn))
+                    {
+                        // Returning to editing mode since the grid was in that mode prior to the EndCellEdit call above.
+                        BeginCellEdit(new RoutedEventArgs());
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets a list that contains the data items corresponding to the selected rows.
+        /// </summary>
+        public IList SelectedItems
+        {
+            get { return _selectedItems as IList; }
+        }
+
+        internal DataGridColumnCollection ColumnsInternal
+        {
+            get;
+            private set;
+        }
+
+        internal int AnchorSlot
+        {
+            get;
+            private set;
+        }
+
+        internal double ActualRowHeaderWidth
+        {
+            get
+            {
+                if (!AreRowHeadersVisible)
+                {
+                    return 0;
+                }
+                else
+                {
+                    return !double.IsNaN(RowHeaderWidth) ? RowHeaderWidth : RowHeadersDesiredWidth;
+                }
+            }
+        }
+
+        internal double ActualRowsPresenterHeight
+        {
+            get
+            {
+                if (_rowsPresenter != null)
+                {
+                    return _rowsPresenter.Bounds.Height;
+                }
+                return 0;
+            }
+        }
+
+        internal bool AreColumnHeadersVisible
+        {
+            get
+            {
+                return (HeadersVisibility & DataGridHeadersVisibility.Column) == DataGridHeadersVisibility.Column;
+            }
+        }
+
+        internal bool AreRowHeadersVisible
+        {
+            get
+            {
+                return (HeadersVisibility & DataGridHeadersVisibility.Row) == DataGridHeadersVisibility.Row;
+            }
+        }
+
+        /// <summary>
+        /// Indicates whether or not at least one auto-sizing column is waiting for all the rows
+        /// to be measured before its final width is determined.
+        /// </summary>
+        internal bool AutoSizingColumns
+        {
+            get
+            {
+                return _autoSizingColumns;
+            }
+            set
+            {
+                if (_autoSizingColumns && !value && ColumnsInternal != null)
+                {
+                    double adjustment = CellsWidth - ColumnsInternal.VisibleEdgedColumnsWidth;
+                    AdjustColumnWidths(0, adjustment, false);
+                    foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
+                    {
+                        column.IsInitialDesiredWidthDetermined = true;
+                    }
+                    ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+                    ComputeScrollBarsLayout();
+                    InvalidateColumnHeadersMeasure();
+                    InvalidateRowsMeasure(true);
+                }
+                _autoSizingColumns = value;
+            }
+        }
+
+        internal double AvailableSlotElementRoom
+        {
+            get;
+            set;
+        }
+
+        // Height currently available for cells this value is smaller.  This height is reduced by the existence of ColumnHeaders
+        // or a horizontal scrollbar.  Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are 
+        // not reflected immediately.
+        internal double CellsHeight
+        {
+            get
+            {
+                return RowsPresenterEstimatedAvailableHeight ?? 0;
+            }
+        }
+
+        // Width currently available for cells this value is smaller.  This width is reduced by the existence of RowHeaders
+        // or a vertical scrollbar.  Layout is asynchronous so changes to the RowHeaders or the vertical scrollbar are
+        // not reflected immediately
+        internal double CellsWidth
+        {
+            get
+            {
+                double rowsWidth = double.PositiveInfinity;
+                if (RowsPresenterAvailableSize.HasValue)
+                {
+                    rowsWidth = Math.Max(0, RowsPresenterAvailableSize.Value.Width - ActualRowHeaderWidth);
+                }
+                return double.IsPositiveInfinity(rowsWidth) ? ColumnsInternal.VisibleEdgedColumnsWidth : rowsWidth;
+            }
+        }
+
+        internal DataGridColumnHeadersPresenter ColumnHeaders => _columnHeadersPresenter;
+
+        internal List<DataGridColumn> ColumnsItemsInternal => ColumnsInternal.ItemsInternal;
+
+        internal bool ContainsFocus
+        {
+            get;
+            private set;
+        }
+
+        internal int CurrentColumnIndex
+        {
+            get
+            {
+                return CurrentCellCoordinates.ColumnIndex;
+            }
+
+            private set
+            {
+                CurrentCellCoordinates.ColumnIndex = value;
+            }
+        }
+
+        internal int CurrentSlot
+        {
+            get
+            {
+                return CurrentCellCoordinates.Slot;
+            }
+
+            private set
+            {
+                CurrentCellCoordinates.Slot = value;
+            }
+        }
+
+        internal DataGridDataConnection DataConnection
+        {
+            get;
+            private set;
+        }
+
+        internal DataGridDisplayData DisplayData
+        {
+            get;
+            private set;
+        }
+
+        internal int EditingColumnIndex
+        {
+            get;
+            private set;
+        }
+
+        internal DataGridRow EditingRow
+        {
+            get;
+            private set;
+        }
+
+        internal double FirstDisplayedScrollingColumnHiddenWidth => _negHorizontalOffset;
+
+        // When the RowsPresenter's width increases, the HorizontalOffset will be incorrect until
+        // the scrollbar's layout is recalculated, which doesn't occur until after the cells are measured.
+        // This property exists to account for this scenario, and avoid collapsing the incorrect cells.
+        internal double HorizontalAdjustment
+        {
+            get;
+            private set;
+        }
+
+        internal static double HorizontalGridLinesThickness => DATAGRID_horizontalGridLinesThickness;
+
+        // the sum of the widths in pixels of the scrolling columns preceding 
+        // the first displayed scrolling column
+        internal double HorizontalOffset
+        {
+            get
+            {
+                return _horizontalOffset;
+            }
+            set
+            {
+                if (value < 0)
+                {
+                    value = 0;
+                }
+                double widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
+                if (value > widthNotVisible)
+                {
+                    value = widthNotVisible;
+                }
+                if (value == _horizontalOffset)
+                {
+                    return;
+                }
+
+                if (_hScrollBar != null && value != _hScrollBar.Value)
+                {
+                    _hScrollBar.Value = value;
+                }
+                _horizontalOffset = value;
+
+                DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+                // update the lastTotallyDisplayedScrollingCol
+                ComputeDisplayedColumns();
+            }
+        }
+
+        internal ScrollBar HorizontalScrollBar => _hScrollBar;
+
+        internal IndexToValueTable<DataGridRowGroupInfo> RowGroupHeadersTable
+        {
+            get;
+            private set;
+        }
+
+        internal bool LoadingOrUnloadingRow
+        {
+            get;
+            private set;
+        }
+
+        internal bool InDisplayIndexAdjustments
+        {
+            get;
+            set;
+        }
+
+        internal int? MouseOverRowIndex
+        {
+            get
+            {
+                return _mouseOverRowIndex;
+            }
+            set
+            {
+                if (_mouseOverRowIndex != value)
+                {
+                    DataGridRow oldMouseOverRow = null;
+                    if (_mouseOverRowIndex.HasValue)
+                    {
+                        int oldSlot = SlotFromRowIndex(_mouseOverRowIndex.Value);
+                        if (IsSlotVisible(oldSlot))
+                        {
+                            oldMouseOverRow = DisplayData.GetDisplayedElement(oldSlot) as DataGridRow;
+                        }
+                    }
+
+                    _mouseOverRowIndex = value;
+
+                    // State for the old row needs to be applied after setting the new value
+                    if (oldMouseOverRow != null)
+                    {
+                        oldMouseOverRow.UpdatePseudoClasses();
+                    }
+
+                    if (_mouseOverRowIndex.HasValue)
+                    {
+                        int newSlot = SlotFromRowIndex(_mouseOverRowIndex.Value);
+                        if (IsSlotVisible(newSlot))
+                        {
+                            DataGridRow newMouseOverRow = DisplayData.GetDisplayedElement(newSlot) as DataGridRow;
+                            Debug.Assert(newMouseOverRow != null);
+                            if (newMouseOverRow != null)
+                            {
+                                newMouseOverRow.UpdatePseudoClasses();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        internal double NegVerticalOffset
+        {
+            get;
+            private set;
+        }
+
+        internal int NoCurrentCellChangeCount
+        {
+            get
+            {
+                return _noCurrentCellChangeCount;
+            }
+            set
+            {
+                _noCurrentCellChangeCount = value;
+                if (value == 0)
+                {
+                    FlushCurrentCellChanged();
+                }
+            }
+        }
+
+        internal double RowDetailsHeightEstimate
+        {
+            get;
+            private set;
+        }
+
+        internal double RowHeadersDesiredWidth
+        {
+            get
+            {
+                return _rowHeaderDesiredWidth;
+            }
+            set
+            {
+                // We only auto grow
+                if (_rowHeaderDesiredWidth < value)
+                {
+                    double oldActualRowHeaderWidth = ActualRowHeaderWidth;
+                    _rowHeaderDesiredWidth = value;
+                    if (oldActualRowHeaderWidth != ActualRowHeaderWidth)
+                    {
+                        EnsureRowHeaderWidth();
+                    }
+                }
+            }
+        }
+
+        internal double RowGroupHeaderHeightEstimate
+        {
+            get;
+            private set;
+        }
+
+        internal double RowHeightEstimate
+        {
+            get;
+            private set;
+        }
+
+        internal Size? RowsPresenterAvailableSize
+        {
+            get
+            {
+                return _rowsPresenterAvailableSize;
+            }
+            set
+            {
+                if (_rowsPresenterAvailableSize.HasValue && value.HasValue && value.Value.Width > RowsPresenterAvailableSize.Value.Width)
+                {
+                    // When the available cells width increases, the horizontal offset can be incorrect.
+                    // Store away an adjustment to use during the CellsPresenter's measure, so that the
+                    // ShouldDisplayCell method correctly determines if a cell will be in view.
+                    //
+                    //     |   h. offset   |       new available cells width          |
+                    //     |-------------->|----------------------------------------->|
+                    //      __________________________________________________        |
+                    //     |           |           |             |            |       |
+                    //     |  column0  |  column1  |   column2   |  column3   |<----->|
+                    //     |           |           |             |            |  adj. |
+                    //
+                    double adjustment = (_horizontalOffset + value.Value.Width) - ColumnsInternal.VisibleEdgedColumnsWidth;
+                    HorizontalAdjustment = Math.Min(HorizontalOffset, Math.Max(0, adjustment));
+                }
+                else
+                {
+                    HorizontalAdjustment = 0;
+                }
+                _rowsPresenterAvailableSize = value;
+            }
+        }
+        internal double? RowsPresenterEstimatedAvailableHeight
+        {
+            get;
+            set;
+        }
+
+        internal double[] RowGroupSublevelIndents
+        {
+            get;
+            private set;
+        }
+
+        // This flag indicates whether selection has actually changed during a selection operation,
+        // and exists to ensure that FlushSelectionChanged doesn't unnecessarily raise SelectionChanged.
+        internal bool SelectionHasChanged
+        {
+            get;
+            set;
+        }
+
+        internal int SlotCount
+        {
+            get;
+            private set;
+        }
+
+        internal bool UpdatedStateOnMouseLeftButtonDown
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Indicates whether or not to use star-sizing logic.  If the DataGrid has infinite available space,
+        /// then star sizing doesn't make sense.  In this case, all star columns grow to a predefined size of
+        /// 10,000 pixels in order to show the developer that star columns shouldn't be used.
+        /// </summary>
+        internal bool UsesStarSizing
+        {
+            get
+            {
+                if (ColumnsInternal != null)
+                {
+                    return ColumnsInternal.VisibleStarColumnCount > 0 &&
+                        (!RowsPresenterAvailableSize.HasValue || !double.IsPositiveInfinity(RowsPresenterAvailableSize.Value.Width));
+                }
+                return false;
+            }
+        }
+
+        internal ScrollBar VerticalScrollBar => _vScrollBar;
+
+        internal int VisibleSlotCount
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Gets the data item bound to the row that contains the current cell.
+        /// </summary>
+        protected object CurrentItem
+        {
+            get
+            {
+                if (CurrentSlot == -1 || Items == null || RowGroupHeadersTable.Contains(CurrentSlot))
+                {
+                    return null;
+                }
+                return DataConnection.GetDataItem(RowIndexFromSlot(CurrentSlot));
+            }
+        }
+
+        private DataGridCellCoordinates CurrentCellCoordinates
+        {
+            get;
+            set;
+        }
+
+        private int FirstDisplayedNonFillerColumnIndex
+        {
+            get
+            {
+                DataGridColumn column = ColumnsInternal.FirstVisibleNonFillerColumn;
+                if (column != null)
+                {
+                    if (column.IsFrozen)
+                    {
+                        return column.Index;
+                    }
+                    else
+                    {
+                        if (DisplayData.FirstDisplayedScrollingCol >= column.Index)
+                        {
+                            return DisplayData.FirstDisplayedScrollingCol;
+                        }
+                        else
+                        {
+                            return column.Index;
+                        }
+                    }
+                }
+                return -1;
+            }
+        }
+
+        private int NoSelectionChangeCount
+        {
+            get
+            {
+                return _noSelectionChangeCount;
+            }
+            set
+            {
+                _noSelectionChangeCount = value;
+                if (value == 0)
+                {
+                    FlushSelectionChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Enters editing mode for the current cell and current row (if they're not already in editing mode).
+        /// </summary>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool BeginEdit()
+        {
+            return BeginEdit(null);
+        }
+
+        /// <summary>
+        /// Enters editing mode for the current cell and current row (if they're not already in editing mode).
+        /// </summary>
+        /// <param name="editingEventArgs">Provides information about the user gesture that caused the call to BeginEdit. Can be null.</param>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool BeginEdit(RoutedEventArgs editingEventArgs)
+        {
+            if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot))
+            {
+                return false;
+            }
+
+            Debug.Assert(CurrentColumnIndex >= 0);
+            Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(CurrentSlot >= -1);
+            Debug.Assert(CurrentSlot < SlotCount);
+            Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot);
+
+            if (GetColumnEffectiveReadOnlyState(CurrentColumn))
+            {
+                // Current column is read-only
+                return false;
+            }
+            return BeginCellEdit(editingEventArgs);
+        }
+
+        /// <summary>
+        /// Cancels editing mode and restores the original value.
+        /// </summary>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool CancelEdit()
+        {
+            return CancelEdit(DataGridEditingUnit.Row);
+        }
+
+        /// <summary>
+        /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value.
+        /// </summary>
+        /// <param name="editingUnit">Specifies whether to cancel edit for a Cell or Row.</param>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool CancelEdit(DataGridEditingUnit editingUnit)
+        {
+            return CancelEdit(editingUnit, raiseEvents: true);
+        }
+
+        /// <summary>
+        /// Commits editing mode and pushes changes to the backend.
+        /// </summary>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool CommitEdit()
+        {
+            return CommitEdit(DataGridEditingUnit.Row, true);
+        }
+
+        /// <summary>
+        /// Commits editing mode for the specified DataGridEditingUnit and pushes changes to the backend.
+        /// </summary>
+        /// <param name="editingUnit">Specifies whether to commit edit for a Cell or Row.</param>
+        /// <param name="exitEditingMode">Editing mode is left if True.</param>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        public bool CommitEdit(DataGridEditingUnit editingUnit, bool exitEditingMode)
+        {
+            if (!EndCellEdit(
+                    editAction: DataGridEditAction.Commit,
+                    exitEditingMode: editingUnit == DataGridEditingUnit.Cell ? exitEditingMode : true,
+                    keepFocus: ContainsFocus,
+                    raiseEvents: true))
+            {
+                return false;
+            }
+            if (editingUnit == DataGridEditingUnit.Row)
+            {
+                return EndRowEdit(DataGridEditAction.Commit, exitEditingMode, raiseEvents: true);
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Scrolls the specified item or RowGroupHeader and/or column into view.
+        /// If item is not null: scrolls the row representing the item into view;
+        /// If column is not null: scrolls the column into view;
+        /// If both item and column are null, the method returns without scrolling.
+        /// </summary>
+        /// <param name="item">an item from the DataGrid's items source or a CollectionViewGroup from the collection view</param>
+        /// <param name="column">a column from the DataGrid's columns collection</param>
+        public void ScrollIntoView(object item, DataGridColumn column)
+        {
+            if ((column == null && (item == null || FirstDisplayedNonFillerColumnIndex == -1))
+                || (column != null && column.OwningGrid != this))
+            {
+                // no-op
+                return;
+            }
+            if (item == null)
+            {
+                // scroll column into view
+                ScrollSlotIntoView(
+                    column.Index,
+                    DisplayData.FirstScrollingSlot,
+                    forCurrentCellChange: false,
+                    forceHorizontalScroll: true);
+            }
+            else
+            {
+                int slot = -1;
+                DataGridRowGroupInfo rowGroupInfo = null;
+                if (item is DataGridCollectionViewGroup collectionViewGroup)
+                {
+                    rowGroupInfo = RowGroupInfoFromCollectionViewGroup(collectionViewGroup);
+                    if (rowGroupInfo == null)
+                    {
+                        Debug.Assert(false);
+                        return;
+                    }
+                    slot = rowGroupInfo.Slot;
+                }
+                else
+                {
+                    // the row index will be set to -1 if the item is null or not in the list
+                    int rowIndex = DataConnection.IndexOf(item);
+                    if (rowIndex == -1)
+                    {
+                        return;
+                    }
+                    slot = SlotFromRowIndex(rowIndex);
+                }
+
+                int columnIndex = (column == null) ? FirstDisplayedNonFillerColumnIndex : column.Index;
+
+                if (_collapsedSlotsTable.Contains(slot))
+                {
+                    // We need to expand all parent RowGroups so that the slot is visible
+                    if (rowGroupInfo != null)
+                    {
+                        ExpandRowGroupParentChain(rowGroupInfo.Level - 1, rowGroupInfo.Slot);
+                    }
+                    else
+                    {
+                        rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(slot));
+                        Debug.Assert(rowGroupInfo != null);
+                        if (rowGroupInfo != null)
+                        {
+                            ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
+                        }
+                    }
+
+                    // Update Scrollbar and display information
+                    NegVerticalOffset = 0;
+                    SetVerticalOffset(0);
+                    ResetDisplayedRows();
+                    DisplayData.FirstScrollingSlot = 0;
+                    ComputeScrollBarsLayout();
+                }
+
+                ScrollSlotIntoView(
+                    columnIndex, slot,
+                    forCurrentCellChange: true,
+                    forceHorizontalScroll: true);
+            }
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.DataGridRow" />.
+        /// </summary>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.DataGridRow" />.
+        /// </returns>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (_makeFirstDisplayedCellCurrentCellPending)
+            {
+                MakeFirstDisplayedCellCurrentCell();
+            }
+
+            if (Bounds.Width != finalSize.Width)
+            {
+                // If our final width has changed, we might need to update the filler
+                InvalidateColumnHeadersArrange();
+                InvalidateCellsArrange();
+            }
+
+            return base.ArrangeOverride(finalSize);
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.DataGridRow" /> to prepare for 
+        /// arranging them during the 
+        /// <see cref="M:Avalonia.Controls.DataGridRow.ArrangeOverride(System.Windows.Size)" /> pass. 
+        /// </summary>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.DataGridRow" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that 
+        /// child elements should not exceed.
+        /// </param>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            // Delay layout until after the initial measure to avoid invalid calculations when the 
+            // DataGrid is not part of the visual tree
+            if (!_measured)
+            {
+                _measured = true;
+
+                // We don't need to clear the rows because it was already done when the ItemsSource changed
+                RefreshRowsAndColumns(clearRows: false);
+
+                //// Update our estimates now that the DataGrid has all of the information necessary
+                UpdateRowDetailsHeightEstimate();
+
+                // Update frozen columns to account for columns added prior to loading or autogenerated columns
+                if (FrozenColumnCountWithFiller > 0)
+                {
+                    ProcessFrozenColumnCount();
+                }
+            }
+
+            Size desiredSize;
+            // This is a shortcut to skip layout if we don't have any columns
+            if (ColumnsInternal.VisibleEdgedColumnsWidth == 0)
+            {
+                if (_hScrollBar != null && _hScrollBar.IsVisible)
+                {
+                    _hScrollBar.IsVisible = false;
+                }
+                if (_vScrollBar != null && _vScrollBar.IsVisible)
+                {
+                    _vScrollBar.IsVisible = false;
+                }
+                desiredSize = base.MeasureOverride(availableSize);
+            }
+            else
+            {
+                if (_rowsPresenter != null)
+                {
+                    _rowsPresenter.InvalidateMeasure();
+                }
+
+                InvalidateColumnHeadersMeasure();
+
+                desiredSize = base.MeasureOverride(availableSize);
+
+                ComputeScrollBarsLayout();
+            }
+
+            return desiredSize;
+        }
+
+        /// <summary>
+        /// Raises the BeginningEdit event.
+        /// </summary>
+        protected virtual void OnBeginningEdit(DataGridBeginningEditEventArgs e)
+        {
+            BeginningEdit?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the CellEditEnded event.
+        /// </summary>
+        protected virtual void OnCellEditEnded(DataGridCellEditEndedEventArgs e)
+        {
+            CellEditEnded?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the CellEditEnding event.
+        /// </summary>
+        protected virtual void OnCellEditEnding(DataGridCellEditEndingEventArgs e)
+        {
+            CellEditEnding?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the CellPointerPressed event.
+        /// </summary>
+        internal virtual void OnCellPointerPressed(DataGridCellPointerPressedEventArgs e)
+        {
+            CellPointerPressed?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the CurrentCellChanged event.
+        /// </summary>
+        protected virtual void OnCurrentCellChanged(EventArgs e)
+        {
+            CurrentCellChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the LoadingRow event for row preparation.
+        /// </summary>
+        protected virtual void OnLoadingRow(DataGridRowEventArgs e)
+        {
+            EventHandler<DataGridRowEventArgs> handler = LoadingRow;
+            if (handler != null)
+            {
+                Debug.Assert(!_loadedRows.Contains(e.Row));
+                _loadedRows.Add(e.Row);
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+                Debug.Assert(_loadedRows.Contains(e.Row));
+                _loadedRows.Remove(e.Row);
+            }
+        }
+
+        /// <summary>
+        /// Scrolls the DataGrid according to the direction of the delta.
+        /// </summary>
+        /// <param name="e">PointerWheelEventArgs</param>
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0)
+            {
+                double scrollHeight = 0;
+                if (e.Delta.Y > 0)
+                {
+                    scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta);
+                }
+                else if (e.Delta.Y < 0)
+                {
+                    if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible)
+                    {
+                        scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+                    }
+                    else
+                    {
+                        double maximum = EdgedRowsHeightCalculated - CellsHeight;
+                        scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+                    }
+                }
+                if (scrollHeight != 0)
+                {
+                    DisplayData.PendingVerticalScrollHeight = scrollHeight;
+                    InvalidateRowsMeasure(invalidateIndividualElements: false);
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Raises the PreparingCellForEdit event.
+        /// </summary>
+        protected virtual void OnPreparingCellForEdit(DataGridPreparingCellForEditEventArgs e)
+        {
+            PreparingCellForEdit?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the RowEditEnded event.
+        /// </summary>
+        protected virtual void OnRowEditEnded(DataGridRowEditEndedEventArgs e)
+        {
+            RowEditEnded?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the RowEditEnding event.
+        /// </summary>
+        protected virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e)
+        {
+            RowEditEnding?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Raises the SelectionChanged event and clears the _selectionChanged.
+        /// This event won't get raised again until after _selectionChanged is set back to true.
+        /// </summary>
+        protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
+        {
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Raises the UnloadingRow event for row recycling.
+        /// </summary>
+        protected virtual void OnUnloadingRow(DataGridRowEventArgs e)
+        {
+            EventHandler<DataGridRowEventArgs> handler = UnloadingRow;
+            if (handler != null)
+            {
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+            }
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the column header when a new template is applied.
+        /// </summary>
+        //TODO Validation UI
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            // The template has changed, so we need to refresh the visuals
+            _measured = false;
+
+            if (_columnHeadersPresenter != null)
+            {
+                // If we're applying a new template, we want to remove the old column headers first
+                _columnHeadersPresenter.Children.Clear();
+            }
+
+            _columnHeadersPresenter = e.NameScope.Find<DataGridColumnHeadersPresenter>(DATAGRID_elementColumnHeadersPresenterName);
+
+            if (_columnHeadersPresenter != null)
+            {
+                if (ColumnsInternal.FillerColumn != null)
+                {
+                    ColumnsInternal.FillerColumn.IsRepresented = false;
+                }
+                _columnHeadersPresenter.OwningGrid = this;
+                // Columns were added before before our Template was applied, add the ColumnHeaders now
+                foreach (DataGridColumn column in ColumnsItemsInternal)
+                {
+                    InsertDisplayedColumnHeader(column);
+                }
+            }
+
+            if (_rowsPresenter != null)
+            {
+                // If we're applying a new template, we want to remove the old rows first
+                UnloadElements(recycle: false);
+            }
+
+            _rowsPresenter = e.NameScope.Find<DataGridRowsPresenter>(DATAGRID_elementRowsPresenterName);
+
+            if (_rowsPresenter != null)
+            {
+                _rowsPresenter.OwningGrid = this;
+                InvalidateRowHeightEstimate();
+                UpdateRowDetailsHeightEstimate();
+            }
+
+            _frozenColumnScrollBarSpacer = e.NameScope.Find<Control>(DATAGRID_elementFrozenColumnScrollBarSpacerName);
+
+            if (_hScrollBar != null)
+            {
+                _hScrollBar.Scroll -= HorizontalScrollBar_Scroll;
+            }
+
+            _hScrollBar = e.NameScope.Find<ScrollBar>(DATAGRID_elementHorizontalScrollbarName);
+
+            if (_hScrollBar != null)
+            {
+                //_hScrollBar.IsTabStop = false;
+                _hScrollBar.Maximum = 0.0;
+                _hScrollBar.Orientation = Orientation.Horizontal;
+                _hScrollBar.IsVisible = false;
+                _hScrollBar.Scroll += HorizontalScrollBar_Scroll;
+            }
+
+            if (_vScrollBar != null)
+            {
+                _vScrollBar.Scroll -= VerticalScrollBar_Scroll;
+            }
+
+            _vScrollBar = e.NameScope.Find<ScrollBar>(DATAGRID_elementVerticalScrollbarName);
+
+            if (_vScrollBar != null)
+            {
+                //_vScrollBar.IsTabStop = false;
+                _vScrollBar.Maximum = 0.0;
+                _vScrollBar.Orientation = Orientation.Vertical;
+                _vScrollBar.IsVisible = false;
+                _vScrollBar.Scroll += VerticalScrollBar_Scroll;
+            }
+
+            _topLeftCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopLeftCornerHeaderName);
+            EnsureTopLeftCornerHeader(); // EnsureTopLeftCornerHeader checks for a null _topLeftCornerHeader;
+            _topRightCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopRightCornerHeaderName);
+        }
+
+        /// <summary>
+        /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value.
+        /// </summary>
+        /// <param name="editingUnit">Specifies whether to cancel edit for a Cell or Row.</param>
+        /// <param name="raiseEvents">Specifies whether or not to raise editing events</param>
+        /// <returns>True if operation was successful. False otherwise.</returns>
+        internal bool CancelEdit(DataGridEditingUnit editingUnit, bool raiseEvents)
+        {
+            if (!EndCellEdit(
+                    DataGridEditAction.Cancel,
+                    exitEditingMode: true,
+                    keepFocus: ContainsFocus,
+                    raiseEvents: raiseEvents))
+            {
+                return false;
+            }
+
+            if (editingUnit == DataGridEditingUnit.Row)
+            {
+                return EndRowEdit(DataGridEditAction.Cancel, true, raiseEvents);
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// call when: selection changes or SelectedItems object changes
+        /// </summary>
+        internal void CoerceSelectedItem()
+        {
+            object selectedItem = null;
+
+            if (SelectionMode == DataGridSelectionMode.Extended &&
+                CurrentSlot != -1 &&
+                _selectedItems.ContainsSlot(CurrentSlot))
+            {
+                selectedItem = CurrentItem;
+            }
+            else if (_selectedItems.Count > 0)
+            {
+                selectedItem = _selectedItems[0];
+            }
+
+            SetValueNoCallback(SelectedItemProperty, selectedItem);
+
+            // Update the SelectedIndex
+            int newIndex = -1;
+
+            if (selectedItem != null)
+            {
+                newIndex = DataConnection.IndexOf(selectedItem);
+            }
+
+            SetValueNoCallback(SelectedIndexProperty, newIndex);
+        }
+
+        internal static DataGridCell GetOwningCell(Control element)
+        {
+            Debug.Assert(element != null);
+            DataGridCell cell = element as DataGridCell;
+            while (element != null && cell == null)
+            {
+                element = element.Parent as Control;
+                cell = element as DataGridCell;
+            }
+            return cell;
+        }
+
+        internal IEnumerable<object> GetSelectionInclusive(int startRowIndex, int endRowIndex)
+        {
+            int endSlot = SlotFromRowIndex(endRowIndex);
+            foreach (int slot in _selectedItems.GetSlots(SlotFromRowIndex(startRowIndex)))
+            {
+                if (slot > endSlot)
+                {
+                    break;
+                }
+                yield return DataConnection.GetDataItem(RowIndexFromSlot(slot));
+            }
+        }
+
+        internal void InitializeElements(bool recycleRows)
+        {
+            try
+            {
+                _noCurrentCellChangeCount++;
+
+                // The underlying collection has changed and our editing row (if there is one)
+                // is no longer relevant, so we should force a cancel edit.
+                CancelEdit(DataGridEditingUnit.Row, raiseEvents: false);
+
+                // We want to persist selection throughout a reset, so store away the selected items
+                List<object> selectedItemsCache = new List<object>(_selectedItems.SelectedItemsCache);
+
+                if (recycleRows)
+                {
+                    RefreshRows(recycleRows, clearRows: true);
+                }
+                else
+                {
+                    RefreshRowsAndColumns(clearRows: true);
+                }
+
+                // Re-select the old items
+                _selectedItems.SelectedItemsCache = selectedItemsCache;
+                CoerceSelectedItem();
+                if (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.Collapsed)
+                {
+                    UpdateRowDetailsVisibilityMode(RowDetailsVisibilityMode);
+                }
+
+                // The currently displayed rows may have incorrect visual states because of the selection change
+                ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
+            }
+            finally
+            {
+                NoCurrentCellChangeCount--;
+            }
+        }
+
+        internal bool IsDoubleClickRecordsClickOnCall(Control element)
+        {
+            if (_clickedElement == element)
+            {
+                _clickedElement = null;
+                return true;
+            }
+            else
+            {
+                _clickedElement = element;
+                return false;
+            }
+        }
+
+        // Returns the item or the CollectionViewGroup that is used as the DataContext for a given slot.
+        // If the DataContext is an item, rowIndex is set to the index of the item within the collection
+        internal object ItemFromSlot(int slot, ref int rowIndex)
+        {
+            if (RowGroupHeadersTable.Contains(slot))
+            {
+                return RowGroupHeadersTable.GetValueAt(slot)?.CollectionViewGroup;
+            }
+            else
+            {
+                rowIndex = RowIndexFromSlot(slot);
+                return DataConnection.GetDataItem(rowIndex);
+            }
+        }
+
+        internal bool ProcessDownKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessDownKeyInternal(shift, ctrl);
+        }
+
+        internal bool ProcessEndKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessEndKey(shift, ctrl);
+        }
+
+        internal bool ProcessEnterKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessEnterKey(shift, ctrl);
+        }
+
+        internal bool ProcessHomeKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessHomeKey(shift, ctrl);
+        }
+
+        internal void ProcessHorizontalScroll(ScrollEventType scrollEventType)
+        {
+            if (_horizontalScrollChangesIgnored > 0)
+            {
+                return;
+            }
+
+            // If the user scrolls with the buttons, we need to update the new value of the scroll bar since we delay
+            // this calculation.  If they scroll in another other way, the scroll bar's correct value has already been set
+            double scrollBarValueDifference = 0;
+            if (scrollEventType == ScrollEventType.SmallIncrement)
+            {
+                scrollBarValueDifference = GetHorizontalSmallScrollIncrease();
+            }
+            else if (scrollEventType == ScrollEventType.SmallDecrement)
+            {
+                scrollBarValueDifference = -GetHorizontalSmallScrollDecrease();
+            }
+            _horizontalScrollChangesIgnored++;
+            try
+            {
+                if (scrollBarValueDifference != 0)
+                {
+                    Debug.Assert(_horizontalOffset + scrollBarValueDifference >= 0);
+                    _hScrollBar.Value = _horizontalOffset + scrollBarValueDifference;
+                }
+                UpdateHorizontalOffset(_hScrollBar.Value);
+            }
+            finally
+            {
+                _horizontalScrollChangesIgnored--;
+            }
+        }
+
+        internal bool ProcessLeftKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessLeftKey(shift, ctrl);
+        }
+
+        internal bool ProcessNextKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessNextKey(shift, ctrl);
+        }
+
+        internal bool ProcessPriorKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessPriorKey(shift, ctrl);
+        }
+
+        internal bool ProcessRightKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessRightKey(shift, ctrl);
+        }
+
+        /// <summary>
+        /// Selects items and updates currency based on parameters
+        /// </summary>
+        /// <param name="columnIndex">column index to make current</param>
+        /// <param name="item">data item or CollectionViewGroup to make current</param>
+        /// <param name="backupSlot">slot to use in case the item is no longer valid</param>
+        /// <param name="action">selection action to perform</param>
+        /// <param name="scrollIntoView">whether or not the new current item should be scrolled into view</param>
+        internal void ProcessSelectionAndCurrency(int columnIndex, object item, int backupSlot, DataGridSelectionAction action, bool scrollIntoView)
+        {
+            _noSelectionChangeCount++;
+            _noCurrentCellChangeCount++;
+            try
+            {
+                int slot = -1;
+                if (item is DataGridCollectionViewGroup group)
+                {
+                    DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(group);
+                    if (groupInfo != null)
+                    {
+                        slot = groupInfo.Slot;
+                    }
+                }
+                else
+                {
+                    slot = SlotFromRowIndex(DataConnection.IndexOf(item));
+                }
+                if (slot == -1)
+                {
+                    slot = backupSlot;
+                }
+                if (slot < 0 || slot > SlotCount)
+                {
+                    return;
+                }
+
+                switch (action)
+                {
+                    case DataGridSelectionAction.AddCurrentToSelection:
+                        SetRowSelection(slot, isSelected: true, setAnchorSlot: true);
+                        break;
+                    case DataGridSelectionAction.RemoveCurrentFromSelection:
+                        SetRowSelection(slot, isSelected: false, setAnchorSlot: false);
+                        break;
+                    case DataGridSelectionAction.SelectFromAnchorToCurrent:
+                        if (SelectionMode == DataGridSelectionMode.Extended && AnchorSlot != -1)
+                        {
+                            int anchorSlot = AnchorSlot;
+                            ClearRowSelection(slot, setAnchorSlot: false);
+                            if (slot <= anchorSlot)
+                            {
+                                SetRowsSelection(slot, anchorSlot);
+                            }
+                            else
+                            {
+                                SetRowsSelection(anchorSlot, slot);
+                            }
+                        }
+                        else
+                        {
+                            goto case DataGridSelectionAction.SelectCurrent;
+                        }
+                        break;
+                    case DataGridSelectionAction.SelectCurrent:
+                        ClearRowSelection(slot, setAnchorSlot: true);
+                        break;
+                    case DataGridSelectionAction.None:
+                        break;
+                }
+
+                if (CurrentSlot != slot || (CurrentColumnIndex != columnIndex && columnIndex != -1))
+                {
+                    if (columnIndex == -1)
+                    {
+                        if (CurrentColumnIndex != -1)
+                        {
+                            columnIndex = CurrentColumnIndex;
+                        }
+                        else
+                        {
+                            DataGridColumn firstVisibleColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+                            if (firstVisibleColumn != null)
+                            {
+                                columnIndex = firstVisibleColumn.Index;
+                            }
+                        }
+                    }
+                    if (columnIndex != -1)
+                    {
+                        if (!SetCurrentCellCore(
+                                columnIndex, slot,
+                                commitEdit: true,
+                                endRowEdit: SlotFromRowIndex(SelectedIndex) != slot)
+                            || (scrollIntoView &&
+                                !ScrollSlotIntoView(
+                                    columnIndex, slot,
+                                    forCurrentCellChange: true,
+                                    forceHorizontalScroll: false)))
+                        {
+                            return;
+                        }
+                    }
+                }
+                _successfullyUpdatedSelection = true;
+            }
+            finally
+            {
+                NoCurrentCellChangeCount--;
+                NoSelectionChangeCount--;
+            }
+        }
+
+        internal bool ProcessUpKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessUpKey(shift, ctrl);
+        }
+
+        //internal void ProcessVerticalScroll(double oldValue, double newValue)
+        internal void ProcessVerticalScroll(ScrollEventType scrollEventType)
+        {
+            if (_verticalScrollChangesIgnored > 0)
+            {
+                return;
+            }
+            Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
+
+            _verticalScrollChangesIgnored++;
+            try
+            {
+                Debug.Assert(_vScrollBar != null);
+                if (scrollEventType == ScrollEventType.SmallIncrement)
+                {
+                    DisplayData.PendingVerticalScrollHeight = GetVerticalSmallScrollIncrease();
+                    double newVerticalOffset = _verticalOffset + DisplayData.PendingVerticalScrollHeight;
+                    if (newVerticalOffset > _vScrollBar.Maximum)
+                    {
+                        DisplayData.PendingVerticalScrollHeight -= newVerticalOffset - _vScrollBar.Maximum;
+                    }
+                }
+                else if (scrollEventType == ScrollEventType.SmallDecrement)
+                {
+                    if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
+                    {
+                        DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset;
+                    }
+                    else
+                    {
+                        int previousScrollingSlot = GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot);
+                        if (previousScrollingSlot >= 0)
+                        {
+                            ScrollSlotIntoView(previousScrollingSlot, scrolledHorizontally: false);
+                        }
+                        return;
+                    }
+                }
+                else
+                {
+                    DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset;
+                }
+
+                if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
+                {
+                    // Invalidate so the scroll happens on idle
+                    InvalidateRowsMeasure(invalidateIndividualElements: false);
+                }
+            }
+            finally
+            {
+                _verticalScrollChangesIgnored--;
+            }
+        }
+
+        internal void RefreshRowsAndColumns(bool clearRows)
+        {
+            if (_measured)
+            {
+                try
+                {
+                    _noCurrentCellChangeCount++;
+
+                    if (clearRows)
+                    {
+                        ClearRows(false);
+                        ClearRowGroupHeadersTable();
+                        PopulateRowGroupHeadersTable();
+                    }
+                    if (AutoGenerateColumns)
+                    {
+                        //Column auto-generation refreshes the rows too
+                        AutoGenerateColumnsPrivate();
+                    }
+                    foreach (DataGridColumn column in ColumnsItemsInternal)
+                    {
+                        //We don't need to refresh the state of AutoGenerated column headers because they're up-to-date
+                        if (!column.IsAutoGenerated && column.HasHeaderCell)
+                        {
+                            column.HeaderCell.ApplyState();
+                        }
+                    }
+
+                    RefreshRows(recycleRows: false, clearRows: false);
+
+                    if (Columns.Count > 0 && CurrentColumnIndex == -1)
+                    {
+                        MakeFirstDisplayedCellCurrentCell();
+                    }
+                    else
+                    {
+                        _makeFirstDisplayedCellCurrentCellPending = false;
+                        _desiredCurrentColumnIndex = -1;
+                        FlushCurrentCellChanged();
+                    }
+                }
+                finally
+                {
+                    NoCurrentCellChangeCount--;
+                }
+            }
+            else
+            {
+                if (clearRows)
+                {
+                    ClearRows(recycle: false);
+                }
+                ClearRowGroupHeadersTable();
+                PopulateRowGroupHeadersTable();
+            }
+        }
+
+        internal bool ScrollSlotIntoView(int columnIndex, int slot, bool forCurrentCellChange, bool forceHorizontalScroll)
+        {
+            Debug.Assert(columnIndex >= 0 && columnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= -1 && DisplayData.FirstDisplayedScrollingCol < ColumnsItemsInternal.Count);
+            Debug.Assert(DisplayData.LastTotallyDisplayedScrollingCol >= -1 && DisplayData.LastTotallyDisplayedScrollingCol < ColumnsItemsInternal.Count);
+            Debug.Assert(!IsSlotOutOfBounds(slot));
+            Debug.Assert(DisplayData.FirstScrollingSlot >= -1 && DisplayData.FirstScrollingSlot < SlotCount);
+            Debug.Assert(ColumnsItemsInternal[columnIndex].IsVisible);
+
+            if (CurrentColumnIndex >= 0 &&
+                (CurrentColumnIndex != columnIndex || CurrentSlot != slot))
+            {
+                if (!CommitEditForOperation(columnIndex, slot, forCurrentCellChange) || IsInnerCellOutOfBounds(columnIndex, slot))
+                {
+                    return false;
+                }
+            }
+
+            double oldHorizontalOffset = HorizontalOffset;
+
+            //scroll horizontally unless we're on a RowGroupHeader and we're not forcing horizontal scrolling
+            if ((forceHorizontalScroll || (slot != -1))
+                && !ScrollColumnIntoView(columnIndex))
+            {
+                return false;
+            }
+
+            //scroll vertically
+            if (!ScrollSlotIntoView(slot, scrolledHorizontally: oldHorizontalOffset != HorizontalOffset))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        // Convenient overload that commits the current edit.
+        internal bool SetCurrentCellCore(int columnIndex, int slot)
+        {
+            return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true);
+        }
+
+        internal void UpdateHorizontalOffset(double newValue)
+        {
+            if (HorizontalOffset != newValue)
+            {
+                HorizontalOffset = newValue;
+
+                InvalidateColumnHeadersMeasure();
+                InvalidateRowsMeasure(true);
+            }
+        }
+
+        internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView)
+        {
+            _successfullyUpdatedSelection = false;
+
+            _noSelectionChangeCount++;
+            _noCurrentCellChangeCount++;
+            try
+            {
+                if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented &&
+                    columnIndex == ColumnsInternal.RowGroupSpacerColumn.Index)
+                {
+                    columnIndex = -1;
+                }
+                if (IsSlotOutOfSelectionBounds(slot) || (columnIndex != -1 && IsColumnOutOfBounds(columnIndex)))
+                {
+                    return false;
+                }
+
+                int newCurrentPosition = -1;
+                object item = ItemFromSlot(slot, ref newCurrentPosition);
+
+                if (EditingRow != null && slot != EditingRow.Slot && !CommitEdit(DataGridEditingUnit.Row, true))
+                {
+                    return false;
+                }
+
+                if (DataConnection.CollectionView != null &&
+                    DataConnection.CollectionView.CurrentPosition != newCurrentPosition)
+                {
+                    DataConnection.MoveCurrentTo(item, slot, columnIndex, action, scrollIntoView);
+                }
+                else
+                {
+                    ProcessSelectionAndCurrency(columnIndex, item, slot, action, scrollIntoView);
+                }
+            }
+            finally
+            {
+                NoCurrentCellChangeCount--;
+                NoSelectionChangeCount--;
+            }
+
+            return _successfullyUpdatedSelection;
+        }
+
+        internal void UpdateStateOnCurrentChanged(object currentItem, int currentPosition)
+        {
+            if (currentItem == CurrentItem && currentItem == SelectedItem && currentPosition == SelectedIndex)
+            {
+                // The DataGrid's CurrentItem is already up-to-date, so we don't need to do anything
+                return;
+            }
+
+            int columnIndex = CurrentColumnIndex;
+            if (columnIndex == -1)
+            {
+                if (IsColumnOutOfBounds(_desiredCurrentColumnIndex) ||
+                    (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && _desiredCurrentColumnIndex == ColumnsInternal.RowGroupSpacerColumn.Index))
+                {
+                    columnIndex = FirstDisplayedNonFillerColumnIndex;
+                }
+                else
+                {
+                    columnIndex = _desiredCurrentColumnIndex;
+                }
+            }
+            _desiredCurrentColumnIndex = -1;
+
+            try
+            {
+                _noSelectionChangeCount++;
+                _noCurrentCellChangeCount++;
+
+                if (!CommitEdit())
+                {
+                    CancelEdit(DataGridEditingUnit.Row, false);
+                }
+
+                ClearRowSelection(true);
+                if (currentItem == null)
+                {
+                    SetCurrentCellCore(-1, -1);
+                }
+                else
+                {
+                    int slot = SlotFromRowIndex(currentPosition);
+                    ProcessSelectionAndCurrency(columnIndex, currentItem, slot, DataGridSelectionAction.SelectCurrent, false);
+                }
+            }
+            finally
+            {
+                NoCurrentCellChangeCount--;
+                NoSelectionChangeCount--;
+            }
+        }
+
+        //TODO: Ensure left button is checked for
+        internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
+        {
+            KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.InputModifiers, out bool ctrl, out bool shift);
+            return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
+        }
+
+        internal void UpdateVerticalScrollBar()
+        {
+            if (_vScrollBar != null && _vScrollBar.IsVisible)
+            {
+                double cellsHeight = CellsHeight;
+                double edgedRowsHeightCalculated = EdgedRowsHeightCalculated;
+                UpdateVerticalScrollBar(
+                    needVertScrollbar: edgedRowsHeightCalculated > cellsHeight,
+                    forceVertScrollbar: VerticalScrollBarVisibility == ScrollBarVisibility.Visible,
+                    totalVisibleHeight: edgedRowsHeightCalculated,
+                    cellsHeight: cellsHeight);
+            }
+        }
+
+        /// <summary>
+        /// If the editing element has focus, this method will set focus to the DataGrid itself
+        /// in order to force the element to lose focus.  It will then wait for the editing element's
+        /// LostFocus event, at which point it will perform the specified action.
+        /// 
+        /// NOTE: It is important to understand that the specified action will be performed when the editing
+        /// element loses focus only if this method returns true.  If it returns false, then the action
+        /// will not be performed later on, and should instead be performed by the caller, if necessary.
+        /// </summary>
+        /// <param name="action">Action to perform after the editing element loses focus</param>
+        /// <returns>True if the editing element had focus and the action was cached away; false otherwise</returns>
+        //TODO TabStop
+        internal bool WaitForLostFocus(Action action)
+        {
+            if (EditingRow != null && EditingColumnIndex != -1 && !_executingLostFocusActions)
+            {
+                DataGridColumn editingColumn = ColumnsItemsInternal[EditingColumnIndex];
+                IControl editingElement = editingColumn.GetCellContent(EditingRow);
+                if (editingElement != null && editingElement.ContainsChild(_focusedObject))
+                {
+                    Debug.Assert(_lostFocusActions != null);
+                    _lostFocusActions.Enqueue(action);
+                    editingElement.LostFocus += EditingElement_LostFocus;
+                    //IsTabStop = true;
+                    Focus();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Raises the LoadingRowDetails for row details preparation
+        /// </summary>
+        protected virtual void OnLoadingRowDetails(DataGridRowDetailsEventArgs e)
+        {
+            EventHandler<DataGridRowDetailsEventArgs> handler = LoadingRowDetails;
+            if (handler != null)
+            {
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+            }
+        }
+
+        /// <summary>
+        /// Raises the UnloadingRowDetails event
+        /// </summary>
+        protected virtual void OnUnloadingRowDetails(DataGridRowDetailsEventArgs e)
+        {
+            EventHandler<DataGridRowDetailsEventArgs> handler = UnloadingRowDetails;
+            if (handler != null)
+            {
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+            }
+        }
+
+        internal void OnRowDetailsChanged()
+        {
+            if (!_scrollingByHeight)
+            {
+                // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough 
+                // since rows could be added or removed
+                InvalidateMeasure();
+            }
+        }
+
+        private void UpdateRowDetailsVisibilityMode(DataGridRowDetailsVisibilityMode newDetailsMode)
+        {
+            int itemCount = DataConnection.Count;
+            if (_rowsPresenter != null && itemCount > 0)
+            {
+                bool newDetailsVisibility = false;
+                switch (newDetailsMode)
+                {
+                    case DataGridRowDetailsVisibilityMode.Visible:
+                        newDetailsVisibility = true;
+                        _showDetailsTable.AddValues(0, itemCount, true);
+                        break;
+                    case DataGridRowDetailsVisibilityMode.Collapsed:
+                        newDetailsVisibility = false;
+                        _showDetailsTable.AddValues(0, itemCount, false);
+                        break;
+                    case DataGridRowDetailsVisibilityMode.VisibleWhenSelected:
+                        _showDetailsTable.Clear();
+                        break;
+                }
+
+                bool updated = false;
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    if (row.IsVisible)
+                    {
+                        if (newDetailsMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected)
+                        {
+                            // For VisibleWhenSelected, we need to calculate the value for each individual row
+                            newDetailsVisibility = _selectedItems.ContainsSlot(row.Slot);
+                        }
+                        if (row.AreDetailsVisible != newDetailsVisibility)
+                        {
+                            updated = true;
+
+                            row.SetDetailsVisibilityInternal(newDetailsVisibility, raiseNotification: true, animate: false);
+                        }
+                    }
+                }
+                if (updated)
+                {
+                    UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
+                    InvalidateRowsMeasure(invalidateIndividualElements: false);
+                }
+            }
+        }
+
+        //TODO Styles
+        private void AddNewCellPrivate(DataGridRow row, DataGridColumn column)
+        {
+            DataGridCell newCell = new DataGridCell();
+            PopulateCellContent(
+                isCellEdited: false,
+                dataGridColumn: column,
+                dataGridRow: row,
+                dataGridCell: newCell);
+            if (row.OwningGrid != null)
+            {
+                newCell.OwningColumn = column;
+                newCell.IsVisible = column.IsVisible;
+            }
+            //newCell.EnsureStyle(null);
+            row.Cells.Insert(column.Index, newCell);
+        }
+
+        private bool BeginCellEdit(RoutedEventArgs editingEventArgs)
+        {
+            if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot))
+            {
+                return false;
+            }
+
+            Debug.Assert(CurrentColumnIndex >= 0);
+            Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(CurrentSlot >= -1);
+            Debug.Assert(CurrentSlot < SlotCount);
+            Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot);
+            Debug.Assert(!GetColumnEffectiveReadOnlyState(CurrentColumn));
+            Debug.Assert(CurrentColumn.IsVisible);
+
+            if (_editingColumnIndex != -1)
+            {
+                // Current cell is already in edit mode
+                Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+                return true;
+            }
+
+            // Get or generate the editing row if it doesn't exist
+            DataGridRow dataGridRow = EditingRow;
+            if (dataGridRow == null)
+            {
+                if (IsSlotVisible(CurrentSlot))
+                {
+                    dataGridRow = DisplayData.GetDisplayedElement(CurrentSlot) as DataGridRow;
+                    Debug.Assert(dataGridRow != null);
+                }
+                else
+                {
+                    dataGridRow = GenerateRow(RowIndexFromSlot(CurrentSlot), CurrentSlot);
+                }
+            }
+            Debug.Assert(dataGridRow != null);
+
+            // Cache these to see if they change later
+            int currentRowIndex = CurrentSlot;
+            int currentColumnIndex = CurrentColumnIndex;
+
+            // Raise the BeginningEdit event
+            DataGridCell dataGridCell = dataGridRow.Cells[CurrentColumnIndex];
+            DataGridBeginningEditEventArgs e = new DataGridBeginningEditEventArgs(CurrentColumn, dataGridRow, editingEventArgs);
+            OnBeginningEdit(e);
+            if (e.Cancel
+                || currentRowIndex != CurrentSlot
+                || currentColumnIndex != CurrentColumnIndex
+                || !GetRowSelection(CurrentSlot)
+                || (EditingRow == null && !BeginRowEdit(dataGridRow)))
+            {
+                // If either BeginningEdit was canceled, currency/selection was changed in the event handler,
+                // or we failed opening the row for edit, then we can no longer continue BeginCellEdit
+                return false;
+            }
+            Debug.Assert(EditingRow != null);
+            Debug.Assert(EditingRow.Slot == CurrentSlot);
+
+            // Finally, we can prepare the cell for editing
+            _editingColumnIndex = CurrentColumnIndex;
+            _editingEventArgs = editingEventArgs;
+            EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses();
+            PopulateCellContent(
+                isCellEdited: true,
+                dataGridColumn: CurrentColumn,
+                dataGridRow: dataGridRow,
+                dataGridCell: dataGridCell);
+            return true;
+        }
+
+        //TODO Validation
+        private bool BeginRowEdit(DataGridRow dataGridRow)
+        {
+            Debug.Assert(EditingRow == null);
+            Debug.Assert(dataGridRow != null);
+
+            Debug.Assert(CurrentSlot >= -1);
+            Debug.Assert(CurrentSlot < SlotCount);
+
+            if (DataConnection.BeginEdit(dataGridRow.DataContext))
+            {
+                EditingRow = dataGridRow;
+                GenerateEditingElements();
+                return true;
+            }
+            return false;
+        }
+
+        private bool CancelRowEdit(bool exitEditingMode)
+        {
+            if (EditingRow == null)
+            {
+                return true;
+            }
+            Debug.Assert(EditingRow != null && EditingRow.Index >= -1);
+            Debug.Assert(EditingRow.Slot < SlotCount);
+            Debug.Assert(CurrentColumn != null);
+
+            object dataItem = EditingRow.DataContext;
+            if (!DataConnection.CancelEdit(dataItem))
+            {
+                return false;
+            }
+            foreach (DataGridColumn column in Columns)
+            {
+                if (!exitEditingMode && column.Index == _editingColumnIndex && column is DataGridBoundColumn)
+                {
+                    continue;
+                }
+                PopulateCellContent(
+                    isCellEdited: !exitEditingMode && column.Index == _editingColumnIndex,
+                    dataGridColumn: column,
+                    dataGridRow: EditingRow,
+                    dataGridCell: EditingRow.Cells[column.Index]);
+            }
+            return true;
+        }
+
+        private bool CommitEditForOperation(int columnIndex, int slot, bool forCurrentCellChange)
+        {
+            if (forCurrentCellChange)
+            {
+                if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true))
+                {
+                    return false;
+                }
+                if (CurrentSlot != slot &&
+                    !EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true))
+                {
+                    return false;
+                }
+            }
+
+            if (IsColumnOutOfBounds(columnIndex))
+            {
+                return false;
+            }
+            if (slot >= SlotCount)
+            {
+                // Current cell was reset because the commit deleted row(s).
+                // Since the user wants to change the current cell, we don't
+                // want to end up with no current cell. We pick the last row 
+                // in the grid which may be the 'new row'.
+                int lastSlot = LastVisibleSlot;
+                if (forCurrentCellChange &&
+                    CurrentColumnIndex == -1 &&
+                    lastSlot != -1)
+                {
+                    SetAndSelectCurrentCell(columnIndex, lastSlot, forceCurrentCellSelection: false);
+                }
+                // Interrupt operation because it has become invalid.
+                return false;
+            }
+            return true;
+        }
+
+        //TODO Validation
+        private bool CommitRowEdit(bool exitEditingMode)
+        {
+            if (EditingRow == null)
+            {
+                return true;
+            }
+            Debug.Assert(EditingRow != null && EditingRow.Index >= -1);
+            Debug.Assert(EditingRow.Slot < SlotCount);
+
+            //if (!ValidateEditingRow(scrollIntoView: true, wireEvents: false))
+            if (!EditingRow.IsValid)
+            {
+                return false;
+            }
+
+            DataConnection.EndEdit(EditingRow.DataContext);
+
+            if (!exitEditingMode)
+            {
+                DataConnection.BeginEdit(EditingRow.DataContext);
+            }
+            return true;
+        }
+
+        private void CompleteCellsCollection(DataGridRow dataGridRow)
+        {
+            Debug.Assert(dataGridRow != null);
+            int cellsInCollection = dataGridRow.Cells.Count;
+            if (ColumnsItemsInternal.Count > cellsInCollection)
+            {
+                for (int columnIndex = cellsInCollection; columnIndex < ColumnsItemsInternal.Count; columnIndex++)
+                {
+                    AddNewCellPrivate(dataGridRow, ColumnsItemsInternal[columnIndex]);
+                }
+            }
+        }
+
+        private void ComputeScrollBarsLayout()
+        {
+            if (_ignoreNextScrollBarsLayout)
+            {
+                _ignoreNextScrollBarsLayout = false;
+                //  
+
+            }
+            double cellsWidth = CellsWidth;
+            double cellsHeight = CellsHeight;
+
+            bool allowHorizScrollbar = false;
+            bool forceHorizScrollbar = false;
+            double horizScrollBarHeight = 0;
+            if (_hScrollBar != null)
+            {
+                forceHorizScrollbar = HorizontalScrollBarVisibility == ScrollBarVisibility.Visible;
+                allowHorizScrollbar = forceHorizScrollbar || (ColumnsInternal.VisibleColumnCount > 0 &&
+                    HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled &&
+                    HorizontalScrollBarVisibility != ScrollBarVisibility.Hidden);
+                // Compensate if the horizontal scrollbar is already taking up space
+                if (!forceHorizScrollbar && _hScrollBar.IsVisible)
+                {
+                    cellsHeight += _hScrollBar.DesiredSize.Height;
+                }
+                horizScrollBarHeight = _hScrollBar.Height + _hScrollBar.Margin.Top + _hScrollBar.Margin.Bottom;
+            }
+            bool allowVertScrollbar = false;
+            bool forceVertScrollbar = false;
+            double vertScrollBarWidth = 0;
+            if (_vScrollBar != null)
+            {
+                forceVertScrollbar = VerticalScrollBarVisibility == ScrollBarVisibility.Visible;
+                allowVertScrollbar = forceVertScrollbar || (ColumnsItemsInternal.Count > 0 &&
+                    VerticalScrollBarVisibility != ScrollBarVisibility.Disabled &&
+                    VerticalScrollBarVisibility != ScrollBarVisibility.Hidden);
+                // Compensate if the vertical scrollbar is already taking up space
+                if (!forceVertScrollbar && _vScrollBar.IsVisible)
+                {
+                    cellsWidth += _vScrollBar.DesiredSize.Width;
+                }
+                vertScrollBarWidth = _vScrollBar.Width + _vScrollBar.Margin.Left + _vScrollBar.Margin.Right;
+            }
+
+            // Now cellsWidth is the width potentially available for displaying data cells.
+            // Now cellsHeight is the height potentially available for displaying data cells. 
+
+            bool needHorizScrollbar = false;
+            bool needVertScrollbar = false;
+
+            double totalVisibleWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
+            double totalVisibleFrozenWidth = ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
+
+            UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
+            double totalVisibleHeight = EdgedRowsHeightCalculated;
+
+            if (!forceHorizScrollbar && !forceVertScrollbar)
+            {
+                bool needHorizScrollbarWithoutVertScrollbar = false;
+
+                if (allowHorizScrollbar &&
+                    DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
+                    DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
+                    DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
+                {
+                    double oldDataHeight = cellsHeight;
+                    cellsHeight -= horizScrollBarHeight;
+                    Debug.Assert(cellsHeight >= 0);
+                    needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
+                    if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
+                        DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
+                    {
+                        // Would we still need a horizontal scrollbar without the vertical one?
+                        UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
+                        if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
+                        {
+                            needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
+                        }
+                    }
+
+                    if (!needHorizScrollbar)
+                    {
+                        // Restore old data height because turns out a horizontal scroll bar wouldn't make sense
+                        cellsHeight = oldDataHeight;
+                    }
+                }
+
+                UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
+                if (allowVertScrollbar &&
+                    DoubleUtil.GreaterThan(cellsHeight, 0) &&
+                    DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
+                    DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
+                {
+                    cellsWidth -= vertScrollBarWidth;
+                    Debug.Assert(cellsWidth >= 0);
+                    needVertScrollbar = true;
+                }
+
+                DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+                // we compute the number of visible columns only after we set up the vertical scroll bar.
+                ComputeDisplayedColumns();
+
+                if (allowHorizScrollbar &&
+                    needVertScrollbar && !needHorizScrollbar &&
+                    DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
+                    DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
+                    DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
+                {
+                    cellsWidth += vertScrollBarWidth;
+                    cellsHeight -= horizScrollBarHeight;
+                    Debug.Assert(cellsHeight >= 0);
+                    needVertScrollbar = false;
+
+                    UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
+                    if (cellsHeight > 0 &&
+                        vertScrollBarWidth <= cellsWidth &&
+                        DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
+                    {
+                        cellsWidth -= vertScrollBarWidth;
+                        Debug.Assert(cellsWidth >= 0);
+                        needVertScrollbar = true;
+                    }
+                    if (needVertScrollbar)
+                    {
+                        needHorizScrollbar = true;
+                    }
+                    else
+                    {
+                        needHorizScrollbar = needHorizScrollbarWithoutVertScrollbar;
+                    }
+                }
+            }
+            else if (forceHorizScrollbar && !forceVertScrollbar)
+            {
+                if (allowVertScrollbar)
+                {
+                    if (cellsHeight > 0 &&
+                        DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
+                        DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
+                    {
+                        cellsWidth -= vertScrollBarWidth;
+                        Debug.Assert(cellsWidth >= 0);
+                        needVertScrollbar = true;
+                    }
+                    DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+                    ComputeDisplayedColumns();
+                }
+                needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth;
+            }
+            else if (!forceHorizScrollbar && forceVertScrollbar)
+            {
+                if (allowHorizScrollbar)
+                {
+                    if (cellsWidth > 0 &&
+                        DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
+                        DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
+                        DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth))
+                    {
+                        cellsHeight -= horizScrollBarHeight;
+                        Debug.Assert(cellsHeight >= 0);
+                        needHorizScrollbar = true;
+                        UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
+                    }
+                    DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+                    ComputeDisplayedColumns();
+                }
+                needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount;
+            }
+            else
+            {
+                Debug.Assert(forceHorizScrollbar && forceVertScrollbar);
+                Debug.Assert(allowHorizScrollbar && allowVertScrollbar);
+                DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+                ComputeDisplayedColumns();
+                needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount;
+                needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth;
+            }
+
+            UpdateHorizontalScrollBar(needHorizScrollbar, forceHorizScrollbar, totalVisibleWidth, totalVisibleFrozenWidth, cellsWidth);
+            UpdateVerticalScrollBar(needVertScrollbar, forceVertScrollbar, totalVisibleHeight, cellsHeight);
+
+            if (_topRightCornerHeader != null)
+            {
+                // Show the TopRightHeaderCell based on vertical ScrollBar visibility
+                if (AreColumnHeadersVisible &&
+                    _vScrollBar != null && _vScrollBar.IsVisible)
+                {
+                    _topRightCornerHeader.IsVisible = true; ;
+                }
+                else
+                {
+                    _topRightCornerHeader.IsVisible = false;
+                }
+            }
+            DisplayData.FullyRecycleElements();
+        }
+
+        /// <summary>
+        /// Handles the current editing element's LostFocus event by performing any actions that
+        /// were cached by the WaitForLostFocus method.
+        /// </summary>
+        /// <param name="sender">Editing element</param>
+        /// <param name="e">RoutedEventArgs</param>
+        private void EditingElement_LostFocus(object sender, RoutedEventArgs e)
+        {
+            if (sender is Control editingElement)
+            {
+                editingElement.LostFocus -= EditingElement_LostFocus;
+                if (EditingRow != null && EditingColumnIndex != -1)
+                {
+                    FocusEditingCell(true);
+                }
+                Debug.Assert(_lostFocusActions != null);
+                try
+                {
+                    _executingLostFocusActions = true;
+                    while (_lostFocusActions.Count > 0)
+                    {
+                        _lostFocusActions.Dequeue()();
+                    }
+                }
+                finally
+                {
+                    _executingLostFocusActions = false;
+                }
+            }
+        }
+
+        // Makes sure horizontal layout is updated to reflect any changes that affect it
+        private void EnsureHorizontalLayout()
+        {
+            ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+            InvalidateColumnHeadersMeasure();
+            InvalidateRowsMeasure(true);
+            InvalidateMeasure();
+        }
+
+        private void EnsureRowHeaderWidth()
+        {
+            if (AreRowHeadersVisible)
+            {
+                if (AreColumnHeadersVisible)
+                {
+                    EnsureTopLeftCornerHeader();
+                }
+
+                if (_rowsPresenter != null)
+                {
+
+                    bool updated = false;
+
+                    foreach (Control element in _rowsPresenter.Children)
+                    {
+                        if (element is DataGridRow row)
+                        {
+                            // If the RowHeader resulted in a different width the last time it was measured, we need
+                            // to re-measure it
+                            if (row.HeaderCell != null && row.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth)
+                            {
+                                row.HeaderCell.InvalidateMeasure();
+                                updated = true;
+                            }
+                        }
+                        else if (element is DataGridRowGroupHeader groupHeader && groupHeader.HeaderCell != null && groupHeader.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth)
+                        {
+                            groupHeader.HeaderCell.InvalidateMeasure();
+                            updated = true;
+                        }
+                    }
+
+                    if (updated)
+                    {
+                        // We need to update the width of the horizontal scrollbar if the rowHeaders' width actually changed
+                        InvalidateMeasure();
+                    }
+                }
+            }
+        }
+
+        private void EnsureRowsPresenterVisibility()
+        {
+            if (_rowsPresenter != null)
+            {
+                // RowCount doesn't need to be considered, doing so might cause extra Visibility changes
+                _rowsPresenter.IsVisible = (ColumnsInternal.FirstVisibleNonFillerColumn != null);
+            }
+        }
+
+        private void EnsureTopLeftCornerHeader()
+        {
+            if (_topLeftCornerHeader != null)
+            {
+                _topLeftCornerHeader.IsVisible = (HeadersVisibility == DataGridHeadersVisibility.All);
+
+                if (_topLeftCornerHeader.IsVisible)
+                {
+                    if (!double.IsNaN(RowHeaderWidth))
+                    {
+                        // RowHeaderWidth is set explicitly so we should use that
+                        _topLeftCornerHeader.Width = RowHeaderWidth;
+                    }
+                    else if (VisibleSlotCount > 0)
+                    {
+                        // RowHeaders AutoSize and we have at least 1 row so take the desired width
+                        _topLeftCornerHeader.Width = RowHeadersDesiredWidth;
+                    }
+                }
+            }
+        }
+
+        private void InvalidateCellsArrange()
+        {
+            foreach (DataGridRow row in GetAllRows())
+            {
+                row.InvalidateHorizontalArrange();
+            }
+        }
+
+        private void InvalidateColumnHeadersArrange()
+        {
+            if (_columnHeadersPresenter != null)
+            {
+                _columnHeadersPresenter.InvalidateArrange();
+            }
+        }
+
+        private void InvalidateColumnHeadersMeasure()
+        {
+            if (_columnHeadersPresenter != null)
+            {
+                EnsureColumnHeadersVisibility();
+                _columnHeadersPresenter.InvalidateMeasure();
+            }
+        }
+
+        private void InvalidateRowsArrange()
+        {
+            if (_rowsPresenter != null)
+            {
+                _rowsPresenter.InvalidateArrange();
+            }
+        }
+
+        private void InvalidateRowsMeasure(bool invalidateIndividualElements)
+        {
+            if (_rowsPresenter != null)
+            {
+                _rowsPresenter.InvalidateMeasure();
+
+                if (invalidateIndividualElements)
+                {
+                    foreach (Control element in _rowsPresenter.Children)
+                    {
+                        element.InvalidateMeasure();
+                    }
+                }
+            }
+        }
+
+        //TODO: Make override?
+        private void DataGrid_GotFocus(object sender, RoutedEventArgs e)
+        {
+            if (!ContainsFocus)
+            {
+                ContainsFocus = true;
+                ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
+                if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
+                {
+                    if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
+                    {
+                        row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
+                    }
+                }
+            }
+
+            // Keep track of which row contains the newly focused element
+            DataGridRow focusedRow = null;
+            IVisual focusedElement = e.Source as IVisual;
+            _focusedObject = focusedElement;
+            while (focusedElement != null)
+            {
+                focusedRow = focusedElement as DataGridRow;
+                if (focusedRow != null && focusedRow.OwningGrid == this && _focusedRow != focusedRow)
+                {
+                    ResetFocusedRow();
+                    _focusedRow = focusedRow.IsVisible ? focusedRow : null;
+                    break;
+                }
+                focusedElement = focusedElement.GetVisualParent();
+            }
+        }
+
+        //TODO: Check
+        private void DataGrid_IsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+        }
+
+        private void DataGrid_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (!e.Handled)
+            {
+                e.Handled = ProcessDataGridKey(e);
+            }
+        }
+
+        private void DataGrid_KeyUp(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.Tab && CurrentColumnIndex != -1 && e.Source == this)
+            {
+                bool success =
+                    ScrollSlotIntoView(
+                        CurrentColumnIndex, CurrentSlot,
+                        forCurrentCellChange: false,
+                        forceHorizontalScroll: true);
+                Debug.Assert(success);
+                if (CurrentColumnIndex != -1 && SelectedItem == null)
+                {
+                    SetRowSelection(CurrentSlot, isSelected: true, setAnchorSlot: true);
+                }
+            }
+        }
+
+        //TODO: Make override?
+        private void DataGrid_LostFocus(object sender, RoutedEventArgs e)
+        {
+            _focusedObject = null;
+            if (ContainsFocus)
+            {
+                bool focusLeftDataGrid = true;
+                bool dataGridWillReceiveRoutedEvent = true;
+                IVisual focusedObject = FocusManager.Instance.Current;
+
+                while (focusedObject != null)
+                {
+                    if (focusedObject == this)
+                    {
+                        focusLeftDataGrid = false;
+                        break;
+                    }
+
+                    // Walk up the visual tree.  If we hit the root, try using the framework element's
+                    // parent.  We do this because Popups behave differently with respect to the visual tree,
+                    // and it could have a parent even if the VisualTreeHelper doesn't find it.
+                    IVisual parent = focusedObject.GetVisualParent();
+                    if (parent == null)
+                    {
+                        if (focusedObject is Control element)
+                        {
+                            parent = element.Parent;
+                            if (parent != null)
+                            {
+                                dataGridWillReceiveRoutedEvent = false;
+                            }
+                        }
+                    }
+                    focusedObject = parent;
+                }
+
+                if (focusLeftDataGrid)
+                {
+                    ContainsFocus = false;
+                    if (EditingRow != null)
+                    {
+                        CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true);
+                    }
+                    ResetFocusedRow();
+                    ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
+                    if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
+                    {
+                        if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
+                        {
+                            row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
+                        }
+                    }
+                }
+                else if (!dataGridWillReceiveRoutedEvent)
+                {
+                    if (focusedObject is Control focusedElement)
+                    {
+                        focusedElement.LostFocus += ExternalEditingElement_LostFocus;
+                    }
+                }
+            }
+        }
+
+        private void EditingElement_Initialized(object sender, EventArgs e)
+        {
+            var element = sender as Control;
+            if (element != null)
+            {
+                element.Initialized -= EditingElement_Initialized;
+            }
+            PreparingCellForEditPrivate(element);
+        }
+
+        //TODO Validation
+        //TODO Binding
+        //TODO TabStop
+        private bool EndCellEdit(DataGridEditAction editAction, bool exitEditingMode, bool keepFocus, bool raiseEvents)
+        {
+            if (_editingColumnIndex == -1)
+            {
+                return true;
+            }
+
+            Debug.Assert(EditingRow != null);
+            Debug.Assert(_editingColumnIndex >= 0);
+            Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+            Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
+
+            // Cache these to see if they change later
+            int currentSlot = CurrentSlot;
+            int currentColumnIndex = CurrentColumnIndex;
+
+            // We're ready to start ending, so raise the event
+            DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex];
+            var editingElement = editingCell.Content as Control;
+            if (editingElement == null)
+            {
+                return false;
+            }
+            if (raiseEvents)
+            {
+                DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction);
+                OnCellEditEnding(e);
+                if (e.Cancel)
+                {
+                    // CellEditEnding has been cancelled
+                    return false;
+                }
+
+                // Ensure that the current cell wasn't changed in the user's CellEditEnding handler
+                if (_editingColumnIndex == -1 ||
+                    currentSlot != CurrentSlot ||
+                    currentColumnIndex != CurrentColumnIndex)
+                {
+                    return true;
+                }
+                Debug.Assert(EditingRow != null);
+                Debug.Assert(EditingRow.Slot == currentSlot);
+                Debug.Assert(_editingColumnIndex != -1);
+                Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+            }
+
+            // If we're canceling, let the editing column repopulate its old value if it wants
+            if (editAction == DataGridEditAction.Cancel)
+            {
+                CurrentColumn.CancelCellEditInternal(editingElement, _uneditedValue);
+
+                // Ensure that the current cell wasn't changed in the user column's CancelCellEdit
+                if (_editingColumnIndex == -1 ||
+                    currentSlot != CurrentSlot ||
+                    currentColumnIndex != CurrentColumnIndex)
+                {
+                    return true;
+                }
+                Debug.Assert(EditingRow != null);
+                Debug.Assert(EditingRow.Slot == currentSlot);
+                Debug.Assert(_editingColumnIndex != -1);
+                Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+            }
+
+            // If we're committing, explicitly update the source but watch out for any validation errors
+            if (editAction == DataGridEditAction.Commit)
+            {
+                void SetValidationStatus(ICellEditBinding binding)
+                {
+                    if (binding.IsValid)
+                    {
+                        ResetValidationStatus();
+                        if (editingElement != null)
+                        {
+                            DataValidationErrors.ClearErrors(editingElement);
+                        }
+                    }
+                    else
+                    {
+                        if (EditingRow != null)
+                        {
+                            if (editingCell.IsValid)
+                            {
+                                editingCell.IsValid = false;
+                                editingCell.UpdatePseudoClasses();
+                            }
+
+                            if (EditingRow.IsValid)
+                            {
+                                EditingRow.IsValid = false;
+                                EditingRow.UpdatePseudoClasses();
+                            }
+                        }
+
+                        if (editingElement != null)
+                        {
+                            var errorList =
+                                binding.ValidationErrors
+                                       .SelectMany(ex => ValidationUtil.UnpackException(ex))
+                                       .ToList();
+
+                            DataValidationErrors.SetErrors(editingElement, errorList);
+                        }
+                    }
+                }
+
+                var editBinding = CurrentColumn?.CellEditBinding;
+                if (editBinding != null && !editBinding.CommitEdit())
+                {
+                    SetValidationStatus(editBinding);
+                    _validationSubscription?.Dispose();
+                    _validationSubscription = editBinding.ValidationChanged.Subscribe(v => SetValidationStatus(editBinding));
+
+                    ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true);
+                    return false;
+                }
+            }
+
+            ResetValidationStatus();
+
+            if (exitEditingMode)
+            {
+                _editingColumnIndex = -1;
+                editingCell.UpdatePseudoClasses();
+
+                //IsTabStop = true;
+                if (keepFocus && editingElement.ContainsFocusedElement())
+                {
+                    Focus();
+                }
+
+                PopulateCellContent(
+                    isCellEdited: !exitEditingMode,
+                    dataGridColumn: CurrentColumn,
+                    dataGridRow: EditingRow,
+                    dataGridCell: editingCell);
+            }
+
+            // We're done, so raise the CellEditEnded event
+            if (raiseEvents)
+            {
+                OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction));
+            }
+
+            // There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
+            // so we should return false if we were supposed to exit editing mode, but we didn't
+            return !(exitEditingMode && currentColumnIndex == _editingColumnIndex);
+        }
+
+        //TODO Validation
+        private bool EndRowEdit(DataGridEditAction editAction, bool exitEditingMode, bool raiseEvents)
+        {
+            if (EditingRow == null || DataConnection.CommittingEdit)
+            {
+                return true;
+            }
+            if (_editingColumnIndex != -1 || (editAction == DataGridEditAction.Cancel && raiseEvents &&
+                !((DataConnection.EditableCollectionView != null && DataConnection.EditableCollectionView.CanCancelEdit) || (EditingRow.DataContext is IEditableObject))))
+            {
+                // Ending the row edit will fail immediately under the following conditions:
+                // 1. We haven't ended the cell edit yet.
+                // 2. We're trying to cancel edit when the underlying DataType is not an IEditableObject,
+                //    because we have no way to properly restore the old value.  We will only allow this to occur
+                //    if raiseEvents == false, which means we're internally forcing a cancel.
+                return false;
+            }
+            DataGridRow editingRow = EditingRow;
+
+            if (raiseEvents)
+            {
+                DataGridRowEditEndingEventArgs e = new DataGridRowEditEndingEventArgs(EditingRow, editAction);
+                OnRowEditEnding(e);
+                if (e.Cancel)
+                {
+                    // RowEditEnding has been cancelled
+                    return false;
+                }
+
+                // Editing states might have been changed in the RowEditEnding handlers
+                if (_editingColumnIndex != -1)
+                {
+                    return false;
+                }
+                if (editingRow != EditingRow)
+                {
+                    return true;
+                }
+            }
+
+            // Call the appropriate commit or cancel methods
+            if (editAction == DataGridEditAction.Commit)
+            {
+                if (!CommitRowEdit(exitEditingMode))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                if (!CancelRowEdit(exitEditingMode) && raiseEvents)
+                {
+                    // We failed to cancel edit so we should abort unless we're forcing a cancel
+                    return false;
+                }
+            }
+            ResetValidationStatus();
+
+            // Update the previously edited row's state
+            if (exitEditingMode && editingRow == EditingRow)
+            {
+                RemoveEditingElements();
+                ResetEditingRow();
+            }
+
+            // Raise the RowEditEnded event
+            if (raiseEvents)
+            {
+                OnRowEditEnded(new DataGridRowEditEndedEventArgs(editingRow, editAction));
+            }
+
+            return true;
+        }
+
+        private void EnsureColumnHeadersVisibility()
+        {
+            if (_columnHeadersPresenter != null)
+            {
+                _columnHeadersPresenter.IsVisible = AreColumnHeadersVisible;
+            }
+        }
+
+        private void EnsureVerticalGridLines()
+        {
+            if (AreColumnHeadersVisible)
+            {
+                double totalColumnsWidth = 0;
+                foreach (DataGridColumn column in ColumnsInternal)
+                {
+                    totalColumnsWidth += column.ActualWidth;
+
+                    column.HeaderCell.AreSeparatorsVisible = (column != ColumnsInternal.LastVisibleColumn || totalColumnsWidth < CellsWidth);
+                }
+            }
+
+            foreach (DataGridRow row in GetAllRows())
+            {
+                row.EnsureGridLines();
+            }
+        }
+
+        /// <summary>
+        /// Exits editing mode without trying to commit or revert the editing, and 
+        /// without repopulating the edited row's cell.
+        /// </summary>
+        //TODO TabStop
+        private void ExitEdit(bool keepFocus)
+        {
+            if (EditingRow == null || DataConnection.CommittingEdit)
+            {
+                Debug.Assert(_editingColumnIndex == -1);
+                return;
+            }
+
+            if (_editingColumnIndex != -1)
+            {
+                Debug.Assert(_editingColumnIndex >= 0);
+                Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
+                Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+                Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
+
+                _editingColumnIndex = -1;
+                EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses();
+            }
+            //IsTabStop = true;
+            if (IsSlotVisible(EditingRow.Slot))
+            {
+                EditingRow.UpdatePseudoClasses();
+            }
+            ResetEditingRow();
+            if (keepFocus)
+            {
+                Focus();
+            }
+        }
+
+        private void ExternalEditingElement_LostFocus(object sender, RoutedEventArgs e)
+        {
+            if (sender is Control element)
+            {
+                element.LostFocus -= ExternalEditingElement_LostFocus;
+                DataGrid_LostFocus(sender, e);
+            }
+        }
+
+        private void FlushCurrentCellChanged()
+        {
+            if (_makeFirstDisplayedCellCurrentCellPending)
+            {
+                return;
+            }
+            if (SelectionHasChanged)
+            {
+                // selection is changing, don't raise CurrentCellChanged until it's done
+                _flushCurrentCellChanged = true;
+                FlushSelectionChanged();
+                return;
+            }
+
+            // We don't want to expand all intermediate currency positions, so we only expand
+            // the last current item before we flush the event
+            if (_collapsedSlotsTable.Contains(CurrentSlot))
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot));
+                Debug.Assert(rowGroupInfo != null);
+                if (rowGroupInfo != null)
+                {
+                    ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
+                }
+            }
+
+            if (CurrentColumn != _previousCurrentColumn
+                || CurrentItem != _previousCurrentItem)
+            {
+                CoerceSelectedItem();
+                _previousCurrentColumn = CurrentColumn;
+                _previousCurrentItem = CurrentItem;
+
+                OnCurrentCellChanged(EventArgs.Empty);
+            }
+
+            _flushCurrentCellChanged = false;
+        }
+
+        private void FlushSelectionChanged()
+        {
+            if (SelectionHasChanged && _noSelectionChangeCount == 0 && !_makeFirstDisplayedCellCurrentCellPending)
+            {
+                CoerceSelectedItem();
+                if (NoCurrentCellChangeCount != 0)
+                {
+                    // current cell is changing, don't raise SelectionChanged until it's done
+                    return;
+                }
+                SelectionHasChanged = false;
+
+                if (_flushCurrentCellChanged)
+                {
+                    FlushCurrentCellChanged();
+                }
+
+                SelectionChangedEventArgs e = _selectedItems.GetSelectionChangedEventArgs();
+                if (e.AddedItems.Count > 0 || e.RemovedItems.Count > 0)
+                {
+                    OnSelectionChanged(e);
+                }
+            }
+        }
+
+        //TODO TabStop
+        private bool FocusEditingCell(bool setFocus)
+        {
+            Debug.Assert(CurrentColumnIndex >= 0);
+            Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(CurrentSlot >= -1);
+            Debug.Assert(CurrentSlot < SlotCount);
+            Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
+            Debug.Assert(_editingColumnIndex != -1);
+
+            //IsTabStop = false;
+            _focusEditingControl = false;
+
+            bool success = false;
+            DataGridCell dataGridCell = EditingRow.Cells[_editingColumnIndex];
+            if (setFocus)
+            {
+                if (dataGridCell.ContainsFocusedElement())
+                {
+                    success = true;
+                }
+                else
+                {
+                    dataGridCell.Focus();
+                    success = dataGridCell.ContainsFocusedElement();
+                }
+                //TODO Check
+                //success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus();
+                _focusEditingControl = !success;
+            }
+            return success;
+        }
+
+        // Calculates the amount to scroll for the ScrollLeft button
+        // This is a method rather than a property to emphasize a calculation
+        private double GetHorizontalSmallScrollDecrease()
+        {
+            // If the first column is covered up, scroll to the start of it when the user clicks the left button
+            if (_negHorizontalOffset > 0)
+            {
+                return _negHorizontalOffset;
+            }
+            else
+            {
+                // The entire first column is displayed, show the entire previous column when the user clicks
+                // the left button
+                DataGridColumn previousColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(
+                    ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]);
+                if (previousColumn != null)
+                {
+                    return GetEdgedColumnWidth(previousColumn);
+                }
+                else
+                {
+                    // There's no previous column so don't move
+                    return 0;
+                }
+            }
+        }
+
+        // Calculates the amount to scroll for the ScrollRight button
+        // This is a method rather than a property to emphasize a calculation
+        private double GetHorizontalSmallScrollIncrease()
+        {
+            if (DisplayData.FirstDisplayedScrollingCol >= 0)
+            {
+                return GetEdgedColumnWidth(ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]) - _negHorizontalOffset;
+            }
+            return 0;
+        }
+
+        // Calculates the amount the ScrollDown button should scroll
+        // This is a method rather than a property to emphasize that calculations are taking place
+        private double GetVerticalSmallScrollIncrease()
+        {
+            if (DisplayData.FirstScrollingSlot >= 0)
+            {
+                return GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) - NegVerticalOffset;
+            }
+            return 0;
+        }
+
+        private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
+        {
+            ProcessHorizontalScroll(e.ScrollEventType);
+        }
+
+        private bool IsColumnOutOfBounds(int columnIndex)
+        {
+            return columnIndex >= ColumnsItemsInternal.Count || columnIndex < 0;
+        }
+
+        private bool IsInnerCellOutOfBounds(int columnIndex, int slot)
+        {
+            return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfBounds(slot);
+        }
+
+        private bool IsInnerCellOutOfSelectionBounds(int columnIndex, int slot)
+        {
+            return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfSelectionBounds(slot);
+        }
+
+        private bool IsSlotOutOfBounds(int slot)
+        {
+            return slot >= SlotCount || slot < -1 || _collapsedSlotsTable.Contains(slot);
+        }
+
+        private bool IsSlotOutOfSelectionBounds(int slot)
+        {
+            if (RowGroupHeadersTable.Contains(slot))
+            {
+                Debug.Assert(slot >= 0 && slot < SlotCount);
+                return false;
+            }
+            else
+            {
+                int rowIndex = RowIndexFromSlot(slot);
+                return rowIndex < 0 || rowIndex >= DataConnection.Count;
+            }
+        }
+
+        private void MakeFirstDisplayedCellCurrentCell()
+        {
+            if (CurrentColumnIndex != -1)
+            {
+                _makeFirstDisplayedCellCurrentCellPending = false;
+                _desiredCurrentColumnIndex = -1;
+                FlushCurrentCellChanged();
+                return;
+            }
+            if (SlotCount != SlotFromRowIndex(DataConnection.Count))
+            {
+                _makeFirstDisplayedCellCurrentCellPending = true;
+                return;
+            }
+
+            // No current cell, therefore no selection either - try to set the current cell to the
+            // ItemsSource's ICollectionView.CurrentItem if it exists, otherwise use the first displayed cell.
+            int slot = 0;
+            if (DataConnection.CollectionView != null)
+            {
+                if (DataConnection.CollectionView.IsCurrentBeforeFirst ||
+                    DataConnection.CollectionView.IsCurrentAfterLast)
+                {
+                    slot = RowGroupHeadersTable.Contains(0) ? 0 : -1;
+                }
+                else
+                {
+                    slot = SlotFromRowIndex(DataConnection.CollectionView.CurrentPosition);
+                }
+            }
+            else
+            {
+                if (SelectedIndex == -1)
+                {
+                    // Try to default to the first row
+                    slot = SlotFromRowIndex(0);
+                    if (!IsSlotVisible(slot))
+                    {
+                        slot = -1;
+                    }
+                }
+                else
+                {
+                    slot = SlotFromRowIndex(SelectedIndex);
+                }
+            }
+            int columnIndex = FirstDisplayedNonFillerColumnIndex;
+            if (_desiredCurrentColumnIndex >= 0 && _desiredCurrentColumnIndex < ColumnsItemsInternal.Count)
+            {
+                columnIndex = _desiredCurrentColumnIndex;
+            }
+
+            SetAndSelectCurrentCell(columnIndex, slot, forceCurrentCellSelection: false);
+            AnchorSlot = slot;
+            _makeFirstDisplayedCellCurrentCellPending = false;
+            _desiredCurrentColumnIndex = -1;
+            FlushCurrentCellChanged();
+        }
+
+        //TODO Styles
+        private void PopulateCellContent(bool isCellEdited,
+                                         DataGridColumn dataGridColumn,
+                                         DataGridRow dataGridRow,
+                                         DataGridCell dataGridCell)
+        {
+            Debug.Assert(dataGridColumn != null);
+            Debug.Assert(dataGridRow != null);
+            Debug.Assert(dataGridCell != null);
+
+            IControl element = null;
+            DataGridBoundColumn dataGridBoundColumn = dataGridColumn as DataGridBoundColumn;
+            if (isCellEdited)
+            {
+                // Generate EditingElement and apply column style if available
+                element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext);
+                if (element != null)
+                {
+                    // Subscribe to the new element's events
+                    element.Initialized += EditingElement_Initialized;
+                }
+            }
+            else
+            {
+                // Generate Element and apply column style if available
+                element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext);
+            }
+
+            dataGridCell.Content = element;
+        }
+
+        private void PreparingCellForEditPrivate(Control editingElement)
+        {
+            if (_editingColumnIndex == -1 ||
+                CurrentColumnIndex == -1 ||
+                EditingRow.Cells[CurrentColumnIndex].Content != editingElement)
+            {
+                // The current cell has changed since the call to BeginCellEdit, so the fact
+                // that this element has loaded is no longer relevant
+                return;
+            }
+
+            Debug.Assert(EditingRow != null);
+            Debug.Assert(_editingColumnIndex >= 0);
+            Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
+            Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
+
+            FocusEditingCell(setFocus: ContainsFocus || _focusEditingControl);
+
+            // Prepare the cell for editing and raise the PreparingCellForEdit event for all columns
+            DataGridColumn dataGridColumn = CurrentColumn;
+            _uneditedValue = dataGridColumn.PrepareCellForEditInternal(editingElement, _editingEventArgs);
+            OnPreparingCellForEdit(new DataGridPreparingCellForEditEventArgs(dataGridColumn, EditingRow, _editingEventArgs, editingElement));
+        }
+
+        private bool ProcessAKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift, out bool alt);
+
+            if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended)
+            {
+                SelectAll();
+                return true;
+            }
+            return false;
+        }
+
+        //TODO TabStop
+        //TODO FlowDirection
+        private bool ProcessDataGridKey(KeyEventArgs e)
+        {
+            bool focusDataGrid = false;
+            switch (e.Key)
+            {
+                case Key.Tab:
+                    return ProcessTabKey(e);
+
+                case Key.Up:
+                    focusDataGrid = ProcessUpKey(e);
+                    break;
+
+                case Key.Down:
+                    focusDataGrid = ProcessDownKey(e);
+                    break;
+
+                case Key.PageDown:
+                    focusDataGrid = ProcessNextKey(e);
+                    break;
+
+                case Key.PageUp:
+                    focusDataGrid = ProcessPriorKey(e);
+                    break;
+
+                case Key.Left:
+                    focusDataGrid = ProcessLeftKey(e);
+                    break;
+
+                case Key.Right:
+                    focusDataGrid = ProcessRightKey(e);
+                    break;
+
+                case Key.F2:
+                    return ProcessF2Key(e);
+
+                case Key.Home:
+                    focusDataGrid = ProcessHomeKey(e);
+                    break;
+
+                case Key.End:
+                    focusDataGrid = ProcessEndKey(e);
+                    break;
+
+                case Key.Enter:
+                    focusDataGrid = ProcessEnterKey(e);
+                    break;
+
+                case Key.Escape:
+                    return ProcessEscapeKey();
+
+                case Key.A:
+                    return ProcessAKey(e);
+
+                case Key.C:
+                    return ProcessCopyKey(e.Modifiers);
+
+                case Key.Insert:
+                    return ProcessCopyKey(e.Modifiers);
+            }
+            if (focusDataGrid)
+            {
+                Focus();
+            }
+            return focusDataGrid;
+        }
+
+        private bool ProcessDownKeyInternal(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int lastSlot = LastVisibleSlot;
+            if (firstVisibleColumnIndex == -1 || lastSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessDownKeyInternal(shift, ctrl)))
+            {
+                return true;
+            }
+
+            int nextSlot = -1;
+            if (CurrentSlot != -1)
+            {
+                nextSlot = GetNextVisibleSlot(CurrentSlot);
+                if (nextSlot >= SlotCount)
+                {
+                    nextSlot = -1;
+                }
+            }
+
+            _noSelectionChangeCount++;
+            try
+            {
+                int desiredSlot;
+                int columnIndex;
+                DataGridSelectionAction action;
+                if (CurrentColumnIndex == -1)
+                {
+                    desiredSlot = FirstVisibleSlot;
+                    columnIndex = firstVisibleColumnIndex;
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else if (ctrl)
+                {
+                    if (shift)
+                    {
+                        // Both Ctrl and Shift
+                        desiredSlot = lastSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = (SelectionMode == DataGridSelectionMode.Extended)
+                            ? DataGridSelectionAction.SelectFromAnchorToCurrent
+                            : DataGridSelectionAction.SelectCurrent;
+                    }
+                    else
+                    {
+                        // Ctrl without Shift
+                        desiredSlot = lastSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                }
+                else
+                {
+                    if (nextSlot == -1)
+                    {
+                        return true;
+                    }
+                    if (shift)
+                    {
+                        // Shift without Ctrl
+                        desiredSlot = nextSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectFromAnchorToCurrent;
+                    }
+                    else
+                    {
+                        // Neither Ctrl nor Shift
+                        desiredSlot = nextSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                }
+
+                UpdateSelectionAndCurrency(columnIndex, desiredSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessEndKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn;
+            int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int firstVisibleSlot = FirstVisibleSlot;
+            int lastVisibleSlot = LastVisibleSlot;
+            if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessEndKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            _noSelectionChangeCount++;
+            try
+            {
+                if (!ctrl)
+                {
+                    return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot);
+                }
+                else
+                {
+                    DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended)
+                        ? DataGridSelectionAction.SelectFromAnchorToCurrent
+                        : DataGridSelectionAction.SelectCurrent;
+
+                    UpdateSelectionAndCurrency(lastVisibleColumnIndex, lastVisibleSlot, action, scrollIntoView: true);
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessEnterKey(bool shift, bool ctrl)
+        {
+            int oldCurrentSlot = CurrentSlot;
+
+            if (!ctrl)
+            {
+                // If Enter was used by a TextBox, we shouldn't handle the key
+                if (FocusManager.Instance.Current is TextBox focusedTextBox && focusedTextBox.AcceptsReturn)
+                {
+                    return false;
+                }
+
+                if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl)))
+                {
+                    return true;
+                }
+
+                // Enter behaves like down arrow - it commits the potential editing and goes down one cell.
+                if (!ProcessDownKeyInternal(false, ctrl))
+                {
+                    return false;
+                }
+            }
+            else if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            // Try to commit the potential editing
+            if (oldCurrentSlot == CurrentSlot &&
+                EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true) &&
+                EditingRow != null)
+            {
+                EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true);
+                ScrollIntoView(CurrentItem, CurrentColumn);
+            }
+
+            return true;
+        }
+
+        private bool ProcessEscapeKey()
+        {
+            if (WaitForLostFocus(() => ProcessEscapeKey()))
+            {
+                return true;
+            }
+
+            if (_editingColumnIndex != -1)
+            {
+                // Revert the potential cell editing and exit cell editing.
+                EndCellEdit(DataGridEditAction.Cancel, exitEditingMode: true, keepFocus: true, raiseEvents: true);
+                return true;
+            }
+            else if (EditingRow != null)
+            {
+                // Revert the potential row editing and exit row editing.
+                EndRowEdit(DataGridEditAction.Cancel, exitEditingMode: true, raiseEvents: true);
+                return true;
+            }
+            return false;
+        }
+
+        private bool ProcessF2Key(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+
+            if (!shift && !ctrl &&
+                _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) &&
+                !GetColumnEffectiveReadOnlyState(CurrentColumn))
+            {
+                if (ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true))
+                {
+                    BeginCellEdit(e);
+                }
+                return true;
+            }
+
+            return false;
+        }
+
+        private bool ProcessHomeKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int firstVisibleSlot = FirstVisibleSlot;
+            if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessHomeKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            _noSelectionChangeCount++;
+            try
+            {
+                if (!ctrl)
+                {
+                    return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot);
+                }
+                else
+                {
+                    DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended)
+                        ? DataGridSelectionAction.SelectFromAnchorToCurrent
+                        : DataGridSelectionAction.SelectCurrent;
+
+                    UpdateSelectionAndCurrency(firstVisibleColumnIndex, firstVisibleSlot, action, scrollIntoView: true);
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessLeftKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int firstVisibleSlot = FirstVisibleSlot;
+            if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessLeftKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            int previousVisibleColumnIndex = -1;
+            if (CurrentColumnIndex != -1)
+            {
+                dataGridColumn = ColumnsInternal.GetPreviousVisibleNonFillerColumn(ColumnsItemsInternal[CurrentColumnIndex]);
+                if (dataGridColumn != null)
+                {
+                    previousVisibleColumnIndex = dataGridColumn.Index;
+                }
+            }
+
+            _noSelectionChangeCount++;
+            try
+            {
+                if (ctrl)
+                {
+                    return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot);
+                }
+                else
+                {
+                    if (RowGroupHeadersTable.Contains(CurrentSlot))
+                    {
+                        CollapseRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, collapseAllSubgroups: false);
+                    }
+                    else if (CurrentColumnIndex == -1)
+                    {
+                        UpdateSelectionAndCurrency(
+                            firstVisibleColumnIndex,
+                            firstVisibleSlot,
+                            DataGridSelectionAction.SelectCurrent,
+                            scrollIntoView: true);
+                    }
+                    else
+                    {
+                        if (previousVisibleColumnIndex == -1)
+                        {
+                            return true;
+                        }
+
+                        UpdateSelectionAndCurrency(
+                            previousVisibleColumnIndex,
+                            CurrentSlot,
+                            DataGridSelectionAction.None,
+                            scrollIntoView: true);
+                    }
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        // Ctrl Left <==> Home
+        private bool ProcessLeftMost(int firstVisibleColumnIndex, int firstVisibleSlot)
+        {
+            _noSelectionChangeCount++;
+            try
+            {
+                int desiredSlot;
+                DataGridSelectionAction action;
+                if (CurrentColumnIndex == -1)
+                {
+                    desiredSlot = firstVisibleSlot;
+                    action = DataGridSelectionAction.SelectCurrent;
+                    Debug.Assert(_selectedItems.Count == 0);
+                }
+                else
+                {
+                    desiredSlot = CurrentSlot;
+                    action = DataGridSelectionAction.None;
+                }
+
+                UpdateSelectionAndCurrency(firstVisibleColumnIndex, desiredSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessNextKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessNextKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            int nextPageSlot = CurrentSlot == -1 ? DisplayData.FirstScrollingSlot : CurrentSlot;
+            Debug.Assert(nextPageSlot != -1);
+            int slot = GetNextVisibleSlot(nextPageSlot);
+
+            int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements;
+            while (scrollCount > 0 && slot < SlotCount)
+            {
+                nextPageSlot = slot;
+                scrollCount--;
+                slot = GetNextVisibleSlot(slot);
+            }
+
+            _noSelectionChangeCount++;
+            try
+            {
+                DataGridSelectionAction action;
+                int columnIndex;
+                if (CurrentColumnIndex == -1)
+                {
+                    columnIndex = firstVisibleColumnIndex;
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else
+                {
+                    columnIndex = CurrentColumnIndex;
+                    action = (shift && SelectionMode == DataGridSelectionMode.Extended)
+                        ? action = DataGridSelectionAction.SelectFromAnchorToCurrent
+                        : action = DataGridSelectionAction.SelectCurrent;
+                }
+
+                UpdateSelectionAndCurrency(columnIndex, nextPageSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessPriorKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessPriorKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            int previousPageSlot = (CurrentSlot == -1) ? DisplayData.FirstScrollingSlot : CurrentSlot;
+            Debug.Assert(previousPageSlot != -1);
+
+            int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements;
+            int slot = GetPreviousVisibleSlot(previousPageSlot);
+            while (scrollCount > 0 && slot != -1)
+            {
+                previousPageSlot = slot;
+                scrollCount--;
+                slot = GetPreviousVisibleSlot(slot);
+            }
+            Debug.Assert(previousPageSlot != -1);
+
+            _noSelectionChangeCount++;
+            try
+            {
+                int columnIndex;
+                DataGridSelectionAction action;
+                if (CurrentColumnIndex == -1)
+                {
+                    columnIndex = firstVisibleColumnIndex;
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else
+                {
+                    columnIndex = CurrentColumnIndex;
+                    action = (shift && SelectionMode == DataGridSelectionMode.Extended)
+                        ? DataGridSelectionAction.SelectFromAnchorToCurrent
+                        : DataGridSelectionAction.SelectCurrent;
+                }
+
+                UpdateSelectionAndCurrency(columnIndex, previousPageSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessRightKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn;
+            int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int firstVisibleSlot = FirstVisibleSlot;
+            if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(delegate { ProcessRightKey(shift, ctrl); }))
+            {
+                return true;
+            }
+
+            int nextVisibleColumnIndex = -1;
+            if (CurrentColumnIndex != -1)
+            {
+                dataGridColumn = ColumnsInternal.GetNextVisibleColumn(ColumnsItemsInternal[CurrentColumnIndex]);
+                if (dataGridColumn != null)
+                {
+                    nextVisibleColumnIndex = dataGridColumn.Index;
+                }
+            }
+            _noSelectionChangeCount++;
+            try
+            {
+                if (ctrl)
+                {
+                    return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot);
+                }
+                else
+                {
+                    if (RowGroupHeadersTable.Contains(CurrentSlot))
+                    {
+                        ExpandRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, expandAllSubgroups: false);
+                    }
+                    else if (CurrentColumnIndex == -1)
+                    {
+                        int firstVisibleColumnIndex = ColumnsInternal.FirstVisibleColumn == null ? -1 : ColumnsInternal.FirstVisibleColumn.Index;
+
+                        UpdateSelectionAndCurrency(
+                            firstVisibleColumnIndex,
+                            firstVisibleSlot,
+                            DataGridSelectionAction.SelectCurrent,
+                            scrollIntoView: true);
+                    }
+                    else
+                    {
+                        if (nextVisibleColumnIndex == -1)
+                        {
+                            return true;
+                        }
+
+                        UpdateSelectionAndCurrency(
+                            nextVisibleColumnIndex,
+                            CurrentSlot,
+                            DataGridSelectionAction.None,
+                            scrollIntoView: true);
+                    }
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        // Ctrl Right <==> End
+        private bool ProcessRightMost(int lastVisibleColumnIndex, int firstVisibleSlot)
+        {
+            _noSelectionChangeCount++;
+            try
+            {
+                int desiredSlot;
+                DataGridSelectionAction action;
+                if (CurrentColumnIndex == -1)
+                {
+                    desiredSlot = firstVisibleSlot;
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else
+                {
+                    desiredSlot = CurrentSlot;
+                    action = DataGridSelectionAction.None;
+                }
+
+                UpdateSelectionAndCurrency(lastVisibleColumnIndex, desiredSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private bool ProcessTabKey(KeyEventArgs e)
+        {
+            KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift);
+            return ProcessTabKey(e, shift, ctrl);
+        }
+
+        private bool ProcessTabKey(KeyEventArgs e, bool shift, bool ctrl)
+        {
+            if (ctrl || _editingColumnIndex == -1 || IsReadOnly)
+            {
+                //Go to the next/previous control on the page when 
+                // - Ctrl key is used
+                // - Potential current cell is not edited, or the datagrid is read-only. 
+                return false;
+            }
+
+            // Try to locate a writable cell before/after the current cell
+            Debug.Assert(CurrentColumnIndex != -1);
+            Debug.Assert(CurrentSlot != -1);
+
+            int neighborVisibleWritableColumnIndex, neighborSlot;
+            DataGridColumn dataGridColumn;
+            if (shift)
+            {
+                dataGridColumn = ColumnsInternal.GetPreviousVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]);
+                neighborSlot = GetPreviousVisibleSlot(CurrentSlot);
+                if (EditingRow != null)
+                {
+                    while (neighborSlot != -1 && RowGroupHeadersTable.Contains(neighborSlot))
+                    {
+                        neighborSlot = GetPreviousVisibleSlot(neighborSlot);
+                    }
+                }
+            }
+            else
+            {
+                dataGridColumn = ColumnsInternal.GetNextVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]);
+                neighborSlot = GetNextVisibleSlot(CurrentSlot);
+                if (EditingRow != null)
+                {
+                    while (neighborSlot < SlotCount && RowGroupHeadersTable.Contains(neighborSlot))
+                    {
+                        neighborSlot = GetNextVisibleSlot(neighborSlot);
+                    }
+                }
+            }
+            neighborVisibleWritableColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+
+            if (neighborVisibleWritableColumnIndex == -1 && (neighborSlot == -1 || neighborSlot >= SlotCount))
+            {
+                // There is no previous/next row and no previous/next writable cell on the current row
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessTabKey(e, shift, ctrl)))
+            {
+                return true;
+            }
+
+            int targetSlot = -1, targetColumnIndex = -1;
+
+            _noSelectionChangeCount++;
+            try
+            {
+                if (neighborVisibleWritableColumnIndex == -1)
+                {
+                    targetSlot = neighborSlot;
+                    if (shift)
+                    {
+                        Debug.Assert(ColumnsInternal.LastVisibleWritableColumn != null);
+                        targetColumnIndex = ColumnsInternal.LastVisibleWritableColumn.Index;
+                    }
+                    else
+                    {
+                        Debug.Assert(ColumnsInternal.FirstVisibleWritableColumn != null);
+                        targetColumnIndex = ColumnsInternal.FirstVisibleWritableColumn.Index;
+                    }
+                }
+                else
+                {
+                    targetSlot = CurrentSlot;
+                    targetColumnIndex = neighborVisibleWritableColumnIndex;
+                }
+
+                DataGridSelectionAction action;
+                if (targetSlot != CurrentSlot || (SelectionMode == DataGridSelectionMode.Extended))
+                {
+                    if (IsSlotOutOfBounds(targetSlot))
+                    {
+                        return true;
+                    }
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else
+                {
+                    action = DataGridSelectionAction.None;
+                }
+
+                UpdateSelectionAndCurrency(targetColumnIndex, targetSlot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+
+            if (_successfullyUpdatedSelection && !RowGroupHeadersTable.Contains(targetSlot))
+            {
+                BeginCellEdit(e);
+            }
+
+            // Return true to say we handled the key event even if the operation was unsuccessful. If we don't
+            // say we handled this event, the framework will continue to process the tab key and change focus.
+            return true;
+        }
+
+        private bool ProcessUpKey(bool shift, bool ctrl)
+        {
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+            int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            int firstVisibleSlot = FirstVisibleSlot;
+            if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
+            {
+                return false;
+            }
+
+            if (WaitForLostFocus(() => ProcessUpKey(shift, ctrl)))
+            {
+                return true;
+            }
+
+            int previousVisibleSlot = (CurrentSlot != -1) ? GetPreviousVisibleSlot(CurrentSlot) : -1;
+
+            _noSelectionChangeCount++;
+
+            try
+            {
+                int slot;
+                int columnIndex;
+                DataGridSelectionAction action;
+                if (CurrentColumnIndex == -1)
+                {
+                    slot = firstVisibleSlot;
+                    columnIndex = firstVisibleColumnIndex;
+                    action = DataGridSelectionAction.SelectCurrent;
+                }
+                else if (ctrl)
+                {
+                    if (shift)
+                    {
+                        // Both Ctrl and Shift
+                        slot = firstVisibleSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = (SelectionMode == DataGridSelectionMode.Extended)
+                            ? DataGridSelectionAction.SelectFromAnchorToCurrent
+                            : DataGridSelectionAction.SelectCurrent;
+                    }
+                    else
+                    {
+                        // Ctrl without Shift
+                        slot = firstVisibleSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                }
+                else
+                {
+                    if (previousVisibleSlot == -1)
+                    {
+                        return true;
+                    }
+                    if (shift)
+                    {
+                        // Shift without Ctrl
+                        slot = previousVisibleSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectFromAnchorToCurrent;
+                    }
+                    else
+                    {
+                        // Neither Shift nor Ctrl
+                        slot = previousVisibleSlot;
+                        columnIndex = CurrentColumnIndex;
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                }
+                UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: true);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+            return _successfullyUpdatedSelection;
+        }
+
+        private void RemoveDisplayedColumnHeader(DataGridColumn dataGridColumn)
+        {
+            if (_columnHeadersPresenter != null)
+            {
+                _columnHeadersPresenter.Children.Remove(dataGridColumn.HeaderCell);
+            }
+        }
+
+        private void RemoveDisplayedColumnHeaders()
+        {
+            if (_columnHeadersPresenter != null)
+            {
+                _columnHeadersPresenter.Children.Clear();
+            }
+            ColumnsInternal.FillerColumn.IsRepresented = false;
+        }
+
+        private bool ResetCurrentCellCore()
+        {
+            return (CurrentColumnIndex == -1 || SetCurrentCellCore(-1, -1));
+        }
+
+        private void ResetEditingRow()
+        {
+            if (EditingRow != null
+                && EditingRow != _focusedRow
+                && !IsSlotVisible(EditingRow.Slot))
+            {
+                // Unload the old editing row if it's off screen
+                EditingRow.Clip = null;
+                UnloadRow(EditingRow);
+                DisplayData.FullyRecycleElements();
+            }
+            EditingRow = null;
+        }
+
+        private void ResetFocusedRow()
+        {
+            if (_focusedRow != null
+                && _focusedRow != EditingRow
+                && !IsSlotVisible(_focusedRow.Slot))
+            {
+                // Unload the old focused row if it's off screen
+                _focusedRow.Clip = null;
+                UnloadRow(_focusedRow);
+                DisplayData.FullyRecycleElements();
+            }
+            _focusedRow = null;
+        }
+
+        private void SelectAll()
+        {
+            SetRowsSelection(0, SlotCount - 1);
+        }
+
+        private void SetAndSelectCurrentCell(int columnIndex,
+                                             int slot,
+                                             bool forceCurrentCellSelection)
+        {
+            DataGridSelectionAction action = forceCurrentCellSelection ? DataGridSelectionAction.SelectCurrent : DataGridSelectionAction.None;
+            UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false);
+        }
+
+        // columnIndex = 2, rowIndex = -1 --> current cell belongs to the 'new row'.
+        // columnIndex = 2, rowIndex = 2 --> current cell is an inner cell
+        // columnIndex = -1, rowIndex = -1 --> current cell is reset
+        // columnIndex = -1, rowIndex = 2 --> Unexpected
+        private bool SetCurrentCellCore(int columnIndex, int slot, bool commitEdit, bool endRowEdit)
+        {
+            Debug.Assert(columnIndex < ColumnsItemsInternal.Count);
+            Debug.Assert(slot < SlotCount);
+            Debug.Assert(columnIndex == -1 || ColumnsItemsInternal[columnIndex].IsVisible);
+            Debug.Assert(!(columnIndex > -1 && slot == -1));
+
+            if (columnIndex == CurrentColumnIndex &&
+                slot == CurrentSlot)
+            {
+                Debug.Assert(DataConnection != null);
+                Debug.Assert(_editingColumnIndex == -1 || _editingColumnIndex == CurrentColumnIndex);
+                Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot || DataConnection.CommittingEdit);
+                return true;
+            }
+
+            Control oldDisplayedElement = null;
+            DataGridCellCoordinates oldCurrentCell = new DataGridCellCoordinates(CurrentCellCoordinates);
+
+            object newCurrentItem = null;
+            if (!RowGroupHeadersTable.Contains(slot))
+            {
+                int rowIndex = RowIndexFromSlot(slot);
+                if (rowIndex >= 0 && rowIndex < DataConnection.Count)
+                {
+                    newCurrentItem = DataConnection.GetDataItem(rowIndex);
+                }
+            }
+
+            if (CurrentColumnIndex > -1)
+            {
+                Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+                Debug.Assert(CurrentSlot < SlotCount);
+
+                if (!IsInnerCellOutOfBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot) &&
+                    IsSlotVisible(oldCurrentCell.Slot))
+                {
+                    oldDisplayedElement = DisplayData.GetDisplayedElement(oldCurrentCell.Slot);
+                }
+
+                if (!RowGroupHeadersTable.Contains(oldCurrentCell.Slot) && !_temporarilyResetCurrentCell)
+                {
+                    bool keepFocus = ContainsFocus;
+                    if (commitEdit)
+                    {
+                        if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: keepFocus, raiseEvents: true))
+                        {
+                            return false;
+                        }
+                        // Resetting the current cell: setting it to (-1, -1) is not considered setting it out of bounds
+                        if ((columnIndex != -1 && slot != -1 && IsInnerCellOutOfSelectionBounds(columnIndex, slot)) ||
+                            IsInnerCellOutOfSelectionBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot))
+                        {
+                            return false;
+                        }
+
+                        if (endRowEdit && !EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true))
+                        {
+                            return false;
+                        }
+                    }
+                    else
+                    {
+                        CancelEdit(DataGridEditingUnit.Row, false);
+                        ExitEdit(keepFocus);
+                    }
+                }
+            }
+
+            if (newCurrentItem != null)
+            {
+                slot = SlotFromRowIndex(DataConnection.IndexOf(newCurrentItem));
+            }
+            if (slot == -1 && columnIndex != -1)
+            {
+                return false;
+            }
+            CurrentColumnIndex = columnIndex;
+            CurrentSlot = slot;
+
+            if (_temporarilyResetCurrentCell)
+            {
+                if (columnIndex != -1)
+                {
+                    _temporarilyResetCurrentCell = false;
+                }
+            }
+            if (!_temporarilyResetCurrentCell && _editingColumnIndex != -1)
+            {
+                _editingColumnIndex = columnIndex;
+            }
+
+            if (oldDisplayedElement != null)
+            {
+                if (oldDisplayedElement is DataGridRow row)
+                {
+                    // Don't reset the state of the current cell if we're editing it because that would put it in an invalid state
+                    UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, !(_temporarilyResetCurrentCell && row.IsEditing && _editingColumnIndex == oldCurrentCell.ColumnIndex));
+                }
+                else
+                {
+                    UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, applyCellState: false);
+                }
+            }
+
+            if (CurrentColumnIndex > -1)
+            {
+                Debug.Assert(CurrentSlot > -1);
+                Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
+                Debug.Assert(CurrentSlot < SlotCount);
+                if (IsSlotVisible(CurrentSlot))
+                {
+                    UpdateCurrentState(DisplayData.GetDisplayedElement(CurrentSlot), CurrentColumnIndex, applyCellState: true);
+                }
+            }
+
+            return true;
+        }
+
+        private void SetVerticalOffset(double newVerticalOffset)
+        {
+            _verticalOffset = newVerticalOffset;
+            if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value))
+            {
+                _vScrollBar.Value = _verticalOffset;
+            }
+        }
+
+        private void UpdateCurrentState(Control displayedElement, int columnIndex, bool applyCellState)
+        {
+            if (displayedElement is DataGridRow row)
+            {
+                if (AreRowHeadersVisible)
+                {
+                    row.ApplyHeaderStatus();
+                }
+                DataGridCell cell = row.Cells[columnIndex];
+                if (applyCellState)
+                {
+                    cell.UpdatePseudoClasses();
+                }
+            }
+            else if (displayedElement is DataGridRowGroupHeader groupHeader)
+            {
+                groupHeader.ApplyState(useTransitions: true);
+                if (AreRowHeadersVisible)
+                {
+                    groupHeader.ApplyHeaderStatus();
+                }
+            }
+        }
+
+        private void UpdateHorizontalScrollBar(bool needHorizScrollbar, bool forceHorizScrollbar, double totalVisibleWidth, double totalVisibleFrozenWidth, double cellsWidth)
+        {
+            if (_hScrollBar != null)
+            {
+                if (needHorizScrollbar || forceHorizScrollbar)
+                {
+                    //          viewportSize
+                    //        v---v
+                    //|<|_____|###|>|
+                    //  ^     ^
+                    //  min   max 
+
+                    // we want to make the relative size of the thumb reflect the relative size of the viewing area
+                    // viewportSize / (max + viewportSize) = cellsWidth / max
+                    // -> viewportSize = max * cellsWidth / (max - cellsWidth) 
+
+                    // always zero
+                    _hScrollBar.Minimum = 0;
+                    if (needHorizScrollbar)
+                    {
+                        // maximum travel distance -- not the total width
+                        _hScrollBar.Maximum = totalVisibleWidth - cellsWidth;
+                        Debug.Assert(totalVisibleFrozenWidth >= 0);
+                        if (_frozenColumnScrollBarSpacer != null)
+                        {
+                            _frozenColumnScrollBarSpacer.Width = totalVisibleFrozenWidth;
+                        }
+                        Debug.Assert(_hScrollBar.Maximum >= 0);
+
+                        // width of the scrollable viewing area
+                        double viewPortSize = Math.Max(0, cellsWidth - totalVisibleFrozenWidth);
+                        _hScrollBar.ViewportSize = viewPortSize;
+                        _hScrollBar.LargeChange = viewPortSize;
+                        // The ScrollBar should be in sync with HorizontalOffset at this point.  There's a resize case
+                        // where the ScrollBar will coerce an old value here, but we don't want that
+                        if (_hScrollBar.Value != _horizontalOffset)
+                        {
+                            _hScrollBar.Value = _horizontalOffset;
+                        }
+                        _hScrollBar.IsEnabled = true;
+                    }
+                    else
+                    {
+                        _hScrollBar.Maximum = 0;
+                        _hScrollBar.ViewportSize = 0;
+                        _hScrollBar.IsEnabled = false;
+                    }
+
+                    if (!_hScrollBar.IsVisible)
+                    {
+                        // This will trigger a call to this method via Cells_SizeChanged for
+                        _ignoreNextScrollBarsLayout = true;
+                        // which no processing is needed.
+                        _hScrollBar.IsVisible = true;
+                        if (_hScrollBar.DesiredSize.Height == 0)
+                        {
+                            // We need to know the height for the rest of layout to work correctly so measure it now
+                            _hScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                        }
+                    }
+                }
+                else
+                {
+                    _hScrollBar.Maximum = 0;
+                    if (_hScrollBar.IsVisible)
+                    {
+                        // This will trigger a call to this method via Cells_SizeChanged for 
+                        // which no processing is needed.
+                        _hScrollBar.IsVisible = false;
+                        _ignoreNextScrollBarsLayout = true;
+                    }
+                }
+            }
+        }
+
+        private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrollbar, double totalVisibleHeight, double cellsHeight)
+        {
+            if (_vScrollBar != null)
+            {
+                if (needVertScrollbar || forceVertScrollbar)
+                {
+                    //          viewportSize
+                    //        v---v
+                    //|<|_____|###|>|
+                    //  ^     ^
+                    //  min   max 
+
+                    // we want to make the relative size of the thumb reflect the relative size of the viewing area
+                    // viewportSize / (max + viewportSize) = cellsWidth / max
+                    // -> viewportSize = max * cellsHeight / (totalVisibleHeight - cellsHeight)
+                    // ->              = max * cellsHeight / (totalVisibleHeight - cellsHeight)
+                    // ->              = max * cellsHeight / max
+                    // ->              = cellsHeight 
+
+                    // always zero
+                    _vScrollBar.Minimum = 0;
+                    if (needVertScrollbar && !double.IsInfinity(cellsHeight))
+                    {
+                        // maximum travel distance -- not the total height
+                        _vScrollBar.Maximum = totalVisibleHeight - cellsHeight;
+                        Debug.Assert(_vScrollBar.Maximum >= 0);
+
+                        // total height of the display area
+                        _vScrollBar.ViewportSize = cellsHeight;
+                        _vScrollBar.IsEnabled = true;
+                    }
+                    else
+                    {
+                        _vScrollBar.Maximum = 0;
+                        _vScrollBar.ViewportSize = 0;
+                        _vScrollBar.IsEnabled = false;
+                    }
+
+                    if (!_vScrollBar.IsVisible)
+                    {
+                        // This will trigger a call to this method via Cells_SizeChanged for 
+                        // which no processing is needed.
+                        _vScrollBar.IsVisible = true; ;
+                        if (_vScrollBar.DesiredSize.Width == 0)
+                        {
+                            // We need to know the width for the rest of layout to work correctly so measure it now
+                            _vScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                        }
+                        _ignoreNextScrollBarsLayout = true;
+                    }
+                }
+                else
+                {
+                    _vScrollBar.Maximum = 0;
+                    if (_vScrollBar.IsVisible)
+                    {
+                        // This will trigger a call to this method via Cells_SizeChanged for 
+                        // which no processing is needed.
+                        _vScrollBar.IsVisible = false;
+                        _ignoreNextScrollBarsLayout = true;
+                    }
+                }
+            }
+        }
+
+        private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
+        {
+            ProcessVerticalScroll(e.ScrollEventType);
+        }
+
+        //TODO: Ensure left button is checked for
+        private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
+        {
+            bool beginEdit;
+
+            Debug.Assert(slot >= 0);
+
+            // Before changing selection, check if the current cell needs to be committed, and
+            // check if the current row needs to be committed. If any of those two operations are required and fail, 
+            // do not change selection, and do not change current cell. 
+
+            bool wasInEdit = EditingColumnIndex != -1;
+
+            if (IsSlotOutOfBounds(slot))
+            {
+                return true;
+            }
+
+            if (wasInEdit && (columnIndex != EditingColumnIndex || slot != CurrentSlot) &&
+                WaitForLostFocus(() => UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl)))
+            {
+                return true;
+            }
+
+            try
+            {
+                _noSelectionChangeCount++;
+
+                beginEdit = allowEdit &&
+                            CurrentSlot == slot &&
+                            columnIndex != -1 &&
+                            (wasInEdit || CurrentColumnIndex == columnIndex) &&
+                            !GetColumnEffectiveReadOnlyState(ColumnsItemsInternal[columnIndex]);
+
+                DataGridSelectionAction action;
+                if (SelectionMode == DataGridSelectionMode.Extended && shift)
+                {
+                    // Shift select multiple rows
+                    action = DataGridSelectionAction.SelectFromAnchorToCurrent;
+                }
+                else if (GetRowSelection(slot))  // Unselecting single row or Selecting a previously multi-selected row
+                {
+                    if (!ctrl && SelectionMode == DataGridSelectionMode.Extended && _selectedItems.Count != 0)
+                    {
+                        // Unselect everything except the row that was clicked on
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                    else if (ctrl && EditingRow == null)
+                    {
+                        action = DataGridSelectionAction.RemoveCurrentFromSelection;
+                    }
+                    else
+                    {
+                        action = DataGridSelectionAction.None;
+                    }
+                }
+                else // Selecting a single row or multi-selecting with Ctrl
+                {
+                    if (SelectionMode == DataGridSelectionMode.Single || !ctrl)
+                    {
+                        // Unselect the currectly selected rows except the new selected row
+                        action = DataGridSelectionAction.SelectCurrent;
+                    }
+                    else
+                    {
+                        action = DataGridSelectionAction.AddCurrentToSelection;
+                    }
+                }
+
+                UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false);
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+
+            if (_successfullyUpdatedSelection && beginEdit && BeginCellEdit(pointerPressedEventArgs))
+            {
+                FocusEditingCell(setFocus: true);
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Returns the Group at the indicated level or null if the item is not in the ItemsSource
+        /// </summary>
+        /// <param name="item">item</param>
+        /// <param name="groupLevel">groupLevel</param>
+        /// <returns>The group the given item falls under or null if the item is not in the ItemsSource</returns>
+        public DataGridCollectionViewGroup GetGroupFromItem(object item, int groupLevel)
+        {
+            int itemIndex = DataConnection.IndexOf(item);
+            if (itemIndex == -1)
+            {
+                return null;
+            }
+            int groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(SlotFromRowIndex(itemIndex));
+            DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot);
+            while (rowGroupInfo != null && rowGroupInfo.Level != groupLevel)
+            {
+                groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(rowGroupInfo.Slot);
+                rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot);
+            }
+            return rowGroupInfo?.CollectionViewGroup;
+        }
+
+        /// <summary>
+        /// Raises the LoadingRowGroup event
+        /// </summary>
+        /// <param name="e">EventArgs</param>
+        protected virtual void OnLoadingRowGroup(DataGridRowGroupHeaderEventArgs e)
+        {
+            EventHandler<DataGridRowGroupHeaderEventArgs> handler = LoadingRowGroup;
+            if (handler != null)
+            {
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+            }
+        }
+
+        /// <summary>
+        /// Raises the UnLoadingRowGroup event
+        /// </summary>
+        /// <param name="e">EventArgs</param>
+        protected virtual void OnUnloadingRowGroup(DataGridRowGroupHeaderEventArgs e)
+        {
+            EventHandler<DataGridRowGroupHeaderEventArgs> handler = UnloadingRowGroup;
+            if (handler != null)
+            {
+                LoadingOrUnloadingRow = true;
+                handler(this, e);
+                LoadingOrUnloadingRow = false;
+            }
+        }
+
+        /// <summary>
+        /// Occurs before a DataGridRowGroupHeader header is used.
+        /// </summary>
+        public event EventHandler<DataGridRowGroupHeaderEventArgs> LoadingRowGroup;
+
+        /// <summary>
+        /// Occurs when the DataGridRowGroupHeader is available for reuse.
+        /// </summary>
+        public event EventHandler<DataGridRowGroupHeaderEventArgs> UnloadingRowGroup;
+
+        // Recursively expands parent RowGroupHeaders from the top down
+        private void ExpandRowGroupParentChain(int level, int slot)
+        {
+            if (level < 0)
+            {
+                return;
+            }
+            int previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(slot + 1);
+            DataGridRowGroupInfo rowGroupInfo = null;
+            while (previousHeaderSlot >= 0)
+            {
+                rowGroupInfo = RowGroupHeadersTable.GetValueAt(previousHeaderSlot);
+                Debug.Assert(rowGroupInfo != null);
+                if (level == rowGroupInfo.Level)
+                {
+                    if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot))
+                    {
+                        // Keep going up the chain
+                        ExpandRowGroupParentChain(level - 1, rowGroupInfo.Slot - 1);
+                    }
+                    if (!rowGroupInfo.IsVisible)
+                    {
+                        EnsureRowGroupVisibility(rowGroupInfo, true, false);
+                    }
+                    return;
+                }
+                else
+                {
+                    previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(previousHeaderSlot);
+                }
+            }
+        }
+
+        /// <summary>
+        /// This event is raised by OnCopyingRowClipboardContent method after the default row content is prepared.
+        /// Event listeners can modify or add to the row clipboard content.
+        /// </summary>
+        public event EventHandler<DataGridRowClipboardEventArgs> CopyingRowClipboardContent;
+
+        /// <summary>
+        /// This method raises the CopyingRowClipboardContent event.
+        /// </summary>
+        /// <param name="e">Contains the necessary information for generating the row clipboard content.</param>
+        protected virtual void OnCopyingRowClipboardContent(DataGridRowClipboardEventArgs e)
+        {
+            CopyingRowClipboardContent?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// This method formats a row (specified by a DataGridRowClipboardEventArgs) into
+        /// a single string to be added to the Clipboard when the DataGrid is copying its contents.
+        /// </summary>
+        /// <param name="e">DataGridRowClipboardEventArgs</param>
+        /// <returns>The formatted string.</returns>
+        private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
+        {
+            StringBuilder text = new StringBuilder();
+            for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++)
+            {
+                DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex];
+                if (cellContent != null)
+                {
+                    text.Append(cellContent.Content);
+                }
+                if (cellIndex < e.ClipboardRowContent.Count - 1)
+                {
+                    text.Append('\t');
+                }
+                else
+                {
+                    text.Append('\r');
+                    text.Append('\n');
+                }
+            }
+            return text.ToString();
+        }
+
+        /// <summary>
+        /// Handles the case where a 'Copy' key ('C' or 'Insert') has been pressed.  If pressed in combination with
+        /// the control key, and the necessary prerequisites are met, the DataGrid will copy its contents
+        /// to the Clipboard as text.
+        /// </summary>
+        /// <returns>Whether or not the DataGrid handled the key press.</returns>
+        private bool ProcessCopyKey(InputModifiers modifiers)
+        {
+            KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt);
+
+            if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0)
+            {
+                StringBuilder textBuilder = new StringBuilder();
+
+                if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader)
+                {
+                    DataGridRowClipboardEventArgs headerArgs = new DataGridRowClipboardEventArgs(null, true);
+                    foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
+                    {
+                        headerArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(null, column, column.Header));
+                    }
+                    OnCopyingRowClipboardContent(headerArgs);
+                    textBuilder.Append(FormatClipboardContent(headerArgs));
+                }
+
+                for (int index = 0; index < SelectedItems.Count; index++)
+                {
+                    object item = SelectedItems[index];
+                    DataGridRowClipboardEventArgs itemArgs = new DataGridRowClipboardEventArgs(item, false);
+                    foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
+                    {
+                        object content = column.GetCellValue(item, column.ClipboardContentBinding);
+                        itemArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(item, column, content));
+                    }
+                    OnCopyingRowClipboardContent(itemArgs);
+                    textBuilder.Append(FormatClipboardContent(itemArgs));
+                }
+
+                string text = textBuilder.ToString();
+
+                if (!string.IsNullOrEmpty(text))
+                {
+                    CopyToClipboard(text);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private async void CopyToClipboard(string text)
+        {
+            var clipboard = ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)));
+            await clipboard.SetTextAsync(text);
+        }
+
+        /// <summary>
+        /// This is an empty content control that's used during the DataGrid's copy procedure
+        /// to determine the value of a ClipboardContentBinding for a particular column and item.
+        /// </summary>
+        internal ContentControl ClipboardContentControl
+        {
+            get
+            {
+                if (_clipboardContentControl == null)
+                {
+                    _clipboardContentControl = new ContentControl();
+                }
+                return _clipboardContentControl;
+            }
+        }
+
+        //TODO Validation UI
+        private void ResetValidationStatus()
+        {
+            // Clear the invalid status of the Cell, Row and DataGrid
+            if (EditingRow != null)
+            {
+                EditingRow.IsValid = true;
+                if (EditingRow.Index != -1)
+                {
+                    foreach (DataGridCell cell in EditingRow.Cells)
+                    {
+                        if (!cell.IsValid)
+                        {
+                            cell.IsValid = true;
+                            cell.UpdatePseudoClasses();
+                        }
+                    }
+                    EditingRow.UpdatePseudoClasses();
+                }
+            }
+            IsValid = true;
+
+            _validationSubscription?.Dispose();
+            _validationSubscription = null;
+        }
+
+        /// <summary>
+        /// Raises the AutoGeneratingColumn event.
+        /// </summary>
+        protected virtual void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventArgs e)
+        {
+            AutoGeneratingColumn?.Invoke(this, e);
+        }
+    }
+}

+ 145 - 0
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -0,0 +1,145 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved. 
+
+using Avalonia.Data;
+using Avalonia.Utilities;
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Subjects;
+using Avalonia.Reactive;
+using System.Diagnostics;
+using Avalonia.Controls.Utils; 
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> column that can 
+    /// bind to a property in the grid's data source.
+    /// </summary>
+    public abstract class DataGridBoundColumn : DataGridColumn
+    {
+        private IBinding _binding; 
+
+        /// <summary>
+        /// Gets or sets the binding that associates the column with a property in the data source.
+        /// </summary>
+        //TODO Binding
+        public virtual IBinding Binding
+        {
+            get
+            {
+                return _binding;
+            }
+            set
+            {
+                if (_binding != value)
+                {
+                    if (OwningGrid != null && !OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
+                    {
+                        // Edited value couldn't be committed, so we force a CancelEdit
+                        OwningGrid.CancelEdit(DataGridEditingUnit.Row, raiseEvents: false);
+                    } 
+
+                    _binding = value; 
+
+                    if (_binding != null)
+                    {
+                        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))
+                            {
+                                binding.Mode = BindingMode.TwoWay;
+                            } 
+
+                            if (binding.Converter == null)
+                            {
+                                binding.Converter = DataGridValueConverter.Instance;
+                            }
+                        }  
+
+                        // Apply the new Binding to existing rows in the DataGrid
+                        if (OwningGrid != null)
+                        {
+                            OwningGrid.OnColumnBindingChanged(this);
+                        }
+                    } 
+
+                    RemoveEditingElement();
+                }
+            }
+        } 
+
+        /// <summary>
+        /// The binding that will be used to get or set cell content for the clipboard.
+        /// If the base ClipboardContentBinding is not explicitly set, this will return the value of Binding.
+        /// </summary>
+        public override IBinding ClipboardContentBinding
+        {
+            get
+            {
+                return base.ClipboardContentBinding ?? Binding;
+            }
+            set
+            {
+                base.ClipboardContentBinding = value;
+            }
+        } 
+
+        //TODO Rename
+        //TODO Validation
+        protected sealed override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding editBinding)
+        {
+            IControl element = GenerateEditingElementDirect(cell, dataItem);
+            editBinding = null; 
+
+            if (Binding != null)
+            {
+                editBinding = BindEditingElement(element, BindingTarget, Binding);
+            } 
+
+            return element;
+        } 
+
+        private static ICellEditBinding BindEditingElement(IAvaloniaObject target, AvaloniaProperty property, IBinding binding)
+        {
+            var result = binding.Initiate(target, property, enableDataValidation: true); 
+
+            if (result != null)
+            {
+                if(result.Subject != null)
+                {
+                    var bindingHelper = new CellEditBinding(result.Subject);
+                    var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); 
+
+                    BindingOperations.Apply(target, property, instanceBinding, null);
+                    return bindingHelper;
+                } 
+
+                BindingOperations.Apply(target, property, result, null);
+            } 
+
+            return null;
+        } 
+
+        protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); 
+
+        internal AvaloniaProperty BindingTarget { get; set; } 
+
+        internal void SetHeaderFromBinding()
+        {
+            if (OwningGrid != null && OwningGrid.DataConnection.DataType != null
+                && Header == null && Binding != null && Binding is Binding binding
+                && !String.IsNullOrWhiteSpace(binding.Path))
+            {
+                string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path);
+                if (header != null)
+                {
+                    Header = header;
+                }
+            }
+        }
+    } 
+}

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

@@ -0,0 +1,222 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell.
+    /// </summary>
+    public class DataGridCell : ContentControl
+    {
+        private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine";
+
+        private Rectangle _rightGridLine;
+        private DataGridColumn _owningColumn;
+
+        bool _isValid;
+
+        public static readonly DirectProperty<DataGridCell, bool> IsValidProperty =
+            AvaloniaProperty.RegisterDirect<DataGridCell, bool>(
+                nameof(IsValid),
+                o => o.IsValid);
+
+        static DataGridCell()
+        {
+            PointerPressedEvent.AddClassHandler<DataGridCell>(
+                x => x.DataGridCell_PointerPressed, handledEventsToo: true);
+        }
+        public DataGridCell()
+        { }
+
+        public bool IsValid
+        {
+            get { return _isValid; }
+            internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
+        }
+
+        internal DataGridColumn OwningColumn
+        {
+            get => _owningColumn;
+            set
+            {
+                if (_owningColumn != value)
+                {
+                    _owningColumn = value;
+                    OnOwningColumnSet(value);
+                }
+            }
+        }
+        internal DataGridRow OwningRow
+        {
+            get;
+            set;
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get { return OwningRow?.OwningGrid ?? OwningColumn?.OwningGrid; }
+        }
+
+        internal double ActualRightGridLineWidth
+        {
+            get { return _rightGridLine?.Bounds.Width ?? 0; }
+        }
+
+        internal int ColumnIndex
+        {
+            get { return OwningColumn?.Index ?? -1; }
+        }
+
+        internal int RowIndex
+        {
+            get { return OwningRow?.Index ?? -1; }
+        }
+
+        internal bool IsCurrent
+        {
+            get
+            {
+                return OwningGrid.CurrentColumnIndex == OwningColumn.Index &&
+                       OwningGrid.CurrentSlot == OwningRow.Slot;
+            }
+        }
+
+        private bool IsEdited
+        {
+            get
+            {
+                return OwningGrid.EditingRow == OwningRow &&
+                       OwningGrid.EditingColumnIndex == ColumnIndex;
+            }
+        }
+
+        private bool IsMouseOver
+        {
+            get
+            {
+                return OwningRow != null && OwningRow.MouseOverColumnIndex == ColumnIndex;
+            }
+            set
+            {
+                if (value != IsMouseOver)
+                {
+                    if (value)
+                    {
+                        OwningRow.MouseOverColumnIndex = ColumnIndex;
+                    }
+                    else
+                    {
+                        OwningRow.MouseOverColumnIndex = null;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the cell control when a new template is applied.
+        /// </summary>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            base.OnTemplateApplied(e);
+
+            UpdatePseudoClasses();
+            _rightGridLine = e.NameScope.Find<Rectangle>(DATAGRIDCELL_elementRightGridLine);
+            if (_rightGridLine != null && OwningColumn == null)
+            {
+                // Turn off the right GridLine for filler cells
+                _rightGridLine.IsVisible = false;
+            }
+            else
+            {
+                EnsureGridLine(null);
+            }
+
+        }
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            base.OnPointerEnter(e);
+
+            if (OwningRow != null)
+            {
+                IsMouseOver = true;
+            }
+        }
+        protected override void OnPointerLeave(PointerEventArgs e)
+        {
+            base.OnPointerLeave(e);
+
+            if (OwningRow != null)
+            {
+                IsMouseOver = false;
+            }
+        }
+
+        //TODO TabStop
+        private void DataGridCell_PointerPressed(PointerPressedEventArgs e)
+        {
+            // OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow
+            if (OwningGrid != null)
+            {
+                OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
+                if (e.MouseButton == MouseButton.Left)
+                {
+                    if (!e.Handled)
+                    //if (!e.Handled && OwningGrid.IsTabStop)
+                    {
+                        OwningGrid.Focus();
+                    }
+                    if (OwningRow != null)
+                    {
+                        e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
+                        OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
+                    }
+                }
+            }
+        }
+
+        internal void UpdatePseudoClasses()
+        {
+
+        }
+
+        // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the 
+        // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column
+        internal void EnsureGridLine(DataGridColumn lastVisibleColumn)
+        {
+            if (OwningGrid != null && _rightGridLine != null)
+            {
+                if (OwningGrid.VerticalGridLinesBrush != null && OwningGrid.VerticalGridLinesBrush != _rightGridLine.Fill)
+                {
+                    _rightGridLine.Fill = OwningGrid.VerticalGridLinesBrush;
+                }
+
+                bool newVisibility =
+                    (OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Vertical || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All)
+                        && (OwningGrid.ColumnsInternal.FillerColumn.IsActive || OwningColumn != lastVisibleColumn);
+
+                if (newVisibility != _rightGridLine.IsVisible)
+                {
+                    _rightGridLine.IsVisible = newVisibility;
+                }
+            }
+        }
+
+        private void OnOwningColumnSet(DataGridColumn column)
+        {
+            if (column == null)
+            {
+                Classes.Clear();
+            }
+            else
+            {
+                Classes.Replace(column.CellStyleClasses);
+            }
+        }
+    }
+}

+ 71 - 0
src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs

@@ -0,0 +1,71 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridCellCollection
+    {
+        private List<DataGridCell> _cells;
+        private DataGridRow _owningRow;
+
+        internal event EventHandler<DataGridCellEventArgs> CellAdded;
+        internal event EventHandler<DataGridCellEventArgs> CellRemoved;
+
+        public DataGridCellCollection(DataGridRow owningRow)
+        {
+            _owningRow = owningRow;
+            _cells = new List<DataGridCell>();
+        }
+
+        public int Count
+        {
+            get
+            {
+                return _cells.Count;
+            }
+        }
+
+        public IEnumerator GetEnumerator()
+        {
+            return _cells.GetEnumerator();
+        }
+
+        public void Insert(int cellIndex, DataGridCell cell)
+        {
+            Debug.Assert(cellIndex >= 0 && cellIndex <= _cells.Count);
+            Debug.Assert(cell != null);
+
+            cell.OwningRow = _owningRow;
+            _cells.Insert(cellIndex, cell);
+
+            CellAdded?.Invoke(this, new DataGridCellEventArgs(cell));
+        }
+
+        public void RemoveAt(int cellIndex)
+        {
+            DataGridCell dataGridCell = _cells[cellIndex];
+            _cells.RemoveAt(cellIndex);
+            dataGridCell.OwningRow = null;
+            CellRemoved?.Invoke(this, new DataGridCellEventArgs(dataGridCell));
+        }
+
+        public DataGridCell this[int index]
+        {
+            get
+            {
+                if (index < 0 || index >= _cells.Count)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _cells.Count, false);
+                }
+                return _cells[index];
+            }
+        }
+    }
+}

+ 57 - 0
src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs

@@ -0,0 +1,57 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridCellCoordinates
+    {
+        public DataGridCellCoordinates(int columnIndex, int slot)
+        {
+            ColumnIndex = columnIndex;
+            Slot = slot;
+        }
+
+        public DataGridCellCoordinates(DataGridCellCoordinates dataGridCellCoordinates) : this(dataGridCellCoordinates.ColumnIndex, dataGridCellCoordinates.Slot)
+        {
+        }
+
+        public int ColumnIndex
+        {
+            get;
+            set;
+        }
+
+        public int Slot
+        {
+            get;
+            set;
+        }
+
+        public override bool Equals(object o)
+        {
+            if (o is DataGridCellCoordinates dataGridCellCoordinates)
+            {
+                return dataGridCellCoordinates.ColumnIndex == ColumnIndex && dataGridCellCoordinates.Slot == Slot;
+            }
+            return false;
+        }
+
+        // There is build warning if this is missiing
+        public override int GetHashCode()
+        {
+            return base.GetHashCode();
+        }
+
+#if DEBUG
+        public override string ToString()
+        {
+            return "DataGridCellCoordinates {ColumnIndex = " + ColumnIndex.ToString(CultureInfo.CurrentCulture) +
+                   ", Slot = " + Slot.ToString(CultureInfo.CurrentCulture) + "}";
+        }
+#endif
+    }
+}

+ 316 - 0
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@@ -0,0 +1,316 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents a <see cref="T:System.Windows.Controls.DataGrid" /> column that hosts 
+    /// <see cref="T:System.Windows.Controls.CheckBox" /> controls in its cells.
+    /// </summary>
+    public class DataGridCheckBoxColumn : DataGridBoundColumn
+    {
+
+        private bool _beganEditWithKeyboard;
+        private bool _isThreeState;
+        private CheckBox _currentCheckBox;
+        private DataGrid _owningGrid;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:System.Windows.Controls.DataGridCheckBoxColumn" /> class. 
+        /// </summary>
+        public DataGridCheckBoxColumn()
+        {
+            BindingTarget = CheckBox.IsCheckedProperty;
+        }
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the hosted <see cref="T:System.Windows.Controls.CheckBox" /> controls allow three states or two. 
+        /// </summary>
+        /// <returns>
+        /// true if the hosted controls support three states; false if they support two states. The default is false. 
+        /// </returns>
+        public bool IsThreeState
+        {
+            get
+            {
+                return _isThreeState;
+            }
+            set
+            {
+                if (_isThreeState != value)
+                {
+                    _isThreeState = value;
+                    NotifyPropertyChanged(nameof(IsThreeState));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Causes the column cell being edited to revert to the specified value.
+        /// </summary>
+        /// <param name="editingElement">
+        /// The element that the column displays for a cell in editing mode.
+        /// </param>
+        /// <param name="uneditedValue">
+        /// The previous, unedited value in the cell being edited.
+        /// </param>
+        protected override void CancelCellEdit(IControl editingElement, object uneditedValue)
+        {
+            if (editingElement is CheckBox editingCheckBox)
+            {
+                editingCheckBox.IsChecked = (bool?)uneditedValue;
+            }
+        }
+
+        ///  <summary>
+        ///  Gets a <see cref="T:System.Windows.Controls.CheckBox" /> control that is bound to the column's <see cref="P:System.Windows.Controls.DataGridBoundColumn.Binding" /> property value.
+        ///  </summary>
+        ///  <param name="cell">
+        ///  The cell that will contain the generated element.
+        ///  </param>
+        ///  <param name="dataItem">
+        ///  The data item represented by the row that contains the intended cell.
+        /// </param>
+        ///  <returns>
+        ///  A new <see cref="T:System.Windows.Controls.CheckBox" /> control that is bound to the column's <see cref="P:System.Windows.Controls.DataGridBoundColumn.Binding" /> property value.
+        ///  </returns>
+        protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
+        {
+            var checkBox = new CheckBox
+            {
+                Margin = new Thickness(0)
+            };
+            ConfigureCheckBox(checkBox);
+            return checkBox;
+        }
+
+        /// <summary>                
+        /// Gets a read-only <see cref="T:System.Windows.Controls.CheckBox" /> control that is bound to the column's <see cref="P:System.Windows.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </summary>
+        /// <param name="cell">
+        /// The cell that will contain the generated element.
+        /// </param>
+        /// <param name="dataItem">
+        /// The data item represented by the row that contains the intended cell.
+        /// </param>
+        /// <returns>
+        /// A new, read-only <see cref="T:System.Windows.Controls.CheckBox" /> control that is bound to the column's <see cref="P:System.Windows.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </returns>
+        protected override IControl GenerateElement(DataGridCell cell, object dataItem)
+        {
+            bool isEnabled = false;
+            CheckBox checkBoxElement = new CheckBox();
+            if (EnsureOwningGrid())
+            {
+                if (cell.RowIndex != -1 && cell.ColumnIndex != -1 &&
+                    cell.OwningRow != null &&
+                    cell.OwningRow.Slot == this.OwningGrid.CurrentSlot &&
+                    cell.ColumnIndex == this.OwningGrid.CurrentColumnIndex)
+                {
+                    isEnabled = true;
+                    if (_currentCheckBox != null)
+                    {
+                        _currentCheckBox.IsEnabled = false;
+                    }
+                    _currentCheckBox = checkBoxElement;
+                }
+            }
+            checkBoxElement.IsEnabled = isEnabled;
+            checkBoxElement.IsHitTestVisible = false;
+            ConfigureCheckBox(checkBoxElement);
+            if (Binding != null)
+            {
+                checkBoxElement.Bind(BindingTarget, Binding);
+            }
+            return checkBoxElement;
+        }
+
+        /// <summary>
+        /// Called when a cell in the column enters editing mode.
+        /// </summary>
+        /// <param name="editingElement">
+        /// The element that the column displays for a cell in editing mode.
+        /// </param>
+        /// <param name="editingEventArgs">
+        /// Information about the user gesture that is causing a cell to enter editing mode.
+        /// </param>
+        /// <returns>
+        /// The unedited value. 
+        /// </returns>
+        protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
+        {
+            if (editingElement is CheckBox editingCheckBox)
+            {
+                bool? uneditedValue = editingCheckBox.IsChecked;
+                bool editValue = false;
+                if(editingEventArgs is PointerPressedEventArgs args)
+                {
+                    // Editing was triggered by a mouse click
+                    Point position = args.GetPosition(editingCheckBox);
+                    Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
+                    editValue = rect.Contains(position);
+                }
+                else if (_beganEditWithKeyboard)
+                {
+                    // Editing began by a user pressing spacebar
+                    editValue = true;
+                    _beganEditWithKeyboard = false;
+                }
+
+                if (editValue)
+                {
+                    // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
+                    if (editingCheckBox.IsThreeState)
+                    {
+                        switch (editingCheckBox.IsChecked)
+                        {
+                            case false:
+                                editingCheckBox.IsChecked = true;
+                                break;
+                            case true:
+                                editingCheckBox.IsChecked = null;
+                                break;
+                            case null:
+                                editingCheckBox.IsChecked = false;
+                                break;
+                        }
+                    }
+                    else
+                    {
+                        editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
+                    }
+                }
+                return uneditedValue;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Called by the DataGrid control when this column asks for its elements to be
+        /// updated, because its CheckBoxContent or IsThreeState property changed.
+        /// </summary>
+        protected internal override void RefreshCellContent(IControl element, string propertyName)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException("element");
+            }
+            if(element is CheckBox checkBox)
+            {
+                checkBox.IsThreeState = IsThreeState;
+            }
+            else
+            {
+                throw DataGridError.DataGrid.ValueIsNotAnInstanceOf("element", typeof(CheckBox));
+            }
+        }
+
+        private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems.Contains(this) && _owningGrid != null)
+            {
+                _owningGrid.Columns.CollectionChanged -= Columns_CollectionChanged;
+                _owningGrid.CurrentCellChanged -= OwningGrid_CurrentCellChanged;
+                _owningGrid.KeyDown -= OwningGrid_KeyDown;
+                _owningGrid.LoadingRow -= OwningGrid_LoadingRow;
+                _owningGrid = null;
+            }
+        }
+
+        private void ConfigureCheckBox(CheckBox checkBox)
+        {
+            checkBox.HorizontalAlignment = HorizontalAlignment.Center;
+            checkBox.VerticalAlignment = VerticalAlignment.Center;
+            checkBox.IsThreeState = IsThreeState;
+        }
+
+        private bool EnsureOwningGrid()
+        {
+            if (OwningGrid != null)
+            {
+                if (OwningGrid != _owningGrid)
+                {
+                    _owningGrid = OwningGrid;
+                    _owningGrid.Columns.CollectionChanged += Columns_CollectionChanged;
+                    _owningGrid.CurrentCellChanged += OwningGrid_CurrentCellChanged;
+                    _owningGrid.KeyDown += OwningGrid_KeyDown;
+                    _owningGrid.LoadingRow += OwningGrid_LoadingRow;
+                }
+                return true;
+            }
+            return false;
+        }
+
+        private void OwningGrid_CurrentCellChanged(object sender, EventArgs e)
+        {
+            if (_currentCheckBox != null)
+            {
+                _currentCheckBox.IsEnabled = false;
+            }
+            if (OwningGrid != null && OwningGrid.CurrentColumn == this
+                && OwningGrid.IsSlotVisible(OwningGrid.CurrentSlot))
+            {
+                if (OwningGrid.DisplayData.GetDisplayedElement(OwningGrid.CurrentSlot) is DataGridRow row)
+                {
+                    CheckBox checkBox = GetCellContent(row) as CheckBox;
+                    if (checkBox != null)
+                    {
+                        checkBox.IsEnabled = true;
+                    }
+                    _currentCheckBox = checkBox;
+                }
+            }
+        }
+
+        private void OwningGrid_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.Space && OwningGrid != null &&
+                OwningGrid.CurrentColumn == this)
+            {
+                if (OwningGrid.DisplayData.GetDisplayedElement(OwningGrid.CurrentSlot) is DataGridRow row)
+                {
+                    CheckBox checkBox = GetCellContent(row) as CheckBox;
+                    if (checkBox == _currentCheckBox)
+                    {
+                        _beganEditWithKeyboard = true;
+                        OwningGrid.BeginEdit();
+                        return;
+                    }
+                }
+            }
+            _beganEditWithKeyboard = false;
+        }
+
+        private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)
+        {
+            if (OwningGrid != null)
+            {
+                if (GetCellContent(e.Row) is CheckBox checkBox)
+                {
+                    if (OwningGrid.CurrentColumnIndex == Index && OwningGrid.CurrentSlot == e.Row.Slot)
+                    {
+                        if (_currentCheckBox != null)
+                        {
+                            _currentCheckBox.IsEnabled = false;
+                        }
+                        checkBox.IsEnabled = true;
+                        _currentCheckBox = checkBox;
+                    }
+                    else
+                    {
+                        checkBox.IsEnabled = false;
+                    }
+                }
+            }
+        }
+
+    }
+}

+ 204 - 0
src/Avalonia.Controls.DataGrid/DataGridClipboard.cs

@@ -0,0 +1,204 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines modes that indicates how DataGrid content is copied to the Clipboard. 
+    /// </summary>
+    public enum DataGridClipboardCopyMode
+    {
+        /// <summary>
+        /// Disable the DataGrid's ability to copy selected items as text.
+        /// </summary>
+        None,
+
+        /// <summary>
+        /// Enable the DataGrid's ability to copy selected items as text, but do not include
+        /// the column header content as the first line in the text that gets copied to the Clipboard.
+        /// </summary>
+        ExcludeHeader,
+
+        /// <summary>
+        /// Enable the DataGrid's ability to copy selected items as text, and include
+        /// the column header content as the first line in the text that gets copied to the Clipboard.
+        /// </summary>
+        IncludeHeader
+    }
+
+    /// <summary>
+    /// This structure encapsulate the cell information necessary when clipboard content is prepared.
+    /// </summary>
+    public struct DataGridClipboardCellContent
+    {
+
+        private DataGridColumn _column;
+        private object _content;
+        private object _item;
+
+        /// <summary>
+        /// Creates a new DataGridClipboardCellValue structure containing information about a DataGrid cell.
+        /// </summary>
+        /// <param name="item">DataGrid row item containing the cell.</param>
+        /// <param name="column">DataGridColumn containing the cell.</param>
+        /// <param name="content">DataGrid cell value.</param>
+        public DataGridClipboardCellContent(object item, DataGridColumn column, object content)
+        {
+            this._item = item;
+            this._column = column;
+            this._content = content;
+        }
+
+        /// <summary>
+        /// DataGridColumn containing the cell.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get
+            {
+                return _column;
+            }
+        }
+
+        /// <summary>
+        /// Cell content.
+        /// </summary>
+        public object Content
+        {
+            get
+            {
+                return _content;
+            }
+        }
+
+        /// <summary>
+        /// DataGrid row item containing the cell.
+        /// </summary>
+        public object Item
+        {
+            get
+            {
+                return _item;
+            }
+        }
+
+        /// <summary>
+        /// Field-by-field comparison to avoid reflection-based ValueType.Equals.
+        /// </summary>
+        /// <param name="obj">DataGridClipboardCellContent to compare.</param>
+        /// <returns>True iff this and data are equal</returns>
+        public override bool Equals(object obj)
+        {
+            if(obj is DataGridClipboardCellContent content)
+            {
+                return (((_column == content._column) && (_content == content._content)) && (_item == content._item));
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Returns a deterministic hash code.
+        /// </summary>
+        /// <returns>Hash value.</returns>
+        public override int GetHashCode()
+        {
+            return ((_column.GetHashCode() ^ _content.GetHashCode()) ^ _item.GetHashCode());
+        }
+
+        /// <summary>
+        /// Field-by-field comparison to avoid reflection-based ValueType.Equals.
+        /// </summary>
+        /// <param name="clipboardCellContent1">The first DataGridClipboardCellContent.</param>
+        /// <param name="clipboardCellContent2">The second DataGridClipboardCellContent.</param>
+        /// <returns>True iff clipboardCellContent1 and clipboardCellContent2 are equal.</returns>
+        public static bool operator ==(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2)
+        {
+            return (((clipboardCellContent1._column == clipboardCellContent2._column) && (clipboardCellContent1._content == clipboardCellContent2._content)) && (clipboardCellContent1._item == clipboardCellContent2._item));
+        }
+
+        /// <summary>
+        /// Field-by-field comparison to avoid reflection-based ValueType.Equals.
+        /// </summary>
+        /// <param name="clipboardCellContent1">The first DataGridClipboardCellContent.</param>
+        /// <param name="clipboardCellContent2">The second DataGridClipboardCellContent.</param>
+        /// <returns>True iff clipboardCellContent1 and clipboardCellContent2 are NOT equal.</returns>
+        public static bool operator !=(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2)
+        {
+            if ((clipboardCellContent1._column == clipboardCellContent2._column) && (clipboardCellContent1._content == clipboardCellContent2._content))
+            {
+                return (clipboardCellContent1._item != clipboardCellContent2._item);
+            }
+            return true;
+        }
+
+    }
+
+    /// <summary>
+    /// This class encapsulates a selected row's information necessary for the CopyingRowClipboardContent event.
+    /// </summary>
+    public class DataGridRowClipboardEventArgs : EventArgs
+    {
+
+        private List<DataGridClipboardCellContent> _clipboardRowContent;
+        private bool _isColumnHeadersRow;
+        private object _item;
+
+        /// <summary>
+        /// Creates a DataGridRowClipboardEventArgs object and initializes the properties.
+        /// </summary>
+        /// <param name="item">The row's associated data item.</param>
+        /// <param name="isColumnHeadersRow">Whether or not this EventArgs is for the column headers.</param>
+        internal DataGridRowClipboardEventArgs(object item, bool isColumnHeadersRow)
+        {
+            _isColumnHeadersRow = isColumnHeadersRow;
+            _item = item;
+        }
+
+        /// <summary>
+        /// This list should be used to modify, add ot remove a cell content before it gets stored into the clipboard.
+        /// </summary>
+        public List<DataGridClipboardCellContent> ClipboardRowContent
+        {
+            get
+            {
+                if (_clipboardRowContent == null)
+                {
+                    _clipboardRowContent = new List<DataGridClipboardCellContent>();
+                }
+                return _clipboardRowContent;
+            }
+        }
+
+        /// <summary>
+        /// This property is true when the ClipboardRowContent represents column headers, in which case the Item is null.
+        /// </summary>
+        public bool IsColumnHeadersRow
+        {
+            get
+            {
+                return _isColumnHeadersRow;
+            }
+        }
+
+        /// <summary>
+        /// DataGrid row item used for proparing the ClipboardRowContent.
+        /// </summary>
+        public object Item
+        {
+            get
+            {
+                return _item;
+            }
+        }
+
+    }
+
+}

+ 1050 - 0
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -0,0 +1,1050 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+using Avalonia.Collections;
+using Avalonia.Utilities;
+using System;
+using System.Linq;
+using System.Diagnostics;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls
+{
+    public abstract class DataGridColumn : AvaloniaObject
+    {
+        internal const int DATAGRIDCOLUMN_maximumWidth = 65536;
+        private const bool DATAGRIDCOLUMN_defaultIsReadOnly = false;
+
+        private DataGridLength? _width; // Null by default, null means inherit the Width from the DataGrid
+        private bool? _isReadOnly;
+        private double? _maxWidth;
+        private double? _minWidth;
+        private bool _settingWidthInternally;
+        private int _displayIndexWithFiller;
+        private bool _isVisible;
+        private object _header;
+        private DataGridColumnHeader _headerCell;
+        private IControl _editingElement;
+        private ICellEditBinding _editBinding;
+        private IBinding _clipboardContentBinding;
+        private readonly Classes _cellStyleClasses = new Classes();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumn" /> class.
+        /// </summary>
+        protected internal DataGridColumn()
+        {
+            _isVisible = true;
+            _displayIndexWithFiller = -1;
+            IsInitialDesiredWidthDetermined = false;
+            InheritsWidth = true;
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get;
+            set;
+        }
+
+        internal int Index
+        {
+            get;
+            set;
+        }
+
+        internal bool? CanUserReorderInternal
+        {
+            get;
+            set;
+        }
+
+        internal bool? CanUserResizeInternal
+        {
+            get;
+            set;
+        }
+
+        internal bool? CanUserSortInternal
+        {
+            get;
+            set;
+        }
+
+        internal bool ActualCanUserResize
+        {
+            get
+            {
+                if (OwningGrid == null || OwningGrid.CanUserResizeColumns == false || this is DataGridFillerColumn)
+                {
+                    return false;
+                }
+                return CanUserResizeInternal ?? true;
+            }
+        }
+
+        // MaxWidth from local setting or DataGrid setting
+        internal double ActualMaxWidth
+        {
+            get
+            {
+                return _maxWidth ?? OwningGrid?.MaxColumnWidth ?? double.PositiveInfinity;
+            }
+        }
+
+        // MinWidth from local setting or DataGrid setting
+        internal double ActualMinWidth
+        {
+            get
+            {
+                double minWidth = _minWidth ?? OwningGrid?.MinColumnWidth ?? 0;
+                if (Width.IsStar)
+                {
+                    return Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, minWidth);
+                }
+                return minWidth;
+            }
+        }
+
+        internal bool DisplayIndexHasChanged
+        {
+            get;
+            set;
+        }
+
+        internal int DisplayIndexWithFiller
+        {
+            get { return _displayIndexWithFiller; }
+            set { _displayIndexWithFiller = value; }
+        }
+
+        internal bool HasHeaderCell
+        {
+            get
+            {
+                return _headerCell != null;
+            }
+        }
+
+        internal DataGridColumnHeader HeaderCell
+        {
+            get
+            {
+                if (_headerCell == null)
+                {
+                    _headerCell = CreateHeader();
+                }
+                return _headerCell;
+            }
+        }
+
+        /// <summary>
+        /// Tracks whether or not this column inherits its Width value from the DataGrid.
+        /// </summary>
+        internal bool InheritsWidth
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// When a column is initially added, we won't know its initial desired value
+        /// until all rows have been measured.  We use this variable to track whether or
+        /// not the column has been fully measured.
+        /// </summary>
+        internal bool IsInitialDesiredWidthDetermined
+        {
+            get;
+            set;
+        }
+
+        internal double LayoutRoundedWidth
+        {
+            get;
+            private set;
+        }
+
+        internal ICellEditBinding CellEditBinding
+        {
+            get => _editBinding;
+        }
+
+        /// <summary>
+        /// Determines whether or not this column is visible.
+        /// </summary>
+        public bool IsVisible
+        {
+            get
+            {
+                return _isVisible;
+            }
+            set
+            {
+                if (value != IsVisible)
+                {
+                    OwningGrid?.OnColumnVisibleStateChanging(this);
+                    _isVisible = value;
+
+                    if (_headerCell != null)
+                    {
+                        _headerCell.IsVisible = value;
+                    }
+
+                    OwningGrid?.OnColumnVisibleStateChanged(this);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level
+        /// have been taken into account
+        /// </summary>
+        public double ActualWidth
+        {
+            get
+            {
+                if (OwningGrid == null || double.IsNaN(Width.DisplayValue))
+                {
+                    return ActualMinWidth;
+                }
+                return Width.DisplayValue;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can change the column display position by 
+        /// dragging the column header.
+        /// </summary>
+        /// <returns>
+        /// true if the user can drag the column header to a new position; otherwise, false. The default is the current <see cref="P:Avalonia.Controls.DataGrid.CanUserReorderColumns" /> property value.
+        /// </returns>
+        public bool CanUserReorder
+        {
+            get
+            {
+                return
+                    CanUserReorderInternal ??
+                        OwningGrid?.CanUserReorderColumns ??
+                        DataGrid.DATAGRID_defaultCanUserResizeColumns;
+            }
+            set
+            {
+                CanUserReorderInternal = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can adjust the column width using the mouse.
+        /// </summary>
+        /// <returns>
+        /// true if the user can resize the column; false if the user cannot resize the column. The default is the current <see cref="P:Avalonia.Controls.DataGrid.CanUserResizeColumns" /> property value.
+        /// </returns>
+        public bool CanUserResize
+        {
+            get 
+            {
+                return
+                    CanUserResizeInternal ??
+                    OwningGrid?.CanUserResizeColumns ??
+                    DataGrid.DATAGRID_defaultCanUserResizeColumns;
+            }
+            set 
+            { 
+                CanUserResizeInternal = value;
+                OwningGrid?.OnColumnCanUserResizeChanged(this);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the user can sort the column by clicking the column header.
+        /// </summary>
+        /// <returns>
+        /// true if the user can sort the column; false if the user cannot sort the column. The default is the current <see cref="P:Avalonia.Controls.DataGrid.CanUserSortColumns" /> property value.
+        /// </returns>
+        public bool CanUserSort
+        {
+            get
+            {
+                if (CanUserSortInternal.HasValue)
+                {
+                    return CanUserSortInternal.Value;
+                }
+                else if (OwningGrid != null)
+                {
+                    string propertyPath = GetSortPropertyName();
+                    Type propertyType = OwningGrid.DataConnection.DataType.GetNestedPropertyType(propertyPath);
+
+                    // if the type is nullable, then we will compare the non-nullable type
+                    if (TypeHelper.IsNullableType(propertyType))
+                    {
+                        propertyType = TypeHelper.GetNonNullableType(propertyType);
+                    }
+
+                    // return whether or not the property type can be compared
+                    return (typeof(IComparable).IsAssignableFrom(propertyType)) ? true : false;
+                }
+                else
+                {
+                    return DataGrid.DATAGRID_defaultCanUserSortColumns;
+                }
+            }
+            set
+            {
+                CanUserSortInternal = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the display position of the column relative to the other columns in the <see cref="T:Avalonia.Controls.DataGrid" />.
+        /// </summary>
+        /// <returns>
+        /// The zero-based position of the column as it is displayed in the associated <see cref="T:Avalonia.Controls.DataGrid" />. The default is the index of the corresponding <see cref="P:System.Collections.ObjectModel.Collection`1.Item(System.Int32)" /> in the <see cref="P:Avalonia.Controls.DataGrid.Columns" /> collection.
+        /// </returns>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// When setting this property, the specified value is less than -1 or equal to <see cref="F:System.Int32.MaxValue" />.
+        /// 
+        /// -or-
+        /// 
+        /// When setting this property on a column in a <see cref="T:Avalonia.Controls.DataGrid" />, the specified value is less than zero or greater than or equal to the number of columns in the <see cref="T:Avalonia.Controls.DataGrid" />.
+        /// </exception>
+        /// <exception cref="T:System.InvalidOperationException">
+        /// When setting this property, the <see cref="T:Avalonia.Controls.DataGrid" /> is already making <see cref="P:Avalonia.Controls.DataGridColumn.DisplayIndex" /> adjustments. For example, this exception is thrown when you attempt to set <see cref="P:Avalonia.Controls.DataGridColumn.DisplayIndex" /> in a <see cref="E:Avalonia.Controls.DataGrid.ColumnDisplayIndexChanged" /> event handler.
+        /// 
+        /// -or-
+        /// 
+        /// When setting this property, the specified value would result in a frozen column being displayed in the range of unfrozen columns, or an unfrozen column being displayed in the range of frozen columns.
+        /// </exception>
+        public int DisplayIndex
+        {
+            get
+            {
+                if (OwningGrid != null && OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented)
+                {
+                    return _displayIndexWithFiller - 1;
+                }
+                else
+                {
+                    return _displayIndexWithFiller;
+                }
+            }
+            set
+            {
+                if (value == Int32.MaxValue)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeLessThan(nameof(value), nameof(DisplayIndex), Int32.MaxValue);
+                }
+                if (OwningGrid != null)
+                {
+                    if (OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented)
+                    {
+                        value++;
+                    }
+                    if (_displayIndexWithFiller != value)
+                    {
+                        if (value < 0 || value >= OwningGrid.ColumnsItemsInternal.Count)
+                        {
+                            throw DataGridError.DataGrid.ValueMustBeBetween(nameof(value), nameof(DisplayIndex), 0, true, OwningGrid.Columns.Count, false);
+                        }
+                        // Will throw an error if a visible frozen column is placed inside a non-frozen area or vice-versa.
+                        OwningGrid.OnColumnDisplayIndexChanging(this, value);
+                        _displayIndexWithFiller = value;
+                        try
+                        {
+                            OwningGrid.InDisplayIndexAdjustments = true;
+                            OwningGrid.OnColumnDisplayIndexChanged(this);
+                            OwningGrid.OnColumnDisplayIndexChanged_PostNotification();
+                        }
+                        finally
+                        {
+                            OwningGrid.InDisplayIndexAdjustments = false;
+                        }
+                    }
+                }
+                else
+                {
+                    if (value < -1)
+                    {
+                        throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(DisplayIndex), -1);
+                    }
+                    _displayIndexWithFiller = value;
+                }
+            }
+        }
+
+        public Classes CellStyleClasses
+        {
+            get => _cellStyleClasses;
+            set
+            {
+                if(_cellStyleClasses != value)
+                {
+                    _cellStyleClasses.Replace(value);
+                }
+            }
+        }
+
+        public object Header
+        {
+            get
+            {
+                return _header;
+            }
+            set
+            {
+                if (_header != value)
+                {
+                    _header = value;
+                    if (_headerCell != null)
+                    {
+                        _headerCell.Content = value;
+                    }
+                }
+            }
+        }
+
+        public bool IsAutoGenerated
+        {
+            get;
+            internal set;
+        }
+
+        public bool IsFrozen
+        {
+            get;
+            internal set;
+        }
+
+        public bool IsReadOnly
+        {
+            get
+            {
+                if (OwningGrid == null)
+                {
+                    return _isReadOnly ?? DATAGRIDCOLUMN_defaultIsReadOnly;
+                }
+                if (_isReadOnly != null)
+                {
+                    return _isReadOnly.Value || OwningGrid.IsReadOnly;
+                }
+                return OwningGrid.GetColumnReadOnlyState(this, DATAGRIDCOLUMN_defaultIsReadOnly);
+            }
+            set
+            {
+                if (value != _isReadOnly)
+                {
+                    OwningGrid?.OnColumnReadOnlyStateChanging(this, value);
+                    _isReadOnly = value;
+                }
+            }
+        }
+
+        public double MaxWidth
+        {
+            get
+            {
+                return _maxWidth ?? double.PositiveInfinity;
+            }
+            set
+            {
+                if (value < 0)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", 0);
+                }
+                if (value < ActualMinWidth)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", "MinWidth");
+                }
+                if (!_maxWidth.HasValue || _maxWidth.Value != value)
+                {
+                    double oldValue = ActualMaxWidth;
+                    _maxWidth = value;
+                    if (OwningGrid != null && OwningGrid.ColumnsInternal != null)
+                    {
+                        OwningGrid.OnColumnMaxWidthChanged(this, oldValue);
+                    }
+                }
+            }
+        }
+
+        public double MinWidth
+        {
+            get
+            {
+                return _minWidth ?? 0;
+            }
+            set
+            {
+                if (double.IsNaN(value))
+                {
+                    throw DataGridError.DataGrid.ValueCannotBeSetToNAN("MinWidth");
+                }
+                if (value < 0)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MinWidth", 0);
+                }
+                if (double.IsPositiveInfinity(value))
+                {
+                    throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("MinWidth");
+                }
+                if (value > ActualMaxWidth)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo("value", "MinWidth", "MaxWidth");
+                }
+                if (!_minWidth.HasValue || _minWidth.Value != value)
+                {
+                    double oldValue = ActualMinWidth;
+                    _minWidth = value;
+                    if (OwningGrid != null && OwningGrid.ColumnsInternal != null)
+                    {
+                        OwningGrid.OnColumnMinWidthChanged(this, oldValue);
+                    }
+                }
+            }
+        }
+
+        public DataGridLength Width
+        {
+            get
+            {
+                return
+                    _width ??
+                        OwningGrid?.ColumnWidth ??
+                        // We don't have a good choice here because we don't want to make this property nullable, see DevDiv Bugs 196581
+                        DataGridLength.Auto;
+            }
+            set
+            {
+                if (!_width.HasValue || _width.Value != value)
+                {
+                    if (!_settingWidthInternally)
+                    {
+                        InheritsWidth = false;
+                    }
+
+                    if (OwningGrid != null)
+                    {
+                        DataGridLength width = CoerceWidth(value);
+                        if (width.IsStar != Width.IsStar)
+                        {
+                            // If a column has changed either from or to a star value, we want to recalculate all
+                            // star column widths.  They are recalculated during Measure based off what the value we set here.
+                            SetWidthInternalNoCallback(width);
+                            IsInitialDesiredWidthDetermined = false;
+                            OwningGrid.OnColumnWidthChanged(this);
+                        }
+                        else
+                        {
+                            // If a column width's value is simply changing, we resize it (to the right only).
+                            Resize(width.Value, width.UnitType, width.DesiredValue, width.DisplayValue, false);
+                        }
+                    }
+                    else
+                    {
+                        SetWidthInternalNoCallback(value);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// The binding that will be used to get or set cell content for the clipboard.
+        /// </summary>
+        public virtual IBinding ClipboardContentBinding
+        {
+            get
+            {
+                return _clipboardContentBinding;
+            }
+            set
+            {
+                _clipboardContentBinding = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets the value of a cell according to the the specified binding.
+        /// </summary>
+        /// <param name="item">The item associated with a cell.</param>
+        /// <param name="binding">The binding to get the value of.</param>
+        /// <returns>The resultant cell value.</returns>
+        internal object GetCellValue(object item, IBinding binding)
+        {
+            Debug.Assert(OwningGrid != null);
+
+            object content = null;
+            if (binding != null)
+            {
+                OwningGrid.ClipboardContentControl.DataContext = item;
+                var sub = OwningGrid.ClipboardContentControl.Bind(ContentControl.ContentProperty, binding);
+                content = OwningGrid.ClipboardContentControl.GetValue(ContentControl.ContentProperty);
+                sub.Dispose();
+            }
+            return content;
+        }
+
+        public IControl GetCellContent(DataGridRow dataGridRow)
+        {
+            Contract.Requires<ArgumentNullException>(dataGridRow != null);
+            if (OwningGrid == null)
+            {
+                throw DataGridError.DataGrid.NoOwningGrid(GetType());
+            }
+            if (dataGridRow.OwningGrid == OwningGrid)
+            {
+                DataGridCell dataGridCell = dataGridRow.Cells[Index];
+                if (dataGridCell != null)
+                {
+                    return dataGridCell.Content as IControl;
+                }
+            }
+            return null;
+        }
+
+        public IControl GetCellContent(object dataItem)
+        {
+            Contract.Requires<ArgumentNullException>(dataItem != null);
+            if (OwningGrid == null)
+            {
+                throw DataGridError.DataGrid.NoOwningGrid(GetType());
+            }
+            DataGridRow dataGridRow = OwningGrid.GetRowFromItem(dataItem);
+            if (dataGridRow == null)
+            {
+                return null;
+            }
+            return GetCellContent(dataGridRow);
+        }
+
+        /// <summary>
+        /// Returns the column which contains the given element
+        /// </summary>
+        /// <param name="element">element contained in a column</param>
+        /// <returns>Column that contains the element, or null if not found
+        /// </returns>
+        public static DataGridColumn GetColumnContainingElement(IControl element)
+        {
+            // Walk up the tree to find the DataGridCell or DataGridColumnHeader that contains the element
+            IVisual parent = element;
+            while (parent != null)
+            {
+                if (parent is DataGridCell cell)
+                {
+                    return cell.OwningColumn;
+                }
+                if (parent is DataGridColumnHeader columnHeader)
+                {
+                    return columnHeader.OwningColumn;
+                }
+                parent = parent.GetVisualParent();
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value.
+        /// </summary>
+        /// <param name="editingElement">
+        /// The element that the column displays for a cell in editing mode.
+        /// </param>
+        /// <param name="uneditedValue">
+        /// The previous, unedited value in the cell being edited.
+        /// </param>
+        protected virtual void CancelCellEdit(IControl editingElement, object uneditedValue)
+        { }
+
+        /// <summary>
+        /// When overridden in a derived class, gets an editing element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </summary>
+        /// <param name="cell">
+        /// The cell that will contain the generated element.
+        /// </param>
+        /// <param name="dataItem">
+        /// The data item represented by the row that contains the intended cell.
+        /// </param>
+        /// <returns>
+        /// A new editing element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </returns>
+        protected abstract IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding);
+
+        /// <summary>
+        /// When overridden in a derived class, gets a read-only element that is bound to the column's 
+        /// <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </summary>
+        /// <param name="cell">
+        /// The cell that will contain the generated element.
+        /// </param>
+        /// <param name="dataItem">
+        /// The data item represented by the row that contains the intended cell.
+        /// </param>
+        /// <returns>
+        /// A new, read-only element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </returns>
+        protected abstract IControl GenerateElement(DataGridCell cell, object dataItem);
+
+        /// <summary>
+        /// Called by a specific column type when one of its properties changed, 
+        /// and its current cells need to be updated.
+        /// </summary>
+        /// <param name="propertyName">Indicates which property changed and caused this call</param>
+        protected void NotifyPropertyChanged(string propertyName)
+        {
+            OwningGrid?.RefreshColumnElements(this, propertyName);
+        }
+
+        /// <summary>
+        /// When overridden in a derived class, called when a cell in the column enters editing mode.
+        /// </summary>
+        /// <param name="editingElement">
+        /// The element that the column displays for a cell in editing mode.
+        /// </param>
+        /// <param name="editingEventArgs">
+        /// Information about the user gesture that is causing a cell to enter editing mode.
+        /// </param>
+        /// <returns>
+        /// The unedited value.
+        /// </returns>
+        protected abstract object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs);
+
+        /// <summary>
+        /// Called by the DataGrid control when a column asked for its
+        /// elements to be refreshed, typically because one of its properties changed.
+        /// </summary>
+        /// <param name="element">Indicates the element that needs to be refreshed</param>
+        /// <param name="propertyName">Indicates which property changed and caused this call</param>
+        protected internal virtual void RefreshCellContent(IControl element, string propertyName)
+        { }
+
+        internal void CancelCellEditInternal(IControl editingElement, object uneditedValue)
+        {
+            CancelCellEdit(editingElement, uneditedValue);
+        }
+
+        /// <summary>
+        /// Coerces a DataGridLength to a valid value.  If any value components are double.NaN, this method
+        /// coerces them to a proper initial value.  For star columns, the desired width is calculated based
+        /// on the rest of the star columns.  For pixel widths, the desired value is based on the pixel value.
+        /// For auto widths, the desired value is initialized as the column's minimum width.
+        /// </summary>
+        /// <param name="width">The DataGridLength to coerce.</param>
+        /// <returns>The resultant (coerced) DataGridLength.</returns>
+        internal DataGridLength CoerceWidth(DataGridLength width)
+        {
+            double desiredValue = width.DesiredValue;
+            if (double.IsNaN(desiredValue))
+            {
+                if (width.IsStar && OwningGrid != null && OwningGrid.ColumnsInternal != null)
+                {
+                    double totalStarValues = 0;
+                    double totalStarDesiredValues = 0;
+                    double totalNonStarDisplayWidths = 0;
+                    foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && c != this && !double.IsNaN(c.Width.DesiredValue)))
+                    {
+                        if (column.Width.IsStar)
+                        {
+                            totalStarValues += column.Width.Value;
+                            totalStarDesiredValues += column.Width.DesiredValue;
+                        }
+                        else
+                        {
+                            totalNonStarDisplayWidths += column.ActualWidth;
+                        }
+                    }
+                    if (totalStarValues == 0)
+                    {
+                        // Compute the new star column's desired value based on the available space if there are no other visible star columns
+                        desiredValue = Math.Max(ActualMinWidth, OwningGrid.CellsWidth - totalNonStarDisplayWidths);
+                    }
+                    else
+                    {
+                        // Otherwise, compute its desired value based on those of other visible star columns
+                        desiredValue = totalStarDesiredValues * width.Value / totalStarValues;
+                    }
+                }
+                else if (width.IsAbsolute)
+                {
+                    desiredValue = width.Value;
+                }
+                else
+                {
+                    desiredValue = ActualMinWidth;
+                }
+            }
+
+            double displayValue = width.DisplayValue;
+            if (double.IsNaN(displayValue))
+            {
+                displayValue = desiredValue;
+            }
+            displayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue));
+
+            return new DataGridLength(width.Value, width.UnitType, desiredValue, displayValue);
+        }
+
+        /// <summary>
+        /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to
+        /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal
+        /// rounding process, so we need to do it ourselves.  If we don't, then we'll end up with some
+        /// pixel gaps and/or overlaps between columns.
+        /// </summary>
+        /// <param name="leftEdge"></param>
+        internal void ComputeLayoutRoundedWidth(double leftEdge)
+        {
+            if (OwningGrid != null && OwningGrid.UseLayoutRounding)
+            {
+                double roundedLeftEdge = Math.Floor(leftEdge + 0.5);
+                double roundedRightEdge = Math.Floor(leftEdge + ActualWidth + 0.5);
+                LayoutRoundedWidth = roundedRightEdge - roundedLeftEdge;
+            }
+            else
+            {
+                LayoutRoundedWidth = ActualWidth;
+            }
+        } 
+
+        //TODO Styles
+        internal virtual DataGridColumnHeader CreateHeader()
+        {
+            var result = new DataGridColumnHeader
+            {
+                OwningColumn = this,
+                Content = _header
+            };
+            //result.EnsureStyle(null);
+
+            return result;
+        }
+
+        /// <summary>
+        /// Ensures that this column's width has been coerced to a valid value.
+        /// </summary>
+        internal void EnsureWidth()
+        {
+            SetWidthInternalNoCallback(CoerceWidth(Width));
+        }
+
+        internal IControl GenerateElementInternal(DataGridCell cell, object dataItem)
+        {
+            return GenerateElement(cell, dataItem);
+        }
+
+        internal object PrepareCellForEditInternal(IControl editingElement, RoutedEventArgs editingEventArgs)
+        {
+            var result = PrepareCellForEdit(editingElement, editingEventArgs);
+            editingElement.Focus();
+
+            return result;
+        }
+
+        /// <summary>
+        /// Attempts to resize the column's width to the desired DisplayValue, but limits the final size
+        /// to the column's minimum and maximum values.  If star sizing is being used, then the column
+        /// can only decrease in size by the amount that the columns after it can increase in size.
+        /// Likewise, the column can only increase in size if other columns can spare the width.
+        /// </summary>
+        /// <param name="value">The new Value.</param>
+        /// <param name="unitType">The new UnitType.</param>
+        /// <param name="desiredValue">The new DesiredValue.</param>
+        /// <param name="displayValue">The new DisplayValue.</param>
+        /// <param name="userInitiated">Whether or not this resize was initiated by a user action.</param>
+        internal void Resize(double value, DataGridLengthUnitType unitType, double desiredValue, double displayValue, bool userInitiated)
+        {
+            double newValue = value;
+            double newDesiredValue = desiredValue;
+            double newDisplayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue));
+            DataGridLengthUnitType newUnitType = unitType;
+
+            int starColumnsCount = 0;
+            double totalDisplayWidth = 0;
+            foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+            {
+                column.EnsureWidth();
+                totalDisplayWidth += column.ActualWidth;
+                starColumnsCount += (column != this && column.Width.IsStar) ? 1 : 0;
+            }
+            bool hasInfiniteAvailableWidth = !OwningGrid.RowsPresenterAvailableSize.HasValue || double.IsPositiveInfinity(OwningGrid.RowsPresenterAvailableSize.Value.Width);
+
+            // If we're using star sizing, we can only resize the column as much as the columns to the
+            // right will allow (i.e. until they hit their max or min widths).
+            if (!hasInfiniteAvailableWidth && (starColumnsCount > 0 || (unitType == DataGridLengthUnitType.Star && Width.IsStar && userInitiated)))
+            {
+                double limitedDisplayValue = Width.DisplayValue;
+                double availableIncrease = Math.Max(0, OwningGrid.CellsWidth - totalDisplayWidth);
+                double desiredChange = newDisplayValue - Width.DisplayValue;
+                if (desiredChange > availableIncrease)
+                {
+                    // The desired change is greater than the amount of available space,
+                    // so we need to decrease the widths of columns to the right to make room.
+                    desiredChange -= availableIncrease;
+                    double actualChange = desiredChange + OwningGrid.DecreaseColumnWidths(DisplayIndex + 1, -desiredChange, userInitiated);
+                    limitedDisplayValue += availableIncrease + actualChange;
+                }
+                else if (desiredChange > 0)
+                {
+                    // The desired change is positive but less than the amount of available space,
+                    // so there's no need to decrease the widths of columns to the right.
+                    limitedDisplayValue += desiredChange;
+                }
+                else
+                {
+                    // The desired change is negative, so we need to increase the widths of columns to the right.
+                    limitedDisplayValue += desiredChange + OwningGrid.IncreaseColumnWidths(DisplayIndex + 1, -desiredChange, userInitiated);
+                }
+                if (ActualCanUserResize || (Width.IsStar && !userInitiated))
+                {
+                    newDisplayValue = limitedDisplayValue;
+                }
+            }
+
+            if (userInitiated)
+            {
+                newDesiredValue = newDisplayValue;
+                if (!Width.IsStar)
+                {
+                    InheritsWidth = false;
+                    newValue = newDisplayValue;
+                    newUnitType = DataGridLengthUnitType.Pixel;
+                }
+                else if (starColumnsCount > 0 && !hasInfiniteAvailableWidth)
+                {
+                    // Recalculate star weight of this column based on the new desired value
+                    InheritsWidth = false;
+                    newValue = (Width.Value * newDisplayValue) / ActualWidth;
+                }
+            }
+
+            DataGridLength oldWidth = Width;
+            SetWidthInternalNoCallback(new DataGridLength(Math.Min(double.MaxValue, newValue), newUnitType, newDesiredValue, newDisplayValue));
+            if (Width != oldWidth)
+            {
+                OwningGrid.OnColumnWidthChanged(this);
+            }
+        }
+
+        /// <summary>
+        /// Sets the column's Width to a new DataGridLength with a different DesiredValue.
+        /// </summary>
+        /// <param name="desiredValue">The new DesiredValue.</param>
+        internal void SetWidthDesiredValue(double desiredValue)
+        {
+            SetWidthInternalNoCallback(new DataGridLength(Width.Value, Width.UnitType, desiredValue, Width.DisplayValue));
+        }
+
+        /// <summary>
+        /// Sets the column's Width to a new DataGridLength with a different DisplayValue.
+        /// </summary>
+        /// <param name="displayValue">The new DisplayValue.</param>
+        internal void SetWidthDisplayValue(double displayValue)
+        {
+            SetWidthInternalNoCallback(new DataGridLength(Width.Value, Width.UnitType, Width.DesiredValue, displayValue));
+        }
+
+        /// <summary>
+        /// Set the column's Width without breaking inheritance.
+        /// </summary>
+        /// <param name="width">The new Width.</param>
+        internal void SetWidthInternal(DataGridLength width)
+        {
+            bool originalValue = _settingWidthInternally;
+            _settingWidthInternally = true;
+            try
+            {
+                Width = width;
+            }
+            finally
+            {
+                _settingWidthInternally = originalValue;
+            }
+        }
+
+        /// <summary>
+        /// Sets the column's Width directly, without any callback effects.
+        /// </summary>
+        /// <param name="width">The new Width.</param>
+        internal void SetWidthInternalNoCallback(DataGridLength width)
+        {
+            _width = width;
+        }
+
+        /// <summary>
+        /// Set the column's star value.  Whenever the star value changes, width inheritance is broken.
+        /// </summary>
+        /// <param name="value">The new star value.</param>
+        internal void SetWidthStarValue(double value)
+        {
+            InheritsWidth = false;
+            SetWidthInternalNoCallback(new DataGridLength(value, Width.UnitType, Width.DesiredValue, Width.DisplayValue));
+        }
+
+        //TODO Binding
+        internal IControl GenerateEditingElementInternal(DataGridCell cell, object dataItem)
+        {
+            if (_editingElement == null)
+            {
+                _editingElement = GenerateEditingElement(cell, dataItem, out _editBinding);
+            }
+
+            return _editingElement;
+        }
+
+        /// <summary>
+        /// Clears the cached editing element.
+        /// </summary>
+        //TODO Binding
+        internal void RemoveEditingElement()
+        {
+            _editingElement = null;
+        }
+
+        /// <summary>
+        /// Holds the name of the member to use for sorting, if not using the default.
+        /// </summary>
+        public string SortMemberPath
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// We get the sort description from the data source.  We don't worry whether we can modify sort -- perhaps the sort description
+        /// describes an unchangeable sort that exists on the data.
+        /// </summary>
+        internal DataGridSortDescription GetSortDescription()
+        {
+            if (OwningGrid != null
+                && OwningGrid.DataConnection != null
+                && OwningGrid.DataConnection.SortDescriptions != null)
+            {
+                string propertyName = GetSortPropertyName();
+
+                return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);
+            }
+
+            return null;
+        }
+
+        internal string GetSortPropertyName()
+        {
+            string result = SortMemberPath;
+
+            if (String.IsNullOrEmpty(result))
+            {
+
+                if(this is DataGridBoundColumn boundColumn && 
+                    boundColumn.Binding != null &&
+                    boundColumn.Binding is Binding binding &&
+                    binding.Path != null)
+                {
+                    result = binding.Path;
+                }
+            }
+
+            return result;
+        }
+
+    }
+
+}

+ 586 - 0
src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs

@@ -0,0 +1,586 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridColumnCollection : ObservableCollection<DataGridColumn>
+    {
+        private DataGrid _owningGrid;
+
+        public DataGridColumnCollection(DataGrid owningGrid)
+        {
+            _owningGrid = owningGrid;
+            ItemsInternal = new List<DataGridColumn>();
+            FillerColumn = new DataGridFillerColumn(owningGrid);
+            RowGroupSpacerColumn = new DataGridFillerColumn(owningGrid);
+            DisplayIndexMap = new List<int>();
+        }
+
+        internal int AutogeneratedColumnCount
+        {
+            get;
+            set;
+        }
+
+        internal List<int> DisplayIndexMap
+        {
+            get;
+            set;
+        }
+
+        internal DataGridFillerColumn FillerColumn
+        {
+            get;
+            private set;
+        }
+
+        internal DataGridColumn FirstColumn
+        {
+            get
+            {
+                return GetFirstColumn(null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridColumn FirstVisibleColumn
+        {
+            get
+            {
+                return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridColumn FirstVisibleNonFillerColumn
+        {
+            get
+            {
+                DataGridColumn dataGridColumn = FirstVisibleColumn;
+                if (dataGridColumn == RowGroupSpacerColumn)
+                {
+                    dataGridColumn = GetNextVisibleColumn(dataGridColumn);
+                }
+                return dataGridColumn;
+            }
+        }
+
+        internal DataGridColumn FirstVisibleWritableColumn
+        {
+            get
+            {
+                return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridColumn FirstVisibleScrollingColumn
+        {
+            get
+            {
+                return GetFirstColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/);
+            }
+        }
+
+        internal List<DataGridColumn> ItemsInternal
+        {
+            get;
+            private set;
+        }
+
+        internal DataGridColumn LastVisibleColumn
+        {
+            get
+            {
+                return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridColumn LastVisibleScrollingColumn
+        {
+            get
+            {
+                return GetLastColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridColumn LastVisibleWritableColumn
+        {
+            get
+            {
+                return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/);
+            }
+        }
+
+        internal DataGridFillerColumn RowGroupSpacerColumn
+        {
+            get;
+            private set;
+        }
+
+        internal int VisibleColumnCount
+        {
+            get
+            {
+                int visibleColumnCount = 0;
+                for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
+                {
+                    if (ItemsInternal[columnIndex].IsVisible)
+                    {
+                        visibleColumnCount++;
+                    }
+                }
+                return visibleColumnCount;
+            }
+        }
+
+        internal double VisibleEdgedColumnsWidth
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The number of star columns that are currently visible.
+        /// NOTE: Requires that EnsureVisibleEdgedColumnsWidth has been called.
+        /// </summary>
+        internal int VisibleStarColumnCount
+        {
+            get;
+            private set;
+        }
+
+        protected override void ClearItems()
+        {
+            try
+            {
+                _owningGrid.NoCurrentCellChangeCount++;
+                if (ItemsInternal.Count > 0)
+                {
+                    if (_owningGrid.InDisplayIndexAdjustments)
+                    {
+                        // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes.
+                        throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes();
+                    }
+
+                    _owningGrid.OnClearingColumns();
+                    for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
+                    {
+                        // Detach the column...
+                        ItemsInternal[columnIndex].OwningGrid = null;
+                    }
+                    ItemsInternal.Clear();
+                    DisplayIndexMap.Clear();
+                    AutogeneratedColumnCount = 0;
+                    _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/);
+                    base.ClearItems();
+                    VisibleEdgedColumnsWidth = 0;
+                    _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/);
+                }
+            }
+            finally
+            {
+                _owningGrid.NoCurrentCellChangeCount--;
+            }
+        }
+
+        protected override void InsertItem(int columnIndex, DataGridColumn dataGridColumn)
+        {
+            try
+            {
+                _owningGrid.NoCurrentCellChangeCount++;
+                if (_owningGrid.InDisplayIndexAdjustments)
+                {
+                    // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes.
+                    throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes();
+                }
+                if (dataGridColumn == null)
+                {
+                    throw new ArgumentNullException("dataGridColumn");
+                }
+
+                int columnIndexWithFiller = columnIndex;
+                if (dataGridColumn != RowGroupSpacerColumn && RowGroupSpacerColumn.IsRepresented)
+                {
+                    columnIndexWithFiller++;
+                }
+
+                // get the new current cell coordinates
+                DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnInsertingColumn(columnIndex, dataGridColumn);
+
+                // insert the column into our internal list
+                ItemsInternal.Insert(columnIndexWithFiller, dataGridColumn);
+                dataGridColumn.Index = columnIndexWithFiller;
+                dataGridColumn.OwningGrid = _owningGrid;
+                dataGridColumn.RemoveEditingElement();
+                if (dataGridColumn.IsVisible)
+                {
+                    VisibleEdgedColumnsWidth += dataGridColumn.ActualWidth;
+                }
+
+                // continue with the base insert
+                _owningGrid.OnInsertedColumn_PreNotification(dataGridColumn);
+                _owningGrid.OnColumnCollectionChanged_PreNotification(true /*columnsGrew*/);
+
+                if (dataGridColumn != RowGroupSpacerColumn)
+                {
+                    base.InsertItem(columnIndex, dataGridColumn);
+                }
+                _owningGrid.OnInsertedColumn_PostNotification(newCurrentCellCoordinates, dataGridColumn.DisplayIndex);
+                _owningGrid.OnColumnCollectionChanged_PostNotification(true /*columnsGrew*/);
+            }
+            finally
+            {
+                _owningGrid.NoCurrentCellChangeCount--;
+            }
+        }
+
+        protected override void RemoveItem(int columnIndex)
+        {
+            RemoveItemPrivate(columnIndex, false /*isSpacer*/);
+        }
+
+        protected override void SetItem(int columnIndex, DataGridColumn dataGridColumn)
+        {
+            throw new NotSupportedException();
+        }
+
+        internal bool DisplayInOrder(int columnIndex1, int columnIndex2)
+        {
+            int displayIndex1 = ItemsInternal[columnIndex1].DisplayIndexWithFiller;
+            int displayIndex2 = ItemsInternal[columnIndex2].DisplayIndexWithFiller;
+            return displayIndex1 < displayIndex2;
+        }
+
+        internal bool EnsureRowGrouping(bool rowGrouping)
+        {
+            // The insert below could cause the first column to be added.  That causes a refresh 
+            // which re-enters method so instead of checking RowGroupSpacerColumn.IsRepresented, 
+            // we need to check to see if it's actually in our collection instead.
+            bool spacerRepresented = (ItemsInternal.Count > 0) && (ItemsInternal[0] == RowGroupSpacerColumn);
+            if (rowGrouping && !spacerRepresented)
+            {
+                Insert(0, RowGroupSpacerColumn);
+                RowGroupSpacerColumn.IsRepresented = true;
+                return true;
+            }
+            else if (!rowGrouping && spacerRepresented)
+            {
+                // We need to set IsRepresented to false before removing the RowGroupSpacerColumn
+                // otherwise, we'll remove the column after it
+                RowGroupSpacerColumn.IsRepresented = false;
+                RemoveItemPrivate(0, true /*isSpacer*/);
+                return true;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// In addition to ensuring that column widths are valid, method updates the values of
+        /// VisibleEdgedColumnsWidth and VisibleStarColumnCount.
+        /// </summary>
+        internal void EnsureVisibleEdgedColumnsWidth()
+        {
+            VisibleStarColumnCount = 0;
+            VisibleEdgedColumnsWidth = 0;
+            for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
+            {
+                if (ItemsInternal[columnIndex].IsVisible)
+                {
+                    ItemsInternal[columnIndex].EnsureWidth();
+                    if (ItemsInternal[columnIndex].Width.IsStar)
+                    {
+                        VisibleStarColumnCount++;
+                    }
+                    VisibleEdgedColumnsWidth += ItemsInternal[columnIndex].ActualWidth;
+                }
+            }
+        }
+
+        internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex)
+        {
+            if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count)
+            {
+                return null;
+            }
+            int columnIndex = DisplayIndexMap[displayIndex];
+            return ItemsInternal[columnIndex];
+        }
+
+        internal int GetColumnCount(bool isVisible, bool isFrozen, int fromColumnIndex, int toColumnIndex)
+        {
+            int columnCount = 0;
+            DataGridColumn dataGridColumn = ItemsInternal[fromColumnIndex];
+
+            while (dataGridColumn != ItemsInternal[toColumnIndex])
+            {
+                dataGridColumn = GetNextColumn(dataGridColumn, isVisible, isFrozen, null /*isReadOnly*/);
+                columnCount++;
+            }
+            return columnCount;
+        }
+
+        internal IEnumerable<DataGridColumn> GetDisplayedColumns()
+        {
+            foreach (int columnIndex in DisplayIndexMap)
+            {
+                yield return ItemsInternal[columnIndex];
+            }
+        }
+
+        /// <summary>
+        /// Returns an enumeration of all columns that meet the criteria of the filter predicate.
+        /// </summary>
+        /// <param name="filter">Criteria for inclusion.</param>
+        /// <returns>Columns that meet the criteria, in ascending DisplayIndex order.</returns>
+        internal IEnumerable<DataGridColumn> GetDisplayedColumns(Predicate<DataGridColumn> filter)
+        {
+            Debug.Assert(filter != null);
+            Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count);
+            foreach (int columnIndex in DisplayIndexMap)
+            {
+                DataGridColumn column = ItemsInternal[columnIndex];
+                if (filter(column))
+                {
+                    yield return column;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns an enumeration of all columns that meet the criteria of the filter predicate.
+        /// The columns are returned in the order specified by the reverse flag.
+        /// </summary>
+        /// <param name="reverse">Whether or not to return the columns in descending DisplayIndex order.</param>
+        /// <param name="filter">Criteria for inclusion.</param>
+        /// <returns>Columns that meet the criteria, in the order specified by the reverse flag.</returns>
+        internal IEnumerable<DataGridColumn> GetDisplayedColumns(bool reverse, Predicate<DataGridColumn> filter)
+        {
+            return reverse ? GetDisplayedColumnsReverse(filter) : GetDisplayedColumns(filter);
+        }
+
+        /// <summary>
+        /// Returns an enumeration of all columns that meet the criteria of the filter predicate.
+        /// The columns are returned in descending DisplayIndex order.
+        /// </summary>
+        /// <param name="filter">Criteria for inclusion.</param>
+        /// <returns>Columns that meet the criteria, in descending DisplayIndex order.</returns>
+        internal IEnumerable<DataGridColumn> GetDisplayedColumnsReverse(Predicate<DataGridColumn> filter)
+        {
+            for (int displayIndex = DisplayIndexMap.Count - 1; displayIndex >= 0; displayIndex--)
+            {
+                DataGridColumn column = ItemsInternal[DisplayIndexMap[displayIndex]];
+                if (filter(column))
+                {
+                    yield return column;
+                }
+            }
+        }
+
+        internal DataGridColumn GetFirstColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly)
+        {
+            Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count);
+            int index = 0;
+            while (index < DisplayIndexMap.Count)
+            {
+                DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index);
+                if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) &&
+                    (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) &&
+                    (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly))
+                {
+                    return dataGridColumn;
+                }
+                index++;
+            }
+            return null;
+        }
+
+        internal DataGridColumn GetLastColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly)
+        {
+            Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count);
+            int index = DisplayIndexMap.Count - 1;
+            while (index >= 0)
+            {
+                DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index);
+                if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) &&
+                    (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) &&
+                    (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly))
+                {
+                    return dataGridColumn;
+                }
+                index--;
+            }
+            return null;
+        }
+
+        internal DataGridColumn GetNextColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetNextColumn(dataGridColumnStart, null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+        }
+
+        internal DataGridColumn GetNextColumn(DataGridColumn dataGridColumnStart,
+                                                  bool? isVisible, bool? isFrozen, bool? isReadOnly)
+        {
+            Debug.Assert(dataGridColumnStart != null);
+            Debug.Assert(ItemsInternal.Contains(dataGridColumnStart));
+            Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count);
+
+            int index = dataGridColumnStart.DisplayIndexWithFiller + 1;
+            while (index < DisplayIndexMap.Count)
+            {
+                DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index);
+
+                if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) &&
+                    (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) &&
+                    (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly))
+                {
+                    return dataGridColumn;
+                }
+                index++;
+            }
+            return null;
+        }
+
+        internal DataGridColumn GetNextVisibleColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+        }
+
+        internal DataGridColumn GetNextVisibleFrozenColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetNextColumn(dataGridColumnStart, true /*isVisible*/, true /*isFrozen*/, null /*isReadOnly*/);
+        }
+
+        internal DataGridColumn GetNextVisibleWritableColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/);
+        }
+
+        internal DataGridColumn GetPreviousColumn(DataGridColumn dataGridColumnStart,
+                                                      bool? isVisible, bool? isFrozen, bool? isReadOnly)
+        {
+            int index = dataGridColumnStart.DisplayIndexWithFiller - 1;
+            while (index >= 0)
+            {
+                DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index);
+                if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) &&
+                    (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) &&
+                    (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly))
+                {
+                    return dataGridColumn;
+                }
+                index--;
+            }
+            return null;
+        }
+
+        internal DataGridColumn GetPreviousVisibleNonFillerColumn(DataGridColumn dataGridColumnStart)
+        {
+            DataGridColumn column = GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/);
+            return (column is DataGridFillerColumn) ? null : column;
+        }
+
+        internal DataGridColumn GetPreviousVisibleScrollingColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/);
+        }
+
+        internal DataGridColumn GetPreviousVisibleWritableColumn(DataGridColumn dataGridColumnStart)
+        {
+            return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/);
+        }
+
+        internal int GetVisibleColumnCount(int fromColumnIndex, int toColumnIndex)
+        {
+            int columnCount = 0;
+            DataGridColumn dataGridColumn = ItemsInternal[fromColumnIndex];
+
+            while (dataGridColumn != ItemsInternal[toColumnIndex])
+            {
+                dataGridColumn = GetNextVisibleColumn(dataGridColumn);
+                columnCount++;
+            }
+            return columnCount;
+        }
+
+        internal IEnumerable<DataGridColumn> GetVisibleColumns()
+        {
+            Predicate<DataGridColumn> filter = column => column.IsVisible;
+            return GetDisplayedColumns(filter);
+        }
+
+        internal IEnumerable<DataGridColumn> GetVisibleFrozenColumns()
+        {
+            Predicate<DataGridColumn> filter = column => column.IsVisible && column.IsFrozen;
+            return GetDisplayedColumns(filter);
+        }
+
+        internal double GetVisibleFrozenEdgedColumnsWidth()
+        {
+            double visibleFrozenColumnsWidth = 0;
+            for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
+            {
+                if (ItemsInternal[columnIndex].IsVisible && ItemsInternal[columnIndex].IsFrozen)
+                {
+                    visibleFrozenColumnsWidth += ItemsInternal[columnIndex].ActualWidth;
+                }
+            }
+            return visibleFrozenColumnsWidth;
+        }
+
+        internal IEnumerable<DataGridColumn> GetVisibleScrollingColumns()
+        {
+            Predicate<DataGridColumn> filter = column => column.IsVisible && !column.IsFrozen;
+            return GetDisplayedColumns(filter);
+        }
+
+        private void RemoveItemPrivate(int columnIndex, bool isSpacer)
+        {
+            try
+            {
+                _owningGrid.NoCurrentCellChangeCount++;
+
+                if (_owningGrid.InDisplayIndexAdjustments)
+                {
+                    // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes.
+                    throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes();
+                }
+
+                int columnIndexWithFiller = columnIndex;
+                if (!isSpacer && RowGroupSpacerColumn.IsRepresented)
+                {
+                    columnIndexWithFiller++;
+                }
+
+                DataGridColumn dataGridColumn = ItemsInternal[columnIndexWithFiller];
+                DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnRemovingColumn(dataGridColumn);
+                ItemsInternal.RemoveAt(columnIndexWithFiller);
+                if (dataGridColumn.IsVisible)
+                {
+                    VisibleEdgedColumnsWidth -= dataGridColumn.ActualWidth;
+                }
+                dataGridColumn.OwningGrid = null;
+                dataGridColumn.RemoveEditingElement();
+
+                // continue with the base remove
+                _owningGrid.OnRemovedColumn_PreNotification(dataGridColumn);
+                _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/);
+                if (!isSpacer)
+                {
+                    base.RemoveItem(columnIndex);
+                }
+                _owningGrid.OnRemovedColumn_PostNotification(newCurrentCellCoordinates);
+                _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/);
+            }
+            finally
+            {
+                _owningGrid.NoCurrentCellChangeCount--;
+            }
+        }
+
+    }
+}

+ 806 - 0
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -0,0 +1,806 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Collections;
+using Avalonia.Media;
+using System.ComponentModel;
+using System.Diagnostics;
+using Avalonia.Utilities;
+using System;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> column header.
+    /// </summary>
+    public class DataGridColumnHeader : ContentControl
+    {
+        private enum DragMode
+        {
+            None = 0,
+            MouseDown = 1,
+            Drag = 2,
+            Resize = 3,
+            Reorder = 4
+        }
+
+        private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5;
+        private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1;
+
+        private bool _areHandlersSuspended;
+        private static DragMode _dragMode;
+        private static Point? _lastMousePositionHeaders;
+        private static Cursor _originalCursor;
+        private static double _originalHorizontalOffset;
+        private static double _originalWidth;
+        private bool _desiredSeparatorVisibility;
+        private static Point? _dragStart;
+        private static DataGridColumn _dragColumn;
+        private static double _frozenColumnsWidth;
+        private static Lazy<Cursor> _resizeCursor = new Lazy<Cursor>(() => new Cursor(StandardCursorType.SizeWestEast));
+
+        public static readonly StyledProperty<IBrush> SeparatorBrushProperty =
+            AvaloniaProperty.Register<DataGridColumnHeader, IBrush>(nameof(SeparatorBrush));
+
+        public IBrush SeparatorBrush
+        {
+            get { return GetValue(SeparatorBrushProperty); }
+            set { SetValue(SeparatorBrushProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> AreSeparatorsVisibleProperty =
+            AvaloniaProperty.Register<DataGridColumnHeader, bool>(
+                nameof(AreSeparatorsVisible),
+                defaultValue: true);
+
+        public bool AreSeparatorsVisible
+        {
+            get { return GetValue(AreSeparatorsVisibleProperty); }
+            set { SetValue(AreSeparatorsVisibleProperty, value); }
+        }
+
+        static DataGridColumnHeader()
+        {
+            AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>(x => x.OnAreSeparatorsVisibleChanged);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.Primitives.DataGridColumnHeader" /> class. 
+        /// </summary>
+        //TODO Implement
+        public DataGridColumnHeader()
+        {
+            PointerPressed += DataGridColumnHeader_PointerPressed;
+            PointerReleased += DataGridColumnHeader_PointerReleased;
+            PointerMoved += DataGridColumnHeader_PointerMove;
+            PointerEnter += DataGridColumnHeader_PointerEnter;
+            PointerLeave += DataGridColumnHeader_PointerLeave;
+        }
+
+        private void OnAreSeparatorsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                _desiredSeparatorVisibility = (bool)e.NewValue;
+                if (OwningGrid != null)
+                {
+                    UpdateSeparatorVisibility(OwningGrid.ColumnsInternal.LastVisibleColumn);
+                }
+                else
+                {
+                    UpdateSeparatorVisibility(null);
+                }
+            }
+        }
+
+        internal DataGridColumn OwningColumn
+        {
+            get;
+            set;
+        } 
+        internal DataGrid OwningGrid => OwningColumn?.OwningGrid;
+
+        internal int ColumnIndex
+        {
+            get
+            {
+                if (OwningColumn == null)
+                {
+                    return -1;
+                }
+                return OwningColumn.Index;
+            }
+        } 
+
+        internal ListSortDirection? CurrentSortingState
+        {
+            get;
+            private set;
+        } 
+
+        private bool IsMouseOver
+        {
+            get;
+            set;
+        } 
+
+        private bool IsPressed
+        {
+            get;
+            set;
+        }
+
+        private void SetValueNoCallback<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
+        {
+            _areHandlersSuspended = true;
+            try
+            {
+                SetValue(property, value, priority);
+            }
+            finally
+            {
+                _areHandlersSuspended = false;
+            }
+        }
+
+        //TODO Implement
+        internal void ApplyState()
+        {
+            CurrentSortingState = null;
+            if (OwningGrid != null
+                && OwningGrid.DataConnection != null
+                && OwningGrid.DataConnection.AllowSort)
+            {
+                var sort = OwningColumn.GetSortDescription();
+                if(sort != null)
+                {
+                    CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending;
+                }
+            }
+            PseudoClasses.Set(":sortascending", 
+                CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending);
+            PseudoClasses.Set(":sortdescending", 
+                CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending);
+        }
+
+        internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn)
+        {
+            bool newVisibility = _desiredSeparatorVisibility;
+
+            // Collapse separator for the last column if there is no filler column
+            if (OwningColumn != null &&
+                OwningGrid != null &&
+                _desiredSeparatorVisibility &&
+                OwningColumn == lastVisibleColumn &&
+                !OwningGrid.ColumnsInternal.FillerColumn.IsActive)
+            {
+                newVisibility = false;
+            }
+
+            // Update the public property if it has changed
+            if (AreSeparatorsVisible != newVisibility)
+            {
+                SetValueNoCallback(AreSeparatorsVisibleProperty, newVisibility);
+            }
+        }
+
+        internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled)
+        {
+            // completed a click without dragging, so we're sorting
+            InvokeProcessSort(inputModifiers);
+            handled = true;
+        } 
+
+        internal void InvokeProcessSort(InputModifiers inputModifiers)
+        {
+            Debug.Assert(OwningGrid != null);
+            if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(inputModifiers)))
+            {
+                return;
+            }
+            if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
+            {
+                Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(inputModifiers));
+            }
+        } 
+
+        //TODO GroupSorting
+        internal void ProcessSort(InputModifiers inputModifiers)
+        {
+            // 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
+            // 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)
+            {
+                DataGrid owningGrid = OwningGrid;
+
+                DataGridSortDescription newSort;
+
+                KeyboardHelper.GetMetaKeyState(inputModifiers, out bool ctrl, out bool shift);
+
+                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();
+                    }
+
+                    if (sort != null)
+                    {
+                        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);
+                    }
+                }
+            }
+        } 
+
+        private bool CanReorderColumn(DataGridColumn column)
+        {
+            return OwningGrid.CanUserReorderColumns 
+                && !(column is DataGridFillerColumn)
+                && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue);
+        } 
+
+        /// <summary>
+        /// Determines whether a column can be resized by dragging the border of its header.  If star sizing
+        /// is being used, there are special conditions that can prevent a column from being resized:
+        /// 1. The column is the last visible column.
+        /// 2. All columns are constrained by either their maximum or minimum values.
+        /// </summary>
+        /// <param name="column">Column to check.</param>
+        /// <returns>Whether or not the column can be resized by dragging its header.</returns>
+        private static bool CanResizeColumn(DataGridColumn column)
+        {
+            if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing &&
+                (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
+            {
+                return false;
+            }
+            return column.ActualCanUserResize;
+        }  
+
+        private static bool TrySetResizeColumn(DataGridColumn column)
+        {
+            // If datagrid.CanUserResizeColumns == false, then the column can still override it
+            if (CanResizeColumn(column))
+            {
+                _dragColumn = column;
+
+                _dragMode = DragMode.Resize;
+
+                return true;
+            }
+            return false;
+        } 
+
+        //TODO DragDrop
+
+        internal void OnMouseLeftButtonDown(ref bool handled, PointerEventArgs args, Point mousePosition)
+        {
+            IsPressed = true;
+
+            if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
+            {
+                args.Device.Capture(this);
+
+                _dragMode = DragMode.MouseDown;
+                _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
+                _lastMousePositionHeaders = this.Translate(OwningGrid.ColumnHeaders, mousePosition);
+
+                double distanceFromLeft = mousePosition.X;
+                double distanceFromRight = Bounds.Width - distanceFromLeft;
+                DataGridColumn currentColumn = OwningColumn;
+                DataGridColumn previousColumn = null;
+                if (!(OwningColumn is DataGridFillerColumn))
+                {
+                    previousColumn = OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn);
+                }
+
+                if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight <= DATAGRIDCOLUMNHEADER_resizeRegionWidth))
+                {
+                    handled = TrySetResizeColumn(currentColumn);
+                }
+                else if (_dragMode == DragMode.MouseDown && _dragColumn == null && distanceFromLeft <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && previousColumn != null)
+                {
+                    handled = TrySetResizeColumn(previousColumn);
+                }
+
+                if (_dragMode == DragMode.Resize && _dragColumn != null)
+                {
+                    _dragStart = _lastMousePositionHeaders;
+                    _originalWidth = _dragColumn.ActualWidth;
+                    _originalHorizontalOffset = OwningGrid.HorizontalOffset;
+
+                    handled = true;
+                }
+            }
+        }
+
+        //TODO DragEvents
+        //TODO MouseCapture
+        internal void OnMouseLeftButtonUp(ref bool handled, PointerEventArgs args, Point mousePosition, Point mousePositionHeaders)
+        {
+            IsPressed = false;
+
+            if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
+            {
+                if (_dragMode == DragMode.MouseDown)
+                {
+                   OnMouseLeftButtonUp_Click(args.InputModifiers, ref handled);
+                }
+                else if (_dragMode == DragMode.Reorder)
+                {
+                    // Find header we're hovering over
+                    int targetIndex = GetReorderingTargetDisplayIndex(mousePositionHeaders);
+
+                    if (((!OwningColumn.IsFrozen && targetIndex >= OwningGrid.FrozenColumnCount)
+                          || (OwningColumn.IsFrozen && targetIndex < OwningGrid.FrozenColumnCount)))
+                    {
+                        OwningColumn.DisplayIndex = targetIndex;
+
+                        DataGridColumnEventArgs ea = new DataGridColumnEventArgs(OwningColumn);
+                        OwningGrid.OnColumnReordered(ea);
+                    }
+                }
+
+                SetDragCursor(mousePosition);
+
+                // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture
+                args.Device.Capture(null);
+                OnLostMouseCapture();
+                _dragMode = DragMode.None;
+                handled = true;
+            }
+        }
+
+        //TODO DragEvents
+        internal void OnMouseMove(ref bool handled, Point mousePosition, Point mousePositionHeaders)
+        {
+            if (handled || OwningGrid == null || OwningGrid.ColumnHeaders == null)
+            {
+                return;
+            }
+
+            Debug.Assert(OwningGrid.Parent is InputElement);
+
+            double distanceFromLeft = mousePosition.X;
+            double distanceFromRight = Bounds.Width - distanceFromLeft;
+
+            OnMouseMove_Resize(ref handled, mousePositionHeaders);
+
+            OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight);
+
+            // if we still haven't done anything about moving the mouse while 
+            // the button is down, we remember that we're dragging, but we don't 
+            // claim to have actually handled the event
+            if (_dragMode == DragMode.MouseDown)
+            {
+                _dragMode = DragMode.Drag;
+            }
+
+            _lastMousePositionHeaders = mousePositionHeaders;
+
+            SetDragCursor(mousePosition);
+        }
+
+        private void DataGridColumnHeader_PointerEnter(object sender, PointerEventArgs e)
+        {
+            if (!IsEnabled)
+            {
+                return;
+            }
+
+            Point mousePosition = e.GetPosition(this);
+            OnMouseEnter(mousePosition);
+            ApplyState();
+        }
+
+        private void DataGridColumnHeader_PointerLeave(object sender, PointerEventArgs e)
+        {
+            if (!IsEnabled)
+            {
+                return;
+            }
+
+            OnMouseLeave();
+            ApplyState();
+        } 
+
+        private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left)
+            {
+                return;
+            }
+
+            Point mousePosition = e.GetPosition(this);
+            bool handled = e.Handled;
+            OnMouseLeftButtonDown(ref handled, e, mousePosition);
+            e.Handled = handled;
+
+            ApplyState();
+        }
+
+        private void DataGridColumnHeader_PointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left)
+            {
+                return;
+            }
+
+            Point mousePosition = e.GetPosition(this);
+            Point mousePositionHeaders = e.GetPosition(OwningGrid.ColumnHeaders);
+            bool handled = e.Handled;
+            OnMouseLeftButtonUp(ref handled, e, mousePosition, mousePositionHeaders);
+            e.Handled = handled;
+
+            ApplyState();
+        }
+
+        private void DataGridColumnHeader_PointerMove(object sender, PointerEventArgs e)
+        {
+            if (OwningGrid == null || !IsEnabled)
+            {
+                return;
+            }
+
+            Point mousePosition = e.GetPosition(this);
+            Point mousePositionHeaders = e.GetPosition(OwningGrid.ColumnHeaders);
+
+            bool handled = false;
+            OnMouseMove(ref handled, mousePosition, mousePositionHeaders);
+        }
+
+        /// <summary>
+        /// Returns the column against whose top-left the reordering caret should be positioned
+        /// </summary>
+        /// <param name="mousePositionHeaders">Mouse position within the ColumnHeadersPresenter</param>
+        /// <param name="scroll">Whether or not to scroll horizontally when a column is dragged out of bounds</param>
+        /// <param name="scrollAmount">If scroll is true, returns the horizontal amount that was scrolled</param>
+        /// <returns></returns>
+        private DataGridColumn GetReorderingTargetColumn(Point mousePositionHeaders, bool scroll, out double scrollAmount)
+        {
+            scrollAmount = 0;
+            double leftEdge = OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented ? OwningGrid.ColumnsInternal.RowGroupSpacerColumn.ActualWidth : 0;
+            double rightEdge = OwningGrid.CellsWidth;
+            if (OwningColumn.IsFrozen)
+            {
+                rightEdge = Math.Min(rightEdge, _frozenColumnsWidth);
+            }
+            else if (OwningGrid.FrozenColumnCount > 0)
+            {
+                leftEdge = _frozenColumnsWidth;
+            }
+
+            if (mousePositionHeaders.X < leftEdge)
+            {
+                if (scroll &&
+                    OwningGrid.HorizontalScrollBar != null &&
+                    OwningGrid.HorizontalScrollBar.IsVisible &&
+                    OwningGrid.HorizontalScrollBar.Value > 0)
+                {
+                    double newVal = mousePositionHeaders.X - leftEdge;
+                    scrollAmount = Math.Min(newVal, OwningGrid.HorizontalScrollBar.Value);
+                    OwningGrid.UpdateHorizontalOffset(scrollAmount + OwningGrid.HorizontalScrollBar.Value);
+                }
+                mousePositionHeaders = mousePositionHeaders.WithX(leftEdge);
+            }
+            else if (mousePositionHeaders.X >= rightEdge)
+            {
+                if (scroll &&
+                    OwningGrid.HorizontalScrollBar != null &&
+                    OwningGrid.HorizontalScrollBar.IsVisible &&
+                    OwningGrid.HorizontalScrollBar.Value < OwningGrid.HorizontalScrollBar.Maximum)
+                {
+                    double newVal = mousePositionHeaders.X - rightEdge;
+                    scrollAmount = Math.Min(newVal, OwningGrid.HorizontalScrollBar.Maximum - OwningGrid.HorizontalScrollBar.Value);
+                    OwningGrid.UpdateHorizontalOffset(scrollAmount + OwningGrid.HorizontalScrollBar.Value);
+                }
+                mousePositionHeaders = mousePositionHeaders.WithX(rightEdge - 1);
+            }
+
+            foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetDisplayedColumns())
+            {
+                Point mousePosition = OwningGrid.ColumnHeaders.Translate(column.HeaderCell, mousePositionHeaders);
+                double columnMiddle = column.HeaderCell.Bounds.Width / 2;
+                if (mousePosition.X >= 0 && mousePosition.X <= columnMiddle)
+                {
+                    return column;
+                }
+                else if (mousePosition.X > columnMiddle && mousePosition.X < column.HeaderCell.Bounds.Width)
+                {
+                    return OwningGrid.ColumnsInternal.GetNextVisibleColumn(column);
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Returns the display index to set the column to
+        /// </summary>
+        /// <param name="mousePositionHeaders">Mouse position relative to the column headers presenter</param>
+        /// <returns></returns>
+        private int GetReorderingTargetDisplayIndex(Point mousePositionHeaders)
+        {
+            DataGridColumn targetColumn = GetReorderingTargetColumn(mousePositionHeaders, false /*scroll*/, out double scrollAmount);
+            if (targetColumn != null)
+            {
+                return targetColumn.DisplayIndex > OwningColumn.DisplayIndex ? targetColumn.DisplayIndex - 1 : targetColumn.DisplayIndex;
+            }
+            else
+            {
+                return OwningGrid.Columns.Count - 1;
+            }
+        } 
+
+        /// <summary>
+        /// Returns true if the mouse is 
+        /// - to the left of the element, or within the left half of the element
+        /// and
+        /// - within the vertical range of the element, or ignoreVertical == true
+        /// </summary>
+        /// <param name="mousePosition"></param>
+        /// <param name="element"></param>
+        /// <param name="ignoreVertical"></param>
+        /// <returns></returns>
+        private bool IsReorderTargeted(Point mousePosition, Control element, bool ignoreVertical)
+        {
+            Point position = this.Translate(element, mousePosition);
+
+            return (position.X < 0 || (position.X >= 0 && position.X <= element.Bounds.Width / 2))
+                && (ignoreVertical || (position.Y >= 0 && position.Y <= element.Bounds.Height));
+        }
+
+        /// <summary>
+        /// Resets the static DataGridColumnHeader properties when a header loses mouse capture
+        /// </summary>
+        private void OnLostMouseCapture()
+        {
+            // When we stop interacting with the column headers, we need to reset the drag mode
+            // and close any popups if they are open.
+
+            if (_dragColumn != null && _dragColumn.HeaderCell != null)
+            {
+                _dragColumn.HeaderCell.Cursor = _originalCursor;
+            }
+            _dragMode = DragMode.None;
+            _dragColumn = null;
+            _dragStart = null;
+            _lastMousePositionHeaders = null;
+
+            if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
+            {
+                OwningGrid.ColumnHeaders.DragColumn = null;
+                OwningGrid.ColumnHeaders.DragIndicator = null;
+                OwningGrid.ColumnHeaders.DropLocationIndicator = null;
+            }
+        }
+
+        /// <summary>
+        /// Sets up the DataGridColumnHeader for the MouseEnter event
+        /// </summary>
+        /// <param name="mousePosition">mouse position relative to the DataGridColumnHeader</param>
+        private void OnMouseEnter(Point mousePosition)
+        {
+            IsMouseOver = true;
+            SetDragCursor(mousePosition);
+        }
+
+        /// <summary>
+        /// Sets up the DataGridColumnHeader for the MouseLeave event
+        /// </summary>
+        private void OnMouseLeave()
+        {
+            IsMouseOver = false;
+        }
+
+        //TODO Styles DragIndicator
+        private void OnMouseMove_BeginReorder(Point mousePosition)
+        {
+            DataGridColumnHeader dragIndicator = new DataGridColumnHeader
+            {
+                OwningColumn = OwningColumn,
+                IsEnabled = false,
+                Content = Content,
+                ContentTemplate = ContentTemplate
+            };
+
+            dragIndicator.PseudoClasses.Add(":dragIndicator");
+
+            IControl dropLocationIndicator = OwningGrid.DropLocationIndicatorTemplate?.Build();
+
+            // If the user didn't style the dropLocationIndicator's Height, default to the column header's height
+            if (dropLocationIndicator != null && double.IsNaN(dropLocationIndicator.Height) && dropLocationIndicator is Control element)
+            {
+                element.Height = Bounds.Height;
+            }
+
+            // pass the caret's data template to the user for modification
+            DataGridColumnReorderingEventArgs columnReorderingEventArgs = new DataGridColumnReorderingEventArgs(OwningColumn)
+            {
+                DropLocationIndicator = dropLocationIndicator,
+                DragIndicator = dragIndicator
+            };
+            OwningGrid.OnColumnReordering(columnReorderingEventArgs);
+            if (columnReorderingEventArgs.Cancel)
+            {
+                return;
+            }
+
+            // The user didn't cancel, so prepare for the reorder
+            _dragColumn = OwningColumn;
+            _dragMode = DragMode.Reorder;
+            _dragStart = mousePosition;
+
+            // Display the reordering thumb
+            OwningGrid.ColumnHeaders.DragColumn = OwningColumn;
+            OwningGrid.ColumnHeaders.DragIndicator = columnReorderingEventArgs.DragIndicator;
+            OwningGrid.ColumnHeaders.DropLocationIndicator = columnReorderingEventArgs.DropLocationIndicator;
+
+            // If the user didn't style the dragIndicator's Width, default it to the column header's width
+            if (double.IsNaN(dragIndicator.Width))
+            {
+                dragIndicator.Width = Bounds.Width;
+            }
+        }
+
+        //TODO DragEvents
+        private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight)
+        {
+            if (handled)
+            {
+                return;
+            }
+
+            //handle entry into reorder mode
+            if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
+            {
+                handled = CanReorderColumn(OwningColumn);
+
+                if (handled)
+                {
+                    OnMouseMove_BeginReorder(mousePosition);
+                }
+            }
+
+            //handle reorder mode (eg, positioning of the popup)
+            if (_dragMode == DragMode.Reorder && OwningGrid.ColumnHeaders.DragIndicator != null)
+            {
+                // Find header we're hovering over
+                DataGridColumn targetColumn = GetReorderingTargetColumn(mousePositionHeaders, !OwningColumn.IsFrozen /*scroll*/, out double scrollAmount);
+
+                OwningGrid.ColumnHeaders.DragIndicatorOffset = mousePosition.X - _dragStart.Value.X + scrollAmount;
+                OwningGrid.ColumnHeaders.InvalidateArrange();
+
+                if (OwningGrid.ColumnHeaders.DropLocationIndicator != null)
+                {
+                    Point targetPosition = new Point(0, 0);
+                    if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen)
+                    {
+                        targetColumn = 
+                            OwningGrid.ColumnsInternal.GetLastColumn(
+                                isVisible: true,
+                                isFrozen: OwningColumn.IsFrozen,
+                                isReadOnly: null);
+                        targetPosition = targetColumn.HeaderCell.Translate(OwningGrid.ColumnHeaders, targetPosition);
+
+                        targetPosition = targetPosition.WithX(targetPosition.X + targetColumn.ActualWidth);
+                    }
+                    else
+                    {
+                        targetPosition = targetColumn.HeaderCell.Translate(OwningGrid.ColumnHeaders, targetPosition);
+                    }
+                    OwningGrid.ColumnHeaders.DropLocationIndicatorOffset = targetPosition.X - scrollAmount;
+                }
+
+                handled = true;
+            }
+        } 
+
+        private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders)
+        {
+            if (handled)
+            {
+                return;
+            }
+
+            if (_dragMode == DragMode.Resize && _dragColumn != null && _dragStart.HasValue)
+            {
+                // resize column
+
+                double mouseDelta = mousePositionHeaders.X - _dragStart.Value.X;
+                double desiredWidth = _originalWidth + mouseDelta;
+
+                desiredWidth = Math.Max(_dragColumn.ActualMinWidth, Math.Min(_dragColumn.ActualMaxWidth, desiredWidth));
+                _dragColumn.Resize(_dragColumn.Width.Value, _dragColumn.Width.UnitType, _dragColumn.Width.DesiredValue, desiredWidth, true);
+
+                OwningGrid.UpdateHorizontalOffset(_originalHorizontalOffset);
+
+                handled = true;
+            }
+        } 
+
+        private void SetDragCursor(Point mousePosition)
+        {
+            if (_dragMode != DragMode.None || OwningGrid == null || OwningColumn == null)
+            {
+                return;
+            }
+
+            // set mouse if we can resize column
+
+            double distanceFromLeft = mousePosition.X;
+            double distanceFromRight = Bounds.Width - distanceFromLeft;
+            DataGridColumn currentColumn = OwningColumn;
+            DataGridColumn previousColumn = null;
+
+            if (!(OwningColumn is DataGridFillerColumn))
+            {
+                previousColumn = OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn);
+            }
+
+            if ((distanceFromRight <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && currentColumn != null && CanResizeColumn(currentColumn)) ||
+                (distanceFromLeft <= DATAGRIDCOLUMNHEADER_resizeRegionWidth && previousColumn != null && CanResizeColumn(previousColumn)))
+            {
+                var resizeCursor = _resizeCursor.Value;
+                if (Cursor != resizeCursor)
+                {
+                    _originalCursor = Cursor;
+                    Cursor = resizeCursor;
+                }
+            }
+            else
+            {
+                Cursor = _originalCursor;
+            }
+        }
+
+    }
+
+}

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

@@ -0,0 +1,1764 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Utilities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Reflection;
+
+namespace Avalonia.Controls
+{
+    public partial class DataGrid
+    {
+
+        protected virtual void OnColumnDisplayIndexChanged(DataGridColumnEventArgs e)
+        {
+            ColumnDisplayIndexChanged?.Invoke(this, e);
+        }
+
+        protected internal virtual void OnColumnReordered(DataGridColumnEventArgs e)
+        {
+            EnsureVerticalGridLines();
+            ColumnReordered?.Invoke(this, e);
+        }
+
+        protected internal virtual void OnColumnReordering(DataGridColumnReorderingEventArgs e)
+        {
+            ColumnReordering?.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
+        /// could not be met, the remaining amount of adjustment is returned.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="amount">Adjustment amount (positive for increase, negative for decrease).</param>
+        /// <param name="userInitiated">Whether or not this adjustment was initiated by a user action.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated)
+        {
+            if (!DoubleUtil.IsZero(amount))
+            {
+                if (amount < 0)
+                {
+                    amount = DecreaseColumnWidths(displayIndex, amount, userInitiated);
+                }
+                else
+                {
+                    amount = IncreaseColumnWidths(displayIndex, amount, userInitiated);
+                }
+            }
+            return amount;
+        }
+
+        /// <summary>
+        /// Grows an auto-column's width to the desired width.
+        /// </summary>
+        /// <param name="column">Auto-column to adjust.</param>
+        /// <param name="desiredWidth">The new desired width of the column.</param>
+        internal void AutoSizeColumn(DataGridColumn column, double desiredWidth)
+        {
+            Debug.Assert(column.Width.IsAuto || column.Width.IsSizeToCells || column.Width.IsSizeToHeader || (!UsesStarSizing && column.Width.IsStar));
+
+            // If we're using star sizing and this is the first time we've measured this particular auto-column,
+            // we want to allow all rows to get measured before we setup the star widths.  We won't know the final
+            // desired value of the column until all rows have been measured.  Because of this, we wait until
+            // an Arrange occurs before we adjust star widths.
+            if (UsesStarSizing && !column.IsInitialDesiredWidthDetermined)
+            {
+                AutoSizingColumns = true;
+            }
+
+            // Update the column's DesiredValue if it needs to grow to fit the new desired value
+            if (desiredWidth > column.Width.DesiredValue || double.IsNaN(column.Width.DesiredValue))
+            {
+                // If this auto-growth occurs after the column's initial desired width has been determined,
+                // then the growth should act like a resize (squish columns to the right).  Otherwise, if
+                // this column is newly added, we'll just set its display value directly.
+                if (UsesStarSizing && column.IsInitialDesiredWidthDetermined)
+                {
+                    column.Resize(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth, false);
+                }
+                else
+                {
+                    column.SetWidthInternalNoCallback(new DataGridLength(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth));
+                    OnColumnWidthChanged(column);
+                }
+            }
+        }
+
+        internal bool ColumnRequiresRightGridLine(DataGridColumn dataGridColumn, bool includeLastRightGridLineWhenPresent)
+        {
+            return (GridLinesVisibility == DataGridGridLinesVisibility.Vertical || GridLinesVisibility == DataGridGridLinesVisibility.All) && VerticalGridLinesBrush != null &&
+                   (dataGridColumn != ColumnsInternal.LastVisibleColumn || (includeLastRightGridLineWhenPresent && ColumnsInternal.FillerColumn.IsActive));
+        }
+
+        internal DataGridColumnCollection CreateColumnsInstance()
+        {
+            return new DataGridColumnCollection(this);
+        }
+
+        /// <summary>
+        /// Decreases the widths of all columns with DisplayIndex >= displayIndex such that the total
+        /// width is decreased by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="amount">Amount to decrease (in pixels).</param>
+        /// <param name="userInitiated">Whether or not this adjustment was initiated by a user action.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        internal double DecreaseColumnWidths(int displayIndex, double amount, bool userInitiated)
+        {
+            // 1. Take space from non-star columns with widths larger than desired widths (left to right).
+            amount = DecreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false);
+
+            // 2. Take space from star columns until they reach their min.
+            amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated);
+
+            // 3. Take space from non-star columns that have already been initialized, until they reach their min (right to left).
+            amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, false);
+
+            // 4. Take space from all non-star columns until they reach their min, even if they are new (right to left).
+            amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, true);
+
+            return amount;
+        }
+
+        internal bool GetColumnReadOnlyState(DataGridColumn dataGridColumn, bool isReadOnly)
+        {
+            Debug.Assert(dataGridColumn != null);
+
+            if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn && 
+                dataGridBoundColumn.Binding is Binding binding)
+            {
+                string path = binding.Path;
+
+                if (string.IsNullOrWhiteSpace(path))
+                {
+                    return true;
+                }
+                else
+                {
+                    return DataConnection.GetPropertyIsReadOnly(path) || isReadOnly;
+                }
+            }
+
+            return isReadOnly;
+        }
+
+        // Returns the column's width
+        internal static double GetEdgedColumnWidth(DataGridColumn dataGridColumn)
+        {
+            Debug.Assert(dataGridColumn != null);
+            return dataGridColumn.ActualWidth;
+        }
+
+        /// <summary>
+        /// Increases the widths of all columns with DisplayIndex >= displayIndex such that the total
+        /// width is increased by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="amount">Amount of increase (in pixels).</param>
+        /// <param name="userInitiated">Whether or not this adjustment was initiated by a user action.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        internal double IncreaseColumnWidths(int displayIndex, double amount, bool userInitiated)
+        {
+            // 1. Give space to non-star columns that are smaller than their desired widths (left to right).
+            amount = IncreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false);
+
+            // 2. Give space to star columns until they reach their max.
+            amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated);
+
+            // 3. Give space to non-star columns that have already been initialized, until they reach their max (right to left).
+            amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false);
+
+            // 4. Give space to all non-star columns until they reach their max, even if they are new (right to left).
+            amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false);
+
+            return amount;
+        }
+
+        internal void OnClearingColumns()
+        {
+            // Rows need to be cleared first. There cannot be rows without also having columns.
+            ClearRows(false);
+
+            // Removing all the column header cells
+            RemoveDisplayedColumnHeaders();
+
+            _horizontalOffset = _negHorizontalOffset = 0;
+            if (_hScrollBar != null && _hScrollBar.IsVisible) // 
+            {
+                _hScrollBar.Value = 0;
+            }
+        }
+
+        /// <summary>
+        /// Invalidates the widths of all columns because the resizing behavior of an individual column has changed.
+        /// </summary>
+        /// <param name="column">Column with CanUserResize property that has changed.</param>
+        internal void OnColumnCanUserResizeChanged(DataGridColumn column)
+        {
+            if (column.IsVisible)
+            {
+                EnsureHorizontalLayout();
+            }
+        }
+
+        internal void OnColumnCollectionChanged_PostNotification(bool columnsGrew)
+        {
+            if (columnsGrew &&
+                CurrentColumnIndex == -1)
+            {
+                MakeFirstDisplayedCellCurrentCell();
+            }
+
+            if (_autoGeneratingColumnOperationCount == 0)
+            {
+                EnsureRowsPresenterVisibility();
+                InvalidateRowHeightEstimate();
+            }
+        }
+
+        internal void OnColumnCollectionChanged_PreNotification(bool columnsGrew)
+        {
+            // dataGridColumn==null means the collection was refreshed.
+
+            if (columnsGrew && _autoGeneratingColumnOperationCount == 0 && ColumnsItemsInternal.Count == 1)
+            {
+                RefreshRows(false /*recycleRows*/, true /*clearRows*/);
+            }
+            else
+            {
+                InvalidateMeasure();
+            }
+        }
+
+        internal void OnColumnDisplayIndexChanged(DataGridColumn dataGridColumn)
+        {
+            Debug.Assert(dataGridColumn != null);
+            DataGridColumnEventArgs e = new DataGridColumnEventArgs(dataGridColumn);
+
+            // Call protected method to raise event
+            if (dataGridColumn != ColumnsInternal.RowGroupSpacerColumn)
+            {
+                OnColumnDisplayIndexChanged(e);
+            }
+        }
+
+        internal void OnColumnDisplayIndexChanged_PostNotification()
+        {
+            // Notifications for adjusted display indexes.
+            FlushDisplayIndexChanged(true /*raiseEvent*/);
+
+            // Our displayed columns may have changed so recompute them
+            UpdateDisplayedColumns();
+
+            // Invalidate layout
+            CorrectColumnFrozenStates();
+            EnsureHorizontalLayout();
+        }
+
+        internal void OnColumnDisplayIndexChanging(DataGridColumn targetColumn, int newDisplayIndex)
+        {
+            Debug.Assert(targetColumn != null);
+            Debug.Assert(newDisplayIndex != targetColumn.DisplayIndexWithFiller);
+
+            if (InDisplayIndexAdjustments)
+            {
+                // We are within columns display indexes adjustments. We do not allow changing display indexes while adjusting them.
+                throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes();
+            }
+
+            try
+            {
+                InDisplayIndexAdjustments = true;
+
+                bool trackChange = targetColumn != ColumnsInternal.RowGroupSpacerColumn;
+
+                DataGridColumn column;
+                // Move is legal - let's adjust the affected display indexes.
+                if (newDisplayIndex < targetColumn.DisplayIndexWithFiller)
+                {
+                    // DisplayIndex decreases. All columns with newDisplayIndex <= DisplayIndex < targetColumn.DisplayIndex
+                    // get their DisplayIndex incremented.
+                    for (int i = newDisplayIndex; i < targetColumn.DisplayIndexWithFiller; i++)
+                    {
+                        column = ColumnsInternal.GetColumnAtDisplayIndex(i);
+                        column.DisplayIndexWithFiller = column.DisplayIndexWithFiller + 1;
+                        if (trackChange)
+                        {
+                            column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on
+                        }
+                    }
+                }
+                else
+                {
+                    // DisplayIndex increases. All columns with targetColumn.DisplayIndex < DisplayIndex <= newDisplayIndex
+                    // get their DisplayIndex decremented.
+                    for (int i = newDisplayIndex; i > targetColumn.DisplayIndexWithFiller; i--)
+                    {
+                        column = ColumnsInternal.GetColumnAtDisplayIndex(i);
+                        column.DisplayIndexWithFiller = column.DisplayIndexWithFiller - 1;
+                        if (trackChange)
+                        {
+                            column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on
+                        }
+                    }
+                }
+                // Now let's actually change the order of the DisplayIndexMap
+                if (targetColumn.DisplayIndexWithFiller != -1)
+                {
+                    ColumnsInternal.DisplayIndexMap.Remove(targetColumn.Index);
+                }
+                ColumnsInternal.DisplayIndexMap.Insert(newDisplayIndex, targetColumn.Index);
+            }
+            finally
+            {
+                InDisplayIndexAdjustments = false;
+            }
+
+            // Note that displayIndex of moved column is updated by caller.
+        }
+
+        internal void OnColumnBindingChanged(DataGridBoundColumn column)
+        {
+            // Update Binding in Displayed rows by regenerating the affected elements
+            if (_rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    PopulateCellContent(false /*isCellEdited*/, column, row, row.Cells[column.Index]);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adjusts the specified column's width according to its new maximum value.
+        /// </summary>
+        /// <param name="column">The column to adjust.</param>
+        /// <param name="oldValue">The old ActualMaxWidth of the column.</param>
+        internal void OnColumnMaxWidthChanged(DataGridColumn column, double oldValue)
+        {
+            Debug.Assert(column != null);
+
+            if (column.IsVisible && oldValue != column.ActualMaxWidth)
+            {
+                if (column.ActualMaxWidth < column.Width.DisplayValue)
+                {
+                    // If the maximum width has caused the column to decrease in size, try first to resize
+                    // the columns to the right to make up for the difference in width, but don't limit the column's
+                    // final display value to how much they could be resized.
+                    AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMaxWidth, false);
+                    column.SetWidthDisplayValue(column.ActualMaxWidth);
+                }
+                else if (column.Width.DisplayValue == oldValue && column.Width.DesiredValue > column.Width.DisplayValue)
+                {
+                    // If the column was previously limited by its maximum value but has more room now, 
+                    // attempt to resize the column to its desired width.
+                    column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false);
+                }
+                OnColumnWidthChanged(column);
+            }
+        }
+
+        /// <summary>
+        /// Adjusts the specified column's width according to its new minimum value.
+        /// </summary>
+        /// <param name="column">The column to adjust.</param>
+        /// <param name="oldValue">The old ActualMinWidth of the column.</param>
+        internal void OnColumnMinWidthChanged(DataGridColumn column, double oldValue)
+        {
+            Debug.Assert(column != null);
+
+            if (column.IsVisible && oldValue != column.ActualMinWidth)
+            {
+                if (column.ActualMinWidth > column.Width.DisplayValue)
+                {
+                    // If the minimum width has caused the column to increase in size, try first to resize
+                    // the columns to the right to make up for the difference in width, but don't limit the column's
+                    // final display value to how much they could be resized.
+                    AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMinWidth, false);
+                    column.SetWidthDisplayValue(column.ActualMinWidth);
+                }
+                else if (column.Width.DisplayValue == oldValue && column.Width.DesiredValue < column.Width.DisplayValue)
+                {
+                    // If the column was previously limited by its minimum value but but can be smaller now, 
+                    // attempt to resize the column to its desired width.
+                    column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false);
+                }
+                OnColumnWidthChanged(column);
+            }
+        }
+
+        internal void OnColumnReadOnlyStateChanging(DataGridColumn dataGridColumn, bool isReadOnly)
+        {
+            Debug.Assert(dataGridColumn != null);
+            if (isReadOnly && CurrentColumnIndex == dataGridColumn.Index)
+            {
+                // Edited column becomes read-only. Exit editing mode.
+                if (!EndCellEdit(DataGridEditAction.Commit, true /*exitEditingMode*/, ContainsFocus /*keepFocus*/, true /*raiseEvents*/))
+                {
+                    EndCellEdit(DataGridEditAction.Cancel, true /*exitEditingMode*/, ContainsFocus /*keepFocus*/, false /*raiseEvents*/);
+                }
+            }
+        }
+
+        internal void OnColumnVisibleStateChanged(DataGridColumn updatedColumn)
+        {
+            Debug.Assert(updatedColumn != null);
+
+            CorrectColumnFrozenStates();
+            UpdateDisplayedColumns();
+            EnsureRowsPresenterVisibility();
+            EnsureHorizontalLayout();
+            InvalidateColumnHeadersMeasure();
+
+            if (updatedColumn.IsVisible &&
+                ColumnsInternal.VisibleColumnCount == 1 && CurrentColumnIndex == -1)
+            {
+                Debug.Assert(SelectedIndex == DataConnection.IndexOf(SelectedItem));
+                if (SelectedIndex != -1)
+                {
+                    SetAndSelectCurrentCell(updatedColumn.Index, SelectedIndex, true /*forceCurrentCellSelection*/);
+                }
+                else
+                {
+                    MakeFirstDisplayedCellCurrentCell();
+                }
+            }
+
+            // We need to explicitly collapse the cells of the invisible column because layout only goes through
+            // visible ones
+            if (!updatedColumn.IsVisible)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    row.Cells[updatedColumn.Index].IsVisible = false;
+                }
+            }
+        }
+
+        internal void OnColumnVisibleStateChanging(DataGridColumn targetColumn)
+        {
+            Debug.Assert(targetColumn != null);
+
+            if (targetColumn.IsVisible &&
+                CurrentColumn == targetColumn)
+            {
+                // Column of the current cell is made invisible. Trying to move the current cell to a neighbor column. May throw an exception.
+                DataGridColumn dataGridColumn = ColumnsInternal.GetNextVisibleColumn(targetColumn);
+                if (dataGridColumn == null)
+                {
+                    dataGridColumn = ColumnsInternal.GetPreviousVisibleNonFillerColumn(targetColumn);
+                }
+                if (dataGridColumn == null)
+                {
+                    SetCurrentCellCore(-1, -1);
+                }
+                else
+                {
+                    SetCurrentCellCore(dataGridColumn.Index, CurrentSlot);
+                }
+            }
+        }
+
+        internal void OnColumnWidthChanged(DataGridColumn updatedColumn)
+        {
+            Debug.Assert(updatedColumn != null);
+            if (updatedColumn.IsVisible)
+            {
+                EnsureHorizontalLayout();
+            }
+        }
+
+        internal void OnFillerColumnWidthNeeded(double finalWidth)
+        {
+            DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
+            double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
+            if (finalWidth > totalColumnsWidth)
+            {
+                fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
+            }
+            else
+            {
+                fillerColumn.FillerWidth = 0;
+            }
+        }
+
+        internal void OnInsertedColumn_PostNotification(DataGridCellCoordinates newCurrentCellCoordinates, int newDisplayIndex)
+        {
+            // Update current cell if needed
+            if (newCurrentCellCoordinates.ColumnIndex != -1)
+            {
+                Debug.Assert(CurrentColumnIndex == -1);
+                SetAndSelectCurrentCell(newCurrentCellCoordinates.ColumnIndex,
+                                        newCurrentCellCoordinates.Slot,
+                                        ColumnsInternal.VisibleColumnCount == 1 /*forceCurrentCellSelection*/);
+
+                if (newDisplayIndex < FrozenColumnCountWithFiller)
+                {
+                    CorrectColumnFrozenStates();
+                }
+            }
+        }
+
+        internal void OnInsertedColumn_PreNotification(DataGridColumn insertedColumn)
+        {
+            // Fix the Index of all following columns
+            CorrectColumnIndexesAfterInsertion(insertedColumn, 1);
+
+            Debug.Assert(insertedColumn.Index >= 0);
+            Debug.Assert(insertedColumn.Index < ColumnsItemsInternal.Count);
+            Debug.Assert(insertedColumn.OwningGrid == this);
+
+            CorrectColumnDisplayIndexesAfterInsertion(insertedColumn);
+
+            InsertDisplayedColumnHeader(insertedColumn);
+
+            // Insert the missing data cells
+            if (SlotCount > 0)
+            {
+                int newColumnCount = ColumnsItemsInternal.Count;
+
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    if (row.Cells.Count < newColumnCount)
+                    {
+                        AddNewCellPrivate(row, insertedColumn);
+                    }
+                }
+            }
+
+            if (insertedColumn.IsVisible)
+            {
+                EnsureHorizontalLayout();
+            }
+
+            if (insertedColumn is DataGridBoundColumn boundColumn && !boundColumn.IsAutoGenerated)
+            {
+                boundColumn.SetHeaderFromBinding();
+            }
+        }
+
+        internal DataGridCellCoordinates OnInsertingColumn(int columnIndexInserted, DataGridColumn insertColumn)
+        {
+            DataGridCellCoordinates newCurrentCellCoordinates;
+            Debug.Assert(insertColumn != null);
+
+            if (insertColumn.OwningGrid != null && insertColumn != ColumnsInternal.RowGroupSpacerColumn)
+            {
+                throw DataGridError.DataGrid.ColumnCannotBeReassignedToDifferentDataGrid();
+            }
+
+            // Reset current cell if there is one, no matter the relative position of the columns involved
+            if (CurrentColumnIndex != -1)
+            {
+                _temporarilyResetCurrentCell = true;
+                newCurrentCellCoordinates = new DataGridCellCoordinates(columnIndexInserted <= CurrentColumnIndex ? CurrentColumnIndex + 1 : CurrentColumnIndex,
+                     CurrentSlot);
+                ResetCurrentCellCore();
+            }
+            else
+            {
+                newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1);
+            }
+            return newCurrentCellCoordinates;
+        }
+
+        internal void OnRemovedColumn_PostNotification(DataGridCellCoordinates newCurrentCellCoordinates)
+        {
+            // Update current cell if needed
+            if (newCurrentCellCoordinates.ColumnIndex != -1)
+            {
+                Debug.Assert(CurrentColumnIndex == -1);
+                SetAndSelectCurrentCell(newCurrentCellCoordinates.ColumnIndex, newCurrentCellCoordinates.Slot, false /*forceCurrentCellSelection*/);
+            }
+        }
+
+        internal void OnRemovedColumn_PreNotification(DataGridColumn removedColumn)
+        {
+            Debug.Assert(removedColumn.Index >= 0);
+            Debug.Assert(removedColumn.OwningGrid == null);
+
+            // Intentionally keep the DisplayIndex intact after detaching the column.
+            CorrectColumnIndexesAfterDeletion(removedColumn);
+
+            CorrectColumnDisplayIndexesAfterDeletion(removedColumn);
+
+            // If the detached column was frozen, a new column needs to take its place
+            if (removedColumn.IsFrozen)
+            {
+                removedColumn.IsFrozen = false;
+                CorrectColumnFrozenStates();
+            }
+
+            UpdateDisplayedColumns();
+
+            // Fix the existing rows by removing cells at correct index
+            int newColumnCount = ColumnsItemsInternal.Count;
+
+            if (_rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    if (row.Cells.Count > newColumnCount)
+                    {
+                        row.Cells.RemoveAt(removedColumn.Index);
+                    }
+                }
+                _rowsPresenter.InvalidateArrange();
+            }
+
+            RemoveDisplayedColumnHeader(removedColumn);
+        }
+
+        internal DataGridCellCoordinates OnRemovingColumn(DataGridColumn dataGridColumn)
+        {
+            Debug.Assert(dataGridColumn != null);
+            Debug.Assert(dataGridColumn.Index >= 0 && dataGridColumn.Index < ColumnsItemsInternal.Count);
+
+            DataGridCellCoordinates newCurrentCellCoordinates;
+
+            _temporarilyResetCurrentCell = false;
+            int columnIndex = dataGridColumn.Index;
+
+            // Reset the current cell's address if there is one.
+            if (CurrentColumnIndex != -1)
+            {
+                int newCurrentColumnIndex = CurrentColumnIndex;
+                if (columnIndex == newCurrentColumnIndex)
+                {
+                    DataGridColumn dataGridColumnNext = ColumnsInternal.GetNextVisibleColumn(ColumnsItemsInternal[columnIndex]);
+                    if (dataGridColumnNext != null)
+                    {
+                        if (dataGridColumnNext.Index > columnIndex)
+                        {
+                            newCurrentColumnIndex = dataGridColumnNext.Index - 1;
+                        }
+                        else
+                        {
+                            newCurrentColumnIndex = dataGridColumnNext.Index;
+                        }
+                    }
+                    else
+                    {
+                        DataGridColumn dataGridColumnPrevious = ColumnsInternal.GetPreviousVisibleNonFillerColumn(ColumnsItemsInternal[columnIndex]);
+                        if (dataGridColumnPrevious != null)
+                        {
+                            if (dataGridColumnPrevious.Index > columnIndex)
+                            {
+                                newCurrentColumnIndex = dataGridColumnPrevious.Index - 1;
+                            }
+                            else
+                            {
+                                newCurrentColumnIndex = dataGridColumnPrevious.Index;
+                            }
+                        }
+                        else
+                        {
+                            newCurrentColumnIndex = -1;
+                        }
+                    }
+                }
+                else if (columnIndex < newCurrentColumnIndex)
+                {
+                    newCurrentColumnIndex--;
+                }
+                newCurrentCellCoordinates = new DataGridCellCoordinates(newCurrentColumnIndex, (newCurrentColumnIndex == -1) ? -1 : CurrentSlot);
+                if (columnIndex == CurrentColumnIndex)
+                {
+                    // If the commit fails, force a cancel edit
+                    if (!CommitEdit(DataGridEditingUnit.Row, false /*exitEditingMode*/))
+                    {
+                        CancelEdit(DataGridEditingUnit.Row, false /*raiseEvents*/);
+                    }
+                    bool success = SetCurrentCellCore(-1, -1);
+                    Debug.Assert(success);
+                }
+                else
+                {
+                    // Underlying data of deleted column is gone. It cannot be accessed anymore.
+                    // Do not end editing mode so that CellValidation doesn't get raised, since that event needs the current formatted value.
+                    _temporarilyResetCurrentCell = true;
+                    bool success = SetCurrentCellCore(-1, -1);
+                    Debug.Assert(success);
+                }
+            }
+            else
+            {
+                newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1);
+            }
+
+            // If the last column is removed, delete all the rows first.
+            if (ColumnsItemsInternal.Count == 1)
+            {
+                ClearRows(false);
+            }
+
+            // Is deleted column scrolled off screen?
+            if (dataGridColumn.IsVisible &&
+                !dataGridColumn.IsFrozen &&
+                DisplayData.FirstDisplayedScrollingCol >= 0)
+            {
+                // Deleted column is part of scrolling columns.
+                if (DisplayData.FirstDisplayedScrollingCol == dataGridColumn.Index)
+                {
+                    // Deleted column is first scrolling column
+                    _horizontalOffset -= _negHorizontalOffset;
+                    _negHorizontalOffset = 0;
+                }
+                else if (!ColumnsInternal.DisplayInOrder(DisplayData.FirstDisplayedScrollingCol, dataGridColumn.Index))
+                {
+                    // Deleted column is displayed before first scrolling column
+                    Debug.Assert(_horizontalOffset >= GetEdgedColumnWidth(dataGridColumn));
+                    _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn);
+                }
+
+                if (_hScrollBar != null && _hScrollBar.IsVisible) // 
+                {
+                    _hScrollBar.Value = _horizontalOffset;
+                }
+            }
+
+            return newCurrentCellCoordinates;
+        }
+
+        /// <summary>
+        /// Called when a column property changes, and its cells need to 
+        /// adjust that that column change.
+        /// </summary>
+        internal void RefreshColumnElements(DataGridColumn dataGridColumn, string propertyName)
+        {
+            Debug.Assert(dataGridColumn != null);
+
+            // Take care of the non-displayed loaded rows
+            for (int index = 0; index < _loadedRows.Count;)
+            {
+                DataGridRow dataGridRow = _loadedRows[index];
+                Debug.Assert(dataGridRow != null);
+                if (!IsSlotVisible(dataGridRow.Slot))
+                {
+                    RefreshCellElement(dataGridColumn, dataGridRow, propertyName);
+                }
+                index++;
+            }
+
+            // Take care of the displayed rows
+            if (_rowsPresenter != null)
+            {
+                foreach (DataGridRow row in GetAllRows())
+                {
+                    RefreshCellElement(dataGridColumn, row, propertyName);
+                }
+                // This update could change layout so we need to update our estimate and invalidate
+                InvalidateRowHeightEstimate();
+                InvalidateMeasure();
+            }
+        }
+
+        /// <summary>
+        /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total
+        /// width is adjusted by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="adjustment">Adjustment amount (positive for increase, negative for decrease).</param>
+        /// <param name="userInitiated">Whether or not this adjustment was initiated by a user action.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated)
+        {
+            double remainingAdjustment = adjustment;
+            if (DoubleUtil.IsZero(remainingAdjustment))
+            {
+                return remainingAdjustment;
+            }
+            bool increase = remainingAdjustment > 0;
+
+            // Make an initial pass through the star columns to total up some values.
+            bool scaleStarWeights = false;
+            double totalStarColumnsWidth = 0;
+            double totalStarColumnsWidthLimit = 0;
+            double totalStarWeights = 0;
+            List<DataGridColumn> starColumns = new List<DataGridColumn>();
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(c => c.Width.IsStar && c.IsVisible && (c.ActualCanUserResize || !userInitiated)))
+            {
+                if (column.DisplayIndex < displayIndex)
+                {
+                    scaleStarWeights = true;
+                    continue;
+                }
+                starColumns.Add(column);
+                totalStarWeights += column.Width.Value;
+                totalStarColumnsWidth += column.Width.DisplayValue;
+                totalStarColumnsWidthLimit += increase ? column.ActualMaxWidth : column.ActualMinWidth;
+            }
+
+            // Set the new desired widths according to how much all the star columns can be adjusted without any
+            // of them being limited by their minimum or maximum widths (as that would distort their ratios).
+            double adjustmentLimit = totalStarColumnsWidthLimit - totalStarColumnsWidth;
+            adjustmentLimit = increase ? Math.Min(adjustmentLimit, adjustment) : Math.Max(adjustmentLimit, adjustment);
+            foreach (DataGridColumn starColumn in starColumns)
+            {
+                starColumn.SetWidthDesiredValue((totalStarColumnsWidth + adjustmentLimit) * starColumn.Width.Value / totalStarWeights);
+            }
+
+            // Adjust the star column widths first towards their desired values, and then towards their limits.
+            remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => c.Width.DesiredValue);
+            remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => increase ? c.ActualMaxWidth : c.ActualMinWidth);
+
+            // Set the new star value weights according to how much the total column widths have changed.
+            // Only do this if there were other star columns to the left, though.  If there weren't any then that means
+            // all the star columns were adjusted at the same time, and therefore, their ratios have not changed.
+            if (scaleStarWeights)
+            {
+                double starRatio = (totalStarColumnsWidth + adjustment - remainingAdjustment) / totalStarColumnsWidth;
+                foreach (DataGridColumn starColumn in starColumns)
+                {
+                    starColumn.SetWidthStarValue(Math.Min(double.MaxValue, starRatio * starColumn.Width.Value));
+                }
+            }
+
+            return remainingAdjustment;
+        }
+
+        /// <summary>
+        /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total
+        /// width is adjusted by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.  The columns will stop adjusting
+        /// once they hit their target widths.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="remainingAdjustment">Adjustment amount (positive for increase, negative for decrease).</param>
+        /// <param name="userInitiated">Whether or not this adjustment was initiated by a user action.</param>
+        /// <param name="targetWidth">The target width of the column.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func<DataGridColumn, double> targetWidth)
+        {
+            if (DoubleUtil.IsZero(remainingAdjustment))
+            {
+                return remainingAdjustment;
+            }
+            bool increase = remainingAdjustment > 0;
+
+            double totalStarWeights = 0;
+            double totalStarColumnsWidth = 0;
+
+            // Order the star columns according to which one will hit their target width (or min/max limit) first.
+            // Each KeyValuePair represents a column (as the key) and an ordering factor (as the value).  The ordering factor
+            // is computed based on the distance from each column's current display width to its target width.  Because each column
+            // could have different star ratios, though, this distance is then adjusted according to its star value.  A column with
+            // a larger star value, for example, will change size more rapidly than a column with a lower star value.
+            List<KeyValuePair<DataGridColumn, double>> starColumnPairs = new List<KeyValuePair<DataGridColumn, double>>();
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(
+                c => c.Width.IsStar && c.DisplayIndex >= displayIndex && c.IsVisible && c.Width.Value > 0 && (c.ActualCanUserResize || !userInitiated)))
+            {
+                int insertIndex = 0;
+                double distanceToTarget = Math.Min(column.ActualMaxWidth, Math.Max(targetWidth(column), column.ActualMinWidth)) - column.Width.DisplayValue;
+                double factor = (increase ? Math.Max(0, distanceToTarget) : Math.Min(0, distanceToTarget)) / column.Width.Value;
+                foreach (KeyValuePair<DataGridColumn, double> starColumnPair in starColumnPairs)
+                {
+                    if (increase ? factor <= starColumnPair.Value : factor >= starColumnPair.Value)
+                    {
+                        break;
+                    }
+                    insertIndex++;
+                }
+                starColumnPairs.Insert(insertIndex, new KeyValuePair<DataGridColumn, double>(column, factor));
+                totalStarWeights += column.Width.Value;
+                totalStarColumnsWidth += column.Width.DisplayValue;
+            }
+
+            // Adjust the column widths one at a time until they either hit their individual target width
+            // or the total remaining amount to adjust has been depleted.
+            foreach (KeyValuePair<DataGridColumn, double> starColumnPair in starColumnPairs)
+            {
+                double distanceToTarget = starColumnPair.Value * starColumnPair.Key.Width.Value;
+                double distanceAvailable = (starColumnPair.Key.Width.Value * remainingAdjustment) / totalStarWeights;
+                double adjustment = increase ? Math.Min(distanceToTarget, distanceAvailable) : Math.Max(distanceToTarget, distanceAvailable);
+
+                remainingAdjustment -= adjustment;
+                totalStarWeights -= starColumnPair.Key.Width.Value;
+                starColumnPair.Key.SetWidthDisplayValue(Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, starColumnPair.Key.Width.DisplayValue + adjustment));
+            }
+
+            return remainingAdjustment;
+        }
+
+        private bool ComputeDisplayedColumns()
+        {
+            bool invalidate = false;
+            int numVisibleScrollingCols = 0;
+            int visibleScrollingColumnsTmp = 0;
+            double displayWidth = CellsWidth;
+            double cx = 0;
+            int firstDisplayedFrozenCol = -1;
+            int firstDisplayedScrollingCol = DisplayData.FirstDisplayedScrollingCol;
+
+            // the same problem with negative numbers:
+            // if the width passed in is negative, then return 0
+            if (displayWidth <= 0 || ColumnsInternal.VisibleColumnCount == 0)
+            {
+                DisplayData.FirstDisplayedScrollingCol = -1;
+                DisplayData.LastTotallyDisplayedScrollingCol = -1;
+                return invalidate;
+            }
+
+            foreach (DataGridColumn dataGridColumn in ColumnsInternal.GetVisibleFrozenColumns())
+            {
+                if (firstDisplayedFrozenCol == -1)
+                {
+                    firstDisplayedFrozenCol = dataGridColumn.Index;
+                }
+                cx += GetEdgedColumnWidth(dataGridColumn);
+                if (cx >= displayWidth)
+                {
+                    break;
+                }
+            }
+
+            Debug.Assert(cx <= ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth());
+
+            if (cx < displayWidth && firstDisplayedScrollingCol >= 0)
+            {
+                DataGridColumn dataGridColumn = ColumnsItemsInternal[firstDisplayedScrollingCol];
+                if (dataGridColumn.IsFrozen)
+                {
+                    dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn;
+                    _negHorizontalOffset = 0;
+                    if (dataGridColumn == null)
+                    {
+                        DisplayData.FirstDisplayedScrollingCol = DisplayData.LastTotallyDisplayedScrollingCol = -1;
+                        return invalidate;
+                    }
+                    else
+                    {
+                        firstDisplayedScrollingCol = dataGridColumn.Index;
+                    }
+                }
+
+                cx -= _negHorizontalOffset;
+                while (cx < displayWidth && dataGridColumn != null)
+                {
+                    cx += GetEdgedColumnWidth(dataGridColumn);
+                    visibleScrollingColumnsTmp++;
+                    dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn);
+                }
+                numVisibleScrollingCols = visibleScrollingColumnsTmp;
+
+                // if we inflate the data area then we paint columns to the left of firstDisplayedScrollingCol
+                if (cx < displayWidth)
+                {
+                    Debug.Assert(firstDisplayedScrollingCol >= 0);
+                    //first minimize value of _negHorizontalOffset
+                    if (_negHorizontalOffset > 0)
+                    {
+                        invalidate = true;
+                        if (displayWidth - cx > _negHorizontalOffset)
+                        {
+                            cx += _negHorizontalOffset;
+                            _horizontalOffset -= _negHorizontalOffset;
+                            _negHorizontalOffset = 0;
+                        }
+                        else
+                        {
+                            _horizontalOffset -= displayWidth - cx;
+                            _negHorizontalOffset -= displayWidth - cx;
+                            cx = displayWidth;
+                        }
+                    }
+                    // second try to scroll entire columns
+                    if (cx < displayWidth && _horizontalOffset > 0)
+                    {
+                        Debug.Assert(_negHorizontalOffset == 0);
+                        dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(ColumnsItemsInternal[firstDisplayedScrollingCol]);
+                        while (dataGridColumn != null && cx + GetEdgedColumnWidth(dataGridColumn) <= displayWidth)
+                        {
+                            cx += GetEdgedColumnWidth(dataGridColumn);
+                            visibleScrollingColumnsTmp++;
+                            invalidate = true;
+                            firstDisplayedScrollingCol = dataGridColumn.Index;
+                            _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn);
+                            dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumn);
+                        }
+                    }
+                    // third try to partially scroll in first scrolled off column
+                    if (cx < displayWidth && _horizontalOffset > 0)
+                    {
+                        Debug.Assert(_negHorizontalOffset == 0);
+                        dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(ColumnsItemsInternal[firstDisplayedScrollingCol]);
+                        Debug.Assert(dataGridColumn != null);
+                        Debug.Assert(GetEdgedColumnWidth(dataGridColumn) > displayWidth - cx);
+                        firstDisplayedScrollingCol = dataGridColumn.Index;
+                        _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - displayWidth + cx;
+                        _horizontalOffset -= displayWidth - cx;
+                        visibleScrollingColumnsTmp++;
+                        invalidate = true;
+                        cx = displayWidth;
+                        Debug.Assert(_negHorizontalOffset == GetNegHorizontalOffsetFromHorizontalOffset(_horizontalOffset));
+                    }
+
+                    // update the number of visible columns to the new reality
+                    Debug.Assert(numVisibleScrollingCols <= visibleScrollingColumnsTmp, "the number of displayed columns can only grow");
+                    numVisibleScrollingCols = visibleScrollingColumnsTmp;
+                }
+
+                int jumpFromFirstVisibleScrollingCol = numVisibleScrollingCols - 1;
+                if (cx > displayWidth)
+                {
+                    jumpFromFirstVisibleScrollingCol--;
+                }
+
+                Debug.Assert(jumpFromFirstVisibleScrollingCol >= -1);
+
+                if (jumpFromFirstVisibleScrollingCol < 0)
+                {
+                    DisplayData.LastTotallyDisplayedScrollingCol = -1; // no totally visible scrolling column at all
+                }
+                else
+                {
+                    Debug.Assert(firstDisplayedScrollingCol >= 0);
+                    dataGridColumn = ColumnsItemsInternal[firstDisplayedScrollingCol];
+                    for (int jump = 0; jump < jumpFromFirstVisibleScrollingCol; jump++)
+                    {
+                        dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn);
+                        Debug.Assert(dataGridColumn != null);
+                    }
+                    DisplayData.LastTotallyDisplayedScrollingCol = dataGridColumn.Index;
+                }
+            }
+            else
+            {
+                DisplayData.LastTotallyDisplayedScrollingCol = -1;
+            }
+            DisplayData.FirstDisplayedScrollingCol = firstDisplayedScrollingCol;
+
+            return invalidate;
+        }
+
+        private int ComputeFirstVisibleScrollingColumn()
+        {
+            if (ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth() >= CellsWidth)
+            {
+                // Not enough room for scrolling columns.
+                _negHorizontalOffset = 0;
+                return -1;
+            }
+
+            DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn;
+
+            if (_horizontalOffset == 0)
+            {
+                _negHorizontalOffset = 0;
+                return (dataGridColumn == null) ? -1 : dataGridColumn.Index;
+            }
+
+            double cx = 0;
+            while (dataGridColumn != null)
+            {
+                cx += GetEdgedColumnWidth(dataGridColumn);
+                if (cx > _horizontalOffset)
+                {
+                    break;
+                }
+                dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn);
+            }
+
+            if (dataGridColumn == null)
+            {
+                Debug.Assert(cx <= _horizontalOffset);
+                dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn;
+                if (dataGridColumn == null)
+                {
+                    _negHorizontalOffset = 0;
+                    return -1;
+                }
+                else
+                {
+                    if (_negHorizontalOffset != _horizontalOffset)
+                    {
+                        _negHorizontalOffset = 0;
+                    }
+                    return dataGridColumn.Index;
+                }
+            }
+            else
+            {
+                _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - (cx - _horizontalOffset);
+                return dataGridColumn.Index;
+            }
+        }
+
+        private void CorrectColumnDisplayIndexesAfterDeletion(DataGridColumn deletedColumn)
+        {
+            // Column indexes have already been adjusted.
+            // This column has already been detached and has retained its old Index and DisplayIndex
+
+            Debug.Assert(deletedColumn != null);
+            Debug.Assert(deletedColumn.OwningGrid == null);
+            Debug.Assert(deletedColumn.Index >= 0);
+            Debug.Assert(deletedColumn.DisplayIndexWithFiller >= 0);
+
+            try
+            {
+                InDisplayIndexAdjustments = true;
+
+                // The DisplayIndex of columns greater than the deleted column need to be decremented,
+                // as do the DisplayIndexMap values of modified column Indexes
+                DataGridColumn column;
+                ColumnsInternal.DisplayIndexMap.RemoveAt(deletedColumn.DisplayIndexWithFiller);
+                for (int displayIndex = 0; displayIndex < ColumnsInternal.DisplayIndexMap.Count; displayIndex++)
+                {
+                    if (ColumnsInternal.DisplayIndexMap[displayIndex] > deletedColumn.Index)
+                    {
+                        ColumnsInternal.DisplayIndexMap[displayIndex]--;
+                    }
+                    if (displayIndex >= deletedColumn.DisplayIndexWithFiller)
+                    {
+                        column = ColumnsInternal.GetColumnAtDisplayIndex(displayIndex);
+                        column.DisplayIndexWithFiller = column.DisplayIndexWithFiller - 1;
+                        column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on
+                    }
+                }
+
+                // Now raise all the OnColumnDisplayIndexChanged events
+                FlushDisplayIndexChanged(true /*raiseEvent*/);
+            }
+            finally
+            {
+                InDisplayIndexAdjustments = false;
+                FlushDisplayIndexChanged(false /*raiseEvent*/);
+            }
+        }
+
+        private void CorrectColumnDisplayIndexesAfterInsertion(DataGridColumn insertedColumn)
+        {
+            Debug.Assert(insertedColumn != null);
+            Debug.Assert(insertedColumn.OwningGrid == this);
+            if (insertedColumn.DisplayIndexWithFiller == -1 || insertedColumn.DisplayIndexWithFiller >= ColumnsItemsInternal.Count)
+            {
+                // Developer did not assign a DisplayIndex or picked a large number.
+                // Choose the Index as the DisplayIndex.
+                insertedColumn.DisplayIndexWithFiller = insertedColumn.Index;
+            }
+
+            try
+            {
+                InDisplayIndexAdjustments = true;
+
+                // The DisplayIndex of columns greater than the inserted column need to be incremented,
+                // as do the DisplayIndexMap values of modified column Indexes
+                DataGridColumn column;
+                for (int displayIndex = 0; displayIndex < ColumnsInternal.DisplayIndexMap.Count; displayIndex++)
+                {
+                    if (ColumnsInternal.DisplayIndexMap[displayIndex] >= insertedColumn.Index)
+                    {
+                        ColumnsInternal.DisplayIndexMap[displayIndex]++;
+                    }
+                    if (displayIndex >= insertedColumn.DisplayIndexWithFiller)
+                    {
+                        column = ColumnsInternal.GetColumnAtDisplayIndex(displayIndex);
+                        column.DisplayIndexWithFiller++;
+                        column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on
+                    }
+                }
+                ColumnsInternal.DisplayIndexMap.Insert(insertedColumn.DisplayIndexWithFiller, insertedColumn.Index);
+
+                // Now raise all the OnColumnDisplayIndexChanged events
+                FlushDisplayIndexChanged(true /*raiseEvent*/);
+            }
+            finally
+            {
+                InDisplayIndexAdjustments = false;
+                FlushDisplayIndexChanged(false /*raiseEvent*/);
+            }
+        }
+
+        private void CorrectColumnFrozenStates()
+        {
+            int index = 0;
+            double frozenColumnWidth = 0;
+            double oldFrozenColumnWidth = 0;
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
+            {
+                if (column.IsFrozen)
+                {
+                    oldFrozenColumnWidth += column.ActualWidth;
+                }
+                column.IsFrozen = index < FrozenColumnCountWithFiller;
+                if (column.IsFrozen)
+                {
+                    frozenColumnWidth += column.ActualWidth;
+                }
+                index++;
+            }
+            if (HorizontalOffset > Math.Max(0, frozenColumnWidth - oldFrozenColumnWidth))
+            {
+                UpdateHorizontalOffset(HorizontalOffset - frozenColumnWidth + oldFrozenColumnWidth);
+            }
+            else
+            {
+                UpdateHorizontalOffset(0);
+            }
+        }
+
+        private void CorrectColumnIndexesAfterDeletion(DataGridColumn deletedColumn)
+        {
+            Debug.Assert(deletedColumn != null);
+            for (int columnIndex = deletedColumn.Index; columnIndex < ColumnsItemsInternal.Count; columnIndex++)
+            {
+                ColumnsItemsInternal[columnIndex].Index = ColumnsItemsInternal[columnIndex].Index - 1;
+                Debug.Assert(ColumnsItemsInternal[columnIndex].Index == columnIndex);
+            }
+        }
+
+        private void CorrectColumnIndexesAfterInsertion(DataGridColumn insertedColumn, int insertionCount)
+        {
+            Debug.Assert(insertedColumn != null);
+            Debug.Assert(insertionCount > 0);
+            for (int columnIndex = insertedColumn.Index + insertionCount; columnIndex < ColumnsItemsInternal.Count; columnIndex++)
+            {
+                ColumnsItemsInternal[columnIndex].Index = columnIndex;
+            }
+        }
+
+        /// <summary>
+        /// Decreases the width of a non-star column by the given amount, if possible.  If the total desired
+        /// adjustment amount could not be met, the remaining amount of adjustment is returned.  The adjustment
+        /// stops when the column's target width has been met.
+        /// </summary>
+        /// <param name="column">Column to adjust.</param>
+        /// <param name="targetWidth">The target width of the column (in pixels).</param>
+        /// <param name="amount">Amount to decrease (in pixels).</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private static double DecreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount)
+        {
+            Debug.Assert(amount < 0);
+            Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star);
+
+            if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
+            {
+                return amount;
+            }
+
+            double adjustment = Math.Max(
+                column.ActualMinWidth - column.Width.DisplayValue,
+                Math.Max(targetWidth - column.Width.DisplayValue, amount));
+
+            column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment);
+            return amount - adjustment;
+        }
+
+        /// <summary>
+        /// Decreases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total
+        /// width is decreased by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.  The adjustment stops when
+        /// the column's target width has been met.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="targetWidth">The target width of the column (in pixels).</param>
+        /// <param name="amount">Amount to decrease (in pixels).</param>
+        /// <param name="reverse">Whether or not to reverse the order in which the columns are traversed.</param>
+        /// <param name="affectNewColumns">Whether or not to adjust widths of columns that do not yet have their initial desired width.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private double DecreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
+        {
+            if (DoubleUtil.GreaterThanOrClose(amount, 0))
+            {
+                return amount;
+            }
+
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(reverse,
+                column =>
+                    column.IsVisible &&
+                    column.Width.UnitType != DataGridLengthUnitType.Star &&
+                    column.DisplayIndex >= displayIndex &&
+                    column.ActualCanUserResize &&
+                    (affectNewColumns || column.IsInitialDesiredWidthDetermined)))
+            {
+                amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount);
+                if (DoubleUtil.IsZero(amount))
+                {
+                    break;
+                }
+            }
+            return amount;
+        }
+
+        private void FlushDisplayIndexChanged(bool raiseEvent)
+        {
+            foreach (DataGridColumn column in ColumnsItemsInternal)
+            {
+                if (column.DisplayIndexHasChanged)
+                {
+                    column.DisplayIndexHasChanged = false;
+                    if (raiseEvent)
+                    {
+                        Debug.Assert(column != ColumnsInternal.RowGroupSpacerColumn);
+                        OnColumnDisplayIndexChanged(column);
+                    }
+                }
+            }
+        }
+
+        private bool GetColumnEffectiveReadOnlyState(DataGridColumn dataGridColumn)
+        {
+            Debug.Assert(dataGridColumn != null);
+
+            return IsReadOnly || dataGridColumn.IsReadOnly || dataGridColumn is DataGridFillerColumn;
+        }
+
+        /// <devdoc>
+        ///      Returns the absolute coordinate of the left edge of the given column (including
+        ///      the potential gridline - that is the left edge of the gridline is returned). Note that
+        ///      the column does not need to be in the display area.
+        /// </devdoc>
+        private double GetColumnXFromIndex(int index)
+        {
+            Debug.Assert(index < ColumnsItemsInternal.Count);
+            Debug.Assert(ColumnsItemsInternal[index].IsVisible);
+
+            double x = 0;
+            foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
+            {
+                if (index == column.Index)
+                {
+                    break;
+                }
+                x += GetEdgedColumnWidth(column);
+            }
+            return x;
+        }
+
+        private double GetNegHorizontalOffsetFromHorizontalOffset(double horizontalOffset)
+        {
+            foreach (DataGridColumn column in ColumnsInternal.GetVisibleScrollingColumns())
+            {
+                if (GetEdgedColumnWidth(column) > horizontalOffset)
+                {
+                    break;
+                }
+                horizontalOffset -= GetEdgedColumnWidth(column);
+            }
+            return horizontalOffset;
+        }
+
+        /// <summary>
+        /// Increases the width of a non-star column by the given amount, if possible.  If the total desired
+        /// adjustment amount could not be met, the remaining amount of adjustment is returned.  The adjustment
+        /// stops when the column's target width has been met.
+        /// </summary>
+        /// <param name="column">Column to adjust.</param>
+        /// <param name="targetWidth">The target width of the column (in pixels).</param>
+        /// <param name="amount">Amount to increase (in pixels).</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private static double IncreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount)
+        {
+            Debug.Assert(amount > 0);
+            Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star);
+
+            if (targetWidth <= column.Width.DisplayValue)
+            {
+                return amount;
+            }
+
+            double adjustment = Math.Min(
+                column.ActualMaxWidth - column.Width.DisplayValue,
+                Math.Min(targetWidth - column.Width.DisplayValue, amount));
+
+            column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment);
+            return amount - adjustment;
+        }
+
+        /// <summary>
+        /// Increases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total
+        /// width is increased by the given amount, if possible.  If the total desired adjustment amount
+        /// could not be met, the remaining amount of adjustment is returned.  The adjustment stops when
+        /// the column's target width has been met.
+        /// </summary>
+        /// <param name="displayIndex">Starting column DisplayIndex.</param>
+        /// <param name="targetWidth">The target width of the column (in pixels).</param>
+        /// <param name="amount">Amount to increase (in pixels).</param>
+        /// <param name="reverse">Whether or not to reverse the order in which the columns are traversed.</param>
+        /// <param name="affectNewColumns">Whether or not to adjust widths of columns that do not yet have their initial desired width.</param>
+        /// <returns>The remaining amount of adjustment.</returns>
+        private double IncreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
+        {
+            if (DoubleUtil.LessThanOrClose(amount, 0))
+            {
+                return amount;
+            }
+
+            foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(reverse,
+                column =>
+                    column.IsVisible &&
+                    column.Width.UnitType != DataGridLengthUnitType.Star &&
+                    column.DisplayIndex >= displayIndex &&
+                    column.ActualCanUserResize &&
+                    (affectNewColumns || column.IsInitialDesiredWidthDetermined)))
+            {
+                amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount);
+                if (DoubleUtil.IsZero(amount))
+                {
+                    break;
+                }
+            }
+            return amount;
+        }
+
+        private void InsertDisplayedColumnHeader(DataGridColumn dataGridColumn)
+        {
+            Debug.Assert(dataGridColumn != null);
+            if (_columnHeadersPresenter != null)
+            {
+                dataGridColumn.HeaderCell.IsVisible = dataGridColumn.IsVisible;
+                Debug.Assert(!_columnHeadersPresenter.Children.Contains(dataGridColumn.HeaderCell));
+                _columnHeadersPresenter.Children.Insert(dataGridColumn.DisplayIndexWithFiller, dataGridColumn.HeaderCell);
+            }
+        }
+
+        private static void RefreshCellElement(DataGridColumn dataGridColumn, DataGridRow dataGridRow, string propertyName)
+        {
+            Debug.Assert(dataGridColumn != null);
+            Debug.Assert(dataGridRow != null);
+
+            DataGridCell dataGridCell = dataGridRow.Cells[dataGridColumn.Index];
+            Debug.Assert(dataGridCell != null);
+            if (dataGridCell.Content is IControl element)
+            {
+                dataGridColumn.RefreshCellContent(element, propertyName);
+            }
+        }
+
+        private void RemoveAutoGeneratedColumns()
+        {
+            int index = 0;
+            _autoGeneratingColumnOperationCount++;
+            try
+            {
+                while (index < ColumnsInternal.Count)
+                {
+                    // Skip over the user columns
+                    while (index < ColumnsInternal.Count && !ColumnsInternal[index].IsAutoGenerated)
+                    {
+                        index++;
+                    }
+                    // Remove the autogenerated columns
+                    while (index < ColumnsInternal.Count && ColumnsInternal[index].IsAutoGenerated)
+                    {
+                        ColumnsInternal.RemoveAt(index);
+                    }
+                }
+                ColumnsInternal.AutogeneratedColumnCount = 0;
+            }
+            finally
+            {
+                _autoGeneratingColumnOperationCount--;
+            }
+        }
+
+        private bool ScrollColumnIntoView(int columnIndex)
+        {
+            Debug.Assert(columnIndex >= 0 && columnIndex < ColumnsItemsInternal.Count);
+
+            if (DisplayData.FirstDisplayedScrollingCol != -1 &&
+                !ColumnsItemsInternal[columnIndex].IsFrozen &&
+                (columnIndex != DisplayData.FirstDisplayedScrollingCol || _negHorizontalOffset > 0))
+            {
+                int columnsToScroll;
+                if (ColumnsInternal.DisplayInOrder(columnIndex, DisplayData.FirstDisplayedScrollingCol))
+                {
+                    columnsToScroll = ColumnsInternal.GetColumnCount(true /* isVisible */, false /* isFrozen */, columnIndex, DisplayData.FirstDisplayedScrollingCol);
+                    if (_negHorizontalOffset > 0)
+                    {
+                        columnsToScroll++;
+                    }
+                    ScrollColumns(-columnsToScroll);
+                }
+                else if (columnIndex == DisplayData.FirstDisplayedScrollingCol && _negHorizontalOffset > 0)
+                {
+                    ScrollColumns(-1);
+                }
+                else if (DisplayData.LastTotallyDisplayedScrollingCol == -1 ||
+                         (DisplayData.LastTotallyDisplayedScrollingCol != columnIndex &&
+                          ColumnsInternal.DisplayInOrder(DisplayData.LastTotallyDisplayedScrollingCol, columnIndex)))
+                {
+                    double xColumnLeftEdge = GetColumnXFromIndex(columnIndex);
+                    double xColumnRightEdge = xColumnLeftEdge + GetEdgedColumnWidth(ColumnsItemsInternal[columnIndex]);
+                    double change = xColumnRightEdge - HorizontalOffset - CellsWidth;
+                    double widthRemaining = change;
+
+                    DataGridColumn newFirstDisplayedScrollingCol = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol];
+                    DataGridColumn nextColumn = ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol);
+                    double newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol) - _negHorizontalOffset;
+                    while (nextColumn != null && widthRemaining >= newColumnWidth)
+                    {
+                        widthRemaining -= newColumnWidth;
+                        newFirstDisplayedScrollingCol = nextColumn;
+                        newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol);
+                        nextColumn = ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol);
+                        _negHorizontalOffset = 0;
+                    }
+                    _negHorizontalOffset += widthRemaining;
+                    DisplayData.LastTotallyDisplayedScrollingCol = columnIndex;
+                    if (newFirstDisplayedScrollingCol.Index == columnIndex)
+                    {
+                        _negHorizontalOffset = 0;
+                        double frozenColumnWidth = ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
+                        // If the entire column cannot be displayed, we want to start showing it from its LeftEdge
+                        if (newColumnWidth > (CellsWidth - frozenColumnWidth))
+                        {
+                            DisplayData.LastTotallyDisplayedScrollingCol = -1;
+                            change = xColumnLeftEdge - HorizontalOffset - frozenColumnWidth;
+                        }
+                    }
+                    DisplayData.FirstDisplayedScrollingCol = newFirstDisplayedScrollingCol.Index;
+
+                    // At this point DisplayData.FirstDisplayedScrollingColumn and LastDisplayedScrollingColumn 
+                    // should be correct
+                    if (change != 0)
+                    {
+                        UpdateHorizontalOffset(HorizontalOffset + change);
+                    }
+                }
+            }
+            return true;
+        }
+
+        private void ScrollColumns(int columns)
+        {
+            DataGridColumn newFirstVisibleScrollingCol = null;
+            DataGridColumn dataGridColumnTmp;
+            int colCount = 0;
+            if (columns > 0)
+            {
+                if (DisplayData.LastTotallyDisplayedScrollingCol >= 0)
+                {
+                    dataGridColumnTmp = ColumnsItemsInternal[DisplayData.LastTotallyDisplayedScrollingCol];
+                    while (colCount < columns && dataGridColumnTmp != null)
+                    {
+                        dataGridColumnTmp = ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp);
+                        colCount++;
+                    }
+
+                    if (dataGridColumnTmp == null)
+                    {
+                        // no more column to display on the right of the last totally seen column
+                        return;
+                    }
+                }
+                Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= 0);
+                dataGridColumnTmp = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol];
+                colCount = 0;
+                while (colCount < columns && dataGridColumnTmp != null)
+                {
+                    dataGridColumnTmp = ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp);
+                    colCount++;
+                }
+                newFirstVisibleScrollingCol = dataGridColumnTmp;
+            }
+
+            if (columns < 0)
+            {
+                Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= 0);
+                dataGridColumnTmp = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol];
+                if (_negHorizontalOffset > 0)
+                {
+                    colCount++;
+                }
+                while (colCount < -columns && dataGridColumnTmp != null)
+                {
+                    dataGridColumnTmp = ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumnTmp);
+                    colCount++;
+                }
+                newFirstVisibleScrollingCol = dataGridColumnTmp;
+                if (newFirstVisibleScrollingCol == null)
+                {
+                    if (_negHorizontalOffset == 0)
+                    {
+                        // no more column to display on the left of the first seen column
+                        return;
+                    }
+                    else
+                    {
+                        newFirstVisibleScrollingCol = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol];
+                    }
+                }
+            }
+
+            double newColOffset = 0;
+            foreach (DataGridColumn dataGridColumn in ColumnsInternal.GetVisibleScrollingColumns())
+            {
+                if (dataGridColumn == newFirstVisibleScrollingCol)
+                {
+                    break;
+                }
+                newColOffset += GetEdgedColumnWidth(dataGridColumn);
+            }
+
+            UpdateHorizontalOffset(newColOffset);
+        }
+
+        private void UpdateDisplayedColumns()
+        {
+            DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
+            ComputeDisplayedColumns();
+        }
+
+        private static DataGridBoundColumn GetDataGridColumnFromType(Type type)
+        {
+            Debug.Assert(type != null);
+            if (type == typeof(bool))
+            {
+                return new DataGridCheckBoxColumn();
+            }
+            else if (type == typeof(bool?))
+            {
+                return new DataGridCheckBoxColumn
+                {
+                    IsThreeState = true
+                };
+            }
+            return new DataGridTextColumn();
+        }
+
+        private void AutoGenerateColumnsPrivate()
+        {
+            if (!_measured || (_autoGeneratingColumnOperationCount > 0))
+            {
+                // Reading the DataType when we generate columns could cause the CollectionView to 
+                // raise a Reset if its Enumeration changed.  In that case, we don't want to generate again.
+                return;
+            }
+
+            _autoGeneratingColumnOperationCount++;
+            try
+            {
+                // Always remove existing autogenerated columns before generating new ones
+                RemoveAutoGeneratedColumns();
+                GenerateColumnsFromProperties();
+                EnsureRowsPresenterVisibility();
+                InvalidateRowHeightEstimate();
+            }
+            finally
+            {
+                _autoGeneratingColumnOperationCount--;
+            }
+        }
+
+        private void GenerateColumnsFromProperties()
+        {
+            // Autogenerated Columns are added at the end so the user columns appear first
+            if (DataConnection.DataProperties != null && DataConnection.DataProperties.Length > 0)
+            {
+                List<KeyValuePair<int, DataGridAutoGeneratingColumnEventArgs>> columnOrderPairs = new List<KeyValuePair<int, DataGridAutoGeneratingColumnEventArgs>>();
+
+                // Generate the columns
+                foreach (PropertyInfo propertyInfo in DataConnection.DataProperties)
+                {
+                    string columnHeader = propertyInfo.Name;
+                    int columnOrder = DATAGRID_defaultColumnDisplayOrder;
+
+                    // Check if DisplayAttribute is defined on the property
+                    object[] attributes = propertyInfo.GetCustomAttributes(typeof(DisplayAttribute), true);
+                    if (attributes != null && attributes.Length > 0)
+                    {
+                        DisplayAttribute displayAttribute = attributes[0] as DisplayAttribute;
+                        Debug.Assert(displayAttribute != null);
+
+                        bool? autoGenerateField = displayAttribute.GetAutoGenerateField();
+                        if (autoGenerateField.HasValue && autoGenerateField.Value == false)
+                        {
+                            // Abort column generation because we aren't supposed to auto-generate this field
+                            continue;
+                        }
+
+                        string header = displayAttribute.GetShortName();
+                        if (header != null)
+                        {
+                            columnHeader = header;
+                        }
+
+                        int? order = displayAttribute.GetOrder();
+                        if (order.HasValue)
+                        {
+                            columnOrder = order.Value;
+                        }
+                    }
+
+                    // Generate a single column and determine its relative order
+                    int insertIndex = 0;
+                    if (columnOrder == int.MaxValue)
+                    {
+                        insertIndex = columnOrderPairs.Count;
+                    }
+                    else
+                    {
+                        foreach (KeyValuePair<int, DataGridAutoGeneratingColumnEventArgs> columnOrderPair in columnOrderPairs)
+                        {
+                            if (columnOrderPair.Key > columnOrder)
+                            {
+                                break;
+                            }
+                            insertIndex++;
+                        }
+                    }
+                    DataGridAutoGeneratingColumnEventArgs columnArgs = GenerateColumn(propertyInfo.PropertyType, propertyInfo.Name, columnHeader);
+                    columnOrderPairs.Insert(insertIndex, new KeyValuePair<int, DataGridAutoGeneratingColumnEventArgs>(columnOrder, columnArgs));
+                }
+
+                // Add the columns to the DataGrid in the correct order
+                foreach (KeyValuePair<int, DataGridAutoGeneratingColumnEventArgs> columnOrderPair in columnOrderPairs)
+                {
+                    AddGeneratedColumn(columnOrderPair.Value);
+                }
+            }
+            else if (DataConnection.DataIsPrimitive)
+            {
+                AddGeneratedColumn(GenerateColumn(DataConnection.DataType, string.Empty, DataConnection.DataType.Name));
+            }
+        }
+
+        private static DataGridAutoGeneratingColumnEventArgs GenerateColumn(Type propertyType, string propertyName, string header)
+        {
+            // Create a new DataBoundColumn for the Property
+            DataGridBoundColumn newColumn = GetDataGridColumnFromType(propertyType);
+            newColumn.Binding = new Binding(propertyName);
+            newColumn.Header = header;
+            newColumn.IsAutoGenerated = true;
+            return new DataGridAutoGeneratingColumnEventArgs(propertyName, propertyType, newColumn);
+        }
+
+        private bool AddGeneratedColumn(DataGridAutoGeneratingColumnEventArgs e)
+        {
+            // Raise the AutoGeneratingColumn event in case the user wants to Cancel or Replace the
+            // column being generated
+            OnAutoGeneratingColumn(e);
+            if (e.Cancel)
+            {
+                return false;
+            }
+            else
+            {
+                if (e.Column != null)
+                {
+                    // Set the IsAutoGenerated flag here in case the user provides a custom autogenerated column
+                    e.Column.IsAutoGenerated = true;
+                }
+                ColumnsInternal.Add(e.Column);
+                ColumnsInternal.AutogeneratedColumnCount++;
+                return true;
+            }
+        }
+
+    }
+
+}

+ 696 - 0
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@@ -0,0 +1,696 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Collections;
+using System;
+using System.Linq;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Reflection;
+using System.ComponentModel.DataAnnotations;
+using Avalonia.Utilities;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridDataConnection
+    {
+
+        private int _backupSlotForCurrentChanged;
+        private int _columnForCurrentChanged;
+        private PropertyInfo[] _dataProperties;
+        private IEnumerable _dataSource;
+        private Type _dataType;
+        private bool _expectingCurrentChanged;
+        private object _itemToSelectOnCurrentChanged;
+        private DataGrid _owner;
+        private bool _scrollForCurrentChanged;
+        private DataGridSelectionAction _selectionActionForCurrentChanged;
+
+        public DataGridDataConnection(DataGrid owner)
+        {
+            _owner = owner;
+        }
+
+        public bool AllowEdit
+        {
+            get
+            {
+                if (List == null)
+                {
+                    return true;
+                }
+                else
+                {
+                    return !List.IsReadOnly;
+                }
+            }
+        }
+
+        /// <summary>
+        /// True if the collection view says it can sort.
+        /// </summary>
+        public bool AllowSort
+        {
+            get
+            {
+                if (CollectionView == null ||
+                    (EditableCollectionView != null && (EditableCollectionView.IsAddingNew || EditableCollectionView.IsEditingItem)))
+                {
+                    return false;
+                }
+                else
+                {
+                    return CollectionView.CanSort;
+                }
+            }
+        }
+
+        public bool CommittingEdit
+        {
+            get;
+            private set;
+        }
+
+        public int Count
+        {
+            get
+            {
+                IList list = List;
+                if (list != null)
+                {
+                    return list.Count;
+                }
+
+                if(DataSource is DataGridCollectionView cv)
+                {
+                    return cv.Count;
+                }
+
+                return DataSource?.Cast<object>().Count() ?? 0;
+            }
+        }
+
+        public bool DataIsPrimitive
+        {
+            get
+            {
+                return DataTypeIsPrimitive(DataType);
+            }
+        }
+
+        public PropertyInfo[] DataProperties
+        {
+            get
+            {
+                if (_dataProperties == null)
+                {
+                    UpdateDataProperties();
+                }
+                return _dataProperties;
+            }
+        }
+
+        public IEnumerable DataSource
+        {
+            get
+            {
+                return _dataSource;
+            }
+            set
+            {
+                _dataSource = value;
+                // Because the DataSource is changing, we need to reset our cached values for DataType and DataProperties,
+                // which are dependent on the current DataSource
+                _dataType = null;
+                UpdateDataProperties();
+            }
+        }
+
+        public Type DataType
+        {
+            get
+            {
+                // We need to use the raw ItemsSource as opposed to DataSource because DataSource
+                // may be the ItemsSource wrapped in a collection view, in which case we wouldn't
+                // be able to take T to be the type if we're given IEnumerable<T>
+                if (_dataType == null && _owner.Items != null)
+                {
+                    _dataType = _owner.Items.GetItemType();
+                }
+                return _dataType;
+            }
+        }
+
+        public bool EventsWired
+        {
+            get;
+            private set;
+        }
+
+        private bool IsGrouping
+        {
+            get
+            {
+                return (CollectionView != null)
+                    && CollectionView.CanGroup
+                    && CollectionView.IsGrouping
+                    && (CollectionView.GroupingDepth > 0);
+            }
+        }
+
+        public IList List
+        {
+            get
+            {
+                return DataSource as IList;
+            }
+        }
+
+        public bool ShouldAutoGenerateColumns
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public IDataGridCollectionView CollectionView
+        {
+            get
+            {
+                return DataSource as IDataGridCollectionView;
+            }
+        }
+        public IDataGridEditableCollectionView EditableCollectionView
+        {
+            get
+            {
+                return DataSource as IDataGridEditableCollectionView;
+            }
+        }
+
+        public DataGridSortDescriptionCollection SortDescriptions
+        {
+            get
+            {
+                if (CollectionView != null && CollectionView.CanSort)
+                {
+                    return CollectionView.SortDescriptions;
+                }
+                else
+                {
+                    return null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Puts the entity into editing mode if possible
+        /// </summary>
+        /// <param name="dataItem">The entity to edit</param>
+        /// <returns>True if editing was started</returns>
+        public bool BeginEdit(object dataItem)
+        {
+            if (dataItem == null)
+            {
+                return false;
+            }
+
+            IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
+            if (editableCollectionView != null)
+            {
+                if (editableCollectionView.IsEditingItem && (dataItem == editableCollectionView.CurrentEditItem))
+                {
+                    return true;
+                }
+                else
+                {
+                    editableCollectionView.EditItem(dataItem);
+                    return editableCollectionView.IsEditingItem;
+                }
+            }
+
+            if (dataItem is IEditableObject editableDataItem)
+            {
+                editableDataItem.BeginEdit();
+                return true;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Cancels the current entity editing and exits the editing mode.
+        /// </summary>
+        /// <param name="dataItem">The entity being edited</param>
+        /// <returns>True if a cancellation operation was invoked.</returns>
+        public bool CancelEdit(object dataItem)
+        {
+            IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
+            if (editableCollectionView != null)
+            {
+                if (editableCollectionView.CanCancelEdit)
+                {
+                    editableCollectionView.CancelEdit();
+                    return true;
+                }
+                return false;
+            }
+
+            if (dataItem is IEditableObject editableDataItem)
+            {
+                editableDataItem.CancelEdit();
+                return true;
+            }
+
+            return true;
+        }
+
+        public static bool CanEdit(Type type)
+        {
+            Debug.Assert(type != null);
+
+            type = type.GetNonNullableType();
+
+            return
+                type.IsEnum
+                || type == typeof(System.String)
+                || type == typeof(System.Char)
+                || type == typeof(System.DateTime)
+                || type == typeof(System.Boolean)
+                || type == typeof(System.Byte)
+                || type == typeof(System.SByte)
+                || type == typeof(System.Single)
+                || type == typeof(System.Double)
+                || type == typeof(System.Decimal)
+                || type == typeof(System.Int16)
+                || type == typeof(System.Int32)
+                || type == typeof(System.Int64)
+                || type == typeof(System.UInt16)
+                || type == typeof(System.UInt32)
+                || type == typeof(System.UInt64);
+        }
+
+        /// <summary>
+        /// Commits the current entity editing and exits the editing mode.
+        /// </summary>
+        /// <param name="dataItem">The entity being edited</param>
+        /// <returns>True if a commit operation was invoked.</returns>
+        public bool EndEdit(object dataItem)
+        {
+            IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
+            if (editableCollectionView != null)
+            {
+                // IEditableCollectionView.CommitEdit can potentially change currency. If it does,
+                // we don't want to attempt a second commit inside our CurrentChanging event handler.
+                _owner.NoCurrentCellChangeCount++;
+                CommittingEdit = true;
+                try
+                {
+                    editableCollectionView.CommitEdit();
+                }
+                finally
+                {
+                    _owner.NoCurrentCellChangeCount--;
+                    CommittingEdit = false;
+                }
+                return true;
+            }
+
+            if (dataItem is IEditableObject editableDataItem)
+            {
+                editableDataItem.EndEdit();
+            }
+
+            return true;
+        }
+
+        // Assumes index >= 0, returns null if index >= Count
+        public object GetDataItem(int index)
+        {
+            Debug.Assert(index >= 0);
+
+            IList list = List;
+            if (list != null)
+            {
+                return (index < list.Count) ? list[index] : null;
+            }
+
+            if (DataSource is DataGridCollectionView collectionView)
+            {
+                return (index < collectionView.Count) ? collectionView.GetItemAt(index) : null;
+            }
+
+            IEnumerable enumerable = DataSource;
+            if (enumerable != null)
+            {
+                IEnumerator enumerator = enumerable.GetEnumerator();
+                int i = -1;
+                while (enumerator.MoveNext() && i < index)
+                {
+                    i++;
+                    if (i == index)
+                    {
+                        return enumerator.Current;
+                    }
+                }
+            }
+            return null;
+        }
+
+        public bool GetPropertyIsReadOnly(string propertyName)
+        {
+            if (DataType != null)
+            {
+                if (!String.IsNullOrEmpty(propertyName))
+                {
+                    Type propertyType = DataType;
+                    PropertyInfo propertyInfo = null;
+                    List<string> propertyNames = TypeHelper.SplitPropertyPath(propertyName);
+                    for (int i = 0; i < propertyNames.Count; i++)
+                    {
+                        propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out object[] index);
+                        if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly())
+                        {
+                            // Either the data type is read-only, the property doesn't exist, or it does exist but is read-only
+                            return true;
+                        }
+
+                        // Check if EditableAttribute is defined on the property and if it indicates uneditable
+                        object[] attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true);
+                        if (attributes != null && attributes.Length > 0)
+                        {
+                            EditableAttribute editableAttribute = attributes[0] as EditableAttribute;
+                            Debug.Assert(editableAttribute != null);
+                            if (!editableAttribute.AllowEdit)
+                            {
+                                return true;
+                            }
+                        }
+                        propertyType = propertyInfo.PropertyType.GetNonNullableType();
+                    }
+                    return propertyInfo == null || !propertyInfo.CanWrite || !AllowEdit || !CanEdit(propertyType);
+                }
+                else if (DataType.GetIsReadOnly())
+                {
+                    return true;
+                }
+            }
+            return !AllowEdit;
+        }
+
+        public int IndexOf(object dataItem)
+        {
+            IList list = List;
+            if (list != null)
+            {
+                return list.IndexOf(dataItem);
+            }
+
+            if (DataSource is DataGridCollectionView cv)
+            {
+                return cv.IndexOf(dataItem);
+            }
+
+            IEnumerable enumerable = DataSource;
+            if (enumerable != null && dataItem != null)
+            {
+                int index = 0;
+                foreach (object dataItemTmp in enumerable)
+                {
+                    if ((dataItem == null && dataItemTmp == null) ||
+                        dataItem.Equals(dataItemTmp))
+                    {
+                        return index;
+                    }
+                    index++;
+                }
+            }
+            return -1;
+        }
+
+        internal void ClearDataProperties()
+        {
+            _dataProperties = null;
+        }
+
+        /// <summary>
+        /// Creates a collection view around the DataGrid's source. ICollectionViewFactory is
+        /// used if the source implements it. Otherwise a PagedCollectionView is returned.
+        /// </summary>
+        /// <param name="source">Enumerable source for which to create a view</param>
+        /// <returns>ICollectionView view over the provided source</returns>
+        internal static IDataGridCollectionView CreateView(IEnumerable source)
+        {
+            Debug.Assert(source != null, "source unexpectedly null");
+            Debug.Assert(!(source is IDataGridCollectionView), "source is an ICollectionView");
+
+            IDataGridCollectionView collectionView = null;
+
+            if (source is IDataGridCollectionViewFactory collectionViewFactory)
+            {
+                // If the source is a collection view factory, give it a chance to produce a custom collection view.
+                collectionView = collectionViewFactory.CreateView();
+                // Intentionally not catching potential exception thrown by ICollectionViewFactory.CreateView().
+            }
+            if (collectionView == null)
+            {
+                // If we still do not have a collection view, default to a PagedCollectionView.
+                collectionView = new DataGridCollectionView(source);
+            }
+            return collectionView;
+        }
+
+        internal static bool DataTypeIsPrimitive(Type dataType)
+        {
+            if (dataType != null)
+            {
+                Type type = TypeHelper.GetNonNullableType(dataType);  // no-opt if dataType isn't nullable
+                return type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(Decimal);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        internal void MoveCurrentTo(object item, int backupSlot, int columnIndex, DataGridSelectionAction action, bool scrollIntoView)
+        {
+            if (CollectionView != null)
+            {
+                _expectingCurrentChanged = true;
+                _columnForCurrentChanged = columnIndex;
+                _itemToSelectOnCurrentChanged = item;
+                _selectionActionForCurrentChanged = action;
+                _scrollForCurrentChanged = scrollIntoView;
+                _backupSlotForCurrentChanged = backupSlot;
+
+                CollectionView.MoveCurrentTo(item is DataGridCollectionViewGroup ? null : item);
+
+                _expectingCurrentChanged = false;
+            }
+        }
+
+        internal void UnWireEvents(IEnumerable value)
+        {
+            if (value is INotifyCollectionChanged notifyingDataSource)
+            {
+                notifyingDataSource.CollectionChanged -= NotifyingDataSource_CollectionChanged;
+            }
+
+            if (SortDescriptions != null)
+            {
+                SortDescriptions.CollectionChanged -= CollectionView_SortDescriptions_CollectionChanged;
+            }
+
+            if (CollectionView != null)
+            {
+                CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
+                CollectionView.CurrentChanging -= CollectionView_CurrentChanging;
+            }
+
+            EventsWired = false;
+        }
+
+        internal void WireEvents(IEnumerable value)
+        {
+            if (value is INotifyCollectionChanged notifyingDataSource)
+            {
+                notifyingDataSource.CollectionChanged += NotifyingDataSource_CollectionChanged;
+            }
+
+            if (SortDescriptions != null)
+            {
+                SortDescriptions.CollectionChanged += CollectionView_SortDescriptions_CollectionChanged;
+            }
+
+            if (CollectionView != null)
+            {
+                CollectionView.CurrentChanged += CollectionView_CurrentChanged;
+                CollectionView.CurrentChanging += CollectionView_CurrentChanging;
+            }
+
+            EventsWired = true;
+        }
+
+        private void CollectionView_CurrentChanged(object sender, EventArgs e)
+        {
+            if (_expectingCurrentChanged)
+            {
+                // Committing Edit could cause our item to move to a group that no longer exists.  In
+                // this case, we need to update the item.
+                if (_itemToSelectOnCurrentChanged is DataGridCollectionViewGroup collectionViewGroup)
+                {
+                    DataGridRowGroupInfo groupInfo = _owner.RowGroupInfoFromCollectionViewGroup(collectionViewGroup);
+                    if (groupInfo == null)
+                    {
+                        // Move to the next slot if the target slot isn't visible                        
+                        if (!_owner.IsSlotVisible(_backupSlotForCurrentChanged))
+                        {
+                            _backupSlotForCurrentChanged = _owner.GetNextVisibleSlot(_backupSlotForCurrentChanged);
+                        }
+                        // Move to the next best slot if we've moved past all the slots.  This could happen if multiple
+                        // groups were removed.
+                        if (_backupSlotForCurrentChanged >= _owner.SlotCount)
+                        {
+                            _backupSlotForCurrentChanged = _owner.GetPreviousVisibleSlot(_owner.SlotCount);
+                        }
+                        // Update the itemToSelect
+                        int newCurrentPosition = -1;
+                        _itemToSelectOnCurrentChanged = _owner.ItemFromSlot(_backupSlotForCurrentChanged, ref newCurrentPosition);
+                    }
+                }
+
+                _owner.ProcessSelectionAndCurrency(
+                    _columnForCurrentChanged,
+                    _itemToSelectOnCurrentChanged,
+                    _backupSlotForCurrentChanged,
+                    _selectionActionForCurrentChanged,
+                    _scrollForCurrentChanged);
+            }
+            else if (CollectionView != null)
+            {
+                _owner.UpdateStateOnCurrentChanged(CollectionView.CurrentItem, CollectionView.CurrentPosition);
+            }
+        }
+
+        private void CollectionView_CurrentChanging(object sender, DataGridCurrentChangingEventArgs e)
+        {
+            if (_owner.NoCurrentCellChangeCount == 0 &&
+                !_expectingCurrentChanged &&
+                !CommittingEdit &&
+                !_owner.CommitEdit())
+            {
+                // If CommitEdit failed, then the user has most likely input invalid data.
+                // We should cancel the current change if we can, otherwise we have to abort the edit.
+                if (e.IsCancelable)
+                {
+                    e.Cancel = true;
+                }
+                else
+                {
+                    _owner.CancelEdit(DataGridEditingUnit.Row, false);
+                }
+            }
+        }
+
+        private void CollectionView_SortDescriptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (_owner.ColumnsItemsInternal.Count == 0)
+            {
+                return;
+            }
+
+            // refresh sort description
+            foreach (DataGridColumn column in _owner.ColumnsItemsInternal)
+            {
+                column.HeaderCell.ApplyState();
+            }
+        }
+
+        private void NotifyingDataSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (_owner.LoadingOrUnloadingRow)
+            {
+                throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows();
+            }
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+                    Debug.Assert(e.NewItems != null, "Unexpected NotifyCollectionChangedAction.Add notification");
+                    if (ShouldAutoGenerateColumns)
+                    {
+                        // The columns are also affected (not just rows) in this case so we need to reset everything
+                        _owner.InitializeElements(false /*recycleRows*/);
+                    }
+                    else if (!IsGrouping)
+                    {
+                        // If we're grouping then we handle this through the CollectionViewGroup notifications
+                        // According to WPF, Add is a single item operation
+                        Debug.Assert(e.NewItems.Count == 1);
+                        _owner.InsertRowAt(e.NewStartingIndex);
+                    }
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+                    IList removedItems = e.OldItems;
+                    if (removedItems == null || e.OldStartingIndex < 0)
+                    {
+                        Debug.Assert(false, "Unexpected NotifyCollectionChangedAction.Remove notification");
+                        return;
+                    }
+                    if (!IsGrouping)
+                    {
+                        // If we're grouping then we handle this through the CollectionViewGroup notifications
+                        // According to WPF, Remove is a single item operation
+                        foreach (object item in e.OldItems)
+                        {
+                            Debug.Assert(item != null);
+                            _owner.RemoveRowAt(e.OldStartingIndex, item);
+                        }
+                    }
+                    break;
+                case NotifyCollectionChangedAction.Replace:
+                    throw new NotSupportedException(); // 
+
+                case NotifyCollectionChangedAction.Reset:
+                    // Did the data type change during the reset?  If not, we can recycle
+                    // the existing rows instead of having to clear them all.  We still need to clear our cached
+                    // values for DataType and DataProperties, though, because the collection has been reset.
+                    Type previousDataType = _dataType;
+                    _dataType = null;
+                    if (previousDataType != DataType)
+                    {
+                        ClearDataProperties();
+                        _owner.InitializeElements(false /*recycleRows*/);
+                    }
+                    else
+                    {
+                        _owner.InitializeElements(!ShouldAutoGenerateColumns /*recycleRows*/);
+                    }
+                    break;
+            }
+        }
+
+        private void UpdateDataProperties()
+        {
+            Type dataType = DataType;
+
+            if (DataSource != null && dataType != null && !DataTypeIsPrimitive(dataType))
+            {
+                _dataProperties = dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+                Debug.Assert(_dataProperties != null);
+            }
+            else
+            {
+                _dataProperties = null;
+            }
+        }
+
+    }
+}

+ 364 - 0
src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs

@@ -0,0 +1,364 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Media;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridDisplayData
+    {
+        private Stack<DataGridRow> _fullyRecycledRows; // list of Rows that have been fully recycled (Collapsed)
+        private int _headScrollingElements; // index of the row in _scrollingRows that is the first displayed row
+        private DataGrid _owner;
+        private Stack<DataGridRow> _recyclableRows; // list of Rows which have not been fully recycled (avoids Measure in several cases)
+        private List<Control> _scrollingElements; // circular list of displayed elements
+        private Stack<DataGridRowGroupHeader> _fullyRecycledGroupHeaders; // list of GroupHeaders that have been fully recycled (Collapsed)
+        private Stack<DataGridRowGroupHeader> _recyclableGroupHeaders; // list of GroupHeaders which have not been fully recycled (avoids Measure in several cases)
+
+        public DataGridDisplayData(DataGrid owner)
+        {
+            _owner = owner;
+
+            ResetSlotIndexes();
+            FirstDisplayedScrollingCol = -1;
+            LastTotallyDisplayedScrollingCol = -1;
+
+            _scrollingElements = new List<Control>();
+            _recyclableRows = new Stack<DataGridRow>();
+            _fullyRecycledRows = new Stack<DataGridRow>();
+            _recyclableGroupHeaders = new Stack<DataGridRowGroupHeader>();
+            _fullyRecycledGroupHeaders = new Stack<DataGridRowGroupHeader>();
+        }
+
+        public int FirstDisplayedScrollingCol
+        {
+            get;
+            set;
+        }
+
+        public int FirstScrollingSlot
+        {
+            get;
+            set;
+        }
+
+        public int LastScrollingSlot
+        {
+            get;
+            set;
+        }
+
+        public int LastTotallyDisplayedScrollingCol
+        {
+            get;
+            set;
+        }
+
+        public int NumDisplayedScrollingElements
+        {
+            get
+            {
+                return _scrollingElements.Count;
+            }
+        }
+
+        public int NumTotallyDisplayedScrollingElements
+        {
+            get;
+            set;
+        }
+
+        internal double PendingVerticalScrollHeight
+        {
+            get;
+            set;
+        }
+
+        internal void AddRecylableRow(DataGridRow row)
+        {
+            Debug.Assert(!_recyclableRows.Contains(row));
+            row.DetachFromDataGrid(true);
+            _recyclableRows.Push(row);
+        }
+
+        internal DataGridRowGroupHeader GetUsedGroupHeader()
+        {
+            if (_recyclableGroupHeaders.Count > 0)
+            {
+                return _recyclableGroupHeaders.Pop();
+            }
+            else if (_fullyRecycledGroupHeaders.Count > 0)
+            {
+                // For fully recycled rows, we need to set the Visibility back to Visible
+                DataGridRowGroupHeader groupHeader = _fullyRecycledGroupHeaders.Pop();
+                groupHeader.IsVisible = true;
+                return groupHeader;
+            }
+            return null;
+        }
+
+        internal void AddRecylableRowGroupHeader(DataGridRowGroupHeader groupHeader)
+        {
+            Debug.Assert(!_recyclableGroupHeaders.Contains(groupHeader));
+            groupHeader.IsRecycled = true;
+            _recyclableGroupHeaders.Push(groupHeader);
+        }
+
+        internal void ClearElements(bool recycle)
+        {
+            ResetSlotIndexes();
+            if (recycle)
+            {
+                foreach (Control element in _scrollingElements)
+                {
+                    if (element is DataGridRow row)
+                    {
+                        if (row.IsRecyclable)
+                        {
+                            AddRecylableRow(row);
+                        }
+                        else
+                        {
+                            row.Clip = new RectangleGeometry();
+                        }
+                    }
+                    else if (element is DataGridRowGroupHeader groupHeader)
+                    {
+                        AddRecylableRowGroupHeader(groupHeader);
+                    }
+                }
+            }
+            else
+            {
+                _recyclableRows.Clear();
+                _fullyRecycledRows.Clear();
+                _recyclableGroupHeaders.Clear();
+                _fullyRecycledGroupHeaders.Clear();
+            }
+            _scrollingElements.Clear();
+        }
+
+        internal void CorrectSlotsAfterDeletion(int slot, bool wasCollapsed)
+        {
+            if (wasCollapsed)
+            {
+                if (slot > FirstScrollingSlot)
+                {
+                    LastScrollingSlot--;
+                }
+            }
+            else if (_owner.IsSlotVisible(slot))
+            {
+                UnloadScrollingElement(slot, true /*updateSlotInformation*/, true /*wasDeleted*/);
+            }
+            // This cannot be an else condition because if there are 2 rows left, and you delete the first one
+            // then these indexes need to be updated as well
+            if (slot < FirstScrollingSlot)
+            {
+                FirstScrollingSlot--;
+                LastScrollingSlot--;
+            }
+        }
+
+        internal void CorrectSlotsAfterInsertion(int slot, Control element, bool isCollapsed)
+        {
+            if (slot < FirstScrollingSlot)
+            {
+                // The row was inserted above our viewport, just update our indexes
+                FirstScrollingSlot++;
+                LastScrollingSlot++;
+            }
+            else if (isCollapsed && (slot <= LastScrollingSlot))
+            {
+                LastScrollingSlot++;
+            }
+            else if ((_owner.GetPreviousVisibleSlot(slot) <= LastScrollingSlot) || (LastScrollingSlot == -1))
+            {
+                Debug.Assert(element != null);
+                // The row was inserted in our viewport, add it as a scrolling row
+                LoadScrollingSlot(slot, element, true /*updateSlotInformation*/);
+            }
+        }
+
+        private int GetCircularListIndex(int slot, bool wrap)
+        {
+            int index = slot - FirstScrollingSlot - _headScrollingElements - _owner.GetCollapsedSlotCount(FirstScrollingSlot, slot);
+            return wrap ? index % _scrollingElements.Count : index;
+        }
+
+        internal void FullyRecycleElements()
+        {
+            // Fully recycle Recycleable rows and transfer them to Recycled rows
+            while (_recyclableRows.Count > 0)
+            {
+                DataGridRow row = _recyclableRows.Pop();
+                Debug.Assert(row != null);
+                row.IsVisible = false;
+                Debug.Assert(!_fullyRecycledRows.Contains(row));
+                _fullyRecycledRows.Push(row);
+            }
+            // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders
+            while (_recyclableGroupHeaders.Count > 0)
+            {
+                DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop();
+                Debug.Assert(groupHeader != null);
+                groupHeader.IsVisible = false;
+                Debug.Assert(!_fullyRecycledGroupHeaders.Contains(groupHeader));
+                _fullyRecycledGroupHeaders.Push(groupHeader);
+            }
+        }
+
+        internal Control GetDisplayedElement(int slot)
+        {
+            Debug.Assert(slot >= FirstScrollingSlot);
+            Debug.Assert(slot <= LastScrollingSlot);
+
+            return _scrollingElements[GetCircularListIndex(slot, true /*wrap*/)];
+        }
+
+        internal DataGridRow GetDisplayedRow(int rowIndex)
+        {
+
+            return GetDisplayedElement(_owner.SlotFromRowIndex(rowIndex)) as DataGridRow;
+        }
+
+        // Returns an enumeration of the displayed scrolling rows in order starting with the FirstDisplayedScrollingRow
+        internal IEnumerable<Control> GetScrollingElements()
+        {
+            return GetScrollingElements(null);
+        }
+
+        internal IEnumerable<Control> GetScrollingElements(Predicate<object> filter)
+        {
+            for (int i = 0; i < _scrollingElements.Count; i++)
+            {
+                Control element = _scrollingElements[(_headScrollingElements + i) % _scrollingElements.Count];
+                if (filter == null || filter(element))
+                {
+                    // _scrollingRows is a circular list that wraps
+                    yield return element;
+                }
+            }
+        }
+
+        internal IEnumerable<Control> GetScrollingRows()
+        {
+            return GetScrollingElements(element => element is DataGridRow);
+        }
+
+        internal DataGridRow GetUsedRow()
+        {
+            if (_recyclableRows.Count > 0)
+            {
+                return _recyclableRows.Pop();
+            }
+            else if (_fullyRecycledRows.Count > 0)
+            {
+                // For fully recycled rows, we need to set the Visibility back to Visible
+                DataGridRow row = _fullyRecycledRows.Pop();
+                row.IsVisible = true;
+                return row;
+            }
+            return null;
+        }
+
+        // Tracks the row at index rowIndex as a scrolling row
+        internal void LoadScrollingSlot(int slot, Control element, bool updateSlotInformation)
+        {
+            if (_scrollingElements.Count == 0)
+            {
+                SetScrollingSlots(slot);
+                _scrollingElements.Add(element);
+            }
+            else
+            {
+                // The slot should be adjacent to the other slots being displayed
+                Debug.Assert(slot >= _owner.GetPreviousVisibleSlot(FirstScrollingSlot) && slot <= _owner.GetNextVisibleSlot(LastScrollingSlot));
+                if (updateSlotInformation)
+                {
+                    if (slot < FirstScrollingSlot)
+                    {
+                        FirstScrollingSlot = slot;
+                    }
+                    else
+                    {
+                        LastScrollingSlot = _owner.GetNextVisibleSlot(LastScrollingSlot);
+                    }
+                }
+                int insertIndex = GetCircularListIndex(slot, false /*wrap*/);
+                if (insertIndex > _scrollingElements.Count)
+                {
+                    // We need to wrap around from the bottom to the top of our circular list; as a result the head of the list moves forward
+                    insertIndex -= _scrollingElements.Count;
+                    _headScrollingElements++;
+                }
+                _scrollingElements.Insert(insertIndex, element);
+            }
+        }
+
+        private void ResetSlotIndexes()
+        {
+            SetScrollingSlots(-1);
+            NumTotallyDisplayedScrollingElements = 0;
+            _headScrollingElements = 0;
+        }
+
+        private void SetScrollingSlots(int newValue)
+        {
+            FirstScrollingSlot = newValue;
+            LastScrollingSlot = newValue;
+        }
+
+        // Stops tracking the element at the given slot as a scrolling element
+        internal void UnloadScrollingElement(int slot, bool updateSlotInformation, bool wasDeleted)
+        {
+            Debug.Assert(_owner.IsSlotVisible(slot));
+            int elementIndex = GetCircularListIndex(slot, false /*wrap*/);
+            if (elementIndex > _scrollingElements.Count)
+            {
+                // We need to wrap around from the top to the bottom of our circular list
+                elementIndex -= _scrollingElements.Count;
+                _headScrollingElements--;
+            }
+            _scrollingElements.RemoveAt(elementIndex);
+
+            if (updateSlotInformation)
+            {
+                if (slot == FirstScrollingSlot && !wasDeleted)
+                {
+                    FirstScrollingSlot = _owner.GetNextVisibleSlot(FirstScrollingSlot);
+                }
+                else
+                {
+                    LastScrollingSlot = _owner.GetPreviousVisibleSlot(LastScrollingSlot);
+                }
+                if (LastScrollingSlot < FirstScrollingSlot)
+                {
+                    ResetSlotIndexes();
+                }
+            }
+        }
+
+#if DEBUG
+        internal void PrintDisplay()
+        {
+            foreach (Control element in GetScrollingElements())
+            {
+                if (element is DataGridRow row)
+                {
+                    Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Row: {1} ", row.Slot, row.Index));
+                }
+                else if (element is DataGridRowGroupHeader groupHeader)
+                {
+                    Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} GroupHeader: {1}", groupHeader.RowGroupInfo.Slot, groupHeader.RowGroupInfo.CollectionViewGroup.Key));
+                }
+            }
+        }
+#endif
+    }
+}

+ 106 - 0
src/Avalonia.Controls.DataGrid/DataGridEnumerations.cs

@@ -0,0 +1,106 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Used to specify action to take out of edit mode.
+    /// </summary>
+    public enum DataGridEditAction
+    {
+        /// <summary>
+        /// Cancel the changes.
+        /// </summary>
+        Cancel,
+
+        /// <summary>
+        /// Commit edited value.
+        /// </summary>
+        Commit
+    }
+
+    // Determines the location and visibility of the editing row.
+    internal enum DataGridEditingRowLocation
+    {
+        Bottom = 0, // The editing row is collapsed below the displayed rows
+        Inline = 1, // The editing row is visible and displayed
+        Top = 2     // The editing row is collapsed above the displayed rows
+    }
+
+    /// <summary>
+    /// Determines whether the inner cells' vertical/horizontal gridlines are shown or not.
+    /// </summary>
+    [Flags]
+    public enum DataGridGridLinesVisibility
+    {
+        None = 0,
+        Horizontal = 1,
+        Vertical = 2,
+        All = 3,
+    }
+
+    public enum DataGridEditingUnit
+    {
+        Cell = 0,
+        Row = 1,
+    }
+
+    /// <summary>
+    /// Determines whether the row/column headers are shown or not.
+    /// </summary>
+    [Flags]
+    public enum DataGridHeadersVisibility
+    {
+        /// <summary>
+        /// Show Row, Column, and Corner Headers
+        /// </summary>
+        All = Row | Column,
+
+        /// <summary>
+        /// Show only Column Headers with top-right corner Header
+        /// </summary>
+        Column = 0x01,
+
+        /// <summary>
+        /// Show only Row Headers with bottom-left corner
+        /// </summary>
+        Row = 0x02,
+
+        /// <summary>
+        /// Don’t show any Headers
+        /// </summary>
+        None = 0x00
+    }
+
+    public enum DataGridRowDetailsVisibilityMode
+    {
+        Collapsed = 2,          // Show no details.  Developer is in charge of toggling visibility.
+        Visible = 1,	        // Show the details section for all rows.
+        VisibleWhenSelected = 0	// Show the details section only for the selected row(s).
+    }
+
+    /// <summary>
+    /// Determines the type of action to take when selecting items
+    /// </summary>
+    internal enum DataGridSelectionAction
+    {
+        AddCurrentToSelection,
+        None,
+        RemoveCurrentFromSelection,
+        SelectCurrent,
+        SelectFromAnchorToCurrent
+    }
+
+    /// <summary>
+    /// Determines the selection model
+    /// </summary>
+    public enum DataGridSelectionMode
+    {
+        Extended = 0,
+        Single = 1
+    }
+}

+ 190 - 0
src/Avalonia.Controls.DataGrid/DataGridError.cs

@@ -0,0 +1,190 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+
+    internal static class DataGridError
+    {
+        public static class DataGrid
+        {
+            public static InvalidOperationException CannotChangeItemsWhenLoadingRows()
+            {
+                return new InvalidOperationException("Items cannot be added, removed or reset while rows are loading or unloading.");
+            }
+
+            public static InvalidOperationException CannotChangeColumnCollectionWhileAdjustingDisplayIndexes()
+            {
+                return new InvalidOperationException("Column collection cannot be changed while adjusting display indexes.");
+            }
+
+            public static InvalidOperationException ColumnCannotBeCollapsed()
+            {
+                return new InvalidOperationException("Column cannot be collapsed.");
+            }
+
+            public static InvalidOperationException ColumnCannotBeReassignedToDifferentDataGrid()
+            {
+                return new InvalidOperationException("Column already belongs to a DataGrid instance and cannot be reassigned.");
+            }
+
+            public static ArgumentException ColumnNotInThisDataGrid()
+            {
+                return new ArgumentException("Provided column does not belong to this DataGrid.");
+            }
+
+            public static ArgumentException ItemIsNotContainedInTheItemsSource(string paramName)
+            {
+                return new ArgumentException("The item is not contained in the ItemsSource.", paramName);
+            }
+
+            public static InvalidOperationException NoCurrentRow()
+            {
+                return new InvalidOperationException("There is no current row.  Operation cannot be completed.");
+            }
+
+            public static InvalidOperationException NoOwningGrid(Type type)
+            {
+                return new InvalidOperationException(Format("There is no instance of DataGrid assigned to this {0}.  Operation cannot be completed.", type.FullName));
+            }
+
+            public static InvalidOperationException UnderlyingPropertyIsReadOnly(string paramName)
+            {
+                return new InvalidOperationException(Format("{0} cannot be set because the underlying property is read only.", paramName));
+            }
+
+            public static ArgumentException ValueCannotBeSetToInfinity(string paramName)
+            {
+                return new ArgumentException(Format("{0} cannot be set to infinity.", paramName));
+            }
+
+            public static ArgumentException ValueCannotBeSetToNAN(string paramName)
+            {
+                return new ArgumentException(Format("{0} cannot be set to double.NAN.", paramName));
+            }
+
+            public static ArgumentNullException ValueCannotBeSetToNull(string paramName, string valueName)
+            {
+                return new ArgumentNullException(paramName, Format("{0} cannot be set to a null value.", valueName));
+            }
+
+            public static ArgumentException ValueIsNotAnInstanceOf(string paramName, Type type)
+            {
+                return new ArgumentException(paramName, Format("The value is not an instance of {0}.", type.FullName));
+            }
+
+            public static ArgumentException ValueIsNotAnInstanceOfEitherOr(string paramName, Type type1, Type type2)
+            {
+                return new ArgumentException(paramName, Format("The value is not an instance of {0} or {1}.", type1.FullName, type2.FullName));
+            }
+
+            public static ArgumentOutOfRangeException ValueMustBeBetween(string paramName, string valueName, object lowValue, bool lowInclusive, object highValue, bool highInclusive)
+            {
+                string message = null;
+
+                if (lowInclusive && highInclusive)
+                {
+                    message = "{0} must be greater than or equal to {1} and less than or equal to {2}.";
+                }
+                else if (lowInclusive && !highInclusive)
+                {
+                    message = "{0} must be greater than or equal to {1} and less than {2}.";
+                }
+                else if (!lowInclusive && highInclusive)
+                {
+                    message = "{0} must be greater than {1} and less than or equal to {2}.";
+                }
+                else
+                {
+                    message = "{0} must be greater than {1} and less than {2}.";
+                }
+
+                return new ArgumentOutOfRangeException(paramName, Format(message, valueName, lowValue, highValue));
+            }
+
+            public static ArgumentOutOfRangeException ValueMustBeGreaterThanOrEqualTo(string paramName, string valueName, object value)
+            {
+                return new ArgumentOutOfRangeException(paramName, Format("{0} must be greater than or equal to {1}.", valueName, value));
+            }
+
+            public static ArgumentOutOfRangeException ValueMustBeLessThanOrEqualTo(string paramName, string valueName, object value)
+            {
+                return new ArgumentOutOfRangeException(paramName, Format("{0} must be less than or equal to {1}.", valueName, value));
+            }
+
+            public static ArgumentOutOfRangeException ValueMustBeLessThan(string paramName, string valueName, object value)
+            {
+                return new ArgumentOutOfRangeException(paramName, Format("{0} must be less than {1}.", valueName, value));
+            }
+
+        }
+
+        public static class DataGridColumnHeader
+        {
+            public static NotSupportedException ContentDoesNotSupportUIElements()
+            {
+                return new NotSupportedException("Content does not support Controls; use ContentTemplate instead.");
+            }
+        }
+
+        public static class DataGridLength
+        {
+            public static ArgumentException InvalidUnitType(string paramName)
+            {
+                return new ArgumentException(Format("{0} is not a valid DataGridLengthUnitType.", paramName), paramName);
+            }
+        }
+
+        public static class DataGridLengthConverter
+        {
+            public static NotSupportedException CannotConvertFrom(string paramName)
+            {
+                return new NotSupportedException(Format("DataGridLengthConverter cannot convert from {0}.", paramName));
+            }
+
+            public static NotSupportedException CannotConvertTo(string paramName)
+            {
+                return new NotSupportedException(Format("Cannot convert from DataGridLength to {0}.", paramName));
+            }
+
+            public static NotSupportedException InvalidDataGridLength(string paramName)
+            {
+                return new NotSupportedException(Format("Invalid DataGridLength.", paramName));
+            }
+        }
+
+        public static class DataGridRow
+        {
+            public static InvalidOperationException InvalidRowIndexCannotCompleteOperation()
+            {
+                return new InvalidOperationException("Invalid row index. Operation cannot be completed.");
+            }
+        }
+
+        public static class DataGridSelectedItemsCollection
+        {
+            public static InvalidOperationException CannotChangeSelectedItemsCollectionInSingleMode()
+            {
+                return new InvalidOperationException("Can only change SelectedItems collection in Extended selection mode.  Use SelectedItem property in Single selection mode.");
+            }
+        }
+
+        public static class DataGridTemplateColumn
+        {
+            public static TypeInitializationException MissingTemplateForType(Type type)
+            {
+                return new TypeInitializationException(Format("Missing template.  Cannot initialize {0}.", type.FullName), null);
+            }
+        }
+
+        private static string Format(string formatString, params object[] args)
+        {
+            return String.Format(CultureInfo.CurrentCulture, formatString, args);
+        }
+    }
+}

+ 70 - 0
src/Avalonia.Controls.DataGrid/DataGridFillerColumn.cs

@@ -0,0 +1,70 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Utils;
+using Avalonia.Interactivity;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridFillerColumn : DataGridColumn
+    {
+        public DataGridFillerColumn(DataGrid owningGrid)
+        {
+            IsReadOnly = true;
+            OwningGrid = owningGrid;
+            MinWidth = 0;
+            MaxWidth = int.MaxValue;
+        }
+
+        internal double FillerWidth
+        {
+            get;
+            set;
+        }
+
+        // True if there is room for the filler column; otherwise, false
+        internal bool IsActive
+        {
+            get
+            {
+                return FillerWidth > 0;
+            }
+        }
+
+        // True if the FillerColumn's header cell is contained in the visual tree
+        internal bool IsRepresented
+        {
+            get;
+            set;
+        } 
+
+        internal override DataGridColumnHeader CreateHeader()
+        {
+            DataGridColumnHeader headerCell = base.CreateHeader();
+            if (headerCell != null)
+            {
+                headerCell.IsEnabled = false;
+            }
+            return headerCell;
+        }
+
+        protected override IControl GenerateElement(DataGridCell cell, object dataItem)
+        {
+            return null;
+        }
+
+        protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding editBinding)
+        {
+            editBinding = null;
+            return null;
+        } 
+
+        protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
+        {
+            return null;
+        }
+    }
+}

+ 542 - 0
src/Avalonia.Controls.DataGrid/DataGridLength.cs

@@ -0,0 +1,542 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Utilities;
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls
+{
+    public enum DataGridLengthUnitType
+    {
+        Auto = 0,
+        Pixel = 1,
+        SizeToCells = 2,
+        SizeToHeader = 3,
+        Star = 4
+    }
+
+    /// <summary>
+    /// Represents the lengths of elements within the <see cref="T:Avalonia.Controls.DataGrid" /> control.
+    /// </summary>
+    [TypeConverter(typeof(DataGridLengthConverter))]
+    public struct DataGridLength : IEquatable<DataGridLength>
+    {
+
+        private double _desiredValue;   //  desired value storage
+        private double _displayValue;   //  display value storage
+        private double _unitValue;      //  unit value storage
+        private DataGridLengthUnitType _unitType; //  unit type storage
+
+        //  static instances of value invariant DataGridLengths
+        private static readonly DataGridLength _auto = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.Auto);
+        private static readonly DataGridLength _sizeToCells = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.SizeToCells);
+        private static readonly DataGridLength _sizeToHeader = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.SizeToHeader);
+
+        // WPF uses 1.0 as the default value as well
+        internal const double DATAGRIDLENGTH_DefaultValue = 1.0;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridLength" /> class. 
+        /// </summary>
+        /// <param name="value"></param>
+        public DataGridLength(double value)
+            : this(value, DataGridLengthUnitType.Pixel)
+        {
+        }
+        /// <summary>
+        ///     Initializes to a specified value and unit.
+        /// </summary>
+        /// <param name="value">The value to hold.</param>
+        /// <param name="type">The unit of <c>value</c>.</param>
+        /// <remarks> 
+        ///     <c>value</c> is ignored unless <c>type</c> is
+        ///     <c>DataGridLengthUnitType.Pixel</c> or
+        ///     <c>DataGridLengthUnitType.Star</c>
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        ///     If <c>value</c> parameter is <c>double.NaN</c>
+        ///     or <c>value</c> parameter is <c>double.NegativeInfinity</c>
+        ///     or <c>value</c> parameter is <c>double.PositiveInfinity</c>.
+        /// </exception>
+        public DataGridLength(double value, DataGridLengthUnitType type)
+            : this(value, type, (type == DataGridLengthUnitType.Pixel ? value : Double.NaN), (type == DataGridLengthUnitType.Pixel ? value : Double.NaN))
+        {
+        }
+
+        /// <summary>
+        ///     Initializes to a specified value and unit.
+        /// </summary>
+        /// <param name="value">The value to hold.</param>
+        /// <param name="type">The unit of <c>value</c>.</param>
+        /// <param name="desiredValue"></param>
+        /// <param name="displayValue"></param>
+        /// <remarks> 
+        ///     <c>value</c> is ignored unless <c>type</c> is
+        ///     <c>DataGridLengthUnitType.Pixel</c> or
+        ///     <c>DataGridLengthUnitType.Star</c>
+        /// </remarks>
+        /// <exception cref="ArgumentException">
+        ///     If <c>value</c> parameter is <c>double.NaN</c>
+        ///     or <c>value</c> parameter is <c>double.NegativeInfinity</c>
+        ///     or <c>value</c> parameter is <c>double.PositiveInfinity</c>.
+        /// </exception>
+        public DataGridLength(double value, DataGridLengthUnitType type, double desiredValue, double displayValue)
+        {
+            if (double.IsNaN(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToNAN("value");
+            }
+            if (double.IsInfinity(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("value");
+            }
+            if (double.IsInfinity(desiredValue))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("desiredValue");
+            }
+            if (double.IsInfinity(displayValue))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("displayValue");
+            }
+            if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "value", 0);
+            }
+            if (desiredValue < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("desiredValue", "desiredValue", 0);
+            }
+            if (displayValue < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("displayValue", "displayValue", 0);
+            }
+
+            if (type != DataGridLengthUnitType.Auto &&
+                type != DataGridLengthUnitType.SizeToCells &&
+                type != DataGridLengthUnitType.SizeToHeader &&
+                type != DataGridLengthUnitType.Star &&
+                type != DataGridLengthUnitType.Pixel)
+            {
+                throw DataGridError.DataGridLength.InvalidUnitType("type");
+            }
+
+            _desiredValue = desiredValue;
+            _displayValue = displayValue;
+            _unitValue = (type == DataGridLengthUnitType.Auto) ? DATAGRIDLENGTH_DefaultValue : value;
+            _unitType = type;
+        }
+
+        /// <summary>
+        /// Gets a <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the standard automatic sizing mode.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the standard automatic sizing mode.
+        /// </returns>
+        public static DataGridLength Auto
+        {
+            get
+            {
+                return _auto;
+            }
+        }
+
+        /// <summary>
+        ///     Returns the desired value of this instance.
+        /// </summary>
+        public double DesiredValue
+        {
+            get
+            {
+                return _desiredValue;
+            }
+        }
+
+        /// <summary>
+        ///     Returns the display value of this instance.
+        /// </summary>
+        public double DisplayValue
+        {
+            get
+            {
+                return _displayValue;
+            }
+        }
+
+        /// <summary>
+        ///     Returns <c>true</c> if this DataGridLength instance holds 
+        ///     an absolute (pixel) value.
+        /// </summary>
+        public bool IsAbsolute
+        {
+            get
+            {
+                return _unitType == DataGridLengthUnitType.Pixel;
+            }
+        }
+
+        /// <summary>
+        ///     Returns <c>true</c> if this DataGridLength instance is 
+        ///     automatic (not specified).
+        /// </summary>
+        public bool IsAuto
+        {
+            get
+            {
+                return _unitType == DataGridLengthUnitType.Auto;
+            }
+        }
+
+        /// <summary>
+        ///     Returns <c>true</c> if this instance is to size to the cells of a column or row.
+        /// </summary>
+        public bool IsSizeToCells
+        {
+            get
+            {
+                return _unitType == DataGridLengthUnitType.SizeToCells;
+            }
+        }
+
+        /// <summary>
+        ///     Returns <c>true</c> if this instance is to size to the header of a column or row.
+        /// </summary>
+        public bool IsSizeToHeader
+        {
+            get
+            {
+                return _unitType == DataGridLengthUnitType.SizeToHeader;
+            }
+        }
+
+        /// <summary>
+        ///     Returns <c>true</c> if this DataGridLength instance holds a weighted proportion
+        ///     of available space.
+        /// </summary>
+        public bool IsStar
+        {
+            get
+            {
+                return _unitType == DataGridLengthUnitType.Star;
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the cell-based automatic sizing mode.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the cell-based automatic sizing mode.
+        /// </returns>
+        public static DataGridLength SizeToCells
+        {
+            get
+            {
+                return _sizeToCells;
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the header-based automatic sizing mode.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="T:Avalonia.Controls.DataGridLength" /> structure that represents the header-based automatic sizing mode.
+        /// </returns>
+        public static DataGridLength SizeToHeader
+        {
+            get
+            {
+                return _sizeToHeader;
+            }
+        }
+
+        /// <summary>
+        /// Gets the <see cref="T:Avalonia.Controls.DataGridLengthUnitType" /> that represents the current sizing mode.
+        /// </summary>
+        public DataGridLengthUnitType UnitType
+        {
+            get
+            {
+                return _unitType;
+            }
+        }
+
+        /// <summary>
+        /// Gets the absolute value of the <see cref="T:Avalonia.Controls.DataGridLength" /> in pixels.
+        /// </summary>
+        /// <returns>
+        /// The absolute value of the <see cref="T:Avalonia.Controls.DataGridLength" /> in pixels.
+        /// </returns>
+        public double Value
+        {
+            get
+            {
+                return _unitValue;
+            }
+        }
+
+        /// <summary>
+        /// Overloaded operator, compares 2 DataGridLength's.
+        /// </summary>
+        /// <param name="gl1">first DataGridLength to compare.</param>
+        /// <param name="gl2">second DataGridLength to compare.</param>
+        /// <returns>true if specified DataGridLength have same value, 
+        /// unit type, desired value, and display value.</returns>
+        public static bool operator ==(DataGridLength gl1, DataGridLength gl2)
+        {
+            return (gl1.UnitType == gl2.UnitType
+                    && gl1.Value == gl2.Value
+                    && gl1.DesiredValue == gl2.DesiredValue
+                    && gl1.DisplayValue == gl2.DisplayValue);
+        }
+
+        /// <summary>
+        /// Overloaded operator, compares 2 DataGridLength's.
+        /// </summary>
+        /// <param name="gl1">first DataGridLength to compare.</param>
+        /// <param name="gl2">second DataGridLength to compare.</param>
+        /// <returns>true if specified DataGridLength have either different value, 
+        /// unit type, desired value, or display value.</returns>
+        public static bool operator !=(DataGridLength gl1, DataGridLength gl2)
+        {
+            return (gl1.UnitType != gl2.UnitType
+                    || gl1.Value != gl2.Value
+                    || gl1.DesiredValue != gl2.DesiredValue
+                    || gl1.DisplayValue != gl2.DisplayValue);
+        }
+
+        /// <summary>
+        /// Compares this instance of DataGridLength with another instance.
+        /// </summary>
+        /// <param name="other">DataGridLength length instance to compare.</param>
+        /// <returns><c>true</c> if this DataGridLength instance has the same value 
+        /// and unit type as gridLength.</returns>
+        public bool Equals(DataGridLength other)
+        {
+            return (this == other);
+        }
+
+        /// <summary>
+        /// Compares this instance of DataGridLength with another object.
+        /// </summary>
+        /// <param name="obj">Reference to an object for comparison.</param>
+        /// <returns><c>true</c> if this DataGridLength instance has the same value 
+        /// and unit type as oCompare.</returns>
+        public override bool Equals(object obj)
+        {
+            DataGridLength? dataGridLength = obj as DataGridLength?;
+            if (dataGridLength.HasValue)
+            {
+                return (this == dataGridLength);
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Returns a unique hash code for this DataGridLength
+        /// </summary>
+        /// <returns>hash code</returns>
+        public override int GetHashCode()
+        {
+            return ((int)_unitValue + (int)_unitType) + (int)_desiredValue + (int)_displayValue;
+        }
+
+    }
+    /// <summary>
+    /// DataGridLengthConverter - Converter class for converting instances of other types to and from DataGridLength instances.
+    /// </summary> 
+    public class DataGridLengthConverter : TypeConverter
+    {
+        private static string _starSuffix = "*";
+        private static string[] _valueInvariantUnitStrings = { "auto", "sizetocells", "sizetoheader" };
+        private static DataGridLength[] _valueInvariantDataGridLengths = { DataGridLength.Auto, DataGridLength.SizeToCells, DataGridLength.SizeToHeader };
+
+        /// <summary>
+        /// Checks whether or not this class can convert from a given type.
+        /// </summary>
+        /// <param name="context">
+        /// An ITypeDescriptorContext that provides a format context. 
+        /// </param>
+        /// <param name="sourceType">The Type being queried for support.</param>
+        /// <returns>
+        /// <c>true</c> if this converter can convert from the provided type, 
+        /// <c>false</c> otherwise.
+        /// </returns>
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            // We can only handle strings, integral and floating types
+            TypeCode tc = Type.GetTypeCode(sourceType);
+            switch (tc)
+            {
+                case TypeCode.String:
+                case TypeCode.Decimal:
+                case TypeCode.Single:
+                case TypeCode.Double:
+                case TypeCode.Int16:
+                case TypeCode.Int32:
+                case TypeCode.Int64:
+                case TypeCode.UInt16:
+                case TypeCode.UInt32:
+                case TypeCode.UInt64:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        /// <summary>
+        /// Checks whether or not this class can convert to a given type.
+        /// </summary>
+        /// <param name="context">
+        /// An ITypeDescriptorContext that provides a format context. 
+        /// </param>
+        /// <param name="destinationType">The Type being queried for support.</param>
+        /// <returns>
+        /// <c>true</c> if this converter can convert to the provided type, 
+        /// <c>false</c> otherwise.
+        /// </returns>
+        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
+        {
+            return destinationType == typeof(string);
+        }
+
+        /// <summary>
+        /// Attempts to convert to a DataGridLength from the given object.
+        /// </summary>
+        /// <param name="context">
+        /// An ITypeDescriptorContext that provides a format context. 
+        /// </param>
+        /// <param name="culture">
+        /// The CultureInfo to use for the conversion. 
+        /// </param>
+        /// <param name="value">The object to convert to a GridLength.</param>
+        /// <returns>
+        /// The GridLength instance which was constructed.
+        /// </returns>
+        /// <exception cref="NotSupportedException">
+        /// A NotSupportedException is thrown if the example object is null.
+        /// </exception>
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            // GridLengthConverter in WPF throws a NotSupportedException on a null value as well.
+            if (value == null)
+            {
+                throw DataGridError.DataGridLengthConverter.CannotConvertFrom("(null)");
+            }
+
+            if (value is string stringValue)
+            {
+                stringValue = stringValue.Trim();
+
+                if (stringValue.EndsWith(_starSuffix, StringComparison.Ordinal))
+                {
+                    string stringValueWithoutSuffix = stringValue.Substring(0, stringValue.Length - _starSuffix.Length);
+
+                    double starWeight;
+                    if (string.IsNullOrEmpty(stringValueWithoutSuffix))
+                    {
+                        starWeight = 1;
+                    }
+                    else
+                    {
+                        starWeight = Convert.ToDouble(stringValueWithoutSuffix, culture ?? CultureInfo.CurrentCulture);
+                    }
+
+                    return new DataGridLength(starWeight, DataGridLengthUnitType.Star);
+                }
+
+                for (int index = 0; index < _valueInvariantUnitStrings.Length; index++)
+                {
+                    if (stringValue.Equals(_valueInvariantUnitStrings[index], StringComparison.OrdinalIgnoreCase))
+                    {
+                        return _valueInvariantDataGridLengths[index];
+                    }
+                }
+            }
+
+            // Conversion from numeric type, WPF lets Convert exceptions bubble out here as well
+            double doubleValue = Convert.ToDouble(value, culture ?? CultureInfo.CurrentCulture);
+            if (double.IsNaN(doubleValue))
+            {
+                // WPF returns Auto in this case as well
+                return DataGridLength.Auto;
+            }
+            else
+            {
+                return new DataGridLength(doubleValue);
+            }
+        }
+
+        /// <summary>
+        /// Attempts to convert a DataGridLength instance to the given type.
+        /// </summary>
+        /// <param name="context">
+        /// An ITypeDescriptorContext that provides a format context. 
+        /// </param>
+        /// <param name="culture">
+        /// The CultureInfo to use for the conversion. 
+        /// </param>
+        /// <param name="value">The DataGridLength to convert.</param>
+        /// <param name="destinationType">The type to which to convert the DataGridLength instance.</param>
+        /// <returns>
+        /// The object which was constructed.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// An ArgumentNullException is thrown if the example object is null.
+        /// </exception>
+        /// <exception cref="NotSupportedException">
+        /// A NotSupportedException is thrown if the object is not null and is not a DataGridLength,
+        /// or if the destinationType isn't one of the valid destination types.
+        /// </exception>
+        ///<SecurityNote>
+        ///     Critical: calls InstanceDescriptor ctor which LinkDemands
+        ///     PublicOK: can only make an InstanceDescriptor for DataGridLength, not an arbitrary class
+        ///</SecurityNote> 
+        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+        {
+            if (destinationType == null)
+            {
+                throw new ArgumentNullException("destinationType");
+            }
+            if (destinationType != typeof(string))
+            {
+                throw DataGridError.DataGridLengthConverter.CannotConvertTo(destinationType.ToString());
+            }
+            DataGridLength? dataGridLength = value as DataGridLength?;
+            if (!dataGridLength.HasValue)
+            {
+                throw DataGridError.DataGridLengthConverter.InvalidDataGridLength("value");
+            }
+            else
+            {
+                // Convert dataGridLength to a string
+                switch (dataGridLength.Value.UnitType)
+                {
+                    //  for Auto print out "Auto". value is always "1.0"
+                    case DataGridLengthUnitType.Auto:
+                        return "Auto";
+
+                    case DataGridLengthUnitType.SizeToHeader:
+                        return "SizeToHeader";
+
+                    case DataGridLengthUnitType.SizeToCells:
+                        return "SizeToCells";
+
+                    //  Star has one special case when value is "1.0".
+                    //  in this case drop value part and print only "Star"
+                    case DataGridLengthUnitType.Star:
+                        return (
+                            DoubleUtil.AreClose(1.0, dataGridLength.Value.Value)
+                            ? _starSuffix
+                            : Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix);
+
+                    default:
+                        return (Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture));
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,1056 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Utilities;
+using Avalonia.VisualTree;
+using System;
+using System.Diagnostics;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
+    /// </summary>
+    public class DataGridRow : TemplatedControl
+    {
+
+        private const byte DATAGRIDROW_defaultMinHeight = 0;
+        internal const int DATAGRIDROW_maximumHeight = 65536;
+        internal const double DATAGRIDROW_minimumHeight = 0;
+
+        private const string DATAGRIDROW_elementBottomGridLine = "PART_BottomGridLine";
+        private const string DATAGRIDROW_elementCells = "PART_CellsPresenter";
+        private const string DATAGRIDROW_elementDetails = "PART_DetailsPresenter";
+        internal const string DATAGRIDROW_elementRoot = "PART_Root";
+        internal const string DATAGRIDROW_elementRowHeader = "PART_RowHeader";
+
+        private DataGridCellsPresenter _cellsElement;
+        private DataGridCell _fillerCell;
+        private DataGridRowHeader _headerElement;
+        private double _lastHorizontalOffset;
+        private int? _mouseOverColumnIndex;
+        private bool _isValid = true;
+        private Rectangle _bottomGridLine;
+        private bool _areHandlersSuspended;
+
+        // In the case where Details scales vertically when it's arranged at a different width, we
+        // get the wrong height measurement so we need to check it again after arrange
+        private bool _checkDetailsContentHeight;
+
+        // Optimal height of the details based on the Element created by the DataTemplate
+        private double _detailsDesiredHeight;
+
+        private bool _detailsLoaded;
+        private bool _detailsVisibilityNotificationPending;
+        private IControl _detailsContent;
+        private IDisposable _detailsContentSizeSubscription;
+        private DataGridDetailsPresenter _detailsElement;
+
+        // Locally cache whether or not details are visible so we don't run redundant storyboards
+        // The Details Template that is actually applied to the Row
+        private IDataTemplate _appliedDetailsTemplate;
+
+        private bool? _appliedDetailsVisibility;
+
+        /// <summary>
+        /// Identifies the Header dependency property.
+        /// </summary>
+        public static readonly StyledProperty<object> HeaderProperty =
+            AvaloniaProperty.Register<DataGridRow, object>(nameof(Header));
+
+        /// <summary>
+        /// Gets or sets the row header.
+        /// </summary>
+        public object Header
+        {
+            get { return GetValue(HeaderProperty); }
+            set { SetValue(HeaderProperty, value); }
+        }
+
+        public static readonly DirectProperty<DataGridRow, bool> IsValidProperty =
+            AvaloniaProperty.RegisterDirect<DataGridRow, bool>(
+                nameof(IsValid),
+                o => o.IsValid);
+
+        /// <summary>
+        /// Gets a value that indicates whether the data in a row is valid. 
+        /// </summary>
+        public bool IsValid
+        {
+            get { return _isValid; }
+            internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
+        }
+
+        public static readonly StyledProperty<IDataTemplate> DetailsTemplateProperty =
+            AvaloniaProperty.Register<DataGridRow, IDataTemplate>(nameof(DetailsTemplate));
+
+        /// <summary>
+        /// Gets or sets the template that is used to display the details section of the row.
+        /// </summary>
+        public IDataTemplate DetailsTemplate
+        {
+            get { return GetValue(DetailsTemplateProperty); }
+            set { SetValue(DetailsTemplateProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> AreDetailsVisibleProperty =
+            AvaloniaProperty.Register<DataGridRow, bool>(nameof(AreDetailsVisible));
+
+        /// <summary>
+        /// Gets or sets a value that indicates when the details section of the row is displayed.
+        /// </summary>
+        public bool AreDetailsVisible
+        {
+            get { return GetValue(AreDetailsVisibleProperty); }
+            set { SetValue(AreDetailsVisibleProperty, value); }
+        }
+
+        static DataGridRow()
+        {
+            HeaderProperty.Changed.AddClassHandler<DataGridRow>(x => x.OnHeaderChanged);
+            DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>(x => x.OnDetailsTemplateChanged);
+            AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>(x => x.OnAreDetailsVisibleChanged);
+            PointerPressedEvent.AddClassHandler<DataGridRow>(x => x.DataGridRow_PointerPressed, handledEventsToo: true);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridRow" /> class. 
+        /// </summary>
+        public DataGridRow()
+        {
+            MinHeight = DATAGRIDROW_defaultMinHeight;
+
+            Index = -1;
+            IsValid = true;
+            Slot = -1;
+            _mouseOverColumnIndex = null;
+            _detailsDesiredHeight = double.NaN;
+            _detailsLoaded = false;
+            _appliedDetailsVisibility = false;
+            Cells = new DataGridCellCollection(this);
+            Cells.CellAdded += DataGridCellCollection_CellAdded;
+            Cells.CellRemoved += DataGridCellCollection_CellRemoved;
+        }
+
+        private void SetValueNoCallback<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
+        {
+            _areHandlersSuspended = true;
+            try
+            {
+                SetValue(property, value, priority);
+            }
+            finally
+            {
+                _areHandlersSuspended = false;
+            }
+        }
+
+        private void OnHeaderChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_headerElement != null)
+            {
+                _headerElement.Content = e.NewValue;
+            }
+        }
+
+        private void OnDetailsTemplateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (IDataTemplate)e.OldValue;
+            var newValue = (IDataTemplate)e.NewValue;
+
+            if (!_areHandlersSuspended && OwningGrid != null)
+            {
+                IDataTemplate actualDetailsTemplate(IDataTemplate template) => (template ?? OwningGrid.RowDetailsTemplate);
+
+                // We don't always want to apply the new Template because they might have set the same one
+                // we inherited from the DataGrid
+                if (actualDetailsTemplate(newValue) != actualDetailsTemplate(oldValue))
+                {
+                    ApplyDetailsTemplate(initializeDetailsPreferredHeight: false);
+                }
+            }
+        }
+
+        private void OnAreDetailsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (!_areHandlersSuspended)
+            {
+                if (OwningGrid == null)
+                {
+                    throw DataGridError.DataGrid.NoOwningGrid(this.GetType());
+                }
+                if (Index == -1)
+                {
+                    throw DataGridError.DataGridRow.InvalidRowIndexCannotCompleteOperation();
+                }
+
+                var newValue = (bool)e.NewValue;
+                OwningGrid.OnRowDetailsVisibilityPropertyChanged(Index, newValue);
+                SetDetailsVisibilityInternal(newValue, raiseNotification: true, animate: true);
+            }
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Index of the row
+        /// </summary>
+        internal int Index
+        {
+            get;
+            set;
+        }
+
+        internal double ActualBottomGridLineHeight
+        {
+            get
+            {
+                if (_bottomGridLine != null && OwningGrid != null && OwningGrid.AreRowBottomGridLinesRequired)
+                {
+                    // Unfortunately, _bottomGridLine has no size yet so we can't get its actualheight
+                    return DataGrid.HorizontalGridLinesThickness;
+                }
+                return 0;
+            }
+        }
+
+        internal DataGridCellCollection Cells
+        {
+            get;
+            private set;
+        }
+
+        //TODO Styles
+        internal DataGridCell FillerCell
+        {
+            get
+            {
+                if (_fillerCell == null)
+                {
+                    _fillerCell = new DataGridCell
+                    {
+                        IsVisible = false,
+                        OwningRow = this
+                    };
+                    //_fillerCell.EnsureStyle(null);
+                    if (_cellsElement != null)
+                    {
+                        _cellsElement.Children.Add(_fillerCell);
+                    }
+                }
+                return _fillerCell;
+            }
+        }
+
+        internal bool HasBottomGridLine
+        {
+            get
+            {
+                return _bottomGridLine != null;
+            }
+        }
+
+        internal bool HasHeaderCell
+        {
+            get
+            {
+                return _headerElement != null;
+            }
+        }
+
+        internal DataGridRowHeader HeaderCell
+        {
+            get
+            {
+                return _headerElement;
+            }
+        }
+
+        internal bool IsEditing => OwningGrid != null && OwningGrid.EditingRow == this;
+
+        /// <summary>
+        /// Layout when template is applied
+        /// </summary>
+        internal bool IsLayoutDelayed
+        {
+            get;
+            private set;
+        }
+
+        internal bool IsMouseOver
+        {
+            get
+            {
+                return OwningGrid != null && OwningGrid.MouseOverRowIndex == Index;
+            }
+            set
+            {
+                if (OwningGrid != null && value != IsMouseOver)
+                {
+                    if (value)
+                    {
+                        OwningGrid.MouseOverRowIndex = Index;
+                    }
+                    else
+                    {
+                        OwningGrid.MouseOverRowIndex = null;
+                    }
+                }
+            }
+        }
+
+        internal bool IsRecycled
+        {
+            get;
+            private set;
+        }
+
+        internal bool IsRecyclable
+        {
+            get
+            {
+                if (OwningGrid != null)
+                {
+                    return OwningGrid.IsRowRecyclable(this);
+                }
+                return true;
+            }
+        }
+
+        internal bool IsSelected
+        {
+            get
+            {
+                if (OwningGrid == null || Slot == -1)
+                {
+                    // The Slot can be -1 if we're about to reuse or recycle this row, but the layout cycle has not
+                    // passed so we don't know the outcome yet.  We don't care whether or not it's selected in this case
+                    return false;
+                }
+                return OwningGrid.GetRowSelection(Slot);
+            }
+        }
+
+        internal int? MouseOverColumnIndex
+        {
+            get
+            {
+                return _mouseOverColumnIndex;
+            }
+            set
+            {
+                if (_mouseOverColumnIndex != value)
+                {
+                    DataGridCell oldMouseOverCell = null;
+                    if (_mouseOverColumnIndex != null && OwningGrid.IsSlotVisible(Slot))
+                    {
+                        if (_mouseOverColumnIndex > -1)
+                        {
+                            oldMouseOverCell = Cells[_mouseOverColumnIndex.Value];
+                        }
+                    }
+                    _mouseOverColumnIndex = value;
+                    if (oldMouseOverCell != null && IsVisible)
+                    {
+                        oldMouseOverCell.UpdatePseudoClasses();
+                    }
+                    if (_mouseOverColumnIndex != null && OwningGrid != null && OwningGrid.IsSlotVisible(Slot))
+                    {
+                        if (_mouseOverColumnIndex > -1)
+                        {
+                            Cells[_mouseOverColumnIndex.Value].UpdatePseudoClasses();
+                        }
+                    }
+                }
+            }
+        } 
+
+        internal Panel RootElement
+        {
+            get;
+            private set;
+        } 
+
+        internal int Slot
+        {
+            get;
+            set;
+        }
+
+        // Height that the row will eventually end up at after a possible detalis animation has completed
+        internal double TargetHeight
+        {
+            get
+            {
+                if (!double.IsNaN(Height))
+                {
+                    return Height;
+                }
+                else if (_detailsElement != null && _appliedDetailsVisibility == true && _appliedDetailsTemplate != null)
+                {
+                    Debug.Assert(!double.IsNaN(_detailsElement.ContentHeight));
+                    Debug.Assert(!double.IsNaN(_detailsDesiredHeight));
+                    return DesiredSize.Height + _detailsDesiredHeight - _detailsElement.ContentHeight;
+                }
+                else
+                {
+                    return DesiredSize.Height;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the index of the current row.
+        /// </summary>
+        /// <returns>
+        /// The index of the current row.
+        /// </returns>
+        public int GetIndex()
+        {
+            return Index;
+        }
+
+        /// <summary>
+        /// Returns the row which contains the given element
+        /// </summary>
+        /// <param name="element">element contained in a row</param>
+        /// <returns>Row that contains the element, or null if not found
+        /// </returns>
+        public static DataGridRow GetRowContainingElement(Control element)
+        {
+            // Walk up the tree to find the DataGridRow that contains the element
+            IVisual parent = element;
+            DataGridRow row = parent as DataGridRow;
+            while ((parent != null) && (row == null))
+            {
+                parent = parent.GetVisualParent();
+                row = parent as DataGridRow;
+            }
+            return row;
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.DataGridRow" />.
+        /// </summary>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.DataGridRow" />.
+        /// </returns>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+
+            // If the DataGrid was scrolled horizontally after our last Arrange, we need to make sure
+            // the Cells and Details are Arranged again
+            if (_lastHorizontalOffset != OwningGrid.HorizontalOffset)
+            {
+                _lastHorizontalOffset = OwningGrid.HorizontalOffset;
+                InvalidateHorizontalArrange();
+            }
+
+            Size size = base.ArrangeOverride(finalSize);
+
+            if (_checkDetailsContentHeight)
+            {
+                _checkDetailsContentHeight = false;
+                EnsureDetailsContentHeight();
+            }
+
+            if (RootElement != null)
+            {
+                foreach (Control child in RootElement.Children)
+                {
+                    if (DataGridFrozenGrid.GetIsFrozen(child))
+                    {
+                        TranslateTransform transform = new TranslateTransform();
+                        // Automatic layout rounding doesn't apply to transforms so we need to Round this
+                        transform.X = Math.Round(OwningGrid.HorizontalOffset);
+                        child.RenderTransform = transform;
+                    }
+                }
+            }
+
+            if (_bottomGridLine != null)
+            {
+                RectangleGeometry gridlineClipGeometry = new RectangleGeometry();
+                gridlineClipGeometry.Rect = new Rect(OwningGrid.HorizontalOffset, 0, Math.Max(0, DesiredSize.Width - OwningGrid.HorizontalOffset), _bottomGridLine.DesiredSize.Height);
+                _bottomGridLine.Clip = gridlineClipGeometry;
+            }
+
+            return size;
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.DataGridRow" /> to 
+        /// prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridRow" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.MeasureOverride(availableSize);
+            }
+
+            //Allow the DataGrid specific componets to adjust themselves based on new values
+            if (_headerElement != null)
+            {
+                _headerElement.InvalidateMeasure();
+            }
+            if (_cellsElement != null)
+            {
+                _cellsElement.InvalidateMeasure();
+            }
+            if (_detailsElement != null)
+            {
+                _detailsElement.InvalidateMeasure();
+            }
+
+            Size desiredSize = base.MeasureOverride(availableSize);
+            return desiredSize.WithWidth(Math.Max(desiredSize.Width, OwningGrid.CellsWidth));
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the column header when a new template is applied.
+        /// </summary>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            base.OnTemplateApplied(e);
+
+            RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
+            if (RootElement != null)
+            {
+                EnsureBackground();
+                UpdatePseudoClasses();
+            }
+
+            bool updateVerticalScrollBar = false;
+            if (_cellsElement != null)
+            {
+                // If we're applying a new template, we  want to remove the cells from the previous _cellsElement
+                _cellsElement.Children.Clear();
+                updateVerticalScrollBar = true;
+            }
+
+            _cellsElement = e.NameScope.Find<DataGridCellsPresenter>(DATAGRIDROW_elementCells);
+            if (_cellsElement != null)
+            {
+                _cellsElement.OwningRow = this;
+                // Cells that were already added before the Template was applied need to
+                // be added to the Canvas
+                if (Cells.Count > 0)
+                {
+                    foreach (DataGridCell cell in Cells)
+                    {
+                        _cellsElement.Children.Add(cell);
+                    }
+                }
+            }
+
+            _detailsElement = e.NameScope.Find<DataGridDetailsPresenter>(DATAGRIDROW_elementDetails);
+            if (_detailsElement != null && OwningGrid != null)
+            {
+                _detailsElement.OwningRow = this;
+                if (ActualDetailsVisibility && ActualDetailsTemplate != null && _appliedDetailsTemplate == null)
+                {
+                    // Apply the DetailsTemplate now that the row template is applied.
+                    SetDetailsVisibilityInternal(ActualDetailsVisibility, raiseNotification: _detailsVisibilityNotificationPending, animate: false);
+                    _detailsVisibilityNotificationPending = false;
+                }
+            }
+
+            _bottomGridLine = e.NameScope.Find<Rectangle>(DATAGRIDROW_elementBottomGridLine);
+            EnsureGridLines();
+
+            _headerElement = e.NameScope.Find<DataGridRowHeader>(DATAGRIDROW_elementRowHeader);
+            if (_headerElement != null)
+            {
+                _headerElement.Owner = this;
+                if (Header != null)
+                {
+                    _headerElement.Content = Header;
+                }
+                EnsureHeaderStyleAndVisibility(null);
+            }
+
+            //The height of this row might have changed after applying a new style, so fix the vertical scroll bar
+            if (OwningGrid != null && updateVerticalScrollBar)
+            {
+                OwningGrid.UpdateVerticalScrollBar();
+            }
+        }
+
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            base.OnPointerEnter(e);
+            IsMouseOver = true;
+        }
+        protected override void OnPointerLeave(PointerEventArgs e)
+        {
+            IsMouseOver = false;
+            base.OnPointerLeave(e);
+        }
+
+        internal void ApplyCellsState()
+        {
+            foreach (DataGridCell dataGridCell in Cells)
+            {
+                dataGridCell.UpdatePseudoClasses();
+            }
+        }
+
+        internal void ApplyHeaderStatus()
+        {
+            if (_headerElement != null && OwningGrid.AreRowHeadersVisible)
+            {
+                _headerElement.ApplyOwnerStatus();
+            }
+        }
+
+        //TODO Implement
+        internal void UpdatePseudoClasses()
+        {
+            PseudoClasses.Set(":selected", IsSelected);
+            PseudoClasses.Set(":editing", IsEditing);
+            if (RootElement != null && OwningGrid != null && IsVisible)
+            {
+                ApplyHeaderStatus();
+            } 
+        }
+
+        //TODO Animation
+        internal void DetachFromDataGrid(bool recycle)
+        {
+            UnloadDetailsTemplate(recycle);
+
+            if (recycle)
+            {
+                IsRecycled = true;
+
+                if (_cellsElement != null)
+                {
+                    _cellsElement.Recycle();
+                }
+
+                _checkDetailsContentHeight = false;
+
+                // Clear out the old Details cache so it won't be reused for other data
+                //_detailsDesiredHeight = double.NaN;
+                if (_detailsElement != null)
+                {
+                    _detailsElement.ClearValue(DataGridDetailsPresenter.ContentHeightProperty);
+                }
+            }
+
+            Slot = -1;
+        }
+
+        // Make sure the row's background is set to its correct value.  It could be explicity set or inherit
+        // DataGrid.RowBackground or DataGrid.AlternatingRowBackground
+        internal void EnsureBackground()
+        {
+            // Inherit the DataGrid's RowBackground properties only if this row doesn't explicity have a background set
+            if (RootElement != null && OwningGrid != null)
+            {
+                IBrush newBackground = null;
+                if (Background == null)
+                {
+                    if (Index % 2 == 0 || OwningGrid.AlternatingRowBackground == null)
+                    {
+                        // Use OwningGrid.RowBackground if the index is even or if the OwningGrid.AlternatingRowBackground is null
+                        if (OwningGrid.RowBackground != null)
+                        {
+                            newBackground = OwningGrid.RowBackground;
+                        }
+                    }
+                    else
+                    {
+                        // Alternate row
+                        if (OwningGrid.AlternatingRowBackground != null)
+                        {
+                            newBackground = OwningGrid.AlternatingRowBackground;
+                        }
+                    }
+                }
+                else
+                {
+                    newBackground = Background;
+                }
+
+                if (RootElement.Background != newBackground)
+                {
+                    RootElement.Background = newBackground;
+                }
+            }
+        }
+
+        internal void EnsureFillerVisibility()
+        {
+            if (_cellsElement != null)
+            {
+                _cellsElement.EnsureFillerVisibility();
+            }
+        }
+
+        internal void EnsureGridLines()
+        {
+            if (OwningGrid != null)
+            {
+                if (_bottomGridLine != null)
+                {
+                    // It looks like setting Visibility sometimes has side effects so make sure the value is actually
+                    // diffferent before setting it
+                    bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All;
+
+                    if (newVisibility != _bottomGridLine.IsVisible)
+                    {
+                        _bottomGridLine.IsVisible = newVisibility;
+                    }
+                    _bottomGridLine.Fill = OwningGrid.HorizontalGridLinesBrush;
+                }
+
+                foreach (DataGridCell cell in Cells)
+                {
+                    cell.EnsureGridLine(OwningGrid.ColumnsInternal.LastVisibleColumn);
+                }
+            }
+        }
+
+        // Set the proper style for the Header by walking up the Style hierarchy
+        //TODO Styles
+        internal void EnsureHeaderStyleAndVisibility(Styling.Style previousStyle)
+        {
+            if (_headerElement != null && OwningGrid != null)
+            {
+                _headerElement.IsVisible = OwningGrid.AreRowHeadersVisible;
+            }
+        }
+
+        internal void EnsureHeaderVisibility()
+        {
+            if (_headerElement != null && OwningGrid != null)
+            {
+                _headerElement.IsVisible = OwningGrid.AreRowHeadersVisible;
+            }
+        }
+
+        internal void InvalidateHorizontalArrange()
+        {
+            if (_cellsElement != null)
+            {
+                _cellsElement.InvalidateArrange();
+            }
+            if (_detailsElement != null)
+            {
+                _detailsElement.InvalidateArrange();
+            }
+        }
+
+        internal void ResetGridLine()
+        {
+            _bottomGridLine = null;
+        }
+
+        private void DataGridCellCollection_CellAdded(object sender, DataGridCellEventArgs e)
+        {
+            _cellsElement?.Children.Add(e.Cell);
+        }
+
+        private void DataGridCellCollection_CellRemoved(object sender, DataGridCellEventArgs e)
+        {
+            _cellsElement?.Children.Remove(e.Cell);
+        }
+
+        private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
+        {
+            if(e.MouseButton != MouseButton.Left)
+            {
+                return;
+            }
+
+            if (OwningGrid != null)
+            {
+                OwningGrid.IsDoubleClickRecordsClickOnCall(this);
+                if (OwningGrid.UpdatedStateOnMouseLeftButtonDown)
+                {
+                    OwningGrid.UpdatedStateOnMouseLeftButtonDown = false;
+                }
+                else
+                {
+                    e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, -1, Slot, false);
+                }
+            }
+        }
+
+        private void OnRowDetailsChanged()
+        {
+            OwningGrid?.OnRowDetailsChanged();
+        }
+
+        // Returns the actual template that should be sued for Details: either explicity set on this row 
+        // or inherited from the DataGrid
+        private IDataTemplate ActualDetailsTemplate
+        {
+            get
+            {
+                Debug.Assert(OwningGrid != null);
+                return DetailsTemplate ?? OwningGrid.RowDetailsTemplate;
+            }
+        }
+
+        private bool ActualDetailsVisibility
+        {
+            get
+            {
+                if (OwningGrid == null)
+                {
+                    throw DataGridError.DataGrid.NoOwningGrid(GetType());
+                }
+                if (Index == -1)
+                {
+                    throw DataGridError.DataGridRow.InvalidRowIndexCannotCompleteOperation();
+                }
+                return OwningGrid.GetRowDetailsVisibility(Index);
+            }
+        }
+
+        private void UnloadDetailsTemplate(bool recycle)
+        {
+            if (_detailsElement != null)
+            {
+                if (_detailsContent != null)
+                {
+                    if (_detailsLoaded)
+                    {
+                        OwningGrid.OnUnloadingRowDetails(this, _detailsContent);
+                    }
+                    _detailsContent.DataContext = null;
+                    if (!recycle)
+                    {
+                        _detailsContentSizeSubscription?.Dispose();
+                        _detailsContentSizeSubscription = null;
+                        _detailsContent = null;
+                    }
+                }
+
+                if (!recycle)
+                {
+                    _detailsElement.Children.Clear();
+                }
+                _detailsElement.ContentHeight = 0;
+            }
+            if (!recycle)
+            {
+                _appliedDetailsTemplate = null;
+                SetValueNoCallback(DetailsTemplateProperty, null);
+            }
+
+            _detailsLoaded = false;
+            _appliedDetailsVisibility = null;
+            SetValueNoCallback(AreDetailsVisibleProperty, false);
+        }
+
+        //TODO Animation
+        internal void EnsureDetailsContentHeight()
+        {
+            if ((_detailsElement != null)
+                && (_detailsContent != null)
+                && (double.IsNaN(_detailsContent.Height))
+                && (AreDetailsVisible)
+                && (!double.IsNaN(_detailsDesiredHeight))
+                && !DoubleUtil.AreClose(_detailsContent.Bounds.Height, _detailsDesiredHeight)
+                && Slot != -1)
+            {
+                _detailsDesiredHeight = _detailsContent.Bounds.Height;
+
+                if (true)
+                {
+                    _detailsElement.ContentHeight = _detailsDesiredHeight;
+                }
+            }
+        } 
+
+        // Makes sure the _detailsDesiredHeight is initialized.  We need to measure it to know what
+        // height we want to animate to.  Subsequently, we just update that height in response to SizeChanged
+        private void EnsureDetailsDesiredHeight()
+        {
+            Debug.Assert(_detailsElement != null && OwningGrid != null);
+
+            if (_detailsContent != null)
+            {
+                Debug.Assert(_detailsElement.Children.Contains(_detailsContent));
+
+                _detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                _detailsDesiredHeight = _detailsContent.DesiredSize.Height;
+            }
+            else
+            {
+                _detailsDesiredHeight = 0;
+            }
+        }
+
+        //TODO Cleanup
+        double? _previousDetailsHeight = null;
+
+        //TODO Animation 
+        private void DetailsContent_SizeChanged(Rect newValue)
+        {
+            if (_previousDetailsHeight.HasValue)
+            {
+                var oldValue = _previousDetailsHeight.Value;
+                _previousDetailsHeight = newValue.Height;
+                if (newValue.Height != oldValue && newValue.Height != _detailsDesiredHeight)
+                {
+
+                    if (AreDetailsVisible && _appliedDetailsTemplate != null)
+                    {
+                        // Update the new desired height for RowDetails
+                        _detailsDesiredHeight = newValue.Height;
+
+                        _detailsElement.ContentHeight = newValue.Height;
+
+                        // Calling this when details are not visible invalidates during layout when we have no work 
+                        // to do.  In certain scenarios, this could cause a layout cycle
+                        OnRowDetailsChanged();
+                    }
+                }
+            }
+            else
+            {
+                _previousDetailsHeight = newValue.Height;
+            }
+        }
+
+        //TODO Animation
+        // Sets AreDetailsVisible on the row and animates if necessary
+        internal void SetDetailsVisibilityInternal(bool isVisible, bool raiseNotification, bool animate)
+        {
+            Debug.Assert(OwningGrid != null);
+            Debug.Assert(Index != -1);
+
+            if (_appliedDetailsVisibility != isVisible)
+            {
+                if (_detailsElement == null)
+                {
+                    if (raiseNotification)
+                    {
+                        _detailsVisibilityNotificationPending = true;
+                    }
+                    return;
+                }
+
+                _appliedDetailsVisibility = isVisible;
+                SetValueNoCallback(AreDetailsVisibleProperty, isVisible);
+
+                // Applies a new DetailsTemplate only if it has changed either here or at the DataGrid level
+                ApplyDetailsTemplate(initializeDetailsPreferredHeight: true);
+
+                // no template to show
+                if (_appliedDetailsTemplate == null)
+                {
+                    if (_detailsElement.ContentHeight > 0)
+                    {
+                        _detailsElement.ContentHeight = 0;
+                    }
+                    return;
+                }
+
+                if (AreDetailsVisible)
+                {
+                    // Set the details height directly
+                    _detailsElement.ContentHeight = _detailsDesiredHeight;
+                    _checkDetailsContentHeight = true;
+                }
+                else
+                {
+                    _detailsElement.ContentHeight = 0;
+                }
+
+                OnRowDetailsChanged();
+
+                if (raiseNotification)
+                {
+                    OwningGrid.OnRowDetailsVisibilityChanged(new DataGridRowDetailsEventArgs(this, _detailsContent));
+                }
+            }
+        }
+
+        internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
+        {
+            if (_detailsElement != null && AreDetailsVisible)
+            {
+                IDataTemplate oldDetailsTemplate = _appliedDetailsTemplate;
+                if (ActualDetailsTemplate != null && ActualDetailsTemplate != _appliedDetailsTemplate)
+                {
+                    if (_detailsContent != null)
+                    {
+                        _detailsContentSizeSubscription?.Dispose();
+                        _detailsContentSizeSubscription = null;
+                        if (_detailsLoaded)
+                        {
+                            OwningGrid.OnUnloadingRowDetails(this, _detailsContent);
+                            _detailsLoaded = false;
+                        }
+                    }
+                    _detailsElement.Children.Clear();
+
+                    _detailsContent = ActualDetailsTemplate.Build(DataContext);
+                    _appliedDetailsTemplate = ActualDetailsTemplate;
+
+                    if (_detailsContent != null)
+                    {
+                        _detailsContentSizeSubscription =
+                            _detailsContent.GetObservable(BoundsProperty)
+                                           .Subscribe(DetailsContent_SizeChanged);
+                        _detailsElement.Children.Add(_detailsContent);
+                    }
+                }
+
+                if (_detailsContent != null && !_detailsLoaded)
+                {
+                    _detailsLoaded = true;
+                    _detailsContent.DataContext = DataContext;
+                    OwningGrid.OnLoadingRowDetails(this, _detailsContent);
+                }
+                if (initializeDetailsPreferredHeight && double.IsNaN(_detailsDesiredHeight) &&
+                    _appliedDetailsTemplate != null && _detailsElement.Children.Count > 0)
+                {
+                    EnsureDetailsDesiredHeight();
+                }
+                else if (oldDetailsTemplate == null)
+                {
+                    _detailsDesiredHeight = double.NaN;
+                    EnsureDetailsDesiredHeight();
+                    _detailsElement.ContentHeight = _detailsDesiredHeight;
+                }
+            }
+        }
+
+    }
+
+    //TODO Styles
+
+}

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

@@ -0,0 +1,449 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Media;
+using System;
+using System.Diagnostics;
+using System.Reactive.Linq;
+
+namespace Avalonia.Controls
+{
+    public class DataGridRowGroupHeader : TemplatedControl
+    {
+        private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton";
+        private const string DATAGRIDROWGROUPHEADER_indentSpacer = "IndentSpacer";
+        private const string DATAGRIDROWGROUPHEADER_itemCountElement = "ItemCountElement";
+        private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PropertyNameElement";
+
+        private bool _areIsCheckedHandlersSuspended;
+        private ToggleButton _expanderButton;
+        private DataGridRowHeader _headerElement;
+        private Control _indentSpacer;
+        private TextBlock _itemCountElement;
+        private TextBlock _propertyNameElement;
+        private Panel _rootElement;
+        private double _totalIndent;
+
+        public static readonly StyledProperty<bool> IsItemCountVisibleProperty =
+            AvaloniaProperty.Register<DataGridRowGroupHeader, bool>(nameof(IsItemCountVisible));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the item count is visible.
+        /// </summary>
+        public bool IsItemCountVisible
+        {
+            get { return GetValue(IsItemCountVisibleProperty); }
+            set { SetValue(IsItemCountVisibleProperty, value); }
+        }
+
+        public static readonly StyledProperty<string> PropertyNameProperty =
+            AvaloniaProperty.Register<DataGridRowGroupHeader, string>(nameof(PropertyName));
+
+        /// <summary>
+        /// Gets or sets the name of the property that this <see cref="T:Avalonia.Controls.DataGrid" /> row is bound to. 
+        /// </summary>
+        public string PropertyName
+        {
+            get { return GetValue(PropertyNameProperty); }
+            set { SetValue(PropertyNameProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> IsPropertyNameVisibleProperty =
+            AvaloniaProperty.Register<DataGridRowGroupHeader, bool>(nameof(IsPropertyNameVisible));
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the property name is visible.
+        /// </summary>
+        public bool IsPropertyNameVisible
+        {
+            get { return GetValue(IsPropertyNameVisibleProperty); }
+            set { SetValue(IsPropertyNameVisibleProperty, value); }
+        }
+
+        public static readonly StyledProperty<double> SublevelIndentProperty =
+            AvaloniaProperty.Register<DataGridRowGroupHeader, double>(
+                nameof(SublevelIndent),
+                defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent,
+                validate: ValidateSublevelIndent);
+
+        private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value)
+        {
+            // We don't need to revert to the old value if our input is bad because we never read this property value
+            if (double.IsNaN(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent));
+            }
+            else if (double.IsInfinity(value))
+            {
+                throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent));
+            }
+            else if (value < 0)
+            {
+                throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Gets or sets a value that indicates the amount that the 
+        /// children of the <see cref="T:Avalonia.Controls.RowGroupHeader" /> are indented. 
+        /// </summary>
+        public double SublevelIndent
+        {
+            get { return GetValue(SublevelIndentProperty); }
+            set { SetValue(SublevelIndentProperty, value); }
+        }
+
+        private void OnSublevelIndentChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (OwningGrid != null)
+            {
+                OwningGrid.OnSublevelIndentUpdated(this, (double)e.NewValue);
+            }
+        }
+
+        static DataGridRowGroupHeader()
+        {
+            SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>(x => x.OnSublevelIndentChanged);
+        }
+
+        /// <summary>
+        /// Constructs a DataGridRowGroupHeader
+        /// </summary>
+        public DataGridRowGroupHeader()
+        {
+            //DefaultStyleKey = typeof(DataGridRowGroupHeader);
+            AddHandler(InputElement.PointerPressedEvent, (s, e) => DataGridRowGroupHeader_PointerPressed(e), handledEventsToo: true);
+        }
+
+        internal DataGridRowHeader HeaderCell
+        {
+            get
+            {
+                return _headerElement;
+            }
+        }
+
+        private bool IsCurrent
+        {
+            get
+            {
+                Debug.Assert(OwningGrid != null);
+                return (RowGroupInfo.Slot == OwningGrid.CurrentSlot);
+            }
+        }
+
+        private bool IsMouseOver
+        {
+            get;
+            set;
+        }
+
+        internal bool IsRecycled
+        {
+            get;
+            set;
+        }
+
+        internal int Level
+        {
+            get;
+            set;
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get;
+            set;
+        }
+
+        internal DataGridRowGroupInfo RowGroupInfo
+        {
+            get;
+            set;
+        }
+
+        internal double TotalIndent
+        {
+            set
+            {
+                _totalIndent = value;
+                if (_indentSpacer != null)
+                {
+                    _indentSpacer.Width = _totalIndent;
+                }
+            }
+        }
+
+        private IDisposable _expanderButtonSubscription;
+
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            _rootElement = e.NameScope.Find<Panel>(DataGridRow.DATAGRIDROW_elementRoot);
+
+            _expanderButtonSubscription?.Dispose();
+            _expanderButton = e.NameScope.Find<ToggleButton>(DATAGRIDROWGROUPHEADER_expanderButton);
+            if(_expanderButton != null)
+            {
+                EnsureExpanderButtonIsChecked();
+                _expanderButtonSubscription =
+                    _expanderButton.GetObservable(ToggleButton.IsCheckedProperty)
+                                   .Skip(1)
+                                   .Subscribe(v => OnExpanderButtonIsCheckedChanged(v));
+            }
+
+            _headerElement = e.NameScope.Find<DataGridRowHeader>(DataGridRow.DATAGRIDROW_elementRowHeader);
+            if(_headerElement != null)
+            {
+                _headerElement.Owner = this;
+                EnsureHeaderVisibility();
+            }
+
+            _indentSpacer = e.NameScope.Find<Control>(DATAGRIDROWGROUPHEADER_indentSpacer);
+            if(_indentSpacer != null)
+            {
+                _indentSpacer.Width = _totalIndent;
+            }
+
+            _itemCountElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_itemCountElement);
+            _propertyNameElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_propertyNameElement);
+            UpdateTitleElements();
+
+            base.OnTemplateApplied(e);
+        }
+
+        internal void ApplyHeaderStatus()
+        {
+            if (_headerElement != null && OwningGrid.AreRowHeadersVisible)
+            {
+                _headerElement.ApplyOwnerStatus();
+            }
+        }
+
+        //TODO Implement
+        internal void ApplyState(bool useTransitions)
+        {
+
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+
+            Size size = base.ArrangeOverride(finalSize);
+            if (_rootElement != null)
+            {
+                if (OwningGrid.AreRowGroupHeadersFrozen)
+                {
+                    foreach (Control child in _rootElement.Children)
+                    {
+                        child.Clip = null;
+                    }
+                }
+                else
+                {
+                    double frozenLeftEdge = 0;
+                    foreach (Control child in _rootElement.Children)
+                    {
+                        if (DataGridFrozenGrid.GetIsFrozen(child) && child.IsVisible)
+                        {
+                            TranslateTransform transform = new TranslateTransform();
+                            // Automatic layout rounding doesn't apply to transforms so we need to Round this
+                            transform.X = Math.Round(OwningGrid.HorizontalOffset);
+                            child.RenderTransform = transform;
+
+                            double childLeftEdge = child.Translate(this, new Point(child.Bounds.Width, 0)).X - transform.X;
+                            frozenLeftEdge = Math.Max(frozenLeftEdge, childLeftEdge + OwningGrid.HorizontalOffset);
+                        }
+                    }
+                    // Clip the non-frozen elements so they don't overlap the frozen ones
+                    foreach (Control child in _rootElement.Children)
+                    {
+                        if (!DataGridFrozenGrid.GetIsFrozen(child))
+                        {
+                            EnsureChildClip(child, frozenLeftEdge);
+                        }
+                    }
+                }
+            }
+            return size;
+        }
+
+        internal void ClearFrozenStates()
+        {
+            if (_rootElement != null)
+            {
+                foreach (Control child in _rootElement.Children)
+                {
+                    child.RenderTransform = null;
+                }
+            }
+        }
+
+        //TODO TabStop
+        private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e)
+        {
+            if (OwningGrid != null && e.MouseButton == MouseButton.Left)
+            {
+                if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled)
+                {
+                    ToggleExpandCollapse(!RowGroupInfo.IsVisible, true);
+                    e.Handled = true;
+                }
+                else
+                {
+                    //if (!e.Handled && OwningGrid.IsTabStop)
+                    if (!e.Handled)
+                    {
+                        OwningGrid.Focus();
+                    }
+                    e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false);
+                }
+            }
+        }
+
+        private void EnsureChildClip(Visual child, double frozenLeftEdge)
+        {
+            double childLeftEdge = child.Translate(this, new Point(0, 0)).X;
+            if (frozenLeftEdge > childLeftEdge)
+            {
+                double xClip = Math.Round(frozenLeftEdge - childLeftEdge);
+                var rg = new RectangleGeometry();
+                rg.Rect = 
+                    new Rect(xClip, 0, 
+                        Math.Max(0, child.Bounds.Width - xClip), 
+                        child.Bounds.Height);
+                child.Clip = rg;
+            }
+            else
+            {
+                child.Clip = null;
+            }
+        }
+
+        internal void EnsureExpanderButtonIsChecked()
+        {
+            if (_expanderButton != null && RowGroupInfo != null && RowGroupInfo.CollectionViewGroup != null &&
+                RowGroupInfo.CollectionViewGroup.ItemCount != 0)
+            {
+                SetIsCheckedNoCallBack(RowGroupInfo.IsVisible);
+            }
+        }
+
+        //TODO Styles
+        //internal void EnsureHeaderStyleAndVisibility(Style previousStyle)
+        internal void EnsureHeaderVisibility()
+        {
+            if (_headerElement != null && OwningGrid != null)
+            {
+                _headerElement.IsVisible = OwningGrid.AreColumnHeadersVisible;
+            }
+        }
+
+        private void OnExpanderButtonIsCheckedChanged(bool? value)
+        {
+            if(!_areIsCheckedHandlersSuspended)
+            {
+                ToggleExpandCollapse(value ?? false, true);
+            }
+        }
+
+        internal void LoadVisualsForDisplay()
+        {
+            EnsureExpanderButtonIsChecked();
+            EnsureHeaderVisibility();
+            ApplyState(useTransitions: false);
+            ApplyHeaderStatus();
+        }
+
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            if (IsEnabled)
+            {
+                IsMouseOver = true;
+                ApplyState(useTransitions: true);
+            }
+
+            base.OnPointerEnter(e);
+        }
+
+        protected override void OnPointerLeave(PointerEventArgs e)
+        {
+            if (IsEnabled)
+            {
+                IsMouseOver = false;
+                ApplyState(useTransitions: true);
+            }
+
+            base.OnPointerLeave(e);
+        }
+
+        private void SetIsCheckedNoCallBack(bool value)
+        {
+            if (_expanderButton != null && _expanderButton.IsChecked != value)
+            {
+                _areIsCheckedHandlersSuspended = true;
+                try
+                {
+                    _expanderButton.IsChecked = value;
+                }
+                finally
+                {
+                    _areIsCheckedHandlersSuspended = false;
+                }
+            }
+        }
+
+        internal void ToggleExpandCollapse(bool isVisible, bool setCurrent)
+        {
+            if (RowGroupInfo.CollectionViewGroup.ItemCount != 0)
+            {
+                if (OwningGrid == null)
+                {
+                    // Do these even if the OwningGrid is null in case it could improve the Designer experience for a standalone DataGridRowGroupHeader
+                    RowGroupInfo.IsVisible = isVisible;
+                }
+                else if(RowGroupInfo.IsVisible != isVisible)
+                {
+                    OwningGrid.OnRowGroupHeaderToggled(this, isVisible, setCurrent);
+                }
+
+                EnsureExpanderButtonIsChecked();
+
+                ApplyState(true /*useTransitions*/);
+            }
+        }
+
+        internal void UpdateTitleElements()
+        {
+            if (_propertyNameElement != null)
+            {
+                string txt;
+                if (string.IsNullOrWhiteSpace(PropertyName))
+                    txt = String.Empty;
+                else
+                    txt = String.Format("{0}:", PropertyName);
+                _propertyNameElement.Text = txt;
+            }
+            if (_itemCountElement != null && RowGroupInfo != null && RowGroupInfo.CollectionViewGroup != null)
+            {
+                string formatString;
+                if(RowGroupInfo.CollectionViewGroup.ItemCount == 1)
+                    formatString = "({0} Item)";
+                else
+                    formatString = "({0} Items)";
+
+                _itemCountElement.Text = String.Format(formatString, RowGroupInfo.CollectionViewGroup.ItemCount);
+            }
+        }
+
+    }
+}

+ 57 - 0
src/Avalonia.Controls.DataGrid/DataGridRowGroupInfo.cs

@@ -0,0 +1,57 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Collections;
+
+namespace Avalonia.Controls
+{
+
+    internal class DataGridRowGroupInfo
+    {
+        public DataGridRowGroupInfo(
+            DataGridCollectionViewGroup collectionViewGroup,
+            bool isVisible,
+            int level,
+            int slot,
+            int lastSubItemSlot)
+        {
+            CollectionViewGroup = collectionViewGroup;
+            IsVisible = isVisible;
+            Level = level;
+            Slot = slot;
+            LastSubItemSlot = lastSubItemSlot;
+        }
+
+        public DataGridCollectionViewGroup CollectionViewGroup
+        {
+            get;
+            private set;
+        }
+
+        public int LastSubItemSlot
+        {
+            get;
+            set;
+        }
+
+        public int Level
+        {
+            get;
+            private set;
+        }
+
+        public int Slot
+        {
+            get;
+            set;
+        }
+
+        public bool IsVisible
+        {
+            get;
+            set;
+        }
+    }
+}

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

@@ -0,0 +1,192 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+using Avalonia.Media;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header. 
+    /// </summary>
+    public class DataGridRowHeader : ContentControl
+    {
+        private const string DATAGRIDROWHEADER_elementRootName = "Root";
+        private const double DATAGRIDROWHEADER_separatorThickness = 1;
+
+        private Control _rootElement;
+
+        public static readonly StyledProperty<IBrush> SeparatorBrushProperty =
+            AvaloniaProperty.Register<DataGridRowHeader, IBrush>(nameof(SeparatorBrush));
+
+        public IBrush SeparatorBrush
+        {
+            get { return GetValue(SeparatorBrushProperty); }
+            set { SetValue(SeparatorBrushProperty, value); }
+        }
+
+        public static readonly StyledProperty<bool> AreSeparatorsVisibleProperty =
+            AvaloniaProperty.Register<DataGridRowHeader, bool>(
+                nameof(AreSeparatorsVisible));
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the row header separator lines are visible.
+        /// </summary>
+        public bool AreSeparatorsVisible
+        {
+            get { return GetValue(AreSeparatorsVisibleProperty); }
+            set { SetValue(AreSeparatorsVisibleProperty, value); }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.Primitives.DataGridRowHeader" /> class. 
+        /// </summary>
+        public DataGridRowHeader()
+        {
+            AddHandler(PointerPressedEvent, DataGridRowHeader_PointerPressed, handledEventsToo: true);
+        }
+
+        internal Control Owner
+        {
+            get;
+            set;
+        }
+
+        private DataGridRow OwningRow => Owner as DataGridRow;
+
+        private DataGridRowGroupHeader OwningRowGroupHeader => Owner as DataGridRowGroupHeader;
+
+        private DataGrid OwningGrid
+        {
+            get
+            {
+                if (OwningRow != null)
+                {
+                    return OwningRow.OwningGrid;
+                }
+                else if (OwningRowGroupHeader != null)
+                {
+                    return OwningRowGroupHeader.OwningGrid;
+                }
+                return null;
+            }
+        }
+
+        private int Slot
+        {
+            get
+            {
+                if (OwningRow != null)
+                {
+                    return OwningRow.Slot;
+                }
+                else if (OwningRowGroupHeader != null)
+                {
+                    return OwningRowGroupHeader.RowGroupInfo.Slot;
+                }
+                return -1;
+            }
+        }
+
+        /// <summary>
+        /// Builds the visual tree for the row header when a new template is applied. 
+        /// </summary>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            base.OnTemplateApplied(e);
+
+            _rootElement = e.NameScope.Find<Control>(DATAGRIDROWHEADER_elementRootName);
+            if (_rootElement != null)
+            {
+                ApplyOwnerStatus();
+            }
+        } 
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.Primitives.DataGridRowHeader" /> to prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridRowHeader" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (OwningRow == null || OwningGrid == null)
+            {
+                return base.MeasureOverride(availableSize);
+            }
+            double measureHeight = double.IsNaN(OwningGrid.RowHeight) ? availableSize.Height : OwningGrid.RowHeight;
+            double measureWidth = double.IsNaN(OwningGrid.RowHeaderWidth) ? availableSize.Width : OwningGrid.RowHeaderWidth;
+            Size measuredSize = base.MeasureOverride(new Size(measureWidth, measureHeight));
+
+            // Auto grow the row header or force it to a fixed width based on the DataGrid's setting
+            if (!double.IsNaN(OwningGrid.RowHeaderWidth) || measuredSize.Width < OwningGrid.ActualRowHeaderWidth)
+            {
+                return new Size(OwningGrid.ActualRowHeaderWidth, measuredSize.Height);
+            }
+
+            return measuredSize;
+        }
+
+        //TODO Implement
+        internal void ApplyOwnerStatus()
+        {
+            if (_rootElement != null && Owner != null && Owner.IsVisible)
+            {
+
+            }
+        }
+
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            if (OwningRow != null)
+            {
+                OwningRow.IsMouseOver = true;
+            }
+
+            base.OnPointerEnter(e);
+        }
+        protected override void OnPointerLeave(PointerEventArgs e)
+        {
+            if (OwningRow != null)
+            {
+                OwningRow.IsMouseOver = false;
+            }
+
+            base.OnPointerLeave(e);
+        }
+
+        //TODO TabStop
+        private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if(e.MouseButton != MouseButton.Left)
+            {
+                return;
+            }
+
+            if (OwningGrid != null)
+            {
+                if (!e.Handled)
+                //if (!e.Handled && OwningGrid.IsTabStop)
+                {
+                    OwningGrid.Focus();
+                }
+                if (OwningRow != null)
+                {
+                    Debug.Assert(sender is DataGridRowHeader);
+                    Debug.Assert(sender == this);
+                    e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, -1, Slot, false);
+                    OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
+                }
+            }
+        } 
+
+    }
+
+}
+

+ 3027 - 0
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@@ -0,0 +1,3027 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Collections;
+using Avalonia.Controls.Utils;
+using Avalonia.Media;
+using Avalonia.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Avalonia.Controls
+{
+    public partial class DataGrid
+    {
+
+        internal bool AreRowBottomGridLinesRequired
+        {
+            get
+            {
+                return (GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || GridLinesVisibility == DataGridGridLinesVisibility.All) && HorizontalGridLinesBrush != null;
+            }
+        }
+
+        internal int FirstVisibleSlot
+        {
+            get
+            {
+                return (SlotCount > 0) ? GetNextVisibleSlot(-1) : -1;
+            }
+        }
+
+        internal int FrozenColumnCountWithFiller
+        {
+            get
+            {
+                int count = FrozenColumnCount;
+                if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && (AreRowGroupHeadersFrozen || count > 0))
+                {
+                    // Either the RowGroupHeaders are frozen by default or the user set a frozen column count.  In both cases, we need to freeze
+                    // one more column than the what the public value says
+                    count++;
+                }
+                return count;
+            }
+        }
+
+        internal int LastVisibleSlot
+        {
+            get
+            {
+                return (SlotCount > 0) ? GetPreviousVisibleSlot(SlotCount) : -1;
+            }
+        }
+
+        // Cumulated height of all known rows, including the gridlines and details section.
+        // This property returns an approximation of the actual total row heights and also
+        // updates the RowHeightEstimate
+        private double EdgedRowsHeightCalculated
+        {
+            get
+            {
+                // If we're not displaying any rows or if we have infinite space the, relative height of our rows is 0
+                if (DisplayData.LastScrollingSlot == -1 || double.IsPositiveInfinity(AvailableSlotElementRoom))
+                {
+                    return 0;
+                }
+                Debug.Assert(DisplayData.LastScrollingSlot >= 0);
+                Debug.Assert(_verticalOffset >= 0);
+                Debug.Assert(NegVerticalOffset >= 0);
+
+                // Height of all rows above the viewport
+                double totalRowsHeight = _verticalOffset - NegVerticalOffset;
+
+                // Add the height of all the rows currently displayed, AvailableRowRoom
+                // is not always up to date enough for this
+                foreach (Control element in DisplayData.GetScrollingElements())
+                {
+                    if (element is DataGridRow row)
+                    {
+                        totalRowsHeight += row.TargetHeight;
+                    }
+                    else
+                    {
+                        totalRowsHeight += element.DesiredSize.Height;
+                    }
+                }
+
+                // Details up to and including viewport
+                int detailsCount = GetDetailsCountInclusive(0, DisplayData.LastScrollingSlot);
+
+                // Subtract details that were accounted for from the totalRowsHeight
+                totalRowsHeight -= detailsCount * RowDetailsHeightEstimate;
+
+                // Update the RowHeightEstimate if we have more row information
+                if (DisplayData.LastScrollingSlot >= _lastEstimatedRow)
+                {
+                    _lastEstimatedRow = DisplayData.LastScrollingSlot;
+                    RowHeightEstimate = totalRowsHeight / (_lastEstimatedRow + 1 - _collapsedSlotsTable.GetIndexCount(0, _lastEstimatedRow));
+                }
+
+                // Calculate estimates for what's beyond the viewport
+                if (VisibleSlotCount > DisplayData.NumDisplayedScrollingElements)
+                {
+                    int remainingRowCount = (SlotCount - DisplayData.LastScrollingSlot - _collapsedSlotsTable.GetIndexCount(DisplayData.LastScrollingSlot, SlotCount - 1) - 1);
+
+                    // Add estimation for the cell heights of all rows beyond our viewport
+                    totalRowsHeight += RowHeightEstimate * remainingRowCount;
+
+                    // Add the rest of the details beyond the viewport
+                    detailsCount += GetDetailsCountInclusive(DisplayData.LastScrollingSlot + 1, SlotCount - 1);
+                }
+
+                // 
+                double totalDetailsHeight = detailsCount * RowDetailsHeightEstimate;
+
+                return totalRowsHeight + totalDetailsHeight;
+            }
+        }
+
+        /// <summary>
+        /// Clears the entire selection. Displayed rows are deselected explicitly to visualize
+        /// potential transition effects
+        /// </summary>
+        internal void ClearRowSelection(bool resetAnchorSlot)
+        {
+            if (resetAnchorSlot)
+            {
+                AnchorSlot = -1;
+            }
+            if (_selectedItems.Count > 0)
+            {
+                _noSelectionChangeCount++;
+                try
+                {
+                    // Individually deselecting displayed rows to view potential transitions
+                    for (int slot = DisplayData.FirstScrollingSlot;
+                         slot > -1 && slot <= DisplayData.LastScrollingSlot;
+                         slot++)
+                    {
+                        if (DisplayData.GetDisplayedElement(slot) is DataGridRow row)
+                        {
+                            if (_selectedItems.ContainsSlot(row.Slot))
+                            {
+                                SelectSlot(row.Slot, false);
+                            }
+                        }
+                    }
+                    _selectedItems.ClearRows();
+                    SelectionHasChanged = true;
+                }
+                finally
+                {
+                    NoSelectionChangeCount--;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Clears the entire selection except the indicated row. Displayed rows are deselected explicitly to 
+        /// visualize potential transition effects. The row indicated is selected if it is not already.
+        /// </summary>
+        internal void ClearRowSelection(int slotException, bool setAnchorSlot)
+        {
+            _noSelectionChangeCount++;
+            try
+            {
+                bool exceptionAlreadySelected = false;
+                if (_selectedItems.Count > 0)
+                {
+                    // Individually deselecting displayed rows to view potential transitions
+                    for (int slot = DisplayData.FirstScrollingSlot;
+                         slot > -1 && slot <= DisplayData.LastScrollingSlot;
+                         slot++)
+                    {
+                        if (slot != slotException && _selectedItems.ContainsSlot(slot))
+                        {
+                            SelectSlot(slot, false);
+                            SelectionHasChanged = true;
+                        }
+                    }
+                    exceptionAlreadySelected = _selectedItems.ContainsSlot(slotException);
+                    int selectedCount = _selectedItems.Count;
+                    if (selectedCount > 0)
+                    {
+                        if (selectedCount > 1)
+                        {
+                            SelectionHasChanged = true;
+                        }
+                        else
+                        {
+                            int currentlySelectedSlot = _selectedItems.GetIndexes().First();
+                            if (currentlySelectedSlot != slotException)
+                            {
+                                SelectionHasChanged = true;
+                            }
+                        }
+                        _selectedItems.ClearRows();
+                    }
+                }
+                if (exceptionAlreadySelected)
+                {
+                    // Exception row was already selected. It just needs to be marked as selected again.
+                    // No transition involved.
+                    _selectedItems.SelectSlot(slotException, true /*select*/);
+                    if (setAnchorSlot)
+                    {
+                        AnchorSlot = slotException;
+                    }
+                }
+                else
+                {
+                    // Exception row was not selected. It needs to be selected with potential transition
+                    SetRowSelection(slotException, true /*isSelected*/, setAnchorSlot);
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+        }
+
+        internal int GetCollapsedSlotCount(int startSlot, int endSlot)
+        {
+            return _collapsedSlotsTable.GetIndexCount(startSlot, endSlot);
+        }
+
+        internal int GetNextVisibleSlot(int slot)
+        {
+            return _collapsedSlotsTable.GetNextGap(slot);
+        }
+
+        internal int GetPreviousVisibleSlot(int slot)
+        {
+            return _collapsedSlotsTable.GetPreviousGap(slot);
+        }
+
+        /// <summary>
+        /// Returns the row associated to the provided backend data item.
+        /// </summary>
+        /// <param name="dataItem">backend data item</param>
+        /// <returns>null if the DataSource is null, the provided item in not in the source, or the item is not displayed; otherwise, the associated Row</returns>
+        internal DataGridRow GetRowFromItem(object dataItem)
+        {
+            int rowIndex = DataConnection.IndexOf(dataItem);
+            if (rowIndex < 0)
+            {
+                return null;
+            }
+            int slot = SlotFromRowIndex(rowIndex);
+            return IsSlotVisible(slot) ? DisplayData.GetDisplayedElement(slot) as DataGridRow : null;
+        }
+
+        internal bool GetRowSelection(int slot)
+        {
+            Debug.Assert(slot != -1);
+            return _selectedItems.ContainsSlot(slot);
+        }
+
+        internal void InsertElementAt(int slot, int rowIndex, object item, DataGridRowGroupInfo groupInfo, bool isCollapsed)
+        {
+            Debug.Assert(slot >= 0 && slot <= SlotCount);
+
+            bool isRow = rowIndex != -1;
+            if (isCollapsed)
+            {
+                InsertElement(slot, 
+                    element: null, 
+                    updateVerticalScrollBarOnly: true,
+                    isCollapsed: true, 
+                    isRow: isRow);
+            }
+            else if (SlotIsDisplayed(slot))
+            {
+                // Row at that index needs to be displayed
+                if (isRow)
+                {
+                    InsertElement(slot, GenerateRow(rowIndex, slot, item), false /*updateVerticalScrollBarOnly*/, false /*isCollapsed*/, isRow);
+                }
+                else
+                {
+                    InsertElement(slot, GenerateRowGroupHeader(slot, groupInfo), 
+                        updateVerticalScrollBarOnly: false,
+                        isCollapsed: false, 
+                        isRow: isRow);
+                }
+            }
+            else
+            {
+                InsertElement(slot, 
+                    element: null,
+                    updateVerticalScrollBarOnly: _vScrollBar == null || _vScrollBar.IsVisible,
+                    isCollapsed: false, 
+                    isRow: isRow);
+            }
+        }
+
+        internal void InsertRowAt(int rowIndex)
+        {
+            int slot = SlotFromRowIndex(rowIndex);
+            object item = DataConnection.GetDataItem(rowIndex);
+
+            // isCollapsed below is always false because we only use the method if we're not grouping
+            InsertElementAt(slot, rowIndex, item, null/*DataGridRowGroupInfo*/, false /*isCollapsed*/);
+        }
+
+        internal bool IsColumnDisplayed(int columnIndex)
+        {
+            return columnIndex >= FirstDisplayedNonFillerColumnIndex && columnIndex <= DisplayData.LastTotallyDisplayedScrollingCol;
+        }
+
+        internal bool IsRowRecyclable(DataGridRow row)
+        {
+            return (row != EditingRow && row != _focusedRow);
+        }
+
+        internal bool IsSlotVisible(int slot)
+        {
+            return slot >= DisplayData.FirstScrollingSlot
+               && slot <= DisplayData.LastScrollingSlot
+               && slot != -1
+               && !_collapsedSlotsTable.Contains(slot);
+        }
+
+        internal void OnRowsMeasure()
+        {
+            if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
+            {
+                ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight);
+                DisplayData.PendingVerticalScrollHeight = 0;
+            }
+        }
+
+        internal void RefreshRows(bool recycleRows, bool clearRows)
+        {
+            if (_measured)
+            {
+                // _desiredCurrentColumnIndex is used in MakeFirstDisplayedCellCurrentCell to set the
+                // column position back to what it was before the refresh
+                _desiredCurrentColumnIndex = CurrentColumnIndex;
+                double verticalOffset = _verticalOffset;
+                if (DisplayData.PendingVerticalScrollHeight > 0)
+                {
+                    // Use the pending vertical scrollbar position if there is one, in the case that the collection
+                    // has been reset multiple times in a row.
+                    verticalOffset = DisplayData.PendingVerticalScrollHeight;
+                }
+                _verticalOffset = 0;
+                NegVerticalOffset = 0;
+
+                if (clearRows)
+                {
+                    ClearRows(recycleRows);
+                    ClearRowGroupHeadersTable();
+                    PopulateRowGroupHeadersTable();
+                }
+
+                RefreshRowGroupHeaders();
+
+                // Update the CurrentSlot because it might have changed
+                if (recycleRows && DataConnection.CollectionView != null)
+                {
+                    CurrentSlot = DataConnection.CollectionView.CurrentPosition == -1
+                        ? -1 : SlotFromRowIndex(DataConnection.CollectionView.CurrentPosition);
+                    if (CurrentSlot == -1)
+                    {
+                        SetCurrentCellCore(-1, -1);
+                    }
+                }
+
+                if (DataConnection != null && ColumnsItemsInternal.Count > 0)
+                {
+                    AddSlots(DataConnection.Count);
+                    AddSlots(DataConnection.Count + RowGroupHeadersTable.IndexCount);
+
+                    InvalidateMeasure();
+                }
+
+                EnsureRowGroupSpacerColumn();
+
+                if (VerticalScrollBar != null)
+                {
+                    DisplayData.PendingVerticalScrollHeight = Math.Min(verticalOffset, VerticalScrollBar.Maximum);
+                }
+            }
+            else
+            {
+                if (clearRows)
+                {
+                    ClearRows(recycleRows);
+                }
+                ClearRowGroupHeadersTable();
+                PopulateRowGroupHeadersTable();
+            }
+        }
+
+        internal void RemoveRowAt(int rowIndex, object item)
+        {
+            RemoveElementAt(SlotFromRowIndex(rowIndex), item, true);
+        }
+
+        internal int RowIndexFromSlot(int slot)
+        {
+            return slot - RowGroupHeadersTable.GetIndexCount(0, slot);
+        }
+
+        internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally)
+        {
+            Debug.Assert(_collapsedSlotsTable.Contains(slot) || !IsSlotOutOfBounds(slot));
+
+            if (scrolledHorizontally && DisplayData.FirstScrollingSlot <= slot && DisplayData.LastScrollingSlot >= slot)
+            {
+                // If the slot is displayed and we scrolled horizontally, column virtualization could cause the rows to grow.
+                // As a result we need to force measure on the rows we're displaying and recalculate our First and Last slots 
+                // so they're accurate
+                foreach (DataGridRow row in DisplayData.GetScrollingRows())
+                {
+                    row.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                }
+                UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
+            }
+
+            if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot)
+            {
+                // The row is already displayed in its entirety
+                return true;
+            }
+            else if (DisplayData.FirstScrollingSlot == slot && slot != -1)
+            {
+                if (!DoubleUtil.IsZero(NegVerticalOffset))
+                {
+                    // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0.
+                    DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset;
+                    InvalidateRowsMeasure(false /*invalidateIndividualRows*/);
+                }
+                return true;
+            }
+
+            double deltaY = 0;
+            int firstFullSlot;
+            if (DisplayData.FirstScrollingSlot > slot)
+            {
+                // Scroll up to the new row so it becomes the first displayed row
+                firstFullSlot = DisplayData.FirstScrollingSlot - 1;
+                if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
+                {
+                    deltaY = -NegVerticalOffset;
+                }
+                deltaY -= GetSlotElementsHeight(slot, firstFullSlot);
+                if (DisplayData.FirstScrollingSlot - slot > 1)
+                {
+                    // 
+
+                    ResetDisplayedRows();
+                }
+                NegVerticalOffset = 0;
+                UpdateDisplayedRows(slot, CellsHeight);
+            }
+            else if (DisplayData.LastScrollingSlot <= slot)
+            {
+                // Scroll down to the new row so it's entirely displayed.  If the height of the row
+                // is greater than the height of the DataGrid, then show the top of the row at the top
+                // of the grid
+                firstFullSlot = DisplayData.LastScrollingSlot;
+                // Figure out how much of the last row is cut off
+                double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot);
+                double availableHeight = AvailableSlotElementRoom + rowHeight;
+                if (DoubleUtil.AreClose(rowHeight, availableHeight))
+                {
+                    if (DisplayData.LastScrollingSlot == slot)
+                    {
+                        // We're already at the very bottom so we don't need to scroll down further
+                        return true;
+                    }
+                    else
+                    {
+                        // We're already showing the entire last row so don't count it as part of the delta
+                        firstFullSlot++;
+                    }
+                }
+                else if (rowHeight > availableHeight)
+                {
+                    firstFullSlot++;
+                    deltaY += rowHeight - availableHeight;
+                }
+                // sum up the height of the rest of the full rows
+                if (slot >= firstFullSlot)
+                {
+                    deltaY += GetSlotElementsHeight(firstFullSlot, slot);
+                }
+                // If the first row we're displaying is no longer adjacent to the rows we have
+                // simply discard the ones we have
+                if (slot - DisplayData.LastScrollingSlot > 1)
+                {
+                    ResetDisplayedRows();
+                }
+                if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
+                {
+                    // The entire row won't fit in the DataGrid so we start showing it from the top
+                    NegVerticalOffset = 0;
+                    UpdateDisplayedRows(slot, CellsHeight);
+                }
+                else
+                {
+                    UpdateDisplayedRowsFromBottom(slot);
+                }
+            }
+
+            _verticalOffset += deltaY;
+            if (_verticalOffset < 0 || DisplayData.FirstScrollingSlot == 0)
+            {
+                // We scrolled too far because a row's height was larger than its approximation
+                _verticalOffset = NegVerticalOffset;
+            }
+
+            // 
+            Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset));
+
+            SetVerticalOffset(_verticalOffset);
+
+            InvalidateMeasure();
+            InvalidateRowsMeasure(false /*invalidateIndividualRows*/);
+
+            return true;
+        }
+
+        internal void SetRowSelection(int slot, bool isSelected, bool setAnchorSlot)
+        {
+            Debug.Assert(!(!isSelected && setAnchorSlot));
+            Debug.Assert(!IsSlotOutOfSelectionBounds(slot));
+            _noSelectionChangeCount++;
+            try
+            {
+                if (SelectionMode == DataGridSelectionMode.Single && isSelected)
+                {
+                    Debug.Assert(_selectedItems.Count <= 1);
+                    if (_selectedItems.Count > 0)
+                    {
+                        int currentlySelectedSlot = _selectedItems.GetIndexes().First();
+                        if (currentlySelectedSlot != slot)
+                        {
+                            SelectSlot(currentlySelectedSlot, false);
+                            SelectionHasChanged = true;
+                        }
+                    }
+                }
+                if (_selectedItems.ContainsSlot(slot) != isSelected)
+                {
+                    SelectSlot(slot, isSelected);
+                    SelectionHasChanged = true;
+                }
+                if (setAnchorSlot)
+                {
+                    AnchorSlot = slot;
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+        }
+
+        // For now, all scenarios are for isSelected == true.
+        internal void SetRowsSelection(int startSlot, int endSlot /*, bool isSelected*/)
+        {
+            Debug.Assert(startSlot >= 0 && startSlot < SlotCount);
+            Debug.Assert(endSlot >= 0 && endSlot < SlotCount);
+            Debug.Assert(startSlot <= endSlot);
+
+            _noSelectionChangeCount++;
+            try
+            {
+                if (/*isSelected &&*/ !_selectedItems.ContainsAll(startSlot, endSlot))
+                {
+                    // At least one row gets selected
+                    SelectSlots(startSlot, endSlot, true);
+                    SelectionHasChanged = true;
+                }
+            }
+            finally
+            {
+                NoSelectionChangeCount--;
+            }
+        }
+
+        internal int SlotFromRowIndex(int rowIndex)
+        {
+            return rowIndex + RowGroupHeadersTable.GetIndexCountBeforeGap(0, rowIndex);
+        }
+
+        private void AddSlotElement(int slot, Control element)
+        {
+#if DEBUG
+            if (element is DataGridRow row)
+            {
+                Debug.Assert(row.OwningGrid == this);
+                Debug.Assert(row.Cells.Count == ColumnsItemsInternal.Count);
+
+                int columnIndex = 0;
+                foreach (DataGridCell dataGridCell in row.Cells)
+                {
+                    Debug.Assert(dataGridCell.OwningRow == row);
+                    Debug.Assert(dataGridCell.OwningColumn == ColumnsItemsInternal[columnIndex]);
+                    columnIndex++;
+                }
+            }
+#endif
+            Debug.Assert(slot == SlotCount);
+
+            OnAddedElement_Phase1(slot, element);
+            SlotCount++;
+            VisibleSlotCount++;
+            OnAddedElement_Phase2(slot, updateVerticalScrollBarOnly: false);
+            OnElementsChanged(grew: true);
+        }
+
+        private void AddSlots(int totalSlots)
+        {
+            SlotCount = 0;
+            VisibleSlotCount = 0;
+            IEnumerator<int> groupSlots = null;
+            int nextGroupSlot = -1;
+            if (RowGroupHeadersTable.RangeCount > 0)
+            {
+                groupSlots = RowGroupHeadersTable.GetIndexes().GetEnumerator();
+                if (groupSlots != null && groupSlots.MoveNext())
+                {
+                    nextGroupSlot = groupSlots.Current;
+                }
+            }
+            int slot = 0;
+            int addedRows = 0;
+            while (slot < totalSlots && AvailableSlotElementRoom > 0)
+            {
+                if (slot == nextGroupSlot)
+                {
+                    DataGridRowGroupInfo groupRowInfo = RowGroupHeadersTable.GetValueAt(slot);
+                    AddSlotElement(slot, GenerateRowGroupHeader(slot, groupRowInfo));
+                    nextGroupSlot = groupSlots.MoveNext() ? groupSlots.Current : -1;
+                }
+                else
+                {
+                    AddSlotElement(slot, GenerateRow(addedRows, slot));
+                    addedRows++;
+                }
+                slot++;
+            }
+
+            if (slot < totalSlots)
+            {
+                SlotCount += totalSlots - slot;
+                VisibleSlotCount += totalSlots - slot;
+                OnAddedElement_Phase2(0,
+                    updateVerticalScrollBarOnly: _vScrollBar == null || _vScrollBar.IsVisible);
+                OnElementsChanged(grew: true);
+            }
+        }
+
+        private void ApplyDisplayedRowsState(int startSlot, int endSlot)
+        {
+            int firstSlot = Math.Max(DisplayData.FirstScrollingSlot, startSlot);
+            int lastSlot = Math.Min(DisplayData.LastScrollingSlot, endSlot);
+
+            if (firstSlot >= 0)
+            {
+                Debug.Assert(lastSlot >= firstSlot);
+                int slot = GetNextVisibleSlot(firstSlot - 1);
+                while (slot <= lastSlot)
+                {
+                    if (DisplayData.GetDisplayedElement(slot) is DataGridRow row)
+                    {
+                        row.UpdatePseudoClasses(); ;
+                    }
+                    slot = GetNextVisibleSlot(slot);
+                }
+            }
+        }
+
+        private void ClearRows(bool recycle)
+        {
+            // Need to clean up recycled rows even if the RowCount is 0
+            SetCurrentCellCore(-1, -1, commitEdit: false, endRowEdit: false);
+            ClearRowSelection(resetAnchorSlot: true);
+            UnloadElements(recycle);
+
+            _showDetailsTable.Clear();
+            SlotCount = 0;
+            NegVerticalOffset = 0;
+            SetVerticalOffset(0);
+            ComputeScrollBarsLayout();
+        }
+
+        // Updates _collapsedSlotsTable and returns the number of pixels that were collapsed
+        private double CollapseSlotsInTable(int startSlot, int endSlot, ref int slotsExpanded, int lastDisplayedSlot, ref double heightChangeBelowLastDisplayedSlot)
+        {
+            int firstSlot = startSlot;
+            int lastSlot;
+            double totalHeightChange = 0;
+            // Figure out which slots actually need to be expanded since some might already be collapsed
+            while (firstSlot <= endSlot)
+            {
+                firstSlot = _collapsedSlotsTable.GetNextGap(firstSlot - 1);
+                int nextCollapsedSlot = _collapsedSlotsTable.GetNextIndex(firstSlot) - 1;
+                lastSlot = nextCollapsedSlot == -2 ? endSlot : Math.Min(endSlot, nextCollapsedSlot);
+
+                if (firstSlot <= lastSlot)
+                {
+                    double heightChange = GetHeightEstimate(firstSlot, lastSlot);
+                    totalHeightChange -= heightChange;
+                    slotsExpanded -= lastSlot - firstSlot + 1;
+
+                    if (lastSlot > lastDisplayedSlot)
+                    {
+                        if (firstSlot > lastDisplayedSlot)
+                        {
+                            heightChangeBelowLastDisplayedSlot -= heightChange;
+                        }
+                        else
+                        {
+                            heightChangeBelowLastDisplayedSlot -= GetHeightEstimate(lastDisplayedSlot + 1, lastSlot);
+                        }
+                    }
+
+                    firstSlot = lastSlot + 1;
+                }
+            }
+
+            // Update _collapsedSlotsTable in one bulk operation
+            _collapsedSlotsTable.AddValues(startSlot, endSlot - startSlot + 1, false);
+
+            return totalHeightChange;
+        }
+
+        private static void CorrectRowAfterDeletion(DataGridRow row, bool rowDeleted)
+        {
+            row.Slot--;
+            if (rowDeleted)
+            {
+                row.Index--;
+            }
+        }
+
+        private static void CorrectRowAfterInsertion(DataGridRow row, bool rowInserted)
+        {
+            row.Slot++;
+            if (rowInserted)
+            {
+                row.Index++;
+            }
+        }
+
+        /// <summary>
+        /// Adjusts the index of all displayed, loaded and edited rows after a row was deleted.
+        /// Removes the deleted row from the list of loaded rows if present.
+        /// </summary>
+        private void CorrectSlotsAfterDeletion(int slotDeleted, bool wasRow)
+        {
+            Debug.Assert(slotDeleted >= 0);
+
+            // Take care of the non-visible loaded rows
+            for (int index = 0; index < _loadedRows.Count;)
+            {
+                DataGridRow dataGridRow = _loadedRows[index];
+                if (IsSlotVisible(dataGridRow.Slot))
+                {
+                    index++;
+                }
+                else
+                {
+                    if (dataGridRow.Slot > slotDeleted)
+                    {
+                        CorrectRowAfterDeletion(dataGridRow, wasRow);
+                        index++;
+                    }
+                    else if (dataGridRow.Slot == slotDeleted)
+                    {
+                        _loadedRows.RemoveAt(index);
+                    }
+                    else
+                    {
+                        index++;
+                    }
+                }
+            }
+
+            // Take care of the non-visible edited row
+            if (EditingRow != null &&
+                !IsSlotVisible(EditingRow.Slot) &&
+                EditingRow.Slot > slotDeleted)
+            {
+                CorrectRowAfterDeletion(EditingRow, wasRow);
+            }
+
+            // Take care of the non-visible focused row
+            if (_focusedRow != null &&
+                _focusedRow != EditingRow &&
+                !IsSlotVisible(_focusedRow.Slot) &&
+                _focusedRow.Slot > slotDeleted)
+            {
+                CorrectRowAfterDeletion(_focusedRow, wasRow);
+            }
+
+            // Take care of the visible rows
+            foreach (DataGridRow row in DisplayData.GetScrollingRows())
+            {
+                if (row.Slot > slotDeleted)
+                {
+                    CorrectRowAfterDeletion(row, wasRow);
+                    row.EnsureBackground();
+                }
+            }
+
+            // Update the RowGroupHeaders
+            foreach (int slot in RowGroupHeadersTable.GetIndexes())
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (rowGroupInfo.Slot > slotDeleted)
+                {
+                    rowGroupInfo.Slot--;
+                }
+                if (rowGroupInfo.LastSubItemSlot >= slotDeleted)
+                {
+                    rowGroupInfo.LastSubItemSlot--;
+                }
+            }
+
+            // Update which row we've calculated the RowHeightEstimate up to
+            if (_lastEstimatedRow >= slotDeleted)
+            {
+                _lastEstimatedRow--;
+            }
+        }
+
+        /// <summary>
+        /// Adjusts the index of all displayed, loaded and edited rows after rows were deleted.
+        /// </summary>
+        private void CorrectSlotsAfterInsertion(int slotInserted, bool isCollapsed, bool rowInserted)
+        {
+            Debug.Assert(slotInserted >= 0);
+
+            // Take care of the non-visible loaded rows
+            foreach (DataGridRow dataGridRow in _loadedRows)
+            {
+                if (!IsSlotVisible(dataGridRow.Slot) && dataGridRow.Slot >= slotInserted)
+                {
+                    DataGrid.CorrectRowAfterInsertion(dataGridRow, rowInserted);
+                }
+            }
+
+            // Take care of the non-visible focused row
+            if (_focusedRow != null &&
+                _focusedRow != EditingRow &&
+                !(IsSlotVisible(_focusedRow.Slot) || ((_focusedRow.Slot == slotInserted) && isCollapsed)) &&
+                _focusedRow.Slot >= slotInserted)
+            {
+                DataGrid.CorrectRowAfterInsertion(_focusedRow, rowInserted);
+            }
+
+            // Take care of the visible rows
+            foreach (DataGridRow row in DisplayData.GetScrollingRows())
+            {
+                if (row.Slot >= slotInserted)
+                {
+                    DataGrid.CorrectRowAfterInsertion(row, rowInserted);
+                    row.EnsureBackground();
+                }
+            }
+
+            // Re-calculate the EditingRow's Slot and Index and ensure that it is still selected.
+            if (EditingRow != null)
+            {
+                EditingRow.Index = DataConnection.IndexOf(EditingRow.DataContext);
+                EditingRow.Slot = SlotFromRowIndex(EditingRow.Index);
+            }
+
+            // Update the RowGroupHeaders
+            foreach (int slot in RowGroupHeadersTable.GetIndexes(slotInserted))
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (rowGroupInfo.Slot >= slotInserted)
+                {
+                    rowGroupInfo.Slot++;
+                }
+
+                // We are purposefully checking GT and not GTE because the equality case is handled
+                // by the CorrectLastSubItemSlotsAfterInsertion method
+                if (rowGroupInfo.LastSubItemSlot > slotInserted)
+                {
+                    rowGroupInfo.LastSubItemSlot++;
+                }
+            }
+
+            // Update which row we've calculated the RowHeightEstimate up to
+            if (_lastEstimatedRow >= slotInserted)
+            {
+                _lastEstimatedRow++;
+            }
+        }
+
+        private IEnumerable<DataGridRow> GetAllRows()
+        {
+            if (_rowsPresenter != null)
+            {
+                foreach (Control element in _rowsPresenter.Children)
+                {
+                    if (element is DataGridRow row)
+                    {
+                        yield return row;
+                    }
+                }
+            }
+        }
+
+        // Expands slots from startSlot to endSlot inclusive and adds the amount expanded in this suboperation to
+        // the given totalHeightChanged of the entire operation
+        private void ExpandSlots(int startSlot, int endSlot, bool isDisplayed, ref int slotsExpanded, ref double totalHeightChange)
+        {
+            double heightAboveStartSlot = 0;
+            if (isDisplayed)
+            {
+                int slot = DisplayData.FirstScrollingSlot;
+                while (slot < startSlot)
+                {
+                    heightAboveStartSlot += GetExactSlotElementHeight(slot);
+                    slot = GetNextVisibleSlot(slot);
+                }
+
+                // First make the bottom rows available for recycling so we minimize element creation when expanding
+                for (int i = 0; (i < endSlot - startSlot + 1) && (DisplayData.LastScrollingSlot > endSlot); i++)
+                {
+                    RemoveDisplayedElement(DisplayData.LastScrollingSlot, wasDeleted: false, updateSlotInformation: true);
+                }
+            }
+
+            // Figure out which slots actually need to be expanded since some might already be collapsed
+            double currentHeightChange = 0;
+            int firstSlot = startSlot;
+            int lastSlot = endSlot;
+            while (firstSlot <= endSlot)
+            {
+                firstSlot = _collapsedSlotsTable.GetNextIndex(firstSlot - 1);
+                if (firstSlot == -1)
+                {
+                    break;
+                }
+                lastSlot = Math.Min(endSlot, _collapsedSlotsTable.GetNextGap(firstSlot) - 1);
+
+                if (firstSlot <= lastSlot)
+                {
+                    if (!isDisplayed)
+                    {
+                        // Estimate the height change if the slots aren't displayed.  If they are displayed, we can add real values
+                        double rowCount = lastSlot - firstSlot - GetRowGroupHeaderCount(firstSlot, lastSlot, false, out double headerHeight) + 1;
+                        double detailsCount = GetDetailsCountInclusive(firstSlot, lastSlot);
+                        currentHeightChange += headerHeight + (detailsCount * RowDetailsHeightEstimate) + (rowCount * RowHeightEstimate);
+                    }
+                    slotsExpanded += lastSlot - firstSlot + 1;
+                    firstSlot = lastSlot + 1;
+                }
+            }
+
+            // Update _collapsedSlotsTable in one bulk operation
+            _collapsedSlotsTable.RemoveValues(startSlot, endSlot - startSlot + 1);
+
+            if (isDisplayed)
+            {
+                double availableHeight = CellsHeight - heightAboveStartSlot;
+                // Actually expand the displayed slots up to what we can display
+                for (int i = startSlot; (i <= endSlot) && (currentHeightChange < availableHeight); i++)
+                {
+                    Control insertedElement = InsertDisplayedElement(i, updateSlotInformation: false);
+                    currentHeightChange += insertedElement.DesiredSize.Height;
+                    if (i > DisplayData.LastScrollingSlot)
+                    {
+                        DisplayData.LastScrollingSlot = i;
+                    }
+                }
+            }
+
+            // Update the total height for the entire Expand operation
+            totalHeightChange += currentHeightChange;
+        }
+
+        /// <summary>
+        /// Creates all the editing elements for the current editing row, so the bindings
+        /// all exist during validation.
+        /// </summary>
+        private void GenerateEditingElements()
+        {
+            if (EditingRow != null && EditingRow.Cells != null)
+            {
+                Debug.Assert(EditingRow.Cells.Count == ColumnsItemsInternal.Count);
+                foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && !c.IsReadOnly))
+                {
+                    column.GenerateEditingElementInternal(EditingRow.Cells[column.Index], EditingRow.DataContext);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event.
+        /// </summary>
+        private DataGridRow GenerateRow(int rowIndex, int slot)
+        {
+            return GenerateRow(rowIndex, slot, DataConnection.GetDataItem(rowIndex));
+        }
+
+        /// <summary>
+        /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event.
+        /// </summary>
+        private DataGridRow GenerateRow(int rowIndex, int slot, object dataContext)
+        {
+            Debug.Assert(rowIndex > -1);
+            DataGridRow dataGridRow = GetGeneratedRow(dataContext);
+            if (dataGridRow == null)
+            {
+                dataGridRow = DisplayData.GetUsedRow() ?? new DataGridRow();
+                dataGridRow.Index = rowIndex;
+                dataGridRow.Slot = slot;
+                dataGridRow.OwningGrid = this;
+                dataGridRow.DataContext = dataContext;
+                CompleteCellsCollection(dataGridRow);
+
+                OnLoadingRow(new DataGridRowEventArgs(dataGridRow));
+            }
+            return dataGridRow;
+        }
+
+        /// <summary>
+        /// Returns the exact row height, whether it is currently displayed or not.
+        /// The row is generated and added to the displayed rows in case it is not already displayed.
+        /// The horizontal gridlines thickness are added.
+        /// </summary>
+        private double GetExactSlotElementHeight(int slot)
+        {
+            Debug.Assert((slot >= 0) && slot < SlotCount);
+
+            if (IsSlotVisible(slot))
+            {
+                Debug.Assert(DisplayData.GetDisplayedElement(slot) != null);
+                return DisplayData.GetDisplayedElement(slot).DesiredSize.Height;
+            }
+
+            Control slotElement = InsertDisplayedElement(slot, true /*updateSlotInformation*/);
+            Debug.Assert(slotElement != null);
+            return slotElement.DesiredSize.Height;
+        }
+
+        // Returns an estimate for the height of the slots between fromSlot and toSlot
+        private double GetHeightEstimate(int fromSlot, int toSlot)
+        {
+            double rowCount = toSlot - fromSlot - GetRowGroupHeaderCount(fromSlot, toSlot, true, out double headerHeight) + 1;
+            double detailsCount = GetDetailsCountInclusive(fromSlot, toSlot);
+
+            return headerHeight + (detailsCount * RowDetailsHeightEstimate) + (rowCount * RowHeightEstimate);
+        }
+
+        /// <summary>
+        /// If the provided slot is displayed, returns the exact height.
+        /// If the slot is not displayed, returns a default height.
+        /// </summary>
+        private double GetSlotElementHeight(int slot)
+        {
+            Debug.Assert(slot >= 0 && slot < SlotCount);
+            if (IsSlotVisible(slot))
+            {
+                Debug.Assert(DisplayData.GetDisplayedElement(slot) != null);
+                return DisplayData.GetDisplayedElement(slot).DesiredSize.Height;
+            }
+            else
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (rowGroupInfo != null)
+                {
+                    return _rowGroupHeightsByLevel[rowGroupInfo.Level];
+                }
+
+                // Assume it's a row since we're either not grouping or it wasn't a RowGroupHeader
+                return RowHeightEstimate + (GetRowDetailsVisibility(slot) ? RowDetailsHeightEstimate : 0);
+            }
+        }
+
+        /// <summary>
+        /// Cumulates the approximate height of the rows from fromRowIndex to toRowIndex included.
+        /// Including the potential gridline thickness.
+        /// </summary>
+        private double GetSlotElementsHeight(int fromSlot, int toSlot)
+        {
+            Debug.Assert(toSlot >= fromSlot);
+
+            double height = 0;
+            for (int slot = fromSlot; slot <= toSlot; slot++)
+            {
+                height += GetSlotElementHeight(slot);
+            }
+            return height;
+        }
+
+        /// <summary>
+        /// Checks if the row for the provided dataContext has been generated and is present
+        /// in either the loaded rows, pre-fetched rows, or editing row. 
+        /// The displayed rows are *not* searched. Returns null if the row does not belong to those 3 categories.
+        /// </summary>
+        private DataGridRow GetGeneratedRow(object dataContext)
+        {
+            // Check the list of rows being loaded via the LoadingRow event.
+            DataGridRow dataGridRow = GetLoadedRow(dataContext);
+            if (dataGridRow != null)
+            {
+                return dataGridRow;
+            }
+
+            // Check the potential editing row.
+            if (EditingRow != null && dataContext == EditingRow.DataContext)
+            {
+                return EditingRow;
+            }
+
+            // Check the potential focused row.
+            if (_focusedRow != null && dataContext == _focusedRow.DataContext)
+            {
+                return _focusedRow;
+            }
+
+            return null;
+        }
+
+        private DataGridRow GetLoadedRow(object dataContext)
+        {
+            foreach (DataGridRow dataGridRow in _loadedRows)
+            {
+                if (dataGridRow.DataContext == dataContext)
+                {
+                    return dataGridRow;
+                }
+            }
+            return null;
+        }
+
+        private Control InsertDisplayedElement(int slot, bool updateSlotInformation)
+        {
+            Control slotElement;
+            if (RowGroupHeadersTable.Contains(slot))
+            {
+                slotElement = GenerateRowGroupHeader(slot, rowGroupInfo: RowGroupHeadersTable.GetValueAt(slot));
+            }
+            else
+            {
+                // If we're grouping, the GroupLevel needs to be fixed later by methods calling this 
+                // which end up inserting rows. We don't do it here because elements could be inserted 
+                // from top to bottom or bottom to up so it's better to do in one pass
+                slotElement = GenerateRow(RowIndexFromSlot(slot), slot);
+            }
+            InsertDisplayedElement(slot, slotElement, wasNewlyAdded: false, updateSlotInformation: updateSlotInformation);
+            return slotElement;
+        }
+
+        //TODO Styles
+        private void InsertDisplayedElement(int slot, Control element, bool wasNewlyAdded, bool updateSlotInformation)
+        {
+            // We can only support creating new rows that are adjacent to the currently visible rows
+            // since they need to be added to the visual tree for us to Measure them.
+            Debug.Assert(DisplayData.FirstScrollingSlot == -1 || slot >= GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot) && slot <= GetNextVisibleSlot(DisplayData.LastScrollingSlot));
+            Debug.Assert(element != null);
+
+            if (_rowsPresenter != null)
+            {
+                DataGridRowGroupHeader groupHeader = null;
+                DataGridRow row = element as DataGridRow;
+                if (row != null)
+                {
+                    LoadRowVisualsForDisplay(row);
+
+                    if (IsRowRecyclable(row))
+                    {
+                        if (!row.IsRecycled)
+                        {
+                            Debug.Assert(!_rowsPresenter.Children.Contains(element));
+                            _rowsPresenter.Children.Add(row);
+                        }
+                    }
+                    else
+                    {
+                        element.Clip = null;
+                        Debug.Assert(row.Index == RowIndexFromSlot(slot));
+                    }
+                }
+                else
+                {
+                    groupHeader = element as DataGridRowGroupHeader;
+                    Debug.Assert(groupHeader != null);  // Nothig other and Rows and RowGroups now
+                    if (groupHeader != null)
+                    {
+                        groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1];
+                        if (!groupHeader.IsRecycled)
+                        {
+                            _rowsPresenter.Children.Add(element);
+                        }
+                        groupHeader.LoadVisualsForDisplay();
+                    }
+                }
+
+                // Measure the element and update AvailableRowRoom
+                element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                AvailableSlotElementRoom -= element.DesiredSize.Height;
+
+                if (groupHeader != null)
+                {
+                    _rowGroupHeightsByLevel[groupHeader.Level] = groupHeader.DesiredSize.Height;
+                }
+
+                if (row != null && RowHeightEstimate == DataGrid.DATAGRID_defaultRowHeight && double.IsNaN(row.Height))
+                {
+                    RowHeightEstimate = element.DesiredSize.Height;
+                }
+            }
+
+            if (wasNewlyAdded)
+            {
+                DisplayData.CorrectSlotsAfterInsertion(slot, element, isCollapsed: false);
+            }
+            else
+            {
+                DisplayData.LoadScrollingSlot(slot, element, updateSlotInformation);
+            }
+        }
+
+        private void InsertElement(int slot, Control element, bool updateVerticalScrollBarOnly, bool isCollapsed, bool isRow)
+        {
+            Debug.Assert(slot >= 0 && slot <= SlotCount);
+
+            OnInsertingElement(slot, true /*firstInsertion*/, isCollapsed);   // will throw an exception if the insertion is illegal
+
+            OnInsertedElement_Phase1(slot, element, isCollapsed, isRow);
+            SlotCount++;
+            if (!isCollapsed)
+            {
+                VisibleSlotCount++;
+            }
+            OnInsertedElement_Phase2(slot, updateVerticalScrollBarOnly, isCollapsed);
+        }
+
+        private void InvalidateRowHeightEstimate()
+        {
+            // Start from scratch and assume that we haven't estimated any rows
+            _lastEstimatedRow = -1;
+        }
+
+        private void OnAddedElement_Phase1(int slot, Control element)
+        {
+            Debug.Assert(slot >= 0);
+
+            // Row needs to be potentially added to the displayed rows
+            if (SlotIsDisplayed(slot))
+            {
+                InsertDisplayedElement(slot, element, true /*wasNewlyAdded*/, true);
+            }
+        }
+
+        private void OnAddedElement_Phase2(int slot, bool updateVerticalScrollBarOnly)
+        {
+            if (slot < DisplayData.FirstScrollingSlot - 1)
+            {
+                // The element was added above our viewport so it pushes the VerticalOffset down
+                double elementHeight = RowGroupHeadersTable.Contains(slot) ? RowGroupHeaderHeightEstimate : RowHeightEstimate;
+
+                SetVerticalOffset(_verticalOffset + elementHeight);
+            }
+            if (updateVerticalScrollBarOnly)
+            {
+                UpdateVerticalScrollBar();
+            }
+            else
+            {
+                ComputeScrollBarsLayout();
+                // Reposition rows in case we use a recycled one
+                InvalidateRowsArrange();
+            }
+        }
+
+        private void OnElementsChanged(bool grew)
+        {
+            if (grew &&
+                ColumnsItemsInternal.Count > 0 &&
+                CurrentColumnIndex == -1)
+            {
+                MakeFirstDisplayedCellCurrentCell();
+            }
+        }
+
+        private void OnInsertedElement_Phase1(int slot, Control element, bool isCollapsed, bool isRow)
+        {
+            Debug.Assert(slot >= 0);
+
+            // Fix the Index of all following rows
+            CorrectSlotsAfterInsertion(slot, isCollapsed, isRow);
+
+            // Next, same effect as adding a row
+            if (element != null)
+            {
+#if DEBUG
+                if (element is DataGridRow dataGridRow)
+                {
+                    Debug.Assert(dataGridRow.Cells.Count == ColumnsItemsInternal.Count);
+
+                    int columnIndex = 0;
+                    foreach (DataGridCell dataGridCell in dataGridRow.Cells)
+                    {
+                        Debug.Assert(dataGridCell.OwningRow == dataGridRow);
+                        Debug.Assert(dataGridCell.OwningColumn == ColumnsItemsInternal[columnIndex]);
+                        columnIndex++;
+                    }
+                }
+#endif
+                Debug.Assert(!isCollapsed);
+                OnAddedElement_Phase1(slot, element);
+            }
+            else if ((slot <= DisplayData.FirstScrollingSlot) || (isCollapsed && (slot <= DisplayData.LastScrollingSlot)))
+            {
+                DisplayData.CorrectSlotsAfterInsertion(slot, null /*row*/, isCollapsed);
+            }
+        }
+
+        private void OnInsertedElement_Phase2(int slot, bool updateVerticalScrollBarOnly, bool isCollapsed)
+        {
+            Debug.Assert(slot >= 0);
+
+            if (!isCollapsed)
+            {
+                // Same effect as adding a row
+                OnAddedElement_Phase2(slot, updateVerticalScrollBarOnly);
+            }
+        }
+
+        private void OnInsertingElement(int slotInserted,
+                                    bool firstInsertion,
+                                    bool isCollapsed)
+        {
+            // Reset the current cell's address if it's after the inserted row.
+            if (firstInsertion)
+            {
+                if (CurrentSlot != -1 && slotInserted <= CurrentSlot)
+                {
+                    // The underlying data was already added, therefore we need to avoid accessing any back-end data since we might be off by 1 row.
+                    _temporarilyResetCurrentCell = true;
+                    bool success = SetCurrentCellCore(-1, -1);
+                    Debug.Assert(success);
+                }
+            }
+
+            _showDetailsTable.InsertIndex(slotInserted);
+            // Update the slot ranges for the RowGroupHeaders before updating the _selectedItems table,
+            // because it's dependent on the slots being correct with regards to grouping.
+            RowGroupHeadersTable.InsertIndex(slotInserted);
+            _selectedItems.InsertIndex(slotInserted);
+
+            if (isCollapsed)
+            {
+                _collapsedSlotsTable.InsertIndexAndValue(slotInserted, false);
+            }
+            else
+            {
+                _collapsedSlotsTable.InsertIndex(slotInserted);
+            }
+
+            // If we've inserted rows before the current selected item, update its index
+            if (slotInserted <= SelectedIndex)
+            {
+                SetValueNoCallback(SelectedIndexProperty, SelectedIndex + 1);
+            }
+        }
+
+        private void OnRemovedElement(int slotDeleted, object itemDeleted)
+        {
+            SlotCount--;
+            bool wasCollapsed = _collapsedSlotsTable.Contains(slotDeleted);
+            if (!wasCollapsed)
+            {
+                VisibleSlotCount--;
+            }
+
+            // If we're deleting the focused row, we need to clear the cached value
+            if (_focusedRow != null && _focusedRow.Slot == slotDeleted)
+            {
+                ResetFocusedRow();
+            }
+
+            // The element needs to be potentially removed from the displayed elements
+            Control elementDeleted = null;
+            if (slotDeleted <= DisplayData.LastScrollingSlot)
+            {
+                if ((slotDeleted >= DisplayData.FirstScrollingSlot) && !wasCollapsed)
+                {
+                    elementDeleted = DisplayData.GetDisplayedElement(slotDeleted);
+                    // We need to retrieve the Element before updating the tables, but we need
+                    // to update the tables before updating DisplayData in RemoveDisplayedElement
+                    UpdateTablesForRemoval(slotDeleted, itemDeleted);
+
+                    // Displayed row is removed
+                    RemoveDisplayedElement(elementDeleted, slotDeleted, true /*wasDeleted*/, true /*updateSlotInformation*/);
+                }
+                else
+                {
+                    UpdateTablesForRemoval(slotDeleted, itemDeleted);
+
+                    // Removed row is not in view, just update the DisplayData
+                    DisplayData.CorrectSlotsAfterDeletion(slotDeleted, wasCollapsed);
+                }
+            }
+            else
+            {
+                // The element was removed beyond the viewport so we just need to update the tables
+                UpdateTablesForRemoval(slotDeleted, itemDeleted);
+            }
+
+            // If a row was removed before the currently selected row, update its index
+            if (slotDeleted < SelectedIndex)
+            {
+                SetValueNoCallback(SelectedIndexProperty, SelectedIndex - 1);
+            }
+
+            if (!wasCollapsed)
+            {
+                if (slotDeleted >= DisplayData.LastScrollingSlot && elementDeleted == null)
+                {
+                    // Deleted Row is below our Viewport, we just need to adjust the scrollbar
+                    UpdateVerticalScrollBar();
+                }
+                else
+                {
+                    if (elementDeleted != null)
+                    {
+                        // Deleted Row is within our Viewport, update the AvailableRowRoom
+                        AvailableSlotElementRoom += elementDeleted.DesiredSize.Height;
+                    }
+                    else
+                    {
+                        // Deleted Row is above our Viewport, update the vertical offset
+                        SetVerticalOffset(Math.Max(0, _verticalOffset - RowHeightEstimate));
+                    }
+
+                    ComputeScrollBarsLayout();
+                    // Reposition rows in case we use a recycled one
+                    InvalidateRowsArrange();
+                }
+            }
+        }
+
+        private void OnRemovingElement(int slotDeleted)
+        {
+            // Note that the row needs to be deleted no matter what. The underlying data row was already deleted.
+
+            Debug.Assert(slotDeleted >= 0 && slotDeleted < SlotCount);
+            _temporarilyResetCurrentCell = false;
+
+            // Reset the current cell's address if it's on the deleted row, or after it.
+            if (CurrentSlot != -1 && slotDeleted <= CurrentSlot)
+            {
+                _desiredCurrentColumnIndex = CurrentColumnIndex;
+                if (slotDeleted == CurrentSlot)
+                {
+                    // No editing is committed since the underlying entity was already deleted.
+                    bool success = SetCurrentCellCore(-1, -1, false /*commitEdit*/, false /*endRowEdit*/);
+                    Debug.Assert(success);
+                }
+                else
+                {
+                    // Underlying data of deleted row is gone. It cannot be accessed anymore. Skip the commit of the editing.
+                    _temporarilyResetCurrentCell = true;
+                    bool success = SetCurrentCellCore(-1, -1);
+                    Debug.Assert(success);
+                }
+            }
+        }
+
+        //TODO Styles
+        // Makes sure the row shows the proper visuals for selection, currency, details, etc.
+        private void LoadRowVisualsForDisplay(DataGridRow row)
+        {
+            // If the row has been recycled, reapply the BackgroundBrush
+            if (row.IsRecycled)
+            {
+                row.EnsureBackground();
+                row.ApplyCellsState();
+            }
+            else if (row == EditingRow)
+            {
+                row.ApplyCellsState();
+            }
+
+            // Set the Row's Style if we one's defined at the DataGrid level and the user didn't
+            // set one at the row level
+            //EnsureElementStyle(row, null, RowStyle);
+            row.EnsureHeaderStyleAndVisibility(null);
+
+            // Check to see if the row contains the CurrentCell, apply its state.
+            if (CurrentColumnIndex != -1 &&
+                CurrentSlot != -1 &&
+                row.Index == CurrentSlot)
+            {
+                row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
+            }
+
+            if (row.IsSelected || row.IsRecycled)
+            {
+                row.UpdatePseudoClasses();
+            }
+
+            // Show or hide RowDetails based on DataGrid settings
+            EnsureRowDetailsVisibility(row, raiseNotification: false, animate: false);
+        }
+
+        private void RemoveDisplayedElement(int slot, bool wasDeleted, bool updateSlotInformation)
+        {
+            Debug.Assert(slot >= DisplayData.FirstScrollingSlot &&
+                         slot <= DisplayData.LastScrollingSlot);
+
+            RemoveDisplayedElement(DisplayData.GetDisplayedElement(slot), slot, wasDeleted, updateSlotInformation);
+        }
+
+        // Removes an element from display either because it was deleted or it was scrolled out of view.
+        // If the element was provided, it will be the element removed; otherwise, the element will be
+        // retrieved from the slot information
+        private void RemoveDisplayedElement(Control element, int slot, bool wasDeleted, bool updateSlotInformation)
+        {
+            if (element is DataGridRow dataGridRow)
+            {
+                if (IsRowRecyclable(dataGridRow))
+                {
+                    UnloadRow(dataGridRow);
+                }
+                else
+                {
+                    dataGridRow.Clip = new RectangleGeometry();
+                }
+            }
+            else if (element is DataGridRowGroupHeader groupHeader)
+            {
+                OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader));
+                DisplayData.AddRecylableRowGroupHeader(groupHeader);
+            }
+            else if (_rowsPresenter != null)
+            {
+                _rowsPresenter.Children.Remove(element);
+            }
+
+            // Update DisplayData
+            if (wasDeleted)
+            {
+                DisplayData.CorrectSlotsAfterDeletion(slot, wasCollapsed: false);
+            }
+            else
+            {
+                DisplayData.UnloadScrollingElement(slot, updateSlotInformation, wasDeleted: false);
+            }
+        }
+
+        /// <summary>
+        /// Removes all of the editing elements for the row that is just leaving editing mode.
+        /// </summary>
+        private void RemoveEditingElements()
+        {
+            if (EditingRow != null && EditingRow.Cells != null)
+            {
+                Debug.Assert(EditingRow.Cells.Count == ColumnsItemsInternal.Count);
+                foreach (DataGridColumn column in Columns)
+                {
+                    column.RemoveEditingElement();
+                }
+            }
+        }
+
+        private void RemoveElementAt(int slot, object item, bool isRow)
+        {
+            Debug.Assert(slot >= 0 && slot < SlotCount);
+
+            OnRemovingElement(slot);
+
+            CorrectSlotsAfterDeletion(slot, isRow);
+
+            OnRemovedElement(slot, item);
+        }
+
+        private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot)
+        {
+            while (DisplayData.FirstScrollingSlot < newFirstDisplayedSlot)
+            {
+                // Need to add rows above the lastDisplayedScrollingRow
+                RemoveDisplayedElement(DisplayData.FirstScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/);
+            }
+            while (DisplayData.LastScrollingSlot > newLastDisplayedSlot)
+            {
+                // Need to remove rows below the lastDisplayedScrollingRow
+                RemoveDisplayedElement(DisplayData.LastScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/);
+            }
+        }
+
+        private void ResetDisplayedRows()
+        {
+            if (UnloadingRow != null || UnloadingRowGroup != null)
+            {
+                foreach (Control element in DisplayData.GetScrollingElements())
+                {
+                    // Raise Unloading Row for all the rows we're displaying
+                    if (element is DataGridRow row)
+                    {
+                        if (IsRowRecyclable(row))
+                        {
+                            OnUnloadingRow(new DataGridRowEventArgs(row));
+                        }
+                    }
+                    // Raise Unloading Row for all the RowGroupHeaders we're displaying
+                    else if (element is DataGridRowGroupHeader groupHeader)
+                    {
+                        OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader));
+                    }
+                }
+            }
+
+            DisplayData.ClearElements(recycle: true);
+            AvailableSlotElementRoom = CellsHeight;
+        }
+
+        /// <summary>
+        /// Determines whether the row at the provided index must be displayed or not.
+        /// </summary>
+        private bool SlotIsDisplayed(int slot)
+        {
+            Debug.Assert(slot >= 0);
+
+            if (slot >= DisplayData.FirstScrollingSlot &&
+                slot <= DisplayData.LastScrollingSlot)
+            {
+                // Additional row takes the spot of a displayed row - it is necessarilly displayed
+                return true;
+            }
+            else if (DisplayData.FirstScrollingSlot == -1 &&
+                     CellsHeight > 0 &&
+                     CellsWidth > 0)
+            {
+                return true;
+            }
+            else if (slot == GetNextVisibleSlot(DisplayData.LastScrollingSlot))
+            {
+                if (AvailableSlotElementRoom > 0)
+                {
+                    // There is room for this additional row
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // Updates display information and displayed rows after scrolling the given number of pixels
+        private void ScrollSlotsByHeight(double height)
+        {
+            Debug.Assert(DisplayData.FirstScrollingSlot >= 0);
+            Debug.Assert(!DoubleUtil.IsZero(height));
+
+            _scrollingByHeight = true;
+            try
+            {
+                double deltaY = 0;
+                int newFirstScrollingSlot = DisplayData.FirstScrollingSlot;
+                double newVerticalOffset = _verticalOffset + height;
+                if (height > 0)
+                {
+                    // Scrolling Down
+                    int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount);
+                    if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset))
+                    {
+                        // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom
+                        // of the DataGrid.  If this produces very odd behavior, evaluate the coping strategy used by
+                        // OnRowMeasure(Size).  For most data, this should be unnoticeable.
+                        ResetDisplayedRows();
+                        UpdateDisplayedRowsFromBottom(lastVisibleSlot);
+                        newFirstScrollingSlot = DisplayData.FirstScrollingSlot;
+                    }
+                    else
+                    {
+                        deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset;
+                        if (DoubleUtil.LessThan(height, deltaY))
+                        {
+                            // We've merely covered up more of the same row we're on
+                            NegVerticalOffset += height;
+                        }
+                        else
+                        {
+                            // Figure out what row we've scrolled down to and update the value for NegVerticalOffset
+                            NegVerticalOffset = 0;
+                            // 
+                            if (height > 2 * CellsHeight &&
+                                (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || RowDetailsTemplate == null))
+                            {
+                                // Very large scroll occurred. Instead of determining the exact number of scrolled off rows,
+                                // let's estimate the number based on RowHeight.
+                                ResetDisplayedRows();
+                                double singleRowHeightEstimate = RowHeightEstimate + (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible ? RowDetailsHeightEstimate : 0);
+                                int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate);
+                                scrolledToSlot += _collapsedSlotsTable.GetIndexCount(newFirstScrollingSlot, newFirstScrollingSlot + scrolledToSlot);
+                                newFirstScrollingSlot = Math.Min(GetNextVisibleSlot(scrolledToSlot), lastVisibleSlot);
+                            }
+                            else
+                            {
+                                while (DoubleUtil.LessThanOrClose(deltaY, height))
+                                {
+                                    if (newFirstScrollingSlot < lastVisibleSlot)
+                                    {
+                                        if (IsSlotVisible(newFirstScrollingSlot))
+                                        {
+                                            // Make the top row available for reuse
+                                            RemoveDisplayedElement(newFirstScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/);
+                                        }
+                                        newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot);
+                                    }
+                                    else
+                                    {
+                                        // We're being told to scroll beyond the last row, ignore the extra
+                                        NegVerticalOffset = 0;
+                                        break;
+                                    }
+
+                                    double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
+                                    double remainingHeight = height - deltaY;
+                                    if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight))
+                                    {
+                                        deltaY += rowHeight;
+                                    }
+                                    else
+                                    {
+                                        NegVerticalOffset = remainingHeight;
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // Scrolling Up
+                    if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0))
+                    {
+                        // We've merely exposing more of the row we're on
+                        NegVerticalOffset += height;
+                    }
+                    else
+                    {
+                        // Figure out what row we've scrolled up to and update the value for NegVerticalOffset
+                        deltaY = -NegVerticalOffset;
+                        NegVerticalOffset = 0;
+                        // 
+
+                        if (height < -2 * CellsHeight &&
+                            (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || RowDetailsTemplate == null))
+                        {
+                            // Very large scroll occurred. Instead of determining the exact number of scrolled off rows,
+                            // let's estimate the number based on RowHeight.
+                            if (newVerticalOffset == 0)
+                            {
+                                newFirstScrollingSlot = 0;
+                            }
+                            else
+                            {
+                                double singleRowHeightEstimate = RowHeightEstimate + (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible ? RowDetailsHeightEstimate : 0);
+                                int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate);
+                                scrolledToSlot -= _collapsedSlotsTable.GetIndexCount(scrolledToSlot, newFirstScrollingSlot);
+
+                                newFirstScrollingSlot = Math.Max(0, GetPreviousVisibleSlot(scrolledToSlot + 1));
+                            }
+                            ResetDisplayedRows();
+                        }
+                        else
+                        {
+                            int lastScrollingSlot = DisplayData.LastScrollingSlot;
+                            while (DoubleUtil.GreaterThan(deltaY, height))
+                            {
+                                if (newFirstScrollingSlot > 0)
+                                {
+                                    if (IsSlotVisible(lastScrollingSlot))
+                                    {
+                                        // Make the bottom row available for reuse
+                                        RemoveDisplayedElement(lastScrollingSlot, wasDeleted: false, updateSlotInformation: true);
+                                        lastScrollingSlot = GetPreviousVisibleSlot(lastScrollingSlot);
+                                    }
+                                    newFirstScrollingSlot = GetPreviousVisibleSlot(newFirstScrollingSlot);
+                                }
+                                else
+                                {
+                                    NegVerticalOffset = 0;
+                                    break;
+                                }
+                                double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
+                                double remainingHeight = height - deltaY;
+                                if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0))
+                                {
+                                    deltaY -= rowHeight;
+                                }
+                                else
+                                {
+                                    NegVerticalOffset = rowHeight + remainingHeight;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
+                    {
+                        // We've scrolled to the top of the ScrollBar, automatically place the user at the very top
+                        // of the DataGrid.  If this produces very odd behavior, evaluate the RowHeight estimate.  
+                        // strategy. For most data, this should be unnoticeable.
+                        ResetDisplayedRows();
+                        NegVerticalOffset = 0;
+                        UpdateDisplayedRows(0, CellsHeight);
+                        newFirstScrollingSlot = 0;
+                    }
+                }
+
+                double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
+                if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset))
+                {
+                    // We've scrolled off more of the first row than what's possible.  This can happen
+                    // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
+                    // cleanup issue.  In this case, simply try to display the next row as the first row instead
+                    if (newFirstScrollingSlot < SlotCount - 1)
+                    {
+                        newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot);
+                        Debug.Assert(newFirstScrollingSlot != -1);
+                    }
+                    NegVerticalOffset = 0;
+                }
+
+                UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight);
+
+                double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot);
+                if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
+                {
+                    int firstElementSlot = DisplayData.FirstScrollingSlot;
+                    // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element
+                    while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
+                    {
+                        int previousSlot = GetPreviousVisibleSlot(firstElementSlot);
+                        if (previousSlot == -1)
+                        {
+                            NegVerticalOffset = 0;
+                            _verticalOffset = 0;
+                        }
+                        else
+                        {
+                            NegVerticalOffset -= firstElementHeight;
+                            _verticalOffset = Math.Max(0, _verticalOffset - firstElementHeight);
+                            firstElementSlot = previousSlot;
+                            firstElementHeight = GetExactSlotElementHeight(firstElementSlot);
+                        }
+                    }
+                    // We could be smarter about this, but it's not common so we wouldn't gain much from optimizing here
+                    if (firstElementSlot != DisplayData.FirstScrollingSlot)
+                    {
+                        UpdateDisplayedRows(firstElementSlot, CellsHeight);
+                    }
+                }
+
+                Debug.Assert(DisplayData.FirstScrollingSlot >= 0);
+                Debug.Assert(GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) > NegVerticalOffset);
+
+                if (DisplayData.FirstScrollingSlot == 0)
+                {
+                    _verticalOffset = NegVerticalOffset;
+                }
+                else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset))
+                {
+                    // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb
+                    // can stay in the same place
+                    NegVerticalOffset = newVerticalOffset;
+                    _verticalOffset = newVerticalOffset;
+                }
+                else
+                {
+                    _verticalOffset = newVerticalOffset;
+                }
+
+                Debug.Assert(!(_verticalOffset == 0 && NegVerticalOffset == 0 && DisplayData.FirstScrollingSlot > 0));
+
+                SetVerticalOffset(_verticalOffset);
+
+                DisplayData.FullyRecycleElements();
+
+                Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0));
+                Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
+            }
+            finally
+            {
+                _scrollingByHeight = false;
+            }
+        }
+
+        private void SelectDisplayedElement(int slot)
+        {
+            Debug.Assert(IsSlotVisible(slot));
+            Control element = DisplayData.GetDisplayedElement(slot);
+            if (element is DataGridRow row)
+            {
+                row.UpdatePseudoClasses();
+                EnsureRowDetailsVisibility(row, raiseNotification: true, animate: true);
+            }
+            else
+            {
+                // Assume it's a RowGroupHeader
+                DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader;
+                groupHeader.ApplyState(useTransitions: true);
+            }
+        }
+
+        private void SelectSlot(int slot, bool isSelected)
+        {
+            _selectedItems.SelectSlot(slot, isSelected);
+            if (IsSlotVisible(slot))
+            {
+                SelectDisplayedElement(slot);
+            }
+        }
+
+        private void SelectSlots(int startSlot, int endSlot, bool isSelected)
+        {
+            _selectedItems.SelectSlots(startSlot, endSlot, isSelected);
+
+            // Apply the correct row state for display rows and also expand or collapse detail accordingly
+            int firstSlot = Math.Max(DisplayData.FirstScrollingSlot, startSlot);
+            int lastSlot = Math.Min(DisplayData.LastScrollingSlot, endSlot);
+
+            for (int slot = firstSlot; slot <= lastSlot; slot++)
+            {
+                if (IsSlotVisible(slot))
+                {
+                    SelectDisplayedElement(slot);
+                }
+            }
+        }
+
+        private void UnloadElements(bool recycle)
+        {
+            // Since we're unloading all the elements, we can't be in editing mode anymore,
+            // so commit if we can, otherwise force cancel.
+            if (!CommitEdit())
+            {
+                CancelEdit(DataGridEditingUnit.Row, false);
+            }
+            ResetEditingRow();
+
+            // Make sure to clear the focused row (because it's no longer relevant).
+            if (_focusedRow != null)
+            {
+                ResetFocusedRow();
+                Focus();
+            }
+
+            if (_rowsPresenter != null)
+            {
+                foreach (Control element in _rowsPresenter.Children)
+                {
+                    if (element is DataGridRow row)
+                    {
+                        // Raise UnloadingRow for any row that was visible
+                        if (IsSlotVisible(row.Slot))
+                        {
+                            OnUnloadingRow(new DataGridRowEventArgs(row));
+                        }
+                        row.DetachFromDataGrid(recycle && row.IsRecyclable /*recycle*/);
+                    }
+                    else if (element is DataGridRowGroupHeader groupHeader)
+                    {
+                        if (IsSlotVisible(groupHeader.RowGroupInfo.Slot))
+                        {
+                            OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader));
+                        }
+                    }
+                }
+
+                if (!recycle)
+                {
+                    _rowsPresenter.Children.Clear();
+                }
+            }
+            DisplayData.ClearElements(recycle);
+
+            // Update the AvailableRowRoom since we're displaying 0 rows now
+            AvailableSlotElementRoom = CellsHeight;
+            VisibleSlotCount = 0;
+        }
+
+        //TODO Styles
+        private void UnloadRow(DataGridRow dataGridRow)
+        {
+            Debug.Assert(dataGridRow != null);
+            Debug.Assert(_rowsPresenter != null);
+            Debug.Assert(_rowsPresenter.Children.Contains(dataGridRow));
+
+            if (_loadedRows.Contains(dataGridRow))
+            {
+                return; // The row is still referenced, we can't release it.
+            }
+
+            // Raise UnloadingRow regardless of whether the row will be recycled
+            OnUnloadingRow(new DataGridRowEventArgs(dataGridRow));
+            bool recycleRow = CurrentSlot != dataGridRow.Index;
+
+            // Don't recycle if the row has a custom Style set
+            //recycleRow &= (dataGridRow.Style == null || dataGridRow.Style == RowStyle);
+
+            if (recycleRow)
+            {
+                DisplayData.AddRecylableRow(dataGridRow);
+            }
+            else
+            {
+                // 
+                _rowsPresenter.Children.Remove(dataGridRow);
+                dataGridRow.DetachFromDataGrid(false);
+            }
+        }
+
+        private void UpdateDisplayedRows(int newFirstDisplayedSlot, double displayHeight)
+        {
+            Debug.Assert(!_collapsedSlotsTable.Contains(newFirstDisplayedSlot));
+            int firstDisplayedScrollingSlot = newFirstDisplayedSlot;
+            int lastDisplayedScrollingSlot = -1;
+            double deltaY = -NegVerticalOffset;
+            int visibleScrollingRows = 0;
+
+            if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
+            {
+                return;
+            }
+
+            if (firstDisplayedScrollingSlot == -1)
+            {
+                // 0 is fine because the element in the first slot cannot be collapsed
+                firstDisplayedScrollingSlot = 0;
+            }
+
+            int slot = firstDisplayedScrollingSlot;
+            while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight))
+            {
+                deltaY += GetExactSlotElementHeight(slot);
+                visibleScrollingRows++;
+                lastDisplayedScrollingSlot = slot;
+                slot = GetNextVisibleSlot(slot);
+            }
+
+            while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
+            {
+                slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot);
+                if (slot >= 0)
+                {
+                    deltaY += GetExactSlotElementHeight(slot);
+                    firstDisplayedScrollingSlot = slot;
+                    visibleScrollingRows++;
+                }
+            }
+            // If we're up to the first row, and we still have room left, uncover as much of the first row as we can
+            if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight))
+            {
+                double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY);
+                deltaY += NegVerticalOffset - newNegVerticalOffset;
+                NegVerticalOffset = newNegVerticalOffset;
+            }
+
+            if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0)))
+            {
+                DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1;
+            }
+            else
+            {
+                DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows;
+            }
+            if (visibleScrollingRows == 0)
+            {
+                firstDisplayedScrollingSlot = -1;
+                Debug.Assert(lastDisplayedScrollingSlot == -1);
+            }
+
+            Debug.Assert(lastDisplayedScrollingSlot < SlotCount, "lastDisplayedScrollingRow larger than number of rows");
+
+            RemoveNonDisplayedRows(firstDisplayedScrollingSlot, lastDisplayedScrollingSlot);
+
+            Debug.Assert(DisplayData.NumDisplayedScrollingElements >= 0, "the number of visible scrolling rows can't be negative");
+            Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0, "the number of totally visible scrolling rows can't be negative");
+            Debug.Assert(DisplayData.FirstScrollingSlot < SlotCount, "firstDisplayedScrollingRow larger than number of rows");
+            Debug.Assert(DisplayData.FirstScrollingSlot == firstDisplayedScrollingSlot);
+            Debug.Assert(DisplayData.LastScrollingSlot == lastDisplayedScrollingSlot);
+        }
+
+        // Similar to UpdateDisplayedRows except that it starts with the LastDisplayedScrollingRow
+        // and computes the FirstDisplayScrollingRow instead of doing it the other way around.  We use this
+        // when scrolling down to a full row
+        private void UpdateDisplayedRowsFromBottom(int newLastDisplayedScrollingRow)
+        {
+            //Debug.Assert(!_collapsedSlotsTable.Contains(newLastDisplayedScrollingRow));
+
+            int lastDisplayedScrollingRow = newLastDisplayedScrollingRow;
+            int firstDisplayedScrollingRow = -1;
+            double displayHeight = CellsHeight;
+            double deltaY = 0;
+            int visibleScrollingRows = 0;
+
+            if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
+            {
+                ResetDisplayedRows();
+                return;
+            }
+
+            if (lastDisplayedScrollingRow == -1)
+            {
+                lastDisplayedScrollingRow = 0;
+            }
+
+            int slot = lastDisplayedScrollingRow;
+            while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
+            {
+                deltaY += GetExactSlotElementHeight(slot);
+                visibleScrollingRows++;
+                firstDisplayedScrollingRow = slot;
+                slot = GetPreviousVisibleSlot(slot);
+            }
+
+            DisplayData.NumTotallyDisplayedScrollingElements = deltaY > displayHeight ? visibleScrollingRows - 1 : visibleScrollingRows;
+
+            Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0);
+            Debug.Assert(lastDisplayedScrollingRow < SlotCount, "lastDisplayedScrollingRow larger than number of rows");
+
+            NegVerticalOffset = Math.Max(0, deltaY - displayHeight);
+
+            RemoveNonDisplayedRows(firstDisplayedScrollingRow, lastDisplayedScrollingRow);
+
+            Debug.Assert(DisplayData.NumDisplayedScrollingElements >= 0, "the number of visible scrolling rows can't be negative");
+            Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0, "the number of totally visible scrolling rows can't be negative");
+            Debug.Assert(DisplayData.FirstScrollingSlot < SlotCount, "firstDisplayedScrollingRow larger than number of rows");
+        }
+
+        private void UpdateTablesForRemoval(int slotDeleted, object itemDeleted)
+        {
+            if (RowGroupHeadersTable.Contains(slotDeleted))
+            {
+                // A RowGroupHeader was removed
+                RowGroupHeadersTable.RemoveIndexAndValue(slotDeleted);
+                _collapsedSlotsTable.RemoveIndexAndValue(slotDeleted);
+                _selectedItems.DeleteSlot(slotDeleted);
+            }
+            else
+            {
+                // Update the ranges of selected rows
+                if (_selectedItems.ContainsSlot(slotDeleted))
+                {
+                    SelectionHasChanged = true;
+                }
+                _selectedItems.Delete(slotDeleted, itemDeleted);
+                RowGroupHeadersTable.RemoveIndex(slotDeleted);
+                _collapsedSlotsTable.RemoveIndex(slotDeleted);
+            }
+        }
+
+        private void CollectionViewGroup_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            // If we receive this event when the number of GroupDescriptions is different than what we have already
+            // accounted for, that means the ICollectionView is still in the process of updating its groups.  It will
+            // send a reset notification when it's done, at which point we can update our visuals.
+
+            if (_rowGroupHeightsByLevel != null &&
+                DataConnection.CollectionView != null &&
+                DataConnection.CollectionView.IsGrouping &&
+                DataConnection.CollectionView.GroupingDepth == _rowGroupHeightsByLevel.Length)
+            {
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        CollectionViewGroup_CollectionChanged_Add(sender, e);
+                        break;
+                    case NotifyCollectionChangedAction.Remove:
+                        CollectionViewGroup_CollectionChanged_Remove(sender, e);
+                        break;
+                }
+            }
+        }
+
+        private void CollectionViewGroup_CollectionChanged_Add(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (e.NewItems != null && e.NewItems.Count > 0)
+            {
+                // We need to figure out the CollectionViewGroup that the sender belongs to.  We could cache
+                // it by tagging the collections ahead of time, but I think the extra storage might not be worth
+                // it since this lookup should be performant enough
+                int insertSlot = -1;
+                DataGridRowGroupInfo parentGroupInfo = GetParentGroupInfo(sender);
+                DataGridCollectionViewGroup group = e.NewItems[0] as DataGridCollectionViewGroup;
+
+                if (parentGroupInfo != null)
+                {
+                    if (group != null || parentGroupInfo.Level == -1)
+                    {
+                        insertSlot = parentGroupInfo.Slot + 1;
+                        // For groups, we need to skip over subgroups to find the correct slot
+                        DataGridRowGroupInfo groupInfo;
+                        for (int i = 0; i < e.NewStartingIndex; i++)
+                        {
+                            do
+                            {
+                                insertSlot = RowGroupHeadersTable.GetNextIndex(insertSlot);
+                                groupInfo = RowGroupHeadersTable.GetValueAt(insertSlot);
+                            }
+                            while (groupInfo != null && groupInfo.Level > parentGroupInfo.Level + 1);
+                            if (groupInfo == null)
+                            {
+                                // We couldn't find the subchild so this should go at the end
+                                insertSlot = SlotCount;
+                            }
+                        }
+
+                    }
+                    else
+                    {
+                        // For items the slot is a simple calculation
+                        insertSlot = parentGroupInfo.Slot + e.NewStartingIndex + 1;
+                    }
+                }
+
+                // This could not be found when new GroupDescriptions are added to the PagedCollectionView
+                if (insertSlot != -1)
+                {
+                    bool isCollapsed = (parentGroupInfo != null) && (!parentGroupInfo.IsVisible || _collapsedSlotsTable.Contains(parentGroupInfo.Slot));
+                    if (group != null)
+                    {
+                        if (group.Items != null)
+                        {
+                            group.Items.CollectionChanged += CollectionViewGroup_CollectionChanged;
+                        }
+                        var newGroupInfo = new DataGridRowGroupInfo(group, true, parentGroupInfo.Level + 1, insertSlot, insertSlot);
+                        InsertElementAt(insertSlot, 
+                            rowIndex: -1, 
+                            item: null, 
+                            groupInfo: newGroupInfo, 
+                            isCollapsed: isCollapsed);
+                        RowGroupHeadersTable.AddValue(insertSlot, newGroupInfo);
+                    }
+                    else
+                    {
+                        // Assume we're adding a new row
+                        int rowIndex = DataConnection.IndexOf(e.NewItems[0]);
+                        Debug.Assert(rowIndex != -1);
+                        if (SlotCount == 0 && DataConnection.ShouldAutoGenerateColumns)
+                        {
+                            AutoGenerateColumnsPrivate();
+                        }
+                        InsertElementAt(insertSlot, rowIndex, 
+                            item: e.NewItems[0],
+                            groupInfo: null, 
+                            isCollapsed: isCollapsed);
+                    }
+
+                    CorrectLastSubItemSlotsAfterInsertion(parentGroupInfo);
+                    if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1)
+                    {
+                        // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed
+                        EnsureAnscestorsExpanderButtonChecked(parentGroupInfo);
+                    }
+                }
+            }
+        }
+
+        private void CollectionViewGroup_CollectionChanged_Remove(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            Debug.Assert(e.OldItems.Count == 1);
+            if (e.OldItems != null && e.OldItems.Count > 0)
+            {
+                if (e.OldItems[0] is DataGridCollectionViewGroup removedGroup)
+                {
+                    if (removedGroup.Items != null)
+                    {
+                        removedGroup.Items.CollectionChanged -= CollectionViewGroup_CollectionChanged;
+                    }
+                    DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(removedGroup);
+                    Debug.Assert(groupInfo != null);
+                    if ((groupInfo.Level == _rowGroupHeightsByLevel.Length - 1) && (removedGroup.Items != null) && (removedGroup.Items.Count > 0))
+                    {
+                        Debug.Assert((groupInfo.LastSubItemSlot - groupInfo.Slot) == removedGroup.Items.Count);
+                        // If we're removing a leaf Group then remove all of its items before removing the Group
+                        for (int i = 0; i < removedGroup.Items.Count; i++)
+                        {
+                            RemoveElementAt(groupInfo.Slot + 1, item: removedGroup.Items[i], isRow: true);
+                        }
+                    }
+                    RemoveElementAt(groupInfo.Slot, item: null, isRow: false);
+                }
+                else
+                {
+                    // A single item was removed from a leaf group
+                    DataGridRowGroupInfo parentGroupInfo = GetParentGroupInfo(sender);
+                    if (parentGroupInfo != null)
+                    {
+                        int slot;
+                        if (parentGroupInfo.CollectionViewGroup == null && RowGroupHeadersTable.IndexCount > 0)
+                        {
+                            // In this case, we're removing from the root group.  If there are other groups, then this must
+                            // be the new item row that doesn't belong to any group because if there are other groups then
+                            // this item cannot be a child of the root group.
+                            slot = SlotCount - 1;
+                        }
+                        else
+                        {
+                            slot = parentGroupInfo.Slot + e.OldStartingIndex + 1;
+                        }
+                        RemoveElementAt(slot, e.OldItems[0], isRow: true);
+                    }
+                }
+            }
+        }
+
+        private void ClearRowGroupHeadersTable()
+        {
+            // Detach existing handlers on CollectionViewGroup.Items.CollectionChanged
+            foreach (int slot in RowGroupHeadersTable.GetIndexes())
+            {
+                DataGridRowGroupInfo groupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (groupInfo.CollectionViewGroup.Items != null)
+                {
+                    groupInfo.CollectionViewGroup.Items.CollectionChanged -= CollectionViewGroup_CollectionChanged;
+                }
+            }
+            if (_topLevelGroup != null)
+            {
+                // The PagedCollectionView reuses the top level group so we need to detach any existing or else we'll get duplicate handers here
+                _topLevelGroup.CollectionChanged -= CollectionViewGroup_CollectionChanged;
+                _topLevelGroup = null;
+            }
+
+            RowGroupHeadersTable.Clear();
+            // Unfortunately PagedCollectionView does not allow us to preserve expanded or collapsed states for RowGroups since
+            // the CollectionViewGroups are recreated when a Reset happens.  This is true in both SL and WPF
+            _collapsedSlotsTable.Clear();
+
+            _rowGroupHeightsByLevel = null;
+            RowGroupSublevelIndents = null;
+        }
+
+        private void CollectionViewGroup_PropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            if (e.PropertyName == "ItemCount")
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupInfoFromCollectionViewGroup(sender as DataGridCollectionViewGroup);
+                if (rowGroupInfo != null && IsSlotVisible(rowGroupInfo.Slot))
+                {
+                    if (DisplayData.GetDisplayedElement(rowGroupInfo.Slot) is DataGridRowGroupHeader rowGroupHeader)
+                    {
+                        rowGroupHeader.UpdateTitleElements();
+                    }
+                }
+            }
+        }
+
+        // This method is necessary for incrementing the LastSubItemSlot property of the group ancestors
+        // because CorrectSlotsAfterInsertion only increments those that come after the specified group
+        private void CorrectLastSubItemSlotsAfterInsertion(DataGridRowGroupInfo subGroupInfo)
+        {
+            int subGroupSlot;
+            int subGroupLevel;
+            while (subGroupInfo != null)
+            {
+                subGroupLevel = subGroupInfo.Level;
+                subGroupInfo.LastSubItemSlot++;
+
+                while (subGroupInfo != null && subGroupInfo.Level >= subGroupLevel)
+                {
+                    subGroupSlot = RowGroupHeadersTable.GetPreviousIndex(subGroupInfo.Slot);
+                    subGroupInfo = RowGroupHeadersTable.GetValueAt(subGroupSlot);
+                }
+            }
+        }
+
+        private int CountAndPopulateGroupHeaders(object group, int rootSlot, int level)
+        {
+            int treeCount = 1;
+
+            if (group is DataGridCollectionViewGroup collectionViewGroup)
+            {
+                if (collectionViewGroup.Items != null && collectionViewGroup.Items.Count > 0)
+                {
+                    collectionViewGroup.Items.CollectionChanged += CollectionViewGroup_CollectionChanged;
+                    if (collectionViewGroup.Items[0] is DataGridCollectionViewGroup)
+                    {
+                        foreach (object subGroup in collectionViewGroup.Items)
+                        {
+                            treeCount += CountAndPopulateGroupHeaders(subGroup, rootSlot + treeCount, level + 1);
+                        }
+                    }
+                    else
+                    {
+                        // Optimization: don't walk to the bottom level nodes
+                        treeCount += collectionViewGroup.Items.Count;
+                    }
+                }
+                RowGroupHeadersTable.AddValue(rootSlot, new DataGridRowGroupInfo(collectionViewGroup, true, level, rootSlot, rootSlot + treeCount - 1));
+            }
+            return treeCount;
+        }
+
+        private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
+        {
+            if (IsSlotVisible(parentGroupInfo.Slot))
+            {
+                DataGridRowGroupHeader ancestorGroupHeader = DisplayData.GetDisplayedElement(parentGroupInfo.Slot) as DataGridRowGroupHeader;
+                while (ancestorGroupHeader != null)
+                {
+                    ancestorGroupHeader.EnsureExpanderButtonIsChecked();
+                    if (ancestorGroupHeader.Level > 0)
+                    {
+                        int slot = RowGroupHeadersTable.GetPreviousIndex(ancestorGroupHeader.RowGroupInfo.Slot);
+                        if (IsSlotVisible(slot))
+                        {
+                            ancestorGroupHeader = DisplayData.GetDisplayedElement(slot) as DataGridRowGroupHeader;
+                            continue;
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+
+        private void PopulateRowGroupHeadersTable()
+        {
+            if (DataConnection.CollectionView != null
+                && DataConnection.CollectionView.CanGroup
+                && DataConnection.CollectionView.Groups != null)
+            {
+                int totalSlots = 0;
+                _topLevelGroup = (INotifyCollectionChanged)DataConnection.CollectionView.Groups;
+                _topLevelGroup.CollectionChanged += CollectionViewGroup_CollectionChanged;
+                foreach (object group in DataConnection.CollectionView.Groups)
+                {
+                    totalSlots += CountAndPopulateGroupHeaders(group, totalSlots, 0);
+                }
+            }
+            SlotCount = DataConnection.Count + RowGroupHeadersTable.IndexCount;
+            VisibleSlotCount = SlotCount;
+        }
+
+        //TODO Styles
+        private void RefreshRowGroupHeaders()
+        {
+            if (DataConnection.CollectionView != null
+                && DataConnection.CollectionView.CanGroup
+                && DataConnection.CollectionView.Groups != null
+                && DataConnection.CollectionView.IsGrouping 
+                && DataConnection.CollectionView.GroupingDepth > 0)
+            {
+                // Initialize our array for the height of the RowGroupHeaders by Level.
+                // If the Length is the same, we can reuse the old array
+                int groupLevelCount = DataConnection.CollectionView.GroupingDepth;
+                if (_rowGroupHeightsByLevel == null || _rowGroupHeightsByLevel.Length != groupLevelCount)
+                {
+                    _rowGroupHeightsByLevel = new double[groupLevelCount];
+                    for (int i = 0; i < groupLevelCount; i++)
+                    {
+                        // Default height for now, the actual heights are updated as the RowGroupHeaders
+                        // are added and measured
+                        _rowGroupHeightsByLevel[i] = DATAGRID_defaultRowHeight;
+                    }
+                }
+                if (RowGroupSublevelIndents == null || RowGroupSublevelIndents.Length != groupLevelCount)
+                {
+                    RowGroupSublevelIndents = new double[groupLevelCount];
+                    double indent;
+                    for (int i = 0; i < groupLevelCount; i++)
+                    {
+                        indent = DATAGRID_defaultRowGroupSublevelIndent; 
+                        RowGroupSublevelIndents[i] = indent;
+                        if (i > 0)
+                        {
+                            RowGroupSublevelIndents[i] += RowGroupSublevelIndents[i - 1];
+                        }
+                    }
+                }
+                EnsureRowGroupSpacerColumnWidth(groupLevelCount);
+            }
+        }
+
+        private void EnsureRowGroupSpacerColumn()
+        {
+            bool spacerColumnChanged = ColumnsInternal.EnsureRowGrouping(!RowGroupHeadersTable.IsEmpty);
+            if (spacerColumnChanged)
+            {
+                if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && CurrentColumnIndex == 0)
+                {
+                    CurrentColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
+                }
+
+                ProcessFrozenColumnCount();
+            }
+        }
+
+        private void EnsureRowGroupSpacerColumnWidth(int groupLevelCount)
+        {
+            if (groupLevelCount == 0)
+            {
+                ColumnsInternal.RowGroupSpacerColumn.Width = new DataGridLength(0);
+            }
+            else
+            {
+                ColumnsInternal.RowGroupSpacerColumn.Width = new DataGridLength(RowGroupSublevelIndents[groupLevelCount - 1]);
+            }
+        }
+
+        private void EnsureRowGroupVisibility(DataGridRowGroupInfo rowGroupInfo, bool isVisible, bool setCurrent)
+        {
+            if (rowGroupInfo == null)
+            {
+                return;
+            }
+            if (rowGroupInfo.IsVisible != isVisible)
+            {
+                if (IsSlotVisible(rowGroupInfo.Slot))
+                {
+                    DataGridRowGroupHeader rowGroupHeader = DisplayData.GetDisplayedElement(rowGroupInfo.Slot) as DataGridRowGroupHeader;
+                    Debug.Assert(rowGroupHeader != null);
+                    rowGroupHeader.ToggleExpandCollapse(isVisible, setCurrent);
+                }
+                else
+                {
+                    if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot))
+                    {
+                        // Somewhere up the parent chain, there's a collapsed header so all the slots remain the same and
+                        // we just need to mark this header with the new visibility
+                        rowGroupInfo.IsVisible = isVisible;
+                    }
+                    else
+                    {
+                        if (rowGroupInfo.Slot < DisplayData.FirstScrollingSlot)
+                        {
+                            double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false);
+                            // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative
+                            // the user can still scroll to the top since the offset is non-zero
+                            SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange));
+                        }
+                        else
+                        {
+                            UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false);
+                        }
+                        UpdateVerticalScrollBar();
+                    }
+                }
+            }
+        }
+
+        // Returns the inclusive count of expanded RowGroupHeaders from startSlot to endSlot
+        private int GetRowGroupHeaderCount(int startSlot, int endSlot, bool? isVisible, out double headersHeight)
+        {
+            int count = 0;
+            headersHeight = 0;
+            foreach (int slot in RowGroupHeadersTable.GetIndexes(startSlot))
+            {
+                if (slot > endSlot)
+                {
+                    return count;
+                }
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (!isVisible.HasValue ||
+                    (isVisible.Value && !_collapsedSlotsTable.Contains(slot)) ||
+                    (!isVisible.Value && _collapsedSlotsTable.Contains(slot)))
+                {
+                    count++;
+                    headersHeight += _rowGroupHeightsByLevel[rowGroupInfo.Level];
+                }
+            }
+            return count;
+        }
+
+        // This method does not check the state of the parent RowGroupHeaders, it assumes they're ready for this newVisibility to
+        // be applied this header
+        // Returns the number of pixels that were expanded or (collapsed); however, if we're expanding displayed rows, we only expand up
+        // to what we can display
+        private double UpdateRowGroupVisibility(DataGridRowGroupInfo targetRowGroupInfo, bool newIsVisible, bool isDisplayed)
+        {
+            double heightChange = 0;
+            int slotsExpanded = 0;
+            int startSlot = targetRowGroupInfo.Slot + 1;
+            int endSlot;
+
+            targetRowGroupInfo.IsVisible = newIsVisible;
+            if (newIsVisible)
+            {
+                // Expand
+                foreach (int slot in RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1))
+                {
+                    if (slot >= startSlot)
+                    {
+                        DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                        if (rowGroupInfo.Level <= targetRowGroupInfo.Level)
+                        {
+                            break;
+                        }
+                        if (!rowGroupInfo.IsVisible)
+                        {
+                            // Skip over the items in collapsed subgroups
+                            endSlot = rowGroupInfo.Slot;
+                            ExpandSlots(startSlot, endSlot, isDisplayed, ref slotsExpanded, ref heightChange);
+                            startSlot = rowGroupInfo.LastSubItemSlot + 1;
+                        }
+                    }
+                }
+                if (targetRowGroupInfo.LastSubItemSlot >= startSlot)
+                {
+                    ExpandSlots(startSlot, targetRowGroupInfo.LastSubItemSlot, isDisplayed, ref slotsExpanded, ref heightChange);
+                }
+                if (isDisplayed)
+                {
+                    UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
+                }
+            }
+            else
+            {
+                // Collapse
+                endSlot = SlotCount - 1;
+                foreach (int slot in RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1))
+                {
+                    DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                    if (rowGroupInfo.Level <= targetRowGroupInfo.Level)
+                    {
+                        endSlot = slot - 1;
+                        break;
+                    }
+                }
+
+                int oldLastDisplayedSlot = DisplayData.LastScrollingSlot;
+                int endDisplayedSlot = Math.Min(endSlot, DisplayData.LastScrollingSlot);
+                if (isDisplayed)
+                {
+                    // We need to remove all the displayed slots that aren't already collapsed
+                    int elementsToRemove = endDisplayedSlot - startSlot + 1 - _collapsedSlotsTable.GetIndexCount(startSlot, endDisplayedSlot);
+
+                    if (_focusedRow != null && _focusedRow.Slot >= startSlot && _focusedRow.Slot <= endSlot)
+                    {
+                        Debug.Assert(EditingRow == null);
+                        // Don't call ResetFocusedRow here because we're already cleaning it up below, and we don't want to FullyRecycle yet
+                        _focusedRow = null;
+                    }
+
+                    for (int i = 0; i < elementsToRemove; i++)
+                    {
+                        RemoveDisplayedElement(startSlot, wasDeleted: false , updateSlotInformation: false);
+                    }
+                }
+
+                double heightChangeBelowLastDisplayedSlot = 0;
+                if (DisplayData.FirstScrollingSlot >= startSlot && DisplayData.FirstScrollingSlot <= endSlot)
+                {
+                    // Our first visible slot was collapsed, find the replacement
+                    int collapsedSlotsAbove = DisplayData.FirstScrollingSlot - startSlot - _collapsedSlotsTable.GetIndexCount(startSlot, DisplayData.FirstScrollingSlot);
+                    Debug.Assert(collapsedSlotsAbove > 0);
+                    int newFirstScrollingSlot = GetNextVisibleSlot(DisplayData.FirstScrollingSlot);
+                    while (collapsedSlotsAbove > 1 && newFirstScrollingSlot < SlotCount)
+                    {
+                        collapsedSlotsAbove--;
+                        newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot);
+                    }
+                    heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, ref heightChangeBelowLastDisplayedSlot);
+                    if (isDisplayed)
+                    {
+                        if (newFirstScrollingSlot >= SlotCount)
+                        {
+                            // No visible slots below, look up
+                            UpdateDisplayedRowsFromBottom(targetRowGroupInfo.Slot);
+                        }
+                        else
+                        {
+                            UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight);
+                        }
+                    }
+                }
+                else
+                {
+                    heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, ref heightChangeBelowLastDisplayedSlot);
+                }
+
+                if (DisplayData.LastScrollingSlot >= startSlot && DisplayData.LastScrollingSlot <= endSlot)
+                {
+                    // Collapsed the last scrolling row, we need to update it
+                    DisplayData.LastScrollingSlot = GetPreviousVisibleSlot(DisplayData.LastScrollingSlot);
+                }
+
+                // Collapsing could cause the vertical offset to move up if we collapsed a lot of slots
+                // near the bottom of the DataGrid.  To do this, we compare the height we collapsed to
+                // the distance to the last visible row and adjust the scrollbar if we collapsed more
+                if (isDisplayed && _verticalOffset > 0)
+                {
+                    int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount);
+                    int slot = GetNextVisibleSlot(oldLastDisplayedSlot);
+                    // AvailableSlotElementRoom ends up being the amount of the last slot that is partially scrolled off
+                    // as a negative value, heightChangeBelowLastDisplayed slot is also a negative value since we're collapsing
+                    double heightToLastVisibleSlot = AvailableSlotElementRoom + heightChangeBelowLastDisplayedSlot;
+                    while ((heightToLastVisibleSlot > heightChange) && (slot < lastVisibleSlot))
+                    {
+                        heightToLastVisibleSlot -= GetSlotElementHeight(slot);
+                        slot = GetNextVisibleSlot(slot);
+                    }
+                    if (heightToLastVisibleSlot > heightChange)
+                    {
+                        double newVerticalOffset = _verticalOffset + heightChange - heightToLastVisibleSlot;
+                        if (newVerticalOffset > 0)
+                        {
+                            SetVerticalOffset(newVerticalOffset);
+                        }
+                        else
+                        {
+                            // Collapsing causes the vertical offset to go to 0 so we should go back to the first row.
+                            ResetDisplayedRows();
+                            NegVerticalOffset = 0;
+                            SetVerticalOffset(0);
+                            int firstDisplayedRow = GetNextVisibleSlot(-1);
+                            UpdateDisplayedRows(firstDisplayedRow, CellsHeight);
+                        }
+                    }
+                }
+            }
+
+            // Update VisibleSlotCount
+            VisibleSlotCount += slotsExpanded;
+
+            return heightChange;
+        }
+
+        private DataGridRowGroupHeader GenerateRowGroupHeader(int slot, DataGridRowGroupInfo rowGroupInfo)
+        {
+            Debug.Assert(slot > -1);
+            Debug.Assert(rowGroupInfo != null);
+
+            DataGridRowGroupHeader groupHeader = DisplayData.GetUsedGroupHeader() ?? new DataGridRowGroupHeader();
+            groupHeader.OwningGrid = this;
+            groupHeader.RowGroupInfo = rowGroupInfo;
+            groupHeader.DataContext = rowGroupInfo.CollectionViewGroup;
+            groupHeader.Level = rowGroupInfo.Level;
+
+            // Set the RowGroupHeader's PropertyName. Unfortunately, CollectionViewGroup doesn't have this
+            // so we have to set it manually
+            Debug.Assert(DataConnection.CollectionView != null && groupHeader.Level < DataConnection.CollectionView.GroupingDepth);
+            string propertyName = DataConnection.CollectionView.GetGroupingPropertyNameAtDepth(groupHeader.Level);
+
+            if(string.IsNullOrWhiteSpace(propertyName))
+            {
+                groupHeader.PropertyName = null;
+            }
+            else
+            {
+                groupHeader.PropertyName = DataConnection.DataType?.GetDisplayName(propertyName) ?? propertyName;
+            }
+
+            if (rowGroupInfo.CollectionViewGroup is INotifyPropertyChanged inpc)
+            {
+                inpc.PropertyChanged -= new PropertyChangedEventHandler(CollectionViewGroup_PropertyChanged);
+                inpc.PropertyChanged += new PropertyChangedEventHandler(CollectionViewGroup_PropertyChanged);
+            }
+            groupHeader.UpdateTitleElements();
+
+            OnLoadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader));
+
+            return groupHeader;
+        }
+
+        private DataGridRowGroupInfo GetParentGroupInfo(object collection)
+        {
+            if (collection == DataConnection.CollectionView.Groups)
+            {
+                // If the new item is a root level element, it has no parent group, so create an empty RowGroupInfo
+                return new DataGridRowGroupInfo(null, true, -1, -1, -1);
+            }
+            else
+            {
+                foreach (int slot in RowGroupHeadersTable.GetIndexes())
+                {
+                    DataGridRowGroupInfo groupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                    if (groupInfo.CollectionViewGroup.Items == collection)
+                    {
+                        return groupInfo;
+                    }
+                }
+            }
+            return null;
+        }
+
+        internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent)
+        {
+            Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0);
+
+            if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit())
+            {
+                return;
+            }
+
+            if (setCurrent && CurrentSlot != groupHeader.RowGroupInfo.Slot)
+            {
+                // Most of the time this is set by the MouseLeftButtonDown handler but validation could cause that code path to fail
+                UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
+            }
+
+            UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true);
+
+            ComputeScrollBarsLayout();
+            // We need force arrange since our Scrollings Rows could update without automatically triggering layout
+            InvalidateRowsArrange();
+        }
+
+        internal void OnSublevelIndentUpdated(DataGridRowGroupHeader groupHeader, double newValue)
+        {
+            Debug.Assert(DataConnection.CollectionView != null);
+            Debug.Assert(RowGroupSublevelIndents != null);
+
+            int groupLevelCount = DataConnection.CollectionView.GroupingDepth;
+            Debug.Assert(groupHeader.Level >= 0 && groupHeader.Level < groupLevelCount);
+
+            double oldValue = RowGroupSublevelIndents[groupHeader.Level];
+            if (groupHeader.Level > 0)
+            {
+                oldValue -= RowGroupSublevelIndents[groupHeader.Level - 1];
+            }
+            // Update the affected values in our table by the amount affected
+            double change = newValue - oldValue;
+            for (int i = groupHeader.Level; i < groupLevelCount; i++)
+            {
+                RowGroupSublevelIndents[i] += change;
+                Debug.Assert(RowGroupSublevelIndents[i] >= 0);
+            }
+
+            EnsureRowGroupSpacerColumnWidth(groupLevelCount);
+        }
+
+        internal DataGridRowGroupInfo RowGroupInfoFromCollectionViewGroup(DataGridCollectionViewGroup collectionViewGroup)
+        {
+            foreach (int slot in RowGroupHeadersTable.GetIndexes())
+            {
+                DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
+                if (rowGroupInfo.CollectionViewGroup == collectionViewGroup)
+                {
+                    return rowGroupInfo;
+                }
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// Collapses the DataGridRowGroupHeader that represents a given CollectionViewGroup
+        /// </summary>
+        /// <param name="collectionViewGroup">CollectionViewGroup</param>
+        /// <param name="collapseAllSubgroups">Set to true to collapse all Subgroups</param>
+        public void CollapseRowGroup(DataGridCollectionViewGroup collectionViewGroup, bool collapseAllSubgroups)
+        {
+            if (WaitForLostFocus(delegate { CollapseRowGroup(collectionViewGroup, collapseAllSubgroups); }) ||
+                collectionViewGroup == null || !CommitEdit())
+            {
+                return;
+            }
+
+            EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), false, true);
+
+            if (collapseAllSubgroups)
+            {
+                foreach (object groupObj in collectionViewGroup.Items)
+                {
+                    if (groupObj is DataGridCollectionViewGroup subGroup)
+                    {
+                        CollapseRowGroup(subGroup, collapseAllSubgroups);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Expands the DataGridRowGroupHeader that represents a given CollectionViewGroup
+        /// </summary>
+        /// <param name="collectionViewGroup">CollectionViewGroup</param>
+        /// <param name="expandAllSubgroups">Set to true to expand all Subgroups</param>
+        public void ExpandRowGroup(DataGridCollectionViewGroup collectionViewGroup, bool expandAllSubgroups)
+        {
+            if (WaitForLostFocus(delegate { ExpandRowGroup(collectionViewGroup, expandAllSubgroups); }) ||
+                collectionViewGroup == null || !CommitEdit())
+                if (collectionViewGroup == null || !CommitEdit())
+                {
+                    return;
+                }
+
+            EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), true, true);
+
+            if (expandAllSubgroups)
+            {
+                foreach (object groupObj in collectionViewGroup.Items)
+                {
+                    if (groupObj is DataGridCollectionViewGroup subGroup)
+                    {
+                        ExpandRowGroup(subGroup, expandAllSubgroups);
+                    }
+                }
+            }
+        }
+
+        // Returns the number of rows with details visible between lowerBound and upperBound exclusive.
+        // As of now, the caller needs to account for Collapsed slots.  This method assumes everything
+        // is visible
+        private int GetDetailsCountInclusive(int lowerBound, int upperBound)
+        {
+            int indexCount = upperBound - lowerBound + 1;
+            if (indexCount <= 0)
+            {
+                return 0;
+            }
+            if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible)
+            {
+                // Total rows minus ones which explicity turned details off minus the RowGroupHeaders
+                return indexCount - _showDetailsTable.GetIndexCount(lowerBound, upperBound, false) - RowGroupHeadersTable.GetIndexCount(lowerBound, upperBound);
+            }
+            else if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Collapsed)
+            {
+                // Total rows with details explicitly turned on
+                return _showDetailsTable.GetIndexCount(lowerBound, upperBound, true);
+            }
+            else if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected)
+            {
+                // Total number of remaining rows that are selected
+                return _selectedItems.GetIndexCount(lowerBound, upperBound);
+            }
+            Debug.Assert(false); // Shouldn't ever happen
+            return 0;
+        }
+
+        private void EnsureRowDetailsVisibility(DataGridRow row, bool raiseNotification, bool animate)
+        {
+            // Show or hide RowDetails based on DataGrid settings
+            row.SetDetailsVisibilityInternal(GetRowDetailsVisibility(row.Index), raiseNotification, animate);
+        }
+
+        private void UpdateRowDetailsHeightEstimate()
+        {
+            if (_rowsPresenter != null && _measured && RowDetailsTemplate != null)
+            {
+                object dataItem = null;
+                if(VisibleSlotCount > 0)
+                    dataItem = DataConnection.GetDataItem(0);
+                var detailsContent = RowDetailsTemplate.Build(dataItem);
+                if (detailsContent != null)
+                {
+                    _rowsPresenter.Children.Add(detailsContent);
+                    if (dataItem != null)
+                    {
+                        detailsContent.DataContext = dataItem;
+                    }
+                    detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+                    RowDetailsHeightEstimate = detailsContent.DesiredSize.Height;
+                    _rowsPresenter.Children.Remove(detailsContent);
+                }
+            }
+        }
+
+        // detailsElement is the FrameworkElement created by the DetailsTemplate
+        internal void OnUnloadingRowDetails(DataGridRow row, IControl detailsElement)
+        {
+            OnUnloadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement));
+        }
+
+        // detailsElement is the FrameworkElement created by the DetailsTemplate
+        internal void OnLoadingRowDetails(DataGridRow row, IControl detailsElement)
+        {
+            OnLoadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement));
+        }
+
+        internal void OnRowDetailsVisibilityPropertyChanged(int rowIndex, bool isVisible)
+        {
+            Debug.Assert(rowIndex >= 0 && rowIndex < SlotCount);
+
+            _showDetailsTable.AddValue(rowIndex, isVisible);
+        }
+
+        internal bool GetRowDetailsVisibility(int rowIndex)
+        {
+            return GetRowDetailsVisibility(rowIndex, RowDetailsVisibilityMode);
+        }
+
+        internal bool GetRowDetailsVisibility(int rowIndex, DataGridRowDetailsVisibilityMode gridLevelRowDetailsVisibility)
+        {
+            Debug.Assert(rowIndex != -1);
+            if (_showDetailsTable.Contains(rowIndex))
+            {
+                // The user explicity set DetailsVisibility on a row so we should respect that
+                return _showDetailsTable.GetValueAt(rowIndex);
+            }
+            else
+            {
+                return
+                    gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.Visible ||
+                    (gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.VisibleWhenSelected &&
+                     _selectedItems.ContainsSlot(SlotFromRowIndex(rowIndex)));
+            }
+        }
+
+        /// <summary>
+        /// Raises the <see cref="E:Avalonia.Controls.DataGrid.RowDetailsVisibilityChanged" /> event.
+        /// </summary>
+        /// <param name="e">The event data.</param>
+        protected internal virtual void OnRowDetailsVisibilityChanged(DataGridRowDetailsEventArgs e)
+        {
+            RowDetailsVisibilityChanged?.Invoke(this, e);
+        }
+
+#if DEBUG
+        internal void PrintRowGroupInfo()
+        {
+            Debug.WriteLine("-----------------------------------------------RowGroupHeaders");
+            foreach (int slot in RowGroupHeadersTable.GetIndexes())
+            {
+                DataGridRowGroupInfo info = RowGroupHeadersTable.GetValueAt(slot);
+                Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} {1} Slot:{2} Last:{3} Level:{4}", info.CollectionViewGroup.Key, info.IsVisible.ToString(), slot, info.LastSubItemSlot, info.Level));
+            }
+            Debug.WriteLine("-----------------------------------------------CollapsedSlots");
+            _collapsedSlotsTable.PrintIndexes();
+        }
+#endif
+    }
+}

+ 470 - 0
src/Avalonia.Controls.DataGrid/DataGridSelectedItemsCollection.cs

@@ -0,0 +1,470 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Collections;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridSelectedItemsCollection : IList
+    {
+        private List<object> _oldSelectedItemsCache;
+        private IndexToValueTable<bool> _oldSelectedSlotsTable;
+        private List<object> _selectedItemsCache;
+        private IndexToValueTable<bool> _selectedSlotsTable;
+
+        public DataGridSelectedItemsCollection(DataGrid owningGrid)
+        {
+            OwningGrid = owningGrid;
+            _oldSelectedItemsCache = new List<object>();
+            _oldSelectedSlotsTable = new IndexToValueTable<bool>();
+            _selectedItemsCache = new List<object>();
+            _selectedSlotsTable = new IndexToValueTable<bool>();
+        }
+
+        public object this[int index]
+        {
+            get
+            {
+                if (index < 0 || index >= _selectedSlotsTable.IndexCount)
+                {
+                    throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _selectedSlotsTable.IndexCount, false);
+                }
+                int slot = _selectedSlotsTable.GetNthIndex(index);
+                Debug.Assert(slot > -1);
+                return OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(slot));
+            }
+            set
+            {
+                throw new NotSupportedException();
+            }
+        }
+
+        public bool IsFixedSize
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public bool IsReadOnly
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public int Add(object dataItem)
+        {
+            if (OwningGrid.SelectionMode == DataGridSelectionMode.Single)
+            {
+                throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode();
+            }
+
+            int itemIndex = OwningGrid.DataConnection.IndexOf(dataItem);
+            if (itemIndex == -1)
+            {
+                throw DataGridError.DataGrid.ItemIsNotContainedInTheItemsSource("dataItem");
+            }
+            Debug.Assert(itemIndex >= 0);
+
+            int slot = OwningGrid.SlotFromRowIndex(itemIndex);
+            if (_selectedSlotsTable.RangeCount == 0)
+            {
+                OwningGrid.SelectedItem = dataItem;
+            }
+            else
+            {
+                OwningGrid.SetRowSelection(slot, true /*isSelected*/, false /*setAnchorSlot*/);
+            }
+            return _selectedSlotsTable.IndexOf(slot);
+        }
+
+        public void Clear()
+        {
+            if (OwningGrid.SelectionMode == DataGridSelectionMode.Single)
+            {
+                throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode();
+            }
+
+            if (_selectedSlotsTable.RangeCount > 0)
+            {
+                // Clearing the selection does not reset the potential current cell.
+                if (!OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/))
+                {
+                    // Edited value couldn't be committed or aborted
+                    return;
+                }
+                OwningGrid.ClearRowSelection(true /*resetAnchorSlot*/);
+            }
+        }
+
+        public bool Contains(object dataItem)
+        {
+            int itemIndex = OwningGrid.DataConnection.IndexOf(dataItem);
+            if (itemIndex == -1)
+            {
+                return false;
+            }
+            Debug.Assert(itemIndex >= 0);
+
+            return ContainsSlot(OwningGrid.SlotFromRowIndex(itemIndex));
+        }
+
+        public int IndexOf(object dataItem)
+        {
+            int itemIndex = OwningGrid.DataConnection.IndexOf(dataItem);
+            if (itemIndex == -1)
+            {
+                return -1;
+            }
+            Debug.Assert(itemIndex >= 0);
+            int slot = OwningGrid.SlotFromRowIndex(itemIndex);
+            return _selectedSlotsTable.IndexOf(slot);
+        }
+
+        public void Insert(int index, object dataItem)
+        {
+            throw new NotSupportedException();
+        }
+
+        public void Remove(object dataItem)
+        {
+            if (OwningGrid.SelectionMode == DataGridSelectionMode.Single)
+            {
+                throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode();
+            }
+
+            int itemIndex = OwningGrid.DataConnection.IndexOf(dataItem);
+            if (itemIndex == -1)
+            {
+                return;
+            }
+            Debug.Assert(itemIndex >= 0);
+
+            if (itemIndex == OwningGrid.CurrentSlot &&
+                !OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/))
+            {
+                // Edited value couldn't be committed or aborted
+                return;
+            }
+
+            OwningGrid.SetRowSelection(itemIndex, false /*isSelected*/, false /*setAnchorSlot*/);
+        }
+
+        public void RemoveAt(int index)
+        {
+            if (OwningGrid.SelectionMode == DataGridSelectionMode.Single)
+            {
+                throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode();
+            }
+
+            if (index < 0 || index >= _selectedSlotsTable.IndexCount)
+            {
+                throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _selectedSlotsTable.IndexCount, false);
+            }
+            int rowIndex = _selectedSlotsTable.GetNthIndex(index);
+            Debug.Assert(rowIndex > -1);
+
+            if (rowIndex == OwningGrid.CurrentSlot &&
+                !OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/))
+            {
+                // Edited value couldn't be committed or aborted
+                return;
+            }
+
+            OwningGrid.SetRowSelection(rowIndex, false /*isSelected*/, false /*setAnchorSlot*/);
+        }
+
+        public int Count
+        {
+            get
+            {
+                return _selectedSlotsTable.IndexCount;
+            }
+        }
+
+        public bool IsSynchronized
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public object SyncRoot
+        {
+            get
+            {
+                return this;
+            }
+        }
+
+        public void CopyTo(System.Array array, int index)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IEnumerator GetEnumerator()
+        {
+            Debug.Assert(OwningGrid != null);
+            Debug.Assert(OwningGrid.DataConnection != null);
+            Debug.Assert(_selectedSlotsTable != null);
+
+            foreach (int slot in _selectedSlotsTable.GetIndexes())
+            {
+                int rowIndex = OwningGrid.RowIndexFromSlot(slot);
+                Debug.Assert(rowIndex > -1);
+                yield return OwningGrid.DataConnection.GetDataItem(rowIndex);
+            }
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get;
+            private set;
+        }
+
+        internal List<object> SelectedItemsCache
+        {
+            get
+            {
+                return _selectedItemsCache;
+            }
+            set
+            {
+                _selectedItemsCache = value;
+                UpdateIndexes();
+            }
+        }
+
+        internal void ClearRows()
+        {
+            _selectedSlotsTable.Clear();
+            _selectedItemsCache.Clear();
+        }
+
+        internal bool ContainsSlot(int slot)
+        {
+            return _selectedSlotsTable.Contains(slot);
+        }
+
+        internal bool ContainsAll(int startSlot, int endSlot)
+        {
+            int itemSlot = OwningGrid.RowGroupHeadersTable.GetNextGap(startSlot - 1);
+            while (itemSlot <= endSlot)
+            {
+                // Skip over the RowGroupHeaderSlots
+                int nextRowGroupHeaderSlot = OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot);
+                int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endSlot : Math.Min(endSlot, nextRowGroupHeaderSlot - 1);
+                if (!_selectedSlotsTable.ContainsAll(itemSlot, lastItemSlot))
+                {
+                    return false;
+                }
+                itemSlot = OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot);
+            }
+            return true;
+        }
+
+        // Called when an item is deleted from the ItemsSource as opposed to just being unselected
+        internal void Delete(int slot, object item)
+        {
+            if (_oldSelectedSlotsTable.Contains(slot))
+            {
+                OwningGrid.SelectionHasChanged = true;
+            }
+            DeleteSlot(slot);
+            _selectedItemsCache.Remove(item);
+        }
+
+        internal void DeleteSlot(int slot)
+        {
+            _selectedSlotsTable.RemoveIndex(slot);
+            _oldSelectedSlotsTable.RemoveIndex(slot);
+        }
+
+        // Returns the inclusive index count between lowerBound and upperBound of all indexes with the given value
+        internal int GetIndexCount(int lowerBound, int upperBound)
+        {
+            return _selectedSlotsTable.GetIndexCount(lowerBound, upperBound, true);
+        }
+
+        internal IEnumerable<int> GetIndexes()
+        {
+            return _selectedSlotsTable.GetIndexes();
+        }
+
+        internal IEnumerable<int> GetSlots(int startSlot)
+        {
+            return _selectedSlotsTable.GetIndexes(startSlot);
+        }
+
+        internal SelectionChangedEventArgs GetSelectionChangedEventArgs()
+        {
+            List<object> addedSelectedItems = new List<object>();
+            List<object> removedSelectedItems = new List<object>();
+
+            // Compare the old selected indexes with the current selection to determine which items
+            // have been added and removed since the last time this method was called
+            foreach (int newSlot in _selectedSlotsTable.GetIndexes())
+            {
+                object newItem = OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(newSlot));
+                if (_oldSelectedSlotsTable.Contains(newSlot))
+                {
+                    _oldSelectedSlotsTable.RemoveValue(newSlot);
+                    _oldSelectedItemsCache.Remove(newItem);
+                }
+                else
+                {
+                    addedSelectedItems.Add(newItem);
+                }
+            }
+            foreach (object oldItem in _oldSelectedItemsCache)
+            {
+                removedSelectedItems.Add(oldItem);
+            }
+
+            // The current selection becomes the old selection
+            _oldSelectedSlotsTable = _selectedSlotsTable.Copy();
+            _oldSelectedItemsCache = new List<object>(_selectedItemsCache);
+
+            return
+                new SelectionChangedEventArgs(DataGrid.SelectionChangedEvent, removedSelectedItems, addedSelectedItems)
+                {
+                    Source = OwningGrid
+                };
+        }
+
+        internal void InsertIndex(int slot)
+        {
+            _selectedSlotsTable.InsertIndex(slot);
+            _oldSelectedSlotsTable.InsertIndex(slot);
+
+            // It's possible that we're inserting an item that was just removed.  If that's the case,
+            // and the re-inserted item used to be selected, we want to update the _oldSelectedSlotsTable
+            // to include the item's new index within the collection.
+            int rowIndex = OwningGrid.RowIndexFromSlot(slot);
+            if (rowIndex != -1)
+            {
+                object insertedItem = OwningGrid.DataConnection.GetDataItem(rowIndex);
+                if (insertedItem != null && _oldSelectedItemsCache.Contains(insertedItem))
+                {
+                    _oldSelectedSlotsTable.AddValue(slot, true);
+                }
+            }
+        }
+
+        internal void SelectSlot(int slot, bool select)
+        {
+            if (OwningGrid.RowGroupHeadersTable.Contains(slot))
+            {
+                return;
+            }
+            if (select)
+            {
+                if (!_selectedSlotsTable.Contains(slot))
+                {
+                    _selectedItemsCache.Add(OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(slot)));
+                }
+                _selectedSlotsTable.AddValue(slot, true);
+            }
+            else
+            {
+                if (_selectedSlotsTable.Contains(slot))
+                {
+                    _selectedItemsCache.Remove(OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(slot)));
+                }
+                _selectedSlotsTable.RemoveValue(slot);
+            }
+        }
+
+        internal void SelectSlots(int startSlot, int endSlot, bool select)
+        {
+            int itemSlot = OwningGrid.RowGroupHeadersTable.GetNextGap(startSlot - 1);
+            int endItemSlot = OwningGrid.RowGroupHeadersTable.GetPreviousGap(endSlot + 1);
+            if (select)
+            {
+                while (itemSlot <= endItemSlot)
+                {
+                    // Add the newly selected item slots by skipping over the RowGroupHeaderSlots
+                    int nextRowGroupHeaderSlot = OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot);
+                    int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endItemSlot : Math.Min(endItemSlot, nextRowGroupHeaderSlot - 1);
+                    for (int slot = itemSlot; slot <= lastItemSlot; slot++)
+                    {
+                        if (!_selectedSlotsTable.Contains(slot))
+                        {
+                            _selectedItemsCache.Add(OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(slot)));
+                        }
+                    }
+                    _selectedSlotsTable.AddValues(itemSlot, lastItemSlot - itemSlot + 1, true);
+                    itemSlot = OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot);
+                }
+            }
+            else
+            {
+                while (itemSlot <= endItemSlot)
+                {
+                    // Remove the unselected item slots by skipping over the RowGroupHeaderSlots
+                    int nextRowGroupHeaderSlot = OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot);
+                    int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endItemSlot : Math.Min(endItemSlot, nextRowGroupHeaderSlot - 1);
+                    for (int slot = itemSlot; slot <= lastItemSlot; slot++)
+                    {
+                        if (_selectedSlotsTable.Contains(slot))
+                        {
+                            _selectedItemsCache.Remove(OwningGrid.DataConnection.GetDataItem(OwningGrid.RowIndexFromSlot(slot)));
+                        }
+                    }
+                    _selectedSlotsTable.RemoveValues(itemSlot, lastItemSlot - itemSlot + 1);
+                    itemSlot = OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot);
+                }
+            }
+        }
+
+        internal void UpdateIndexes()
+        {
+            _oldSelectedSlotsTable.Clear();
+            _selectedSlotsTable.Clear();
+
+            if (OwningGrid.DataConnection.DataSource == null)
+            {
+                if (SelectedItemsCache.Count > 0)
+                {
+                    OwningGrid.SelectionHasChanged = true;
+                    SelectedItemsCache.Clear();
+                }
+            }
+            else
+            {
+                List<object> tempSelectedItemsCache = new List<object>();
+                foreach (object item in _selectedItemsCache)
+                {
+                    int index = OwningGrid.DataConnection.IndexOf(item);
+                    if (index != -1)
+                    {
+                        tempSelectedItemsCache.Add(item);
+                        _selectedSlotsTable.AddValue(OwningGrid.SlotFromRowIndex(index), true);
+                    }
+                }
+                foreach (object item in _oldSelectedItemsCache)
+                {
+                    int index = OwningGrid.DataConnection.IndexOf(item);
+                    if (index == -1)
+                    {
+                        OwningGrid.SelectionHasChanged = true;
+                    }
+                    else
+                    {
+                        _oldSelectedSlotsTable.AddValue(OwningGrid.SlotFromRowIndex(index), true);
+                    }
+                }
+                _selectedItemsCache = tempSelectedItemsCache;
+            }
+        }
+    }
+}

+ 79 - 0
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@@ -0,0 +1,79 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    public class DataGridTemplateColumn : DataGridColumn
+    {
+        IDataTemplate _cellTemplate;
+
+        public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty =
+            AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
+                nameof(CellTemplate),
+                o => o.CellTemplate,
+                (o, v) => o.CellTemplate = v);
+
+        public IDataTemplate CellTemplate
+        {
+            get { return _cellTemplate; }
+            set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); }
+        }
+
+        private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (IDataTemplate)e.OldValue;
+            var value = (IDataTemplate)e.NewValue;
+        }
+
+        public DataGridTemplateColumn()
+        {
+            IsReadOnly = true;
+        }
+
+        protected override IControl GenerateElement(DataGridCell cell, object dataItem)
+        {
+            if(CellTemplate != null)
+            {
+                return CellTemplate.Build(dataItem);
+            }
+            if (Design.IsDesignMode)
+            {
+                return null;
+            }
+            else
+            {
+                throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn));
+            }
+        }
+
+        protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding)
+        {
+            binding = null;
+            return GenerateElement(cell, dataItem);
+        }
+
+        protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
+        {
+            return null;
+        }
+
+        protected internal override void RefreshCellContent(IControl element, string propertyName)
+        {
+            if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell)
+            {
+                cell.Content = GenerateElement(cell, cell.DataContext);
+            }
+
+            base.RefreshCellContent(element, propertyName);
+        }
+    }
+}

+ 357 - 0
src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs

@@ -0,0 +1,357 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using System;
+using System.ComponentModel;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> column that hosts textual content in its cells.
+    /// </summary>
+    public class DataGridTextColumn : DataGridBoundColumn
+    {
+
+        private double? _fontSize;
+        private FontStyle? _fontStyle;
+        private FontWeight? _fontWeight;
+        private IBrush _foreground;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridTextColumn" /> class.
+        /// </summary>
+        public DataGridTextColumn()
+        {
+            BindingTarget = TextBox.TextProperty;
+        }
+
+        /// <summary>
+        /// Identifies the FontFamily dependency property.
+        /// </summary>
+        public static readonly StyledProperty<string> FontFamilyProperty =
+            AvaloniaProperty.Register<DataGridTextColumn, string>(nameof(FontFamily));
+
+        /// <summary>
+        /// Gets or sets the font name.
+        /// </summary>
+        public string FontFamily
+        {
+            get { return GetValue(FontFamilyProperty); }
+            set { SetValue(FontFamilyProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the font size.
+        /// </summary>
+        // Use DefaultValue here so undo in the Designer will set this to NaN
+        [DefaultValue(double.NaN)]
+        public double FontSize
+        {
+            get
+            {
+                return _fontSize ?? Double.NaN;
+            }
+            set
+            {
+                if (_fontSize != value)
+                {
+                    _fontSize = value;
+                    NotifyPropertyChanged(nameof(FontSize));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the font style.
+        /// </summary>
+        public FontStyle FontStyle
+        {
+            get
+            {
+                return _fontStyle ?? FontStyle.Normal;
+            }
+            set
+            {
+                if (_fontStyle != value)
+                {
+                    _fontStyle = value;
+                    NotifyPropertyChanged(nameof(FontStyle));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the font weight or thickness.
+        /// </summary>
+        public FontWeight FontWeight
+        {
+            get
+            {
+                return _fontWeight ?? FontWeight.Normal;
+            }
+            set
+            {
+                if (_fontWeight != value)
+                {
+                    _fontWeight = value;
+                    NotifyPropertyChanged(nameof(FontWeight));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets a brush that describes the foreground of the column cells.
+        /// </summary>
+        public IBrush Foreground
+        {
+            get
+            {
+                return _foreground;
+            }
+            set
+            {
+                if (_foreground != value)
+                {
+                    _foreground = value;
+                    NotifyPropertyChanged(nameof(Foreground));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Causes the column cell being edited to revert to the specified value.
+        /// </summary>
+        /// <param name="editingElement">The element that the column displays for a cell in editing mode.</param>
+        /// <param name="uneditedValue">The previous, unedited value in the cell being edited.</param>
+        protected override void CancelCellEdit(IControl editingElement, object uneditedValue)
+        {
+            if (editingElement is TextBox textBox)
+            {
+                string uneditedString = uneditedValue as string;
+                textBox.Text = uneditedString ?? string.Empty;
+            }
+        }
+
+        /// <summary>
+        /// Gets a <see cref="T:Avalonia.Controls.TextBox" /> control that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </summary>
+        /// <param name="cell">The cell that will contain the generated element.</param>
+        /// <param name="dataItem">The data item represented by the row that contains the intended cell.</param>
+        /// <returns>A new <see cref="T:Avalonia.Controls.TextBox" /> control that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.</returns>
+        protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
+        {
+            var textBox = new TextBox
+            {
+                VerticalAlignment = VerticalAlignment.Stretch,
+                Background = new SolidColorBrush(Colors.Transparent)
+            };
+
+            if (IsSet(FontFamilyProperty))
+            {
+                textBox.FontFamily = FontFamily;
+            }
+            if (_fontSize.HasValue)
+            {
+                textBox.FontSize = _fontSize.Value;
+            }
+            if (_fontStyle.HasValue)
+            {
+                textBox.FontStyle = _fontStyle.Value;
+            }
+            if (_fontWeight.HasValue)
+            {
+                textBox.FontWeight = _fontWeight.Value;
+            }
+            if (_foreground != null)
+            {
+                textBox.Foreground = _foreground;
+            }
+
+            return textBox;
+        }
+
+        /// <summary>
+        /// Gets a read-only <see cref="T:Avalonia.Controls.TextBlock" /> element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
+        /// </summary>
+        /// <param name="cell">The cell that will contain the generated element.</param>
+        /// <param name="dataItem">The data item represented by the row that contains the intended cell.</param>
+        /// <returns>A new, read-only <see cref="T:Avalonia.Controls.TextBlock" /> element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.</returns>
+        protected override IControl GenerateElement(DataGridCell cell, object dataItem)
+        {
+            TextBlock textBlockElement = new TextBlock
+            {
+                Margin = new Thickness(4),
+                VerticalAlignment = VerticalAlignment.Center
+            };
+
+            if (IsSet(FontFamilyProperty))
+            {
+                textBlockElement.FontFamily = FontFamily;
+            }
+            if (_fontSize.HasValue)
+            {
+                textBlockElement.FontSize = _fontSize.Value;
+            }
+            if (_fontStyle.HasValue)
+            {
+                textBlockElement.FontStyle = _fontStyle.Value;
+            }
+            if (_fontWeight.HasValue)
+            {
+                textBlockElement.FontWeight = _fontWeight.Value;
+            }
+            if (_foreground != null)
+            {
+                textBlockElement.Foreground = _foreground;
+            }
+            if (Binding != null)
+            {
+                textBlockElement.Bind(TextBlock.TextProperty, Binding);
+            }
+            return textBlockElement;
+        }
+
+        /// <summary>
+        /// Called when the cell in the column enters editing mode.
+        /// </summary>
+        /// <param name="editingElement">The element that the column displays for a cell in editing mode.</param>
+        /// <param name="editingEventArgs">Information about the user gesture that is causing a cell to enter editing mode.</param>
+        /// <returns>The unedited value. </returns>
+        protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
+        {
+            if (editingElement is TextBox textBox)
+            {
+                string uneditedText = textBox.Text ?? String.Empty;
+                int len = uneditedText.Length;
+                if (editingEventArgs is KeyEventArgs keyEventArgs && keyEventArgs.Key == Key.F2)
+                {
+                    // Put caret at the end of the text
+                    textBox.SelectionStart = len;
+                    textBox.SelectionEnd = len;
+                }
+                else
+                {
+                    // Select all text
+                    textBox.SelectionStart = 0;
+                    textBox.SelectionEnd = len;
+                    textBox.CaretIndex = len;
+                }
+
+                return uneditedText;
+            }
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Called by the DataGrid control when this column asks for its elements to be
+        /// updated, because a property changed.
+        /// </summary>
+        protected internal override void RefreshCellContent(IControl element, string propertyName)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException("element");
+            }
+
+            if(element is TextBox textBox)
+            {
+                if (propertyName == nameof(FontFamily))
+                {
+                    textBox.FontFamily = FontFamily;
+                }
+                else if (propertyName == nameof(FontSize))
+                {
+                    SetTextFontSize(textBox, TextBox.FontSizeProperty);
+                }
+                else if (propertyName == nameof(FontStyle))
+                {
+                    textBox.FontStyle = FontStyle;
+                }
+                else if (propertyName == nameof(FontWeight))
+                {
+                    textBox.FontWeight = FontWeight;
+                }
+                else if (propertyName == nameof(Foreground))
+                {
+                    textBox.Foreground = Foreground;
+                }
+                else
+                {
+                    if (FontFamily != null)
+                    {
+                        textBox.FontFamily = FontFamily;
+                    }
+                    SetTextFontSize(textBox, TextBox.FontSizeProperty);
+                    textBox.FontStyle = FontStyle;
+                    textBox.FontWeight = FontWeight;
+                    if (Foreground != null)
+                    {
+                        textBox.Foreground = Foreground;
+                    }
+                }
+
+            }
+            else if (element is TextBlock textBlock)
+            {
+                if (propertyName == nameof(FontFamily))
+                {
+                    textBlock.FontFamily = FontFamily;
+                }
+                else if (propertyName == nameof(FontSize))
+                {
+                    SetTextFontSize(textBlock, TextBlock.FontSizeProperty);
+                }
+                else if (propertyName == nameof(FontStyle))
+                {
+                    textBlock.FontStyle = FontStyle;
+                }
+                else if (propertyName == nameof(FontWeight))
+                {
+                    textBlock.FontWeight = FontWeight;
+                }
+                else if (propertyName == nameof(Foreground))
+                {
+                    textBlock.Foreground = Foreground;
+                }
+                else
+                {
+                    if (FontFamily != null)
+                    {
+                        textBlock.FontFamily = FontFamily;
+                    }
+                    SetTextFontSize(textBlock, TextBlock.FontSizeProperty);
+                    textBlock.FontStyle = FontStyle;
+                    textBlock.FontWeight = FontWeight;
+                    if (Foreground != null)
+                    {
+                        textBlock.Foreground = Foreground;
+                    }
+                }
+            }
+            else
+            {
+                throw DataGridError.DataGrid.ValueIsNotAnInstanceOfEitherOr("element", typeof(TextBox), typeof(TextBlock));
+            }
+        }
+
+        private void SetTextFontSize(AvaloniaObject textElement, AvaloniaProperty fontSizeProperty)
+        {
+            double newFontSize = FontSize;
+            if (double.IsNaN(newFontSize))
+            {
+                textElement.ClearValue(fontSizeProperty);
+            }
+            else
+            {
+                textElement.SetValue(fontSizeProperty, newFontSize);
+            }
+        }
+
+    }
+}

+ 40 - 0
src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs

@@ -0,0 +1,40 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Utils;
+using Avalonia.Data.Converters;
+using Avalonia.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+    internal class DataGridValueConverter : IValueConverter
+    {
+        public static DataGridValueConverter Instance = new DataGridValueConverter();
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return DefaultValueConverter.Instance.Convert(value, targetType, parameter, culture);
+        }
+
+        // This suppresses a warning saying that we should use String.IsNullOrEmpty instead of a string
+        // comparison, but in this case we want to explicitly check for Empty and not Null.
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (targetType != null && targetType.IsNullableType())
+            {
+                String strValue = value as String;
+                if (strValue == String.Empty)
+                {
+                    return null;
+                }
+            }
+            return DefaultValueConverter.Instance.ConvertBack(value, targetType, parameter, culture);
+        }
+    }
+}

+ 569 - 0
src/Avalonia.Controls.DataGrid/EventArgs.cs

@@ -0,0 +1,569 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="E:Avalonia.Controls.DataGrid.AutoGeneratingColumn" /> event. 
+    /// </summary>
+    public class DataGridAutoGeneratingColumnEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridAutoGeneratingColumnEventArgs" /> class.
+        /// </summary>
+        /// <param name="propertyName">
+        /// The name of the property bound to the generated column.
+        /// </param>
+        /// <param name="propertyType">
+        /// The <see cref="T:System.Type" /> of the property bound to the generated column.
+        /// </param>
+        /// <param name="column">
+        /// The generated column.
+        /// </param>
+        public DataGridAutoGeneratingColumnEventArgs(string propertyName, Type propertyType, DataGridColumn column)
+        {
+            Column = column;
+            PropertyName = propertyName;
+            PropertyType = propertyType;
+        }
+
+        /// <summary>
+        /// Gets the generated column.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Gets the name of the property bound to the generated column.
+        /// </summary>
+        public string PropertyName
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets the <see cref="T:System.Type" /> of the property bound to the generated column.
+        /// </summary>
+        public Type PropertyType
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the <see cref="E:Avalonia.Controls.DataGrid.BeginningEdit" /> event.
+    /// </summary>
+    public class DataGridBeginningEditEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the 
+        /// <see cref="T:Avalonia.Controls.DataGridBeginningEditEventArgs" /> class.
+        /// </summary>
+        /// <param name="column">
+        /// The column that contains the cell to be edited.
+        /// </param>
+        /// <param name="row">
+        /// The row that contains the cell to be edited.
+        /// </param>
+        /// <param name="editingEventArgs">
+        /// Information about the user gesture that caused the cell to enter edit mode.
+        /// </param>
+        public DataGridBeginningEditEventArgs(DataGridColumn column,
+                                              DataGridRow row,
+                                              RoutedEventArgs editingEventArgs)
+        {
+            this.Column = column;
+            this.Row = row;
+            this.EditingEventArgs = editingEventArgs;
+        }
+
+        /// <summary>
+        /// Gets the column that contains the cell to be edited.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets information about the user gesture that caused the cell to enter edit mode.
+        /// </summary>
+        public RoutedEventArgs EditingEventArgs
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets the row that contains the cell to be edited.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+
+    }
+
+    /// <summary>
+    /// Provides information just after a cell has exited editing mode.
+    /// </summary>
+    public class DataGridCellEditEndedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Instantiates a new instance of this class.
+        /// </summary>
+        /// <param name="column">The column of the cell that has just exited edit mode.</param>
+        /// <param name="row">The row container of the cell container that has just exited edit mode.</param>
+        /// <param name="editAction">The editing action that has been taken.</param>
+        public DataGridCellEditEndedEventArgs(DataGridColumn column, DataGridRow row, DataGridEditAction editAction)
+        {
+            Column = column;
+            Row = row;
+            EditAction = editAction;
+        }
+
+        /// <summary>
+        /// The column of the cell that has just exited edit mode.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The edit action taken when leaving edit mode.
+        /// </summary>
+        public DataGridEditAction EditAction
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The row container of the cell container that has just exited edit mode.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+
+    }
+
+    /// <summary>
+    /// Provides information after the cell has been pressed.
+    /// </summary>
+    public class DataGridCellPointerPressedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Instantiates a new instance of this class.
+        /// </summary>
+        /// <param name="cell">The cell that has been pressed.</param>
+        /// <param name="row">The row container of the cell that has been pressed.</param>
+        /// <param name="column">The column of the cell that has been pressed.</param>
+        /// <param name="e">The pointer action that has been taken.</param>
+        public DataGridCellPointerPressedEventArgs(DataGridCell cell, 
+                                                   DataGridRow row,
+                                                   DataGridColumn column,
+                                                   PointerPressedEventArgs e)
+        {
+            Cell = cell;
+            Row = row;
+            Column = column;
+            PointerPressedEventArgs = e;
+        }
+
+        /// <summary>
+        /// The cell that has been pressed.
+        /// </summary> 
+        public DataGridCell Cell { get; }
+
+        /// <summary>
+        /// The row container of the cell that has been pressed.
+        /// </summary> 
+        public DataGridRow Row { get; }
+
+        /// <summary>
+        /// The column of the cell that has been pressed.
+        /// </summary> 
+        public DataGridColumn Column { get; }
+
+        /// <summary>
+        /// The pointer action that has been taken.
+        /// </summary> 
+        public PointerPressedEventArgs PointerPressedEventArgs { get; }
+    }
+
+    /// <summary>
+    /// Provides information just before a cell exits editing mode.
+    /// </summary>
+    public class DataGridCellEditEndingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Instantiates a new instance of this class.
+        /// </summary>
+        /// <param name="column">The column of the cell that is about to exit edit mode.</param>
+        /// <param name="row">The row container of the cell container that is about to exit edit mode.</param>
+        /// <param name="editingElement">The editing element within the cell.</param>
+        /// <param name="editAction">The editing action that will be taken.</param>
+        public DataGridCellEditEndingEventArgs(DataGridColumn column,
+                                               DataGridRow row,
+                                               Control editingElement,
+                                               DataGridEditAction editAction)
+        {
+            Column = column;
+            Row = row;
+            EditingElement = editingElement;
+            EditAction = editAction;
+        }
+
+        /// <summary>
+        /// The column of the cell that is about to exit edit mode.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The edit action to take when leaving edit mode.
+        /// </summary>
+        public DataGridEditAction EditAction
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The editing element within the cell. 
+        /// </summary>
+        public Control EditingElement
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The row container of the cell container that is about to exit edit mode.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+
+    }
+
+    internal class DataGridCellEventArgs : EventArgs
+    {
+        internal DataGridCellEventArgs(DataGridCell dataGridCell)
+        {
+            Debug.Assert(dataGridCell != null);
+            this.Cell = dataGridCell;
+        }
+
+        internal DataGridCell Cell
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events.
+    /// </summary>
+    public class DataGridColumnEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class.
+        /// </summary>
+        /// <param name="column">The column that the event occurs for.</param>
+        public DataGridColumnEventArgs(DataGridColumn column)
+        {
+            Column = column ?? throw new ArgumentNullException(nameof(column));
+        }
+
+        /// <summary>
+        /// Gets the column that the event occurs for.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the <see cref="E:Avalonia.Controls.DataGrid.ColumnReordering" /> event.
+    /// </summary>
+    public class DataGridColumnReorderingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnReorderingEventArgs" /> class.
+        /// </summary>
+        /// <param name="dataGridColumn"></param>
+        public DataGridColumnReorderingEventArgs(DataGridColumn dataGridColumn)
+        {
+            this.Column = dataGridColumn;
+        }
+
+        /// <summary>
+        /// The column being moved.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The popup indicator displayed while dragging.  If null and Handled = true, then do not display a tooltip.
+        /// </summary>
+        public Control DragIndicator
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// UIElement to display at the insertion position.  If null and Handled = true, then do not display an insertion indicator.
+        /// </summary>
+        public IControl DropLocationIndicator
+        {
+            get;
+            set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> row-related events.
+    /// </summary>
+    public class DataGridRowEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridRowEventArgs" /> class.
+        /// </summary>
+        /// <param name="dataGridRow">The row that the event occurs for.</param>
+        public DataGridRowEventArgs(DataGridRow dataGridRow)
+        {
+            this.Row = dataGridRow;
+        }
+
+        /// <summary>
+        /// Gets the row that the event occurs for.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides information just before a row exits editing mode.
+    /// </summary>
+    public class DataGridRowEditEndingEventArgs : CancelEventArgs
+    {
+        /// <summary>
+        /// Instantiates a new instance of this class.
+        /// </summary>
+        /// <param name="row">The row container of the cell container that is about to exit edit mode.</param>
+        /// <param name="editAction">The editing action that will be taken.</param>
+        public DataGridRowEditEndingEventArgs(DataGridRow row, DataGridEditAction editAction)
+        {
+            this.Row = row;
+            this.EditAction = editAction;
+        }
+
+        /// <summary>
+        /// The editing action that will be taken.
+        /// </summary>
+        public DataGridEditAction EditAction
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The row container of the cell container that is about to exit edit mode.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides information just after a row has exited edit mode.
+    /// </summary>
+    public class DataGridRowEditEndedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Instantiates a new instance of this class.
+        /// </summary>
+        /// <param name="row">The row container of the cell container that has just exited edit mode.</param>
+        /// <param name="editAction">The editing action that has been taken.</param>
+        public DataGridRowEditEndedEventArgs(DataGridRow row, DataGridEditAction editAction)
+        {
+            this.Row = row;
+            this.EditAction = editAction;
+        }
+
+        /// <summary>
+        /// The editing action that has been taken.
+        /// </summary>
+        public DataGridEditAction EditAction
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// The row container of the cell container that has just exited edit mode.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the <see cref="E:Avalonia.Controls.DataGrid.PreparingCellForEdit" /> event.
+    /// </summary>
+    public class DataGridPreparingCellForEditEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridPreparingCellForEditEventArgs" /> class.
+        /// </summary>
+        /// <param name="column">The column that contains the cell to be edited.</param>
+        /// <param name="row">The row that contains the cell to be edited.</param>
+        /// <param name="editingEventArgs">Information about the user gesture that caused the cell to enter edit mode.</param>
+        /// <param name="editingElement">The element that the column displays for a cell in editing mode.</param>
+        public DataGridPreparingCellForEditEventArgs(DataGridColumn column,
+                                                     DataGridRow row,
+                                                     RoutedEventArgs editingEventArgs,
+                                                     Control editingElement)
+        {
+            Column = column;
+            Row = row;
+            EditingEventArgs = editingEventArgs;
+            EditingElement = editingElement;
+        }
+
+        /// <summary>
+        /// Gets the column that contains the cell to be edited.
+        /// </summary>
+        public DataGridColumn Column
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets the element that the column displays for a cell in editing mode.
+        /// </summary>
+        public Control EditingElement
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets information about the user gesture that caused the cell to enter edit mode.
+        /// </summary>
+        public RoutedEventArgs EditingEventArgs
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets the row that contains the cell to be edited.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+
+    }
+
+    /// <summary>
+    /// EventArgs used for the DataGrid's LoadingRowGroup and UnloadingRowGroup events
+    /// </summary>
+    public class DataGridRowGroupHeaderEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Constructs a DataGridRowGroupHeaderEventArgs instance
+        /// </summary>
+        /// <param name="rowGroupHeader"></param>
+        public DataGridRowGroupHeaderEventArgs(DataGridRowGroupHeader rowGroupHeader)
+        {
+            RowGroupHeader = rowGroupHeader;
+        }
+
+        /// <summary>
+        /// DataGridRowGroupHeader associated with this instance
+        /// </summary>
+        public DataGridRowGroupHeader RowGroupHeader
+        {
+            get;
+            private set;
+        }
+    }
+
+    /// <summary>
+    /// Provides data for the <see cref="E:Avalonia.Controls.DataGrid.LoadingRowDetails" />, <see cref="E:Avalonia.Controls.DataGrid.UnloadingRowDetails" />, 
+    /// and <see cref="E:Avalonia.Controls.DataGrid.RowDetailsVisibilityChanged" /> events.
+    /// </summary>
+    public class DataGridRowDetailsEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridRowDetailsEventArgs" /> class. 
+        /// </summary>
+        /// <param name="row">The row that the event occurs for.</param>
+        /// <param name="detailsElement">The row details section as a framework element.</param>
+        public DataGridRowDetailsEventArgs(DataGridRow row, IControl detailsElement)
+        {
+            Row = row;
+            DetailsElement = detailsElement;
+        }
+
+        /// <summary>
+        /// Gets the row details section as a framework element.
+        /// </summary>
+        public IControl DetailsElement
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// Gets the row that the event occurs for.
+        /// </summary>
+        public DataGridRow Row
+        {
+            get;
+            private set;
+        }
+    }
+}

+ 25 - 0
src/Avalonia.Controls.DataGrid/Extensions.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+    internal static class Extensions
+    {
+        internal static Point Translate(this Visual fromElement, Visual toElement, Point fromPoint)
+        {
+            if (fromElement == toElement)
+            {
+                return fromPoint;
+            }
+            else
+            {
+                var transform = fromElement.TransformToVisual(toElement);
+                if (transform.HasValue)
+                    return fromPoint.Transform(transform.Value);
+                else
+                    return fromPoint;
+            }
+        }
+    }
+}

+ 850 - 0
src/Avalonia.Controls.DataGrid/IndexToValueTable.cs

@@ -0,0 +1,850 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+    internal class IndexToValueTable<T> : IEnumerable<Range<T>>
+    {
+        private List<Range<T>> _list;
+
+        public IndexToValueTable()
+        {
+            _list = new List<Range<T>>();
+        }
+
+        /// <summary>
+        /// Total number of indices represented in the table
+        /// </summary>
+        public int IndexCount
+        {
+            get
+            {
+                int indexCount = 0;
+                foreach (Range<T> range in _list)
+                {
+                    indexCount += range.Count;
+                }
+                return indexCount;
+            }
+        }
+
+        /// <summary>
+        /// Returns true if the table is empty
+        /// </summary>
+        public bool IsEmpty
+        {
+            get
+            {
+                return _list.Count == 0;
+            }
+        }
+
+        /// <summary>
+        /// Returns the number of index ranges in the table
+        /// </summary>
+        public int RangeCount
+        {
+            get
+            {
+                return _list.Count;
+            }
+        }
+
+        /// <summary>
+        /// Add a value with an associated index to the table
+        /// </summary>
+        /// <param name="index">Index where the value is to be added or updated</param>
+        /// <param name="value">Value to add</param>
+        public void AddValue(int index, T value)
+        {
+            AddValues(index, 1, value);
+        }
+
+        /// <summary>
+        /// Add multiples values with an associated start index to the table 
+        /// </summary>
+        /// <param name="startIndex">index where first value is added</param>
+        /// <param name="count">Total number of values to add (must be greater than 0)</param>
+        /// <param name="value">Value to add</param>
+        public void AddValues(int startIndex, int count, T value)
+        {
+            Debug.Assert(count > 0);
+            AddValuesPrivate(startIndex, count, value, null);
+        }
+
+        /// <summary>
+        /// Clears the index table
+        /// </summary>
+        public void Clear()
+        {
+            _list.Clear();
+        }
+
+        /// <summary>
+        /// Returns true if the given index is contained in the table
+        /// </summary>
+        /// <param name="index">index to search for</param>
+        /// <returns>True if the index is contained in the table</returns>
+        public bool Contains(int index)
+        {
+            return IsCorrectRangeIndex(this.FindRangeIndex(index), index);
+        }
+
+        /// <summary>
+        /// Returns true if the entire given index range is contained in the table
+        /// </summary>
+        /// <param name="startIndex">beginning of the range</param>
+        /// <param name="endIndex">end of the range</param>
+        /// <returns>True if the entire index range is present in the table</returns>
+        public bool ContainsAll(int startIndex, int endIndex)
+        {
+            int start = -1;
+            int end = -1;
+
+            foreach (Range<T> range in _list)
+            {
+                if (start == -1 && range.UpperBound >= startIndex)
+                {
+                    if (startIndex < range.LowerBound)
+                    {
+                        return false;
+                    }
+                    start = startIndex;
+                    end = range.UpperBound;
+                    if (end >= endIndex)
+                    {
+                        return true;
+                    }
+                }
+                else if (start != -1)
+                {
+                    if (range.LowerBound > end + 1)
+                    {
+                        return false;
+                    }
+                    end = range.UpperBound;
+                    if (end >= endIndex)
+                    {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Returns true if the given index is contained in the table with the the given value
+        /// </summary>
+        /// <param name="index">index to search for</param>
+        /// <param name="value">value expected</param>
+        /// <returns>true if the given index is contained in the table with the the given value</returns>
+        public bool ContainsIndexAndValue(int index, T value)
+        {
+            int lowerRangeIndex = this.FindRangeIndex(index);
+            return ((IsCorrectRangeIndex(lowerRangeIndex, index)) && (_list[lowerRangeIndex].ContainsValue(value)));
+        }
+
+        /// <summary>
+        /// Returns a copy of this IndexToValueTable
+        /// </summary>
+        /// <returns>copy of this IndexToValueTable</returns>
+        public IndexToValueTable<T> Copy()
+        {
+            IndexToValueTable<T> copy = new IndexToValueTable<T>();
+            foreach (Range<T> range in this._list)
+            {
+                copy._list.Add(range.Copy());
+            }
+            return copy;
+        }
+
+        public int GetNextGap(int index)
+        {
+            int targetIndex = index + 1;
+            int rangeIndex = FindRangeIndex(targetIndex);
+            if (IsCorrectRangeIndex(rangeIndex, targetIndex))
+            {
+                while (rangeIndex < _list.Count - 1 && _list[rangeIndex].UpperBound == _list[rangeIndex + 1].LowerBound - 1)
+                {
+                    rangeIndex++;
+                }
+                return _list[rangeIndex].UpperBound + 1;
+            }
+            else
+            {
+                return targetIndex;
+            }
+        }
+
+        public int GetNextIndex(int index)
+        {
+            int targetIndex = index + 1;
+            int rangeIndex = FindRangeIndex(targetIndex);
+            if (IsCorrectRangeIndex(rangeIndex, targetIndex))
+            {
+                return targetIndex;
+            }
+            else
+            {
+                rangeIndex++;
+                return rangeIndex < _list.Count ? _list[rangeIndex].LowerBound : -1;
+            }
+        }
+
+        public int GetPreviousGap(int index)
+        {
+            int targetIndex = index - 1;
+            int rangeIndex = FindRangeIndex(targetIndex);
+            if (IsCorrectRangeIndex(rangeIndex, targetIndex))
+            {
+                while (rangeIndex > 0 && _list[rangeIndex].LowerBound == _list[rangeIndex - 1].UpperBound + 1)
+                {
+                    rangeIndex--;
+                }
+                return _list[rangeIndex].LowerBound - 1;
+            }
+            else
+            {
+                return targetIndex;
+            }
+        }
+
+        public int GetPreviousIndex(int index)
+        {
+            int targetIndex = index - 1;
+            int rangeIndex = FindRangeIndex(targetIndex);
+            if (IsCorrectRangeIndex(rangeIndex, targetIndex))
+            {
+                return targetIndex;
+            }
+            else
+            {
+                return rangeIndex >= 0 && rangeIndex < _list.Count ? _list[rangeIndex].UpperBound : -1;
+            }
+        }
+
+        /// <summary>
+        /// Returns the inclusive index count between lowerBound and upperBound of all indexes with the given value
+        /// </summary>
+        /// <param name="lowerBound">lowerBound criteria</param>
+        /// <param name="upperBound">upperBound criteria</param>
+        /// <param name="value">value to look for</param>
+        /// <returns>Number of indexes contained in the table between lowerBound and upperBound (inclusive)</returns>
+        public int GetIndexCount(int lowerBound, int upperBound, T value)
+        {
+            Debug.Assert(upperBound >= lowerBound);
+            if (_list.Count == 0)
+            {
+                return 0;
+            }
+            int count = 0;
+            int index = FindRangeIndex(lowerBound);
+            if (IsCorrectRangeIndex(index, lowerBound) && _list[index].ContainsValue(value))
+            {
+                count += _list[index].UpperBound - lowerBound + 1;
+            }
+            index++;
+            while (index < _list.Count && _list[index].UpperBound <= upperBound)
+            {
+                if (_list[index].ContainsValue(value))
+                {
+                    count += _list[index].Count;
+                }
+                index++;
+            }
+            if (index < _list.Count && IsCorrectRangeIndex(index, upperBound) && _list[index].ContainsValue(value))
+            {
+                count += upperBound - _list[index].LowerBound;
+            }
+            return count;
+        }
+
+        /// <summary>
+        /// Returns the inclusive index count between lowerBound and upperBound
+        /// </summary>
+        /// <param name="lowerBound">lowerBound criteria</param>
+        /// <param name="upperBound">upperBound criteria</param>
+        /// <returns>Number of indexes contained in the table between lowerBound and upperBound (inclusive)</returns>
+        public int GetIndexCount(int lowerBound, int upperBound)
+        {
+            if (upperBound < lowerBound || _list.Count == 0)
+            {
+                return 0;
+            }
+            int count = 0;
+            int index = this.FindRangeIndex(lowerBound);
+            if (IsCorrectRangeIndex(index, lowerBound))
+            {
+                count += _list[index].UpperBound - lowerBound + 1;
+            }
+            index++;
+            while (index < _list.Count && _list[index].UpperBound <= upperBound)
+            {
+                count += _list[index].Count;
+                index++;
+            }
+            if (index < _list.Count && IsCorrectRangeIndex(index, upperBound))
+            {
+                count += upperBound - _list[index].LowerBound;
+            }
+            return count;
+        }
+
+        /// <summary>
+        /// Returns the number indexes in this table after a given startingIndex but before
+        /// reaching a gap of indexes of a given size
+        /// </summary>
+        /// <param name="startingIndex">Index to start at</param>
+        /// <param name="gapSize">Size of index gap</param>
+        /// <returns></returns>
+        public int GetIndexCountBeforeGap(int startingIndex, int gapSize)
+        {
+            if (_list.Count == 0)
+            {
+                return 0;
+            }
+
+            int count = 0;
+            int currentIndex = startingIndex;
+            int rangeIndex = 0;
+            int gap = 0;
+            while (gap <= gapSize && rangeIndex < _list.Count)
+            {
+                gap += _list[rangeIndex].LowerBound - currentIndex;
+                if (gap <= gapSize)
+                {
+                    count += _list[rangeIndex].UpperBound - _list[rangeIndex].LowerBound + 1;
+                    currentIndex = _list[rangeIndex].UpperBound + 1;
+                    rangeIndex++;
+                }
+            }
+            return count;
+        }
+
+        /// <summary>
+        /// Returns an enumerator that goes through the indexes present in the table
+        /// </summary>
+        /// <returns>an enumerator that enumerates the indexes present in the table</returns>
+        public IEnumerable<int> GetIndexes()
+        {
+            Debug.Assert(_list != null);
+
+            foreach (Range<T> range in _list)
+            {
+                for (int i = range.LowerBound; i <= range.UpperBound; i++)
+                {
+                    yield return i;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns all the indexes on or after a starting index
+        /// </summary>
+        /// <param name="startIndex">start index</param>
+        /// <returns></returns>
+        public IEnumerable<int> GetIndexes(int startIndex)
+        {
+            Debug.Assert(_list != null);
+
+            int rangeIndex = FindRangeIndex(startIndex);
+            if (rangeIndex == -1)
+            {
+                rangeIndex++;
+            }
+
+            while (rangeIndex < _list.Count)
+            {
+                for (int i = _list[rangeIndex].LowerBound; i <= _list[rangeIndex].UpperBound; i++)
+                {
+                    if (i >= startIndex)
+                    {
+                        yield return i;
+                    }
+                }
+                rangeIndex++;
+            }
+        }
+
+        /// <summary>
+        /// Return the index of the Nth element in the table.
+        /// </summary>
+        /// <param name="n">n</param>
+        public int GetNthIndex(int n)
+        {
+            Debug.Assert(n >= 0 && n < this.IndexCount);
+            int cumulatedEntries = 0;
+            foreach (Range<T> range in _list)
+            {
+                if (cumulatedEntries + range.Count > n)
+                {
+                    return range.LowerBound + n - cumulatedEntries;
+                }
+                else
+                {
+                    cumulatedEntries += range.Count;
+                }
+            }
+            return -1;
+        }
+
+        /// <summary>
+        /// Returns the value at a given index or the default value if the index is not in the table
+        /// </summary>
+        /// <param name="index">index to search for</param>
+        /// <returns>the value at the given index or the default value if index is not in the table</returns>
+        public T GetValueAt(int index)
+        {
+            return GetValueAt(index, out bool found);
+        }
+
+        /// <summary>
+        /// Returns the value at a given index or the default value if the index is not in the table
+        /// </summary>
+        /// <param name="index">index to search for</param>
+        /// <param name="found">set to true by the method if the index was found; otherwise, false</param>
+        /// <returns>the value at the given index or the default value if index is not in the table</returns>
+        public T GetValueAt(int index, out bool found)
+        {
+            int rangeIndex = this.FindRangeIndex(index);
+            if (this.IsCorrectRangeIndex(rangeIndex, index))
+            {
+                found = true;
+                return _list[rangeIndex].Value;
+            }
+            else
+            {
+                found = false;
+                return default(T);
+            }
+        }
+
+        /// <summary>
+        /// Returns an index's index within this table
+        /// </summary>
+        /// <param name="index"></param>
+        /// <returns></returns>
+        public int IndexOf(int index)
+        {
+            int cumulatedIndexes = 0;
+            foreach (Range<T> range in _list)
+            {
+                if (range.UpperBound >= index)
+                {
+                    cumulatedIndexes += index - range.LowerBound;
+                    break;
+                }
+                else
+                {
+                    cumulatedIndexes += range.Count;
+                }
+            }
+            return cumulatedIndexes;
+        }
+
+        /// <summary>
+        /// Inserts an index at the given location.  This does not alter values in the table
+        /// </summary>
+        /// <param name="index">index location to insert an index</param>
+        public void InsertIndex(int index)
+        {
+            InsertIndexes(index, 1);
+        }
+
+        /// <summary>
+        /// Inserts an index into the table with the given value 
+        /// </summary>
+        /// <param name="index">index to insert</param>
+        /// <param name="value">value for the index</param>
+        public void InsertIndexAndValue(int index, T value)
+        {
+            InsertIndexesAndValues(index, 1, value);
+        }
+
+        /// <summary>
+        /// Inserts multiple indexes into the table.  This does not alter Values in the table
+        /// </summary>
+        /// <param name="startIndex">first index to insert</param>
+        /// <param name="count">total number of indexes to insert</param>
+        public void InsertIndexes(int startIndex, int count)
+        {
+            Debug.Assert(count > 0);
+            InsertIndexesPrivate(startIndex, count, this.FindRangeIndex(startIndex));
+        }
+
+        /// <summary>
+        /// Inserts multiple indexes into the table with the given value 
+        /// </summary>
+        /// <param name="startIndex">Index to insert first value</param>
+        /// <param name="count">Total number of values to insert (must be greater than 0)</param>
+        /// <param name="value">Value to insert</param>
+        public void InsertIndexesAndValues(int startIndex, int count, T value)
+        {
+            Debug.Assert(count > 0);
+            int lowerRangeIndex = this.FindRangeIndex(startIndex);
+            InsertIndexesPrivate(startIndex, count, lowerRangeIndex);
+            if ((lowerRangeIndex >= 0) && (_list[lowerRangeIndex].LowerBound > startIndex))
+            {
+                // Because of the insert, the original range no longer contains the startIndex
+                lowerRangeIndex--;
+            }
+            AddValuesPrivate(startIndex, count, value, lowerRangeIndex);
+        }
+
+        /// <summary>
+        /// Removes an index from the table.  This does not alter Values in the table
+        /// </summary>
+        /// <param name="index">index to remove</param>
+        public void RemoveIndex(int index)
+        {
+            RemoveIndexes(index, 1);
+        }
+
+        /// <summary>
+        /// Removes a value and its index from the table
+        /// </summary>
+        /// <param name="index">index to remove</param>
+        public void RemoveIndexAndValue(int index)
+        {
+            RemoveIndexesAndValues(index, 1);
+        }
+
+        /// <summary>
+        /// Removes multiple indexes from the table.  This does not alter Values in the table
+        /// </summary>
+        /// <param name="startIndex">first index to remove</param>
+        /// <param name="count">total number of indexes to remove</param>
+        public void RemoveIndexes(int startIndex, int count)
+        {
+            int lowerRangeIndex = this.FindRangeIndex(startIndex);
+            if (lowerRangeIndex < 0)
+            {
+                lowerRangeIndex = 0;
+            }
+            int i = lowerRangeIndex;
+            while (i < _list.Count)
+            {
+                Range<T> range = _list[i];
+                if (range.UpperBound >= startIndex)
+                {
+                    if (range.LowerBound >= startIndex + count)
+                    {
+                        // Both bounds will remain after the removal
+                        range.LowerBound -= count;
+                        range.UpperBound -= count;
+                    }
+                    else
+                    {
+                        int currentIndex = i;
+                        if (range.LowerBound <= startIndex)
+                        {
+                            // Range gets split up
+                            if (range.UpperBound >= startIndex + count)
+                            {
+                                i++;
+                                _list.Insert(i, new Range<T>(startIndex, range.UpperBound - count, range.Value));
+                            }
+                            range.UpperBound = startIndex - 1;
+                        }
+                        else
+                        {
+                            range.LowerBound = startIndex;
+                            range.UpperBound -= count;
+                        }
+                        if (RemoveRangeIfInvalid(range, currentIndex))
+                        {
+                            i--;
+                        }
+                    }
+                }
+                i++;
+            }
+            if (!this.Merge(lowerRangeIndex))
+            {
+                this.Merge(lowerRangeIndex + 1);
+            }
+        }
+
+        /// <summary>
+        /// Removes multiple values and their indexes from the table
+        /// </summary>
+        /// <param name="startIndex">first index to remove</param>
+        /// <param name="count">total number of indexes to remove</param>
+        public void RemoveIndexesAndValues(int startIndex, int count)
+        {
+            RemoveValues(startIndex, count);
+            RemoveIndexes(startIndex, count);
+        }
+
+        /// <summary>
+        /// Removes a value from the table at the given index.  This does not alter other indexes in the table.
+        /// </summary>
+        /// <param name="index">index where value should be removed</param>
+        public void RemoveValue(int index)
+        {
+            RemoveValues(index, 1);
+        }
+
+        /// <summary>
+        /// Removes multiple values from the table.  This does not alter other indexes in the table.
+        /// </summary>
+        /// <param name="startIndex">first index where values should be removed </param>
+        /// <param name="count">total number of values to remove</param>
+        public void RemoveValues(int startIndex, int count)
+        {
+            Debug.Assert(count > 0);
+
+            int lowerRangeIndex = this.FindRangeIndex(startIndex);
+            if (lowerRangeIndex < 0)
+            {
+                lowerRangeIndex = 0;
+            }
+            while ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound < startIndex))
+            {
+                lowerRangeIndex++;
+            }
+            if (lowerRangeIndex >= _list.Count || _list[lowerRangeIndex].LowerBound > startIndex + count - 1)
+            {
+                // If all the values are above our below our values, we have nothing to remove
+                return;
+            }
+            if (_list[lowerRangeIndex].LowerBound < startIndex)
+            {
+                // Need to split this up
+                _list.Insert(lowerRangeIndex, new Range<T>(_list[lowerRangeIndex].LowerBound, startIndex - 1, _list[lowerRangeIndex].Value));
+                lowerRangeIndex++;
+            }
+            _list[lowerRangeIndex].LowerBound = startIndex + count;
+            if (!RemoveRangeIfInvalid(_list[lowerRangeIndex], lowerRangeIndex))
+            {
+                lowerRangeIndex++;
+            }
+            while ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound < startIndex + count))
+            {
+                _list.RemoveAt(lowerRangeIndex);
+            }
+            if ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound >= startIndex + count) &&
+                (_list[lowerRangeIndex].LowerBound < startIndex + count))
+            {
+                // Chop off the start of the remaining Range if it contains values that we're removing
+                _list[lowerRangeIndex].LowerBound = startIndex + count;
+                RemoveRangeIfInvalid(_list[lowerRangeIndex], lowerRangeIndex);
+            }
+        }
+
+        private void AddValuesPrivate(int startIndex, int count, T value, int? startRangeIndex)
+        {
+            Debug.Assert(count > 0);
+
+            int endIndex = startIndex + count - 1;
+            Range<T> newRange = new Range<T>(startIndex, endIndex, value);
+            if (_list.Count == 0)
+            {
+                _list.Add(newRange);
+            }
+            else
+            {
+                int lowerRangeIndex = startRangeIndex ?? FindRangeIndex(startIndex);
+                Range<T> lowerRange = (lowerRangeIndex < 0) ? null : _list[lowerRangeIndex];
+                if (lowerRange == null)
+                {
+                    if (lowerRangeIndex < 0)
+                    {
+                        lowerRangeIndex = 0;
+                    }
+                    _list.Insert(lowerRangeIndex, newRange);
+                }
+                else
+                {
+                    if (!lowerRange.Value.Equals(value) && (lowerRange.UpperBound >= startIndex))
+                    {
+                        // Split up the range
+                        if (lowerRange.UpperBound > endIndex)
+                        {
+                            _list.Insert(lowerRangeIndex + 1, new Range<T>(endIndex + 1, lowerRange.UpperBound, lowerRange.Value));
+                        }
+                        lowerRange.UpperBound = startIndex - 1;
+                        if (!RemoveRangeIfInvalid(lowerRange, lowerRangeIndex))
+                        {
+                            lowerRangeIndex++;
+                        }
+                        _list.Insert(lowerRangeIndex, newRange);
+                    }
+                    else
+                    {
+                        _list.Insert(lowerRangeIndex + 1, newRange);
+                        if (!Merge(lowerRangeIndex))
+                        {
+                            lowerRangeIndex++;
+                        }
+                    }
+                }
+
+                // At this point the newRange has been inserted in the correct place, now we need to remove
+                // any subsequent ranges that no longer make sense and possibly update the one at newRange.UpperBound
+                int upperRangeIndex = lowerRangeIndex + 1;
+                while ((upperRangeIndex < _list.Count) && (_list[upperRangeIndex].UpperBound < endIndex))
+                {
+                    _list.RemoveAt(upperRangeIndex);
+                }
+                if (upperRangeIndex < _list.Count)
+                {
+                    Range<T> upperRange = _list[upperRangeIndex];
+                    if (upperRange.LowerBound <= endIndex)
+                    {
+                        // Update the range
+                        upperRange.LowerBound = endIndex + 1;
+                        RemoveRangeIfInvalid(upperRange, upperRangeIndex);
+                    }
+                    Merge(lowerRangeIndex);
+                }
+            }
+        }
+
+        // Returns the index of the range that contains the input or the range before if the input is not found
+        private int FindRangeIndex(int index)
+        {
+            if (_list.Count == 0)
+            {
+                return -1;
+            }
+
+            // Do a binary search for the index
+            int front = 0;
+            int end = _list.Count - 1;
+            Range<T> range = null;
+            while (end > front)
+            {
+                int median = (front + end) / 2;
+                range = _list[median];
+                if (range.UpperBound < index)
+                {
+                    front = median + 1;
+                }
+                else if (range.LowerBound > index)
+                {
+                    end = median - 1;
+                }
+                else
+                {
+                    // we found it
+                    return median;
+                }
+            }
+
+            if (front == end)
+            {
+                range = _list[front];
+                if (range.ContainsIndex(index) || (range.UpperBound < index))
+                {
+                    // we found it or the index isn't there and we're one range before
+                    return front;
+                }
+                else
+                {
+                    // not found and we're one range after
+                    return front - 1;
+                }
+            }
+            else
+            {
+                // end is one index before front in this case so it's the range before
+                return end;
+            }
+        }
+
+        private bool Merge(int lowerRangeIndex)
+        {
+            int upperRangeIndex = lowerRangeIndex + 1;
+            if ((lowerRangeIndex >= 0) && (upperRangeIndex < _list.Count))
+            {
+                Range<T> lowerRange = _list[lowerRangeIndex];
+                Range<T> upperRange = _list[upperRangeIndex];
+                if ((lowerRange.UpperBound + 1 >= upperRange.LowerBound) && (lowerRange.Value.Equals(upperRange.Value)))
+                {
+                    lowerRange.UpperBound = Math.Max(lowerRange.UpperBound, upperRange.UpperBound);
+                    _list.RemoveAt(upperRangeIndex);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void InsertIndexesPrivate(int startIndex, int count, int lowerRangeIndex)
+        {
+            Debug.Assert(count > 0);
+
+            // Same as AddRange after we fix the indicies affected by the insertion
+            int startRangeIndex = (lowerRangeIndex >= 0) ? lowerRangeIndex : 0;
+            for (int i = startRangeIndex; i < _list.Count; i++)
+            {
+                Range<T> range = _list[i];
+                if (range.LowerBound >= startIndex)
+                {
+                    range.LowerBound += count;
+                }
+                else
+                {
+                    if (range.UpperBound >= startIndex)
+                    {
+                        // Split up this range
+                        i++;
+                        _list.Insert(i, new Range<T>(startIndex, range.UpperBound + count, range.Value));
+                        range.UpperBound = startIndex - 1;
+                        continue;
+                    }
+                }
+
+                if (range.UpperBound >= startIndex)
+                {
+                    range.UpperBound += count;
+                }
+            }
+        }
+
+        private bool IsCorrectRangeIndex(int rangeIndex, int index)
+        {
+            return (-1 != rangeIndex) && (_list[rangeIndex].ContainsIndex(index));
+        }
+
+        private bool RemoveRangeIfInvalid(Range<T> range, int rangeIndex)
+        {
+            if (range.UpperBound < range.LowerBound)
+            {
+                _list.RemoveAt(rangeIndex);
+                return true;
+            }
+            return false;
+        }
+
+        public IEnumerator<Range<T>> GetEnumerator()
+        {
+            return _list.GetEnumerator();
+        }
+
+        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+        {
+            return _list.GetEnumerator();
+        }
+
+#if DEBUG
+
+        public void PrintIndexes()
+        {
+            Debug.WriteLine(this.IndexCount + " indexes");
+            foreach (Range<T> range in _list)
+            {
+                Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} - {1}", range.LowerBound, range.UpperBound));
+            }
+        }
+
+#endif
+    }
+}

+ 315 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@@ -0,0 +1,315 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Media;
+using Avalonia.Utilities;
+using System;
+using System.Diagnostics;
+using Avalonia.Controls;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" />
+    /// to specify the location in the control's visual tree where the cells are to be added. 
+    /// </summary>
+    public sealed class DataGridCellsPresenter : Panel
+    {
+        private double _fillerLeftEdge;
+
+        // The desired height needs to be cached due to column virtualization; otherwise, the cells
+        // would grow and shrink as the DataGrid scrolls horizontally
+        private double DesiredHeight
+        {
+            get;
+            set;
+        }
+
+        private DataGrid OwningGrid
+        {
+            get
+            {
+                return OwningRow?.OwningGrid;
+            }
+        }
+
+        internal DataGridRow OwningRow
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.Primitives.DataGridCellsPresenter" />.
+        /// </summary>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.Primitives.DataGridCellsPresenter" />.
+        /// </returns>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+
+            if (OwningGrid.AutoSizingColumns)
+            {
+                // When we initially load an auto-column, we have to wait for all the rows to be measured
+                // before we know its final desired size.  We need to trigger a new round of measures now
+                // that the final sizes have been calculated.
+                OwningGrid.AutoSizingColumns = false;
+                return base.ArrangeOverride(finalSize);
+            }
+
+            double frozenLeftEdge = 0;
+            double scrollingLeftEdge = -OwningGrid.HorizontalOffset;
+
+            double cellLeftEdge;
+            foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+            {
+                DataGridCell cell = OwningRow.Cells[column.Index];
+                Debug.Assert(cell.OwningColumn == column);
+                Debug.Assert(column.IsVisible);
+
+                if (column.IsFrozen)
+                {
+                    cellLeftEdge = frozenLeftEdge;
+                    // This can happen before or after clipping because frozen cells aren't clipped
+                    frozenLeftEdge += column.ActualWidth;
+                }
+                else
+                {
+                    cellLeftEdge = scrollingLeftEdge;
+                }
+                if (cell.IsVisible)
+                {
+                    cell.Arrange(new Rect(cellLeftEdge, 0, column.LayoutRoundedWidth, finalSize.Height));
+                    EnsureCellClip(cell, column.ActualWidth, finalSize.Height, frozenLeftEdge, scrollingLeftEdge);
+                }
+                scrollingLeftEdge += column.ActualWidth;
+                column.IsInitialDesiredWidthDetermined = true;
+            }
+
+            _fillerLeftEdge = scrollingLeftEdge;
+
+            OwningRow.FillerCell.Arrange(new Rect(_fillerLeftEdge, 0, OwningGrid.ColumnsInternal.FillerColumn.FillerWidth, finalSize.Height));
+
+            return finalSize;
+        }
+
+        private static void EnsureCellClip(DataGridCell cell, double width, double height, double frozenLeftEdge, double cellLeftEdge)
+        {
+            // Clip the cell only if it's scrolled under frozen columns.  Unfortunately, we need to clip in this case
+            // because cells could be transparent
+            if (!cell.OwningColumn.IsFrozen && frozenLeftEdge > cellLeftEdge)
+            {
+                RectangleGeometry rg = new RectangleGeometry();
+                double xClip = Math.Round(Math.Min(width, frozenLeftEdge - cellLeftEdge));
+                rg.Rect = new Rect(xClip, 0, Math.Max(0, width - xClip), height);
+                cell.Clip = rg;
+            }
+            else
+            {
+                cell.Clip = null;
+            }
+        }
+
+        private static void EnsureCellDisplay(DataGridCell cell, bool displayColumn)
+        {
+            if (cell.IsCurrent)
+            {
+                if (displayColumn)
+                {
+                    cell.IsVisible = true;
+                    cell.Clip = null;
+                }
+                else
+                {
+                    // Clip
+                    RectangleGeometry rg = new RectangleGeometry();
+                    rg.Rect = Rect.Empty;
+                    cell.Clip = rg;
+                }
+            }
+            else
+            {
+                cell.IsVisible = displayColumn;
+            }
+        }
+
+        internal void EnsureFillerVisibility()
+        {
+            DataGridFillerColumn fillerColumn = OwningGrid.ColumnsInternal.FillerColumn;
+            bool newVisibility = fillerColumn.IsActive;
+            if (OwningRow.FillerCell.IsVisible != newVisibility)
+            {
+                OwningRow.FillerCell.IsVisible = newVisibility;
+                if (newVisibility)
+                {
+                    OwningRow.FillerCell.Arrange(new Rect(_fillerLeftEdge, 0, fillerColumn.FillerWidth, Bounds.Height));
+                }
+            }
+
+            // This must be done after the Filler visibility is determined.  This also must be done
+            // regardless of whether or not the filler visibility actually changed values because
+            // we could scroll in a cell that didn't have EnsureGridLine called yet
+            DataGridColumn lastVisibleColumn = OwningGrid.ColumnsInternal.LastVisibleColumn;
+            if (lastVisibleColumn != null)
+            {
+                DataGridCell cell = OwningRow.Cells[lastVisibleColumn.Index];
+                cell.EnsureGridLine(lastVisibleColumn);
+            }
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.Primitives.DataGridCellsPresenter" /> to 
+        /// prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridCellsPresenter" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.MeasureOverride(availableSize);
+            }
+
+            bool autoSizeHeight;
+            double measureHeight;
+            if (double.IsNaN(OwningGrid.RowHeight))
+            {
+                // No explicit height values were set so we can autosize
+                autoSizeHeight = true;
+                measureHeight = double.PositiveInfinity;
+            }
+            else
+            {
+                DesiredHeight = OwningGrid.RowHeight;
+                measureHeight = DesiredHeight;
+                autoSizeHeight = false;
+            }
+
+            double frozenLeftEdge = 0;
+            double totalDisplayWidth = 0;
+            double scrollingLeftEdge = -OwningGrid.HorizontalOffset;
+            OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+            DataGridColumn lastVisibleColumn = OwningGrid.ColumnsInternal.LastVisibleColumn;
+            foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+            {
+                DataGridCell cell = OwningRow.Cells[column.Index];
+                // Measure the entire first row to make the horizontal scrollbar more accurate
+                bool shouldDisplayCell = ShouldDisplayCell(column, frozenLeftEdge, scrollingLeftEdge) || OwningRow.Index == 0;
+                EnsureCellDisplay(cell, shouldDisplayCell);
+                if (shouldDisplayCell)
+                {
+                    DataGridLength columnWidth = column.Width;
+                    bool autoGrowWidth = columnWidth.IsSizeToCells || columnWidth.IsAuto;
+                    if (column != lastVisibleColumn)
+                    {
+                        cell.EnsureGridLine(lastVisibleColumn);
+                    }
+
+                    // If we're not using star sizing or the current column can't be resized,
+                    // then just set the display width according to the column's desired width
+                    if (!OwningGrid.UsesStarSizing || (!column.ActualCanUserResize && !column.Width.IsStar))
+                    {
+                        // In the edge-case where we're given infinite width and we have star columns, the 
+                        // star columns grow to their predefined limit of 10,000 (or their MaxWidth)
+                        double newDisplayWidth = column.Width.IsStar ?
+                            Math.Min(column.ActualMaxWidth, DataGrid.DATAGRID_maximumStarColumnWidth) :
+                            Math.Max(column.ActualMinWidth, Math.Min(column.ActualMaxWidth, column.Width.DesiredValue));
+                        column.SetWidthDisplayValue(newDisplayWidth);
+                    }
+
+                    // If we're auto-growing the column based on the cell content, we want to measure it at its maximum value
+                    if (autoGrowWidth)
+                    {
+                        cell.Measure(new Size(column.ActualMaxWidth, measureHeight));
+                        OwningGrid.AutoSizeColumn(column, cell.DesiredSize.Width);
+                        column.ComputeLayoutRoundedWidth(totalDisplayWidth);
+                    }
+                    else if (!OwningGrid.UsesStarSizing)
+                    {
+                        column.ComputeLayoutRoundedWidth(scrollingLeftEdge);
+                        cell.Measure(new Size(column.LayoutRoundedWidth, measureHeight));
+                    }
+
+                    // We need to track the largest height in order to auto-size
+                    if (autoSizeHeight)
+                    {
+                        DesiredHeight = Math.Max(DesiredHeight, cell.DesiredSize.Height);
+                    }
+                }
+
+                if (column.IsFrozen)
+                {
+                    frozenLeftEdge += column.ActualWidth;
+                }
+                scrollingLeftEdge += column.ActualWidth;
+                totalDisplayWidth += column.ActualWidth;
+            }
+
+            // If we're using star sizing (and we're not waiting for an auto-column to finish growing)
+            // then we will resize all the columns to fit the available space.
+            if (OwningGrid.UsesStarSizing && !OwningGrid.AutoSizingColumns)
+            {
+                double adjustment = OwningGrid.CellsWidth - totalDisplayWidth;
+                totalDisplayWidth += adjustment - OwningGrid.AdjustColumnWidths(0, adjustment, false);
+
+                // Since we didn't know the final widths of the columns until we resized,
+                // we waited until now to measure each cell
+                double leftEdge = 0;
+                foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+                {
+                    DataGridCell cell = OwningRow.Cells[column.Index];
+                    column.ComputeLayoutRoundedWidth(leftEdge);
+                    cell.Measure(new Size(column.LayoutRoundedWidth, measureHeight));
+                    if (autoSizeHeight)
+                    {
+                        DesiredHeight = Math.Max(DesiredHeight, cell.DesiredSize.Height);
+                    }
+                    leftEdge += column.ActualWidth;
+                }
+            }
+
+            // Measure FillerCell, we're doing it unconditionally here because we don't know if we'll need the filler
+            // column and we don't want to cause another Measure if we do
+            OwningRow.FillerCell.Measure(new Size(double.PositiveInfinity, DesiredHeight));
+
+            OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+            return new Size(OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, DesiredHeight);
+        }
+
+        internal void Recycle()
+        {
+            // Clear out the cached desired height so it is not reused for other rows
+            DesiredHeight = 0;
+        }
+
+        private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)
+        {
+            if (!column.IsVisible)
+            {
+                return false;
+            }
+
+            scrollingLeftEdge += OwningGrid.HorizontalAdjustment;
+            double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge;
+            double rightEdge = leftEdge + column.ActualWidth;
+            return 
+                DoubleUtil.GreaterThan(rightEdge, 0) &&
+                DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
+                DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
+        }
+    }
+}

+ 395 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs

@@ -0,0 +1,395 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Media;
+using System;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" /> to specify the 
+    /// location in the control's visual tree where the column headers are to be added.
+    /// </summary>
+    public sealed class DataGridColumnHeadersPresenter : Panel
+    {
+        private Control _dragIndicator;
+        private IControl _dropLocationIndicator;
+
+        /// <summary>
+        /// Tracks which column is currently being dragged.
+        /// </summary>
+        internal DataGridColumn DragColumn
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// The current drag indicator control.  This value is null if no column is being dragged.
+        /// </summary>
+        internal Control DragIndicator
+        {
+            get
+            {
+                return _dragIndicator;
+            }
+            set
+            {
+                if (value != _dragIndicator)
+                {
+                    if (Children.Contains(_dragIndicator))
+                    {
+                        Children.Remove(_dragIndicator);
+                    }
+                    _dragIndicator = value;
+                    if (_dragIndicator != null)
+                    {
+                        Children.Add(_dragIndicator);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// The distance, in pixels, that the DragIndicator should be positioned away from the corresponding DragColumn.
+        /// </summary>
+        internal Double DragIndicatorOffset
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// The drop location indicator control.  This value is null if no column is being dragged.
+        /// </summary>
+        internal IControl DropLocationIndicator
+        {
+            get
+            {
+                return _dropLocationIndicator;
+            }
+            set
+            {
+                if (value != _dropLocationIndicator)
+                {
+                    if (Children.Contains(_dropLocationIndicator))
+                    {
+                        Children.Remove(_dropLocationIndicator);
+                    }
+                    _dropLocationIndicator = value;
+                    if (_dropLocationIndicator != null)
+                    {
+                        Children.Add(_dropLocationIndicator);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// The distance, in pixels, that the drop location indicator should be positioned away from the left edge
+        /// of the ColumnsHeaderPresenter.
+        /// </summary>
+        internal double DropLocationIndicatorOffset
+        {
+            get;
+            set;
+        }
+
+        internal DataGrid OwningGrid
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.Primitives.DataGridColumnHeadersPresenter" />.
+        /// </summary>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.Primitives.DataGridColumnHeadersPresenter" />.
+        /// </returns>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+
+            if (OwningGrid.AutoSizingColumns)
+            {
+                // When we initially load an auto-column, we have to wait for all the rows to be measured
+                // before we know its final desired size.  We need to trigger a new round of measures now
+                // that the final sizes have been calculated.
+                OwningGrid.AutoSizingColumns = false;
+                return base.ArrangeOverride(finalSize);
+            }
+
+            double dragIndicatorLeftEdge = 0;
+            double frozenLeftEdge = 0;
+            double scrollingLeftEdge = -OwningGrid.HorizontalOffset;
+            foreach (DataGridColumn dataGridColumn in OwningGrid.ColumnsInternal.GetVisibleColumns())
+            {
+                DataGridColumnHeader columnHeader = dataGridColumn.HeaderCell;
+                Debug.Assert(columnHeader.OwningColumn == dataGridColumn);
+
+                if (dataGridColumn.IsFrozen)
+                {
+                    columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
+                    columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform
+                    if (DragColumn == dataGridColumn && DragIndicator != null)
+                    {
+                        dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset;
+                    }
+                    frozenLeftEdge += dataGridColumn.ActualWidth;
+                }
+                else
+                {
+                    columnHeader.Arrange(new Rect(scrollingLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
+                    EnsureColumnHeaderClip(columnHeader, dataGridColumn.ActualWidth, finalSize.Height, frozenLeftEdge, scrollingLeftEdge);
+                    if (DragColumn == dataGridColumn && DragIndicator != null)
+                    {
+                        dragIndicatorLeftEdge = scrollingLeftEdge + DragIndicatorOffset;
+                    }
+                }
+                scrollingLeftEdge += dataGridColumn.ActualWidth;
+            }
+            if (DragColumn != null)
+            {
+                if (DragIndicator != null)
+                {
+                    EnsureColumnReorderingClip(DragIndicator, finalSize.Height, frozenLeftEdge, dragIndicatorLeftEdge);
+
+                    var height = DragIndicator.Bounds.Height;
+                    if (height <= 0)
+                        height = DragIndicator.DesiredSize.Height;
+
+                    DragIndicator.Arrange(new Rect(dragIndicatorLeftEdge, 0, DragIndicator.Bounds.Width, height));
+                }
+                if (DropLocationIndicator != null)
+                {
+                    if (DropLocationIndicator is Control element)
+                    {
+                        EnsureColumnReorderingClip(element, finalSize.Height, frozenLeftEdge, DropLocationIndicatorOffset);
+                    }
+
+                    DropLocationIndicator.Arrange(new Rect(DropLocationIndicatorOffset, 0, DropLocationIndicator.Bounds.Width, DropLocationIndicator.Bounds.Height));
+                }
+            }
+
+            // Arrange filler
+            OwningGrid.OnFillerColumnWidthNeeded(finalSize.Width);
+            DataGridFillerColumn fillerColumn = OwningGrid.ColumnsInternal.FillerColumn;
+            if (fillerColumn.FillerWidth > 0)
+            {
+                fillerColumn.HeaderCell.IsVisible = true;
+                fillerColumn.HeaderCell.Arrange(new Rect(scrollingLeftEdge, 0, fillerColumn.FillerWidth, finalSize.Height));
+            }
+            else
+            {
+                fillerColumn.HeaderCell.IsVisible = false;
+            }
+
+            // This needs to be updated after the filler column is configured
+            DataGridColumn lastVisibleColumn = OwningGrid.ColumnsInternal.LastVisibleColumn;
+            if (lastVisibleColumn != null)
+            {
+                lastVisibleColumn.HeaderCell.UpdateSeparatorVisibility(lastVisibleColumn);
+            }
+            return finalSize;
+        }
+
+        private static void EnsureColumnHeaderClip(DataGridColumnHeader columnHeader, double width, double height, double frozenLeftEdge, double columnHeaderLeftEdge)
+        {
+            // Clip the cell only if it's scrolled under frozen columns.  Unfortunately, we need to clip in this case
+            // because cells could be transparent
+            if (frozenLeftEdge > columnHeaderLeftEdge)
+            {
+                RectangleGeometry rg = new RectangleGeometry();
+                double xClip = Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge);
+                rg.Rect = new Rect(xClip, 0, width - xClip, height);
+                columnHeader.Clip = rg;
+            }
+            else
+            {
+                columnHeader.Clip = null;
+            }
+        }
+
+        /// <summary>
+        /// Clips the DragIndicator and DropLocationIndicator controls according to current ColumnHeaderPresenter constraints.
+        /// </summary>
+        /// <param name="control">The DragIndicator or DropLocationIndicator</param>
+        /// <param name="height">The available height</param>
+        /// <param name="frozenColumnsWidth">The width of the frozen column region</param>
+        /// <param name="controlLeftEdge">The left edge of the control to clip</param>
+        private void EnsureColumnReorderingClip(Control control, double height, double frozenColumnsWidth, double controlLeftEdge)
+        {
+            double leftEdge = 0;
+            double rightEdge = OwningGrid.CellsWidth;
+            double width = control.Bounds.Width;
+            if (DragColumn.IsFrozen)
+            {
+                // If we're dragging a frozen column, we want to clip the corresponding DragIndicator control when it goes
+                // into the scrolling columns region, but not the DropLocationIndicator.
+                if (control == DragIndicator)
+                {
+                    rightEdge = Math.Min(rightEdge, frozenColumnsWidth);
+                }
+            }
+            else if (OwningGrid.FrozenColumnCount > 0)
+            {
+                // If we're dragging a scrolling column, we want to clip both the DragIndicator and the DropLocationIndicator
+                // controls when they go into the frozen column range.
+                leftEdge = frozenColumnsWidth;
+            }
+            RectangleGeometry rg = null;
+            if (leftEdge > controlLeftEdge)
+            {
+                rg = new RectangleGeometry();
+                double xClip = Math.Min(width, leftEdge - controlLeftEdge);
+                rg.Rect = new Rect(xClip, 0, width - xClip, height);
+            }
+            if (controlLeftEdge + width >= rightEdge)
+            {
+                if (rg == null)
+                {
+                    rg = new RectangleGeometry();
+                }
+                rg.Rect = new Rect(rg.Rect.X, rg.Rect.Y, Math.Max(0, rightEdge - controlLeftEdge - rg.Rect.X), height);
+            }
+            control.Clip = rg;
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.Primitives.DataGridColumnHeadersPresenter" /> to 
+        /// prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridColumnHeadersPresenter" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.MeasureOverride(availableSize);
+            }
+            if (!OwningGrid.AreColumnHeadersVisible)
+            {
+                return Size.Empty;
+            }
+            double height = OwningGrid.ColumnHeaderHeight;
+            bool autoSizeHeight;
+            if (double.IsNaN(height))
+            {
+                // No explicit height values were set so we can autosize
+                height = 0;
+                autoSizeHeight = true;
+            }
+            else
+            {
+                autoSizeHeight = false;
+            }
+
+            double totalDisplayWidth = 0;
+            OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+            DataGridColumn lastVisibleColumn = OwningGrid.ColumnsInternal.LastVisibleColumn;
+            foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+            {
+                // Measure each column header
+                bool autoGrowWidth = column.Width.IsAuto || column.Width.IsSizeToHeader;
+                DataGridColumnHeader columnHeader = column.HeaderCell;
+                if (column != lastVisibleColumn)
+                {
+                    columnHeader.UpdateSeparatorVisibility(lastVisibleColumn);
+                }
+
+                // If we're not using star sizing or the current column can't be resized,
+                // then just set the display width according to the column's desired width
+                if (!OwningGrid.UsesStarSizing || (!column.ActualCanUserResize && !column.Width.IsStar))
+                {
+                    // In the edge-case where we're given infinite width and we have star columns, the 
+                    // star columns grow to their predefined limit of 10,000 (or their MaxWidth)
+                    double newDisplayWidth = column.Width.IsStar ?
+                        Math.Min(column.ActualMaxWidth, DataGrid.DATAGRID_maximumStarColumnWidth) :
+                        Math.Max(column.ActualMinWidth, Math.Min(column.ActualMaxWidth, column.Width.DesiredValue));
+                    column.SetWidthDisplayValue(newDisplayWidth);
+                }
+
+                // If we're auto-growing the column based on the header content, we want to measure it at its maximum value
+                if (autoGrowWidth)
+                {
+                    columnHeader.Measure(new Size(column.ActualMaxWidth, double.PositiveInfinity));
+                    OwningGrid.AutoSizeColumn(column, columnHeader.DesiredSize.Width);
+                    column.ComputeLayoutRoundedWidth(totalDisplayWidth);
+                }
+                else if (!OwningGrid.UsesStarSizing)
+                {
+                    column.ComputeLayoutRoundedWidth(totalDisplayWidth);
+                    columnHeader.Measure(new Size(column.LayoutRoundedWidth, double.PositiveInfinity));
+                }
+
+                // We need to track the largest height in order to auto-size
+                if (autoSizeHeight)
+                {
+                    height = Math.Max(height, columnHeader.DesiredSize.Height);
+                }
+                totalDisplayWidth += column.ActualWidth;
+            }
+
+            // If we're using star sizing (and we're not waiting for an auto-column to finish growing)
+            // then we will resize all the columns to fit the available space.
+            if (OwningGrid.UsesStarSizing && !OwningGrid.AutoSizingColumns)
+            {
+                double adjustment = Double.IsPositiveInfinity(availableSize.Width) ? OwningGrid.CellsWidth : availableSize.Width - totalDisplayWidth;
+                totalDisplayWidth += adjustment - OwningGrid.AdjustColumnWidths(0, adjustment, false);
+
+                // Since we didn't know the final widths of the columns until we resized,
+                // we waited until now to measure each header
+                double leftEdge = 0;
+                foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
+                {
+                    column.ComputeLayoutRoundedWidth(leftEdge);
+                    column.HeaderCell.Measure(new Size(column.LayoutRoundedWidth, double.PositiveInfinity));
+                    if (autoSizeHeight)
+                    {
+                        height = Math.Max(height, column.HeaderCell.DesiredSize.Height);
+                    }
+                    leftEdge += column.ActualWidth;
+                }
+            }
+
+            // Add the filler column if it's not represented.  We won't know whether we need it or not until Arrange
+            DataGridFillerColumn fillerColumn = OwningGrid.ColumnsInternal.FillerColumn;
+            if (!fillerColumn.IsRepresented)
+            {
+                Debug.Assert(!Children.Contains(fillerColumn.HeaderCell));
+                fillerColumn.HeaderCell.AreSeparatorsVisible = false;
+                Children.Insert(OwningGrid.ColumnsInternal.Count, fillerColumn.HeaderCell);
+                fillerColumn.IsRepresented = true;
+                // Optimize for the case where we don't need the filler cell 
+                fillerColumn.HeaderCell.IsVisible = false;
+            }
+            fillerColumn.HeaderCell.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+
+            if (DragIndicator != null)
+            {
+                DragIndicator.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+            }
+            if (DropLocationIndicator != null)
+            {
+                DropLocationIndicator.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+            }
+
+            OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
+            return new Size(OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, height);
+        }
+    }
+}

+ 134 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs

@@ -0,0 +1,134 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" /> to specify the location in the control's visual tree 
+    /// where the row details are to be added.
+    /// </summary>
+    public sealed class DataGridDetailsPresenter : Panel
+    {
+        public static readonly StyledProperty<double> ContentHeightProperty =
+            AvaloniaProperty.Register<DataGridDetailsPresenter, double>(nameof(ContentHeight));
+
+        /// <summary>
+        /// Gets or sets the height of the content.
+        /// </summary>
+        /// <returns>
+        /// The height of the content.
+        /// </returns>
+        public double ContentHeight
+        {
+            get { return GetValue(ContentHeightProperty); }
+            set { SetValue(ContentHeightProperty, value); }
+        }
+
+        internal DataGridRow OwningRow
+        {
+            get;
+            set;
+        }
+        private DataGrid OwningGrid => OwningRow?.OwningGrid;
+
+        public DataGridDetailsPresenter()
+        {
+            AffectsMeasure<DataGridDetailsPresenter>(ContentHeightProperty);
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.Primitives.DataGridDetailsPresenter" />.
+        /// </summary>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.Primitives.DataGridDetailsPresenter" />.
+        /// </returns>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+            double rowGroupSpacerWidth = OwningGrid.ColumnsInternal.RowGroupSpacerColumn.Width.Value;
+            double leftEdge = rowGroupSpacerWidth;
+            double xClip = OwningGrid.AreRowGroupHeadersFrozen ? rowGroupSpacerWidth : 0;
+            double width;
+            if (OwningGrid.AreRowDetailsFrozen)
+            {
+                leftEdge += OwningGrid.HorizontalOffset;
+                width = OwningGrid.CellsWidth;
+            }
+            else
+            {
+                xClip += OwningGrid.HorizontalOffset;
+                width = Math.Max(OwningGrid.CellsWidth, OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth);
+            }
+            // Details should not extend through the indented area
+            width -= rowGroupSpacerWidth;
+            double height = Math.Max(0, double.IsNaN(ContentHeight) ? 0 : ContentHeight);
+
+            foreach (Control child in Children)
+            {
+                child.Arrange(new Rect(leftEdge, 0, width, height));
+            }
+
+            if (OwningGrid.AreRowDetailsFrozen)
+            {
+                // Frozen Details should not be clipped, similar to frozen cells
+                Clip = null;
+            }
+            else
+            {
+                // Clip so Details doesn't obstruct elements to the left (the RowHeader by default) as we scroll to the right
+                Clip = new RectangleGeometry
+                {
+                    Rect = new Rect(xClip, 0, Math.Max(0, width - xClip + rowGroupSpacerWidth), height)
+                };
+            }
+
+            return finalSize;
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.Primitives.DataGridDetailsPresenter" /> to 
+        /// prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridDetailsPresenter" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (OwningGrid == null || Children.Count == 0)
+            {
+                return Size.Empty;
+            }
+
+            double desiredWidth = OwningGrid.AreRowDetailsFrozen ?
+                OwningGrid.CellsWidth :
+                Math.Max(OwningGrid.CellsWidth, OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth);
+
+            desiredWidth -= OwningGrid.ColumnsInternal.RowGroupSpacerColumn.Width.Value;
+
+            foreach (Control child in Children)
+            {
+                child.Measure(new Size(desiredWidth, double.PositiveInfinity));
+            }
+
+            double desiredHeight = Math.Max(0, double.IsNaN(ContentHeight) ? 0 : ContentHeight);
+
+            return new Size(desiredWidth, desiredHeight);
+        }
+    }
+}

+ 45 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs

@@ -0,0 +1,45 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Represents a non-scrollable grid that contains <see cref="T:Avalonia.Controls.DataGrid" /> row headers.
+    /// </summary>
+    public class DataGridFrozenGrid : Grid
+    {
+        public static readonly AvaloniaProperty<bool> IsFrozenProperty =
+            AvaloniaProperty.RegisterAttached<DataGridFrozenGrid, Control, bool>("IsFrozen");
+
+        /// <summary>
+        /// Gets a value that indicates whether the grid is frozen.
+        /// </summary>
+        /// <param name="element">
+        /// The object to get the <see cref="P:Avalonia.Controls.Primitives.DataGridFrozenGrid.IsFrozen" /> value from.
+        /// </param>
+        /// <returns>true if the grid is frozen; otherwise, false. The default is true.</returns>
+        public static bool GetIsFrozen(Control element)
+        {
+            Contract.Requires<ArgumentNullException>(element != null);
+            return element.GetValue(IsFrozenProperty);
+        }
+
+        /// <summary>
+        /// Sets a value that indicates whether the grid is frozen.
+        /// </summary>
+        /// <param name="element">The object to set the <see cref="P:Avalonia.Controls.Primitives.DataGridFrozenGrid.IsFrozen" /> value on.</param>
+        /// <param name="value">true if <paramref name="element" /> is frozen; otherwise, false.</param>
+        /// <exception cref="T:System.ArgumentNullException"><paramref name="element" /> is null.</exception>
+        public static void SetIsFrozen(Control element, bool value)
+        {
+            Contract.Requires<ArgumentNullException>(element != null);
+            element.SetValue(IsFrozenProperty, value);
+        }
+    }
+}

+ 182 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs

@@ -0,0 +1,182 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Media;
+using System;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" /> to specify the 
+    /// location in the control's visual tree where the rows are to be added.
+    /// </summary>
+    public sealed class DataGridRowsPresenter : Panel
+    {
+        internal DataGrid OwningGrid
+        {
+            get;
+            set;
+        }
+
+        private double _measureHeightOffset = 0;
+        private double CalculateEstimatedAvailableHeight(Size availableSize)
+        {
+            if(!Double.IsPositiveInfinity(availableSize.Height))
+            {
+                return availableSize.Height + _measureHeightOffset;
+            }
+            else
+            {
+                return availableSize.Height;
+            }
+        }
+
+        /// <summary>
+        /// Arranges the content of the <see cref="T:Avalonia.Controls.Primitives.DataGridRowsPresenter" />.
+        /// </summary>
+        /// <returns>
+        /// The actual size used by the <see cref="T:Avalonia.Controls.Primitives.DataGridRowsPresenter" />.
+        /// </returns>
+        /// <param name="finalSize">
+        /// The final area within the parent that this element should use to arrange itself and its children.
+        /// </param>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (finalSize.Height == 0 || OwningGrid == null)
+            {
+                return base.ArrangeOverride(finalSize);
+            }
+
+            if(OwningGrid.RowsPresenterAvailableSize.HasValue)
+            {
+                var availableHeight = OwningGrid.RowsPresenterAvailableSize.Value.Height;
+                if(!Double.IsPositiveInfinity(availableHeight))
+                {
+                    _measureHeightOffset = finalSize.Height - availableHeight;
+                    OwningGrid.RowsPresenterEstimatedAvailableHeight = finalSize.Height;
+                }
+            }
+
+            OwningGrid.OnFillerColumnWidthNeeded(finalSize.Width);
+
+            double rowDesiredWidth = OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth + OwningGrid.ColumnsInternal.FillerColumn.FillerWidth;
+            double topEdge = -OwningGrid.NegVerticalOffset;
+            foreach (Control element in OwningGrid.DisplayData.GetScrollingElements())
+            {
+                if (element is DataGridRow row)
+                {
+                    Debug.Assert(row.Index != -1); // A displayed row should always have its index
+
+                    // Visibility for all filler cells needs to be set in one place.  Setting it individually in
+                    // each CellsPresenter causes an NxN layout cycle (see DevDiv Bugs 211557)
+                    row.EnsureFillerVisibility();
+                    row.Arrange(new Rect(-OwningGrid.HorizontalOffset, topEdge, rowDesiredWidth, element.DesiredSize.Height));
+                }
+                else if (element is DataGridRowGroupHeader groupHeader)
+                {
+                    double leftEdge = (OwningGrid.AreRowGroupHeadersFrozen) ? 0 : -OwningGrid.HorizontalOffset;
+                    groupHeader.Arrange(new Rect(leftEdge, topEdge, rowDesiredWidth - leftEdge, element.DesiredSize.Height));
+                }
+
+                topEdge += element.DesiredSize.Height;
+            }
+
+            double finalHeight = Math.Max(topEdge + OwningGrid.NegVerticalOffset, finalSize.Height);
+
+            // Clip the RowsPresenter so rows cannot overlap other elements in certain styling scenarios
+            var rg = new RectangleGeometry
+            {
+                Rect = new Rect(0, 0, finalSize.Width, finalHeight)
+            };
+            Clip = rg;
+
+            return new Size(finalSize.Width, finalHeight);
+        }
+
+        /// <summary>
+        /// Measures the children of a <see cref="T:Avalonia.Controls.Primitives.DataGridRowsPresenter" /> to 
+        /// prepare for arranging them during the <see cref="M:System.Windows.FrameworkElement.ArrangeOverride(System.Windows.Size)" /> pass.
+        /// </summary>
+        /// <param name="availableSize">
+        /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed.
+        /// </param>
+        /// <returns>
+        /// The size that the <see cref="T:Avalonia.Controls.Primitives.DataGridRowsPresenter" /> determines it needs during layout, based on its calculations of child object allocated sizes.
+        /// </returns>
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (availableSize.Height == 0 || OwningGrid == null)
+            {
+                return base.MeasureOverride(availableSize);
+            }
+
+            // If the Width of our RowsPresenter changed then we need to invalidate our rows
+            bool invalidateRows = (!OwningGrid.RowsPresenterAvailableSize.HasValue || availableSize.Width != OwningGrid.RowsPresenterAvailableSize.Value.Width)
+                                  && !double.IsInfinity(availableSize.Width);
+
+            // The DataGrid uses the RowsPresenter available size in order to autogrow
+            // and calculate the scrollbars
+            OwningGrid.RowsPresenterAvailableSize = availableSize;
+            OwningGrid.RowsPresenterEstimatedAvailableHeight = CalculateEstimatedAvailableHeight(availableSize);
+
+            OwningGrid.OnRowsMeasure();
+
+            double totalHeight = -OwningGrid.NegVerticalOffset;
+            double totalCellsWidth = OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth;
+
+            double headerWidth = 0;
+            foreach (Control element in OwningGrid.DisplayData.GetScrollingElements())
+            {
+                DataGridRow row = element as DataGridRow;
+                if (row != null)
+                {
+                    if (invalidateRows)
+                    {
+                        row.InvalidateMeasure();
+                    }
+                }
+
+                element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+
+                if (row != null && row.HeaderCell != null)
+                {
+                    headerWidth = Math.Max(headerWidth, row.HeaderCell.DesiredSize.Width);
+                }
+                else if (element is DataGridRowGroupHeader groupHeader && groupHeader.HeaderCell != null)
+                {
+                    headerWidth = Math.Max(headerWidth, groupHeader.HeaderCell.DesiredSize.Width);
+                }
+
+                totalHeight += element.DesiredSize.Height;
+            }
+
+            OwningGrid.RowHeadersDesiredWidth = headerWidth;
+            // Could be positive infinity depending on the DataGrid's bounds
+            OwningGrid.AvailableSlotElementRoom = availableSize.Height - totalHeight;
+
+            totalHeight = Math.Max(0, totalHeight);
+
+            return new Size(totalCellsWidth + headerWidth, totalHeight);
+        }
+
+#if DEBUG
+        internal void PrintChildren()
+        {
+            foreach (Control element in Children)
+            {
+                if (element is DataGridRow row)
+                {
+                    Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Row: {1} Visibility: {2} ", row.Slot, row.Index, row.IsVisible));
+                }
+                else if (element is DataGridRowGroupHeader groupHeader)
+                {
+                    Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} GroupHeader: {1} Visibility: {2}", groupHeader.RowGroupInfo.Slot, groupHeader.RowGroupInfo.CollectionViewGroup.Key, groupHeader.IsVisible));
+                }
+            }
+        }
+#endif
+    }
+}

+ 14 - 0
src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs

@@ -0,0 +1,14 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Avalonia.Metadata;
+
+[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")]
+[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
+
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]

+ 69 - 0
src/Avalonia.Controls.DataGrid/Range.cs

@@ -0,0 +1,69 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+    internal class Range<T>
+    {
+        public Range(int lowerBound, int upperBound, T value)
+        {
+            LowerBound = lowerBound;
+            UpperBound = upperBound;
+            Value = value;
+        }
+
+        public int Count
+        {
+            get
+            {
+                return UpperBound - LowerBound + 1;
+            }
+        }
+
+        public int LowerBound
+        {
+            get;
+            set;
+        }
+
+        public int UpperBound
+        {
+            get;
+            set;
+        }
+
+        public T Value
+        {
+            get;
+            set;
+        }
+
+        public bool ContainsIndex(int index)
+        {
+            return (LowerBound <= index) && (UpperBound >= index);
+        }
+
+        public bool ContainsValue(object value)
+        {
+            if (Value == null)
+            {
+                return value == null;
+            }
+            else
+            {
+                return Value.Equals(value);
+            }
+        }
+
+        public Range<T> Copy()
+        {
+            return new Range<T>(LowerBound, UpperBound, Value);
+        }
+    }
+}

+ 233 - 0
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -0,0 +1,233 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <!--TODO: Validation and Focus-->
+  <Style Selector="DataGridCell">
+    <Setter Property="Background" Value="Transparent"/>
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Stretch" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="*,Auto"
+              Background="{TemplateBinding Background}">
+          <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}"
+                            Content="{TemplateBinding Content}"
+                            Margin="{TemplateBinding Padding}"
+                            TextBlock.Foreground="{TemplateBinding Foreground}"
+                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
+
+          <Rectangle Name="PART_RightGridLine"
+                     Grid.Column="1"
+                     VerticalAlignment="Stretch"
+                     Width="1" />
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridColumnHeader">
+    <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
+    <Setter Property="HorizontalContentAlignment" Value="Left" />
+    <Setter Property="VerticalContentAlignment" Value="Center" />
+    <Setter Property="SeparatorBrush" Value="{DynamicResource ThemeControlDarkBrush}" />
+    <Setter Property="Padding" Value="4" />
+    <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
+
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="*,Auto"
+              Background="{TemplateBinding Background}">
+
+          <Grid ColumnDefinitions="*,Auto"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                Margin="{TemplateBinding Padding}">
+            <ContentPresenter Content="{TemplateBinding Content}"/>
+
+            <Path Name="SortIcon"
+                  Grid.Column="1"
+                  Fill="#FF444444"
+                  HorizontalAlignment="Left"
+                  VerticalAlignment="Center"
+                  Stretch="Uniform"
+                  Width="8"
+                  Margin="4,0,0,0"
+                  Data="F1 M -5.215,6.099L 5.215,6.099L 0,0L -5.215,6.099 Z "/>
+
+          </Grid>
+
+          <Rectangle Name="VerticalSeparator"
+                     Grid.Column="1" Width="1"
+                     VerticalAlignment="Stretch"
+                     Fill="{TemplateBinding SeparatorBrush}"
+                     IsVisible="{TemplateBinding AreSeparatorsVisible}" />
+
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridColumnHeader:dragIndicator">
+    <Setter Property="Opacity" Value="0.5"/>
+  </Style>
+
+  <Style Selector="DataGridColumnHeader /template/ Path#SortIcon">
+    <Setter Property="IsVisible" Value="False"/>
+    <Setter Property="RenderTransform">
+      <Setter.Value>
+        <ScaleTransform ScaleX="0.9" ScaleY="0.9" />
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon">
+    <Setter Property="IsVisible" Value="True"/>
+  </Style>
+
+  <Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon">
+    <Setter Property="IsVisible" Value="True"/>
+    <Setter Property="RenderTransform">
+      <Setter.Value>
+        <ScaleTransform ScaleX="0.9" ScaleY="-0.9" />
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridRow">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <DataGridFrozenGrid Name="PART_Root"
+                                 RowDefinitions="*,Auto,Auto"
+                                 ColumnDefinitions="Auto,*">
+
+          <Rectangle Name="BackgroundRectangle" Grid.RowSpan="2" Grid.ColumnSpan="2"/>
+
+          <DataGridRowHeader Grid.RowSpan="3" Name="PART_RowHeader" DataGridFrozenGrid.IsFrozen="True" />
+          <DataGridCellsPresenter Grid.Column="1" Name="PART_CellsPresenter" DataGridFrozenGrid.IsFrozen="True" />
+          <DataGridDetailsPresenter Grid.Row="1" Grid.Column="1" Name="PART_DetailsPresenter"/>
+          <Rectangle Grid.Row="2" Grid.Column="1" Name="PART_BottomGridLine" HorizontalAlignment="Stretch" Height="1" />
+
+        </DataGridFrozenGrid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
+    <Setter Property="IsVisible" Value="False"/>
+    <Setter Property="Fill" Value="#FFBADDE9" />
+  </Style>
+
+  <Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
+    <Setter Property="IsVisible" Value="True"/>
+    <Setter Property="Opacity" Value="0.5"/>
+  </Style>
+
+  <Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
+    <Setter Property="IsVisible" Value="True"/>
+    <Setter Property="Opacity" Value="1"/>
+  </Style>
+
+  <Style Selector="DataGridRowHeader">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid
+        RowDefinitions="*,*,Auto"
+        ColumnDefinitions="Auto,*">
+
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridRowGroupHeader">
+    <Setter Property="Background" Value="#FFE4E8EA" />
+    <Setter Property="Height" Value="20"/>
+    <Setter Property="Template">
+      <ControlTemplate>
+        <DataGridFrozenGrid Name="Root"
+                                 Background="{TemplateBinding Background}"
+                                 ColumnDefinitions="Auto,Auto,Auto,Auto"
+                                 RowDefinitions="Auto,*,Auto">
+
+          <Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="#FFFFFFFF" Height="1"/>
+          <Rectangle Grid.Column="1" Grid.Row="1" Name="IndentSpacer" />
+          <ToggleButton Grid.Column="2" Grid.Row="1" Name="ExpanderButton" Margin="2,0,0,0"/>
+
+          <StackPanel Grid.Column="3" Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,1,0,1">
+            <TextBlock Name="PropertyNameElement" Margin="4,0,0,0" IsVisible="{TemplateBinding IsPropertyNameVisible}"/>
+            <TextBlock Margin="4,0,0,0" Text="{Binding Key}" />
+            <TextBlock Name="ItemCountElement" Margin="4,0,0,0" IsVisible="{TemplateBinding IsItemCountVisible}"/>
+          </StackPanel>
+
+          <DataGridRowHeader Name="RowHeader" Grid.RowSpan="3" DataGridFrozenGrid.IsFrozen="True"/>
+
+        </DataGridFrozenGrid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Grid.Column="0" Width="20" Height="20" Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
+          <Path Fill="Black"
+                HorizontalAlignment="Center"
+                VerticalAlignment="Center"
+                Data="M 0 2 L 4 6 L 0 10 Z" />
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path">
+    <Setter Property="RenderTransform">
+      <RotateTransform Angle="90" />
+    </Setter>
+  </Style>
+
+  <Style Selector="DataGrid">
+    <Setter Property="RowBackground" Value="{DynamicResource ThemeAccentBrush4}" />
+    <Setter Property="AlternatingRowBackground" Value="#00FFFFFF" />
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
+    <Setter Property="HeadersVisibility" Value="Column" />
+    <Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="SelectionMode" Value="Extended" />
+    <Setter Property="GridLinesVisibility" Value="Vertical" />
+    <Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
+    <Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
+    <Setter Property="HeadersVisibility" Value="Column" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
+    <Setter Property="DropLocationIndicatorTemplate">
+      <Template>
+        <Rectangle Fill="#FF3F4346" Width="2"/>
+      </Template>
+    </Setter>
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
+          <Grid
+          RowDefinitions="Auto,*,Auto,Auto"
+          ColumnDefinitions="Auto,*,Auto">
+
+            <DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
+            <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>
+            <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
+            <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/>
+
+            <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
+            <Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
+            <Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
+            <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="18" Margin="0,-1,-1,-1"/>
+
+            <Grid Grid.Column="1" Grid.Row="2"
+                  ColumnDefinitions="Auto,*">
+              <Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
+              <ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="18" Margin="-1,0,-1,-1"/>
+            </Grid>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 160 - 0
src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs

@@ -0,0 +1,160 @@
+using Avalonia.Data;
+using Avalonia.Reactive;
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
+using System.Reactive.Subjects;
+using System.Text;
+
+namespace Avalonia.Controls.Utils
+{
+    public interface ICellEditBinding
+    {
+        bool IsValid { get; }
+        IEnumerable<Exception> ValidationErrors { get; }
+        IObservable<bool> ValidationChanged { get; }
+        bool CommitEdit();
+    }
+
+    internal class CellEditBinding : ICellEditBinding
+    {
+        private readonly Subject<bool> _changedSubject = new Subject<bool>();
+        private readonly List<Exception> _validationErrors = new List<Exception>();
+        private readonly SubjectWrapper _inner;
+
+        public bool IsValid => _validationErrors.Count <= 0;
+        public IEnumerable<Exception> ValidationErrors => _validationErrors;
+        public IObservable<bool> ValidationChanged => _changedSubject;
+        public ISubject<object> InternalSubject => _inner;
+
+        public CellEditBinding(ISubject<object> bindingSourceSubject)
+        {
+            _inner = new SubjectWrapper(bindingSourceSubject, this);
+        }
+
+        private void AlterValidationErrors(Action<List<Exception>> action)
+        {
+            var wasValid = IsValid;
+            action(_validationErrors);
+            var isValid = IsValid;
+
+            if (!isValid || !wasValid)
+            {
+                _changedSubject.OnNext(isValid);
+            }
+        }
+
+        public bool CommitEdit()
+        {
+            _inner.CommitEdit();
+            return IsValid;
+        }
+
+        class SubjectWrapper : LightweightObservableBase<object>, ISubject<object>, IDisposable
+        {
+            private readonly ISubject<object> _sourceSubject;
+            private readonly CellEditBinding _editBinding;
+            private IDisposable _subscription;
+            private object _controlValue;
+            private bool _isControlValueSet = false;
+            private bool _settingSourceValue = false;
+
+            public SubjectWrapper(ISubject<object> bindingSourceSubject, CellEditBinding editBinding)
+            {
+                _sourceSubject = bindingSourceSubject;
+                _editBinding = editBinding;
+            }
+
+            private void SetSourceValue(object value)
+            {
+                _settingSourceValue = true;
+
+                _sourceSubject.OnNext(value);
+
+                _settingSourceValue = false;
+            }
+            private void SetControlValue(object value)
+            {
+                PublishNext(value);
+            }
+
+            private void OnValidationError(BindingNotification notification)
+            {
+                if (notification.Error != null)
+                {
+                    _editBinding.AlterValidationErrors(errors =>
+                    {
+                        errors.Clear();
+                        var unpackedErrors = ValidationUtil.UnpackException(notification.Error);
+                        if (unpackedErrors != null)
+                            errors.AddRange(unpackedErrors);
+                    });
+                }
+            }
+            private void OnControlValueUpdated(object value)
+            {
+                _controlValue = value;
+                _isControlValueSet = true;
+
+                if (!_editBinding.IsValid)
+                {
+                    SetSourceValue(value);
+                }
+            }
+            private void OnSourceValueUpdated(object value)
+            {
+                void OnValidValue(object val)
+                {
+                    SetControlValue(val);
+                    _editBinding.AlterValidationErrors(errors => errors.Clear());
+                }
+
+                if (value is BindingNotification notification)
+                {
+                    if (notification.ErrorType != BindingErrorType.None)
+                        OnValidationError(notification);
+                    else
+                        OnValidValue(value);
+                }
+                else
+                {
+                    OnValidValue(value);
+                }
+            }
+
+            protected override void Deinitialize()
+            {
+                _subscription?.Dispose();
+                _subscription = null;
+            }
+            protected override void Initialize()
+            {
+                _subscription = _sourceSubject.Subscribe(OnSourceValueUpdated);
+            }
+
+            void IObserver<object>.OnCompleted()
+            {
+                throw new NotImplementedException();
+            }
+            void IObserver<object>.OnError(Exception error)
+            {
+                throw new NotImplementedException();
+            }
+            void IObserver<object>.OnNext(object value)
+            {
+                OnControlValueUpdated(value);
+            }
+
+            public void Dispose()
+            {
+                _subscription?.Dispose();
+                _subscription = null;
+            }
+            public void CommitEdit()
+            {
+                if (_isControlValueSet)
+                    SetSourceValue(_controlValue);
+            }
+        }
+    }
+}

+ 136 - 0
src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs

@@ -0,0 +1,136 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+
+namespace Avalonia.Controls.Utils
+{
+    internal static class DoubleUtil
+    {
+        internal const double DBL_EPSILON = 1e-6;
+
+        /// <summary>
+        /// AreClose - Returns whether or not two doubles are "close".  That is, whether or 
+        /// not they are within epsilon of each other.  Note that this epsilon is proportional
+        /// to the numbers themselves to that AreClose survives scalar multiplication.
+        /// There are plenty of ways for this to return false even for numbers which
+        /// are theoretically identical, so no code calling this should fail to work if this 
+        /// returns false.  This is important enough to repeat:
+        /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
+        /// used for optimizations *only*.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the AreClose comparison.
+        /// </returns>
+        /// <param name="value1"> The first double to compare. </param>
+        /// <param name="value2"> The second double to compare. </param>
+        public static bool AreClose(double value1, double value2)
+        {
+            //in case they are Infinities (then epsilon check does not work)
+            if (value1 == value2) return true;
+            // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
+            double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
+            double delta = value1 - value2;
+            return (-eps < delta) && (eps > delta);
+        }
+
+        /// <summary>
+        /// GreaterThan - Returns whether or not the first double is greater than the second double.
+        /// That is, whether or not the first is strictly greater than *and* not within epsilon of
+        /// the other number.  Note that this epsilon is proportional to the numbers themselves
+        /// to that AreClose survives scalar multiplication.  Note,
+        /// There are plenty of ways for this to return false even for numbers which
+        /// are theoretically identical, so no code calling this should fail to work if this 
+        /// returns false.  This is important enough to repeat:
+        /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
+        /// used for optimizations *only*.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the GreaterThan comparison.
+        /// </returns>
+        /// <param name="value1"> The first double to compare. </param>
+        /// <param name="value2"> The second double to compare. </param>
+        public static bool GreaterThan(double value1, double value2)
+        {
+            return (value1 > value2) && !AreClose(value1, value2);
+        }
+
+        /// <summary>
+        /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
+        /// the second double.  That is, whether or not the first is strictly greater than or within
+        /// epsilon of the other number.  Note that this epsilon is proportional to the numbers 
+        /// themselves to that AreClose survives scalar multiplication.  Note,
+        /// There are plenty of ways for this to return false even for numbers which
+        /// are theoretically identical, so no code calling this should fail to work if this 
+        /// returns false.  This is important enough to repeat:
+        /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
+        /// used for optimizations *only*.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the GreaterThanOrClose comparison.
+        /// </returns>
+        /// <param name="value1"> The first double to compare. </param>
+        /// <param name="value2"> The second double to compare. </param>
+        public static bool GreaterThanOrClose(double value1, double value2)
+        {
+            return (value1 > value2) || AreClose(value1, value2);
+        }
+
+        /// <summary>
+        /// IsZero - Returns whether or not the double is "close" to 0.  Same as AreClose(double, 0),
+        /// but this is faster.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the IsZero comparison.
+        /// </returns>
+        /// <param name="value"> The double to compare to 0. </param>
+        public static bool IsZero(double value)
+        {
+            return Math.Abs(value) < 10.0 * DBL_EPSILON;
+        }
+
+        /// <summary>
+        /// LessThan - Returns whether or not the first double is less than the second double.
+        /// That is, whether or not the first is strictly less than *and* not within epsilon of
+        /// the other number.  Note that this epsilon is proportional to the numbers themselves
+        /// to that AreClose survives scalar multiplication.  Note,
+        /// There are plenty of ways for this to return false even for numbers which
+        /// are theoretically identical, so no code calling this should fail to work if this 
+        /// returns false.  This is important enough to repeat:
+        /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
+        /// used for optimizations *only*.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the LessThan comparison.
+        /// </returns>
+        /// <param name="value1"> The first double to compare. </param>
+        /// <param name="value2"> The second double to compare. </param>
+        public static bool LessThan(double value1, double value2)
+        {
+            return (value1 < value2) && !AreClose(value1, value2);
+        }
+
+        /// <summary>
+        /// LessThanOrClose - Returns whether or not the first double is less than or close to
+        /// the second double.  That is, whether or not the first is strictly less than or within
+        /// epsilon of the other number.  Note that this epsilon is proportional to the numbers 
+        /// themselves to that AreClose survives scalar multiplication.  Note,
+        /// There are plenty of ways for this to return false even for numbers which
+        /// are theoretically identical, so no code calling this should fail to work if this 
+        /// returns false.  This is important enough to repeat:
+        /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
+        /// used for optimizations *only*.
+        /// </summary>
+        /// <returns>
+        /// bool - the result of the LessThanOrClose comparison.
+        /// </returns>
+        /// <param name="value1"> The first double to compare. </param>
+        /// <param name="value2"> The second double to compare. </param>
+        public static bool LessThanOrClose(double value1, double value2)
+        {
+            return (value1 < value2) || AreClose(value1, value2);
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs

@@ -0,0 +1,24 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Utils
+{
+    internal static class KeyboardHelper
+    {
+        public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift)
+        {
+            ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control;
+            shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift;
+        }
+        public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift, out bool alt)
+        {
+            ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control;
+            shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift;
+            alt = (modifiers & InputModifiers.Alt) == InputModifiers.Alt;
+        }
+    }
+}

+ 522 - 0
src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs

@@ -0,0 +1,522 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Globalization;
+using System.Reflection;
+
+namespace Avalonia.Controls.Utils
+{
+    internal static class TypeHelper
+    {
+        internal const char LeftIndexerToken = '[';
+        internal const char PropertyNameSeparator = '.';
+        internal const char RightIndexerToken = ']';
+
+        private static Type FindGenericType(Type definition, Type type)
+        {
+            while ((type != null) && (type != typeof(object)))
+            {
+                if (type.IsGenericType && (type.GetGenericTypeDefinition() == definition))
+                {
+                    return type;
+                }
+                if (definition.IsInterface)
+                {
+                    foreach (Type type2 in type.GetInterfaces())
+                    {
+                        Type type3 = FindGenericType(definition, type2);
+                        if (type3 != null)
+                        {
+                            return type3;
+                        }
+                    }
+                }
+                type = type.BaseType;
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// Finds an int or string indexer in the specified collection of members, where int indexers take priority
+        /// over string indexers.  If found, this method will return the associated PropertyInfo and set the out index
+        /// argument to its appropriate value.  If not found, the return value will be null, as will the index.
+        /// </summary>
+        /// <param name="members">Collection of members to search through for an indexer.</param>
+        /// <param name="stringIndex">String value of indexer argument.</param>
+        /// <param name="index">Resultant index value.</param>
+        /// <returns>Indexer PropertyInfo if found, null otherwise.</returns>
+        private static PropertyInfo FindIndexerInMembers(MemberInfo[] members, string stringIndex, out object[] index)
+        {
+            index = null;
+            ParameterInfo[] parameters;
+            PropertyInfo stringIndexer = null;
+
+            foreach (PropertyInfo pi in members)
+            {
+                if (pi == null)
+                {
+                    continue;
+                }
+
+                // Only a single parameter is supported and it must be a string or Int32 value.
+                parameters = pi.GetIndexParameters();
+                if (parameters.Length > 1)
+                {
+                    continue;
+                }
+
+                if (parameters[0].ParameterType == typeof(int))
+                {
+                    int intIndex = -1;
+                    if (Int32.TryParse(stringIndex.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out intIndex))
+                    {
+                        index = new object[] { intIndex };
+                        return pi;
+                    }
+                }
+
+                // If string indexer is found save it, in case there is an int indexer.
+                if (parameters[0].ParameterType == typeof(string))
+                {
+                    index = new object[] { stringIndex };
+                    stringIndexer = pi;
+                }
+            }
+
+            return stringIndexer;
+        }
+
+        /// <summary>
+        /// Gets the default member name that is used for an indexer (e.g. "Item").
+        /// </summary>
+        /// <param name="type">Type to check.</param>
+        /// <returns>Default member name.</returns>
+        private static string GetDefaultMemberName(this Type type)
+        {
+            object[] attributes = type.GetCustomAttributes(typeof(DefaultMemberAttribute), true);
+            if (attributes != null && attributes.Length == 1)
+            {
+                DefaultMemberAttribute defaultMemberAttribute = attributes[0] as DefaultMemberAttribute;
+                return defaultMemberAttribute.MemberName;
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Finds the PropertyInfo for the specified property path within this Type, and returns
+        /// the value of GetShortName on its DisplayAttribute, if one exists. GetShortName will return
+        /// the value of Name if there is no ShortName specified.
+        /// </summary>
+        /// <param name="type">Type to search</param>
+        /// <param name="propertyPath">property path</param>
+        /// <returns>DisplayAttribute.ShortName if it exists, null otherwise</returns>
+        internal static string GetDisplayName(this Type type, string propertyPath)
+        {
+            PropertyInfo propertyInfo = type.GetNestedProperty(propertyPath);
+            if (propertyInfo != null)
+            {
+                object[] attributes = propertyInfo.GetCustomAttributes(typeof(DisplayAttribute), true);
+                if (attributes != null && attributes.Length > 0)
+                {
+                    Debug.Assert(attributes.Length == 1);
+                    if (attributes[0] is DisplayAttribute displayAttribute)
+                    {
+                        return displayAttribute.GetShortName();
+                    }
+                }
+            }
+            return null;
+        }
+
+        internal static Type GetEnumerableItemType(this Type enumerableType)
+        {
+            Type type = FindGenericType(typeof(IEnumerable<>), enumerableType);
+            if (type != null)
+            {
+                return type.GetGenericArguments()[0];
+            }
+            return enumerableType;
+        }
+
+        /// <summary>
+        /// Retrieves the value and type of a property. That property can be nested and its path
+        /// can include indexers. Each element of the path needs to be a public instance property.
+        /// </summary>
+        /// <param name="parentType">The parent Type</param>
+        /// <param name="propertyPath">Property path</param>
+        /// <param name="exception">Potential exception</param>
+        /// <param name="item">Parent item which will be set to the property value if non-null.</param>
+        /// <returns></returns>
+        private static PropertyInfo GetNestedProperty(this Type parentType, string propertyPath, out Exception exception, ref object item)
+        {
+            exception = null;
+            if (parentType == null || String.IsNullOrEmpty(propertyPath))
+            {
+                item = null;
+                return null;
+            }
+
+            Type type = parentType;
+            PropertyInfo propertyInfo = null;
+            List<string> propertyNames = SplitPropertyPath(propertyPath);
+            for (int i = 0; i < propertyNames.Count; i++)
+            {
+                // if we can't find the property or it is not of the correct type,
+                // treat it as a null value
+                propertyInfo = type.GetPropertyOrIndexer(propertyNames[i], out object[] index);
+                if (propertyInfo == null)
+                {
+                    item = null;
+                    return null;
+                }
+
+                if (!propertyInfo.CanRead)
+                {
+                    exception =
+                        new InvalidOperationException(
+                            $"The property named '{propertyNames[i]}' on type '{type.GetTypeName()}' cannot be read.");
+                    item = null;
+                    return null;
+                }
+
+                if (item != null)
+                {
+                    item = propertyInfo.GetValue(item, index);
+                }
+                type = propertyInfo.PropertyType.GetNonNullableType();
+            }
+
+            return propertyInfo;
+        }
+
+        /// <summary>
+        /// Finds the leaf PropertyInfo for the specified property path, and returns its value
+        /// if the item is non-null.
+        /// </summary>
+        /// <param name="parentType">Type to search.</param>
+        /// <param name="propertyPath">Property path.</param>
+        /// <param name="item">Parent item which will be set to the property value if non-null.</param>
+        /// <returns>The PropertyInfo.</returns>
+        internal static PropertyInfo GetNestedProperty(this Type parentType, string propertyPath, ref object item)
+        {
+            return parentType.GetNestedProperty(propertyPath, out Exception ex, ref item);
+        }
+
+        internal static PropertyInfo GetNestedProperty(this Type parentType, string propertyPath)
+        {
+            if (parentType != null)
+            {
+                object item = null;
+                return parentType.GetNestedProperty(propertyPath, ref item);
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// Returns the friendly name for a type
+        /// </summary>
+        /// <param name="type">The type to get the name from</param>
+        /// <returns>Textual representation of the input type</returns>
+        internal static string GetTypeName(this Type type)
+        {
+            Type baseType = type.GetNonNullableType();
+            string s = baseType.Name;
+            if (type != baseType)
+            {
+                s += '?';
+            }
+            return s;
+        }
+
+        internal static Type GetNestedPropertyType(this Type parentType, string propertyPath)
+        {
+            if (parentType == null || String.IsNullOrEmpty(propertyPath))
+            {
+                return parentType;
+            }
+
+            PropertyInfo propertyInfo = parentType.GetNestedProperty(propertyPath);
+            if (propertyInfo != null)
+            {
+                return propertyInfo.PropertyType;
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// Retrieves the value of a property. That property can be nested and its path can
+        /// include indexers. Each element of the path needs to be a public instance property.
+        /// The return value will either be of type propertyType or it will be null.
+        /// </summary>
+        /// <param name="item">Object that exposes the property</param>
+        /// <param name="propertyPath">Property path</param>
+        /// <param name="propertyType">Property type</param>
+        /// <param name="exception">Potential exception</param>
+        /// <returns>Property value</returns>
+        internal static object GetNestedPropertyValue(object item, string propertyPath, Type propertyType, out Exception exception)
+        {
+            exception = null;
+
+            // if the item is null, treat the property value as null
+            if (item == null)
+            {
+                return null;
+            }
+
+            // if the propertyPath is null or empty, return the item
+            if (String.IsNullOrEmpty(propertyPath))
+            {
+                return item;
+            }
+
+            object propertyValue = item;
+            Type itemType = item.GetType();
+            if (itemType != null)
+            {
+                PropertyInfo propertyInfo = itemType.GetNestedProperty(propertyPath, out exception, ref propertyValue);
+                if (propertyInfo != null && propertyInfo.PropertyType != propertyType)
+                {
+                    return null;
+                }
+            }
+            return propertyValue;
+        }
+
+        /// <summary>
+        /// Gets the value of a given property path on a particular data item.
+        /// </summary>
+        /// <param name="item">Parent data item.</param>
+        /// <param name="propertyPath">Property path.</param>
+        /// <returns>Value.</returns>
+        internal static object GetNestedPropertyValue(object item, string propertyPath)
+        {
+            if (item != null)
+            {
+                Type parentType = item.GetType();
+                if (String.IsNullOrEmpty(propertyPath))
+                {
+                    return item;
+                }
+                else if (parentType != null)
+                {
+                    object nestedValue = item;
+                    parentType.GetNestedProperty(propertyPath, ref nestedValue);
+                    return nestedValue;
+                }
+            }
+            return null;
+        }
+
+        internal static Type GetNonNullableType(this Type type)
+        {
+            if (IsNullableType(type))
+            {
+                return type.GetGenericArguments()[0];
+            }
+            return type;
+        }
+
+        /// <summary>
+        /// Returns the PropertyInfo for the specified property path.  If the property path
+        /// refers to an indexer (e.g. "[abc]"), then the index out parameter will be set to the value
+        /// specified in the property path.  This method only supports indexers with a single parameter
+        /// that is either an int or a string.  Int parameters take priority over string parameters.
+        /// </summary>
+        /// <param name="type">Type to search.</param>
+        /// <param name="propertyPath">Property path.</param>
+        /// <param name="index">Set to the index if return value is an indexer, otherwise null.</param>
+        /// <returns>PropertyInfo for either a property or an indexer.</returns>
+        internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index)
+        {
+            index = null;
+            if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken)
+            {
+                // Return the default value of GetProperty if the first character is not an indexer token.
+                return type.GetProperty(propertyPath);
+            }
+
+            if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken)
+            {
+                // Return null if the indexer does not meet the standard format (i.e. "[x]").
+                return null;
+            }
+
+            PropertyInfo indexer = null;
+            string stringIndex = propertyPath.Substring(1, propertyPath.Length - 2);
+            indexer = FindIndexerInMembers(type.GetDefaultMembers(), stringIndex, out index);
+            if (indexer != null)
+            {
+                // We found the indexer, so return it.
+                return indexer;
+            }
+
+            if (typeof(IList).IsAssignableFrom(type))
+            {
+                // If the object is of type IList, try to use its default indexer.
+                indexer = FindIndexerInMembers(typeof(IList).GetDefaultMembers(), stringIndex, out index);
+            }
+
+            return indexer;
+        }
+
+        internal static bool IsEnumerableType(this Type enumerableType)
+        {
+            return (FindGenericType(typeof(IEnumerable<>), enumerableType) != null);
+        }
+
+        internal static bool IsNullableType(this Type type)
+        {
+            return (((type != null) && type.IsGenericType) && (type.GetGenericTypeDefinition() == typeof(Nullable<>)));
+        }
+
+        internal static bool IsNullableEnum(this Type type)
+        {
+            return type.IsNullableType() &&
+                 type.GetGenericArguments().Length == 1 &&
+                 type.GetGenericArguments()[0].IsEnum;
+        }
+
+        /// <summary>
+        /// If the specified property is an indexer, this method will prepend the object's
+        /// default member name to it (e.g. "[foo]" returns "Item[foo]").
+        /// </summary>
+        /// <param name="item">Declaring data item.</param>
+        /// <param name="property">Property name.</param>
+        /// <returns>Property with default member name prepended, or property if unchanged.</returns>
+        internal static string PrependDefaultMemberName(object item, string property)
+        {
+            if (item != null && !string.IsNullOrEmpty(property) && property[0] == TypeHelper.LeftIndexerToken)
+            {
+                // The leaf property name is an indexer, so add the default member name.
+                Type declaringType = item.GetType();
+                if (declaringType != null)
+                {
+                    string defaultMemberName = declaringType.GetNonNullableType().GetDefaultMemberName();
+                    if (!string.IsNullOrEmpty(defaultMemberName))
+                    {
+                        return defaultMemberName + property;
+                    }
+                }
+            }
+            return property;
+        }
+
+        /// <summary>
+        /// If the specified property is an indexer, this method will remove the object's
+        /// default member name from it (e.g. "Item[foo]" returns "[foo]").
+        /// </summary>
+        /// <param name="property">Property name.</param>
+        /// <returns>Property with default member name removed, or property if unchanged.</returns>
+        internal static string RemoveDefaultMemberName(string property)
+        {
+            if (!string.IsNullOrEmpty(property) && property[property.Length - 1] == TypeHelper.RightIndexerToken)
+            {
+                // The property is an indexer, so remove the default member name.
+                int leftIndexerToken = property.IndexOf(TypeHelper.LeftIndexerToken);
+                if (leftIndexerToken >= 0)
+                {
+                    return property.Substring(leftIndexerToken);
+                }
+            }
+            return property;
+        }
+
+        /// <summary>
+        /// Returns a list of substrings where each one represents a single property within a nested
+        /// property path which may include indexers.  For example, the string "abc.d[efg][h].ijk"
+        /// would return the substrings: "abc", "d", "[efg]", "[h]", and "ijk".
+        /// </summary>
+        /// <param name="propertyPath">Path to split.</param>
+        /// <returns>List of property substrings.</returns>
+        internal static List<string> SplitPropertyPath(string propertyPath)
+        {
+            List<string> propertyPaths = new List<string>();
+            if (!string.IsNullOrEmpty(propertyPath))
+            {
+                int startIndex = 0;
+                for (int index = 0; index < propertyPath.Length; index++)
+                {
+                    if (propertyPath[index] == PropertyNameSeparator)
+                    {
+                        propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex));
+                        startIndex = index + 1;
+                    }
+                    else if (startIndex != index && propertyPath[index] == LeftIndexerToken)
+                    {
+                        propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex));
+                        startIndex = index;
+                    }
+                    else if (index == propertyPath.Length - 1)
+                    {
+                        propertyPaths.Add(propertyPath.Substring(startIndex));
+                    }
+                }
+            }
+            return propertyPaths;
+        }
+
+        /// <summary>
+        /// Checks a MemberInfo object (e.g. a Type or PropertyInfo) for the ReadOnly attribute
+        /// and returns the value of IsReadOnly if it exists.
+        /// </summary>
+        /// <param name="memberInfo">MemberInfo to check</param>
+        /// <returns>true if MemberInfo is read-only, false otherwise</returns>
+        internal static bool GetIsReadOnly(this MemberInfo memberInfo)
+        {
+            if (memberInfo != null)
+            {
+                // Check if ReadOnlyAttribute is defined on the member
+                object[] attributes = memberInfo.GetCustomAttributes(typeof(ReadOnlyAttribute), true);
+                if (attributes != null && attributes.Length > 0)
+                {
+                    ReadOnlyAttribute readOnlyAttribute = attributes[0] as ReadOnlyAttribute;
+                    Debug.Assert(readOnlyAttribute != null);
+                    return readOnlyAttribute.IsReadOnly;
+                }
+            }
+            return false;
+        }
+
+        internal static Type GetItemType(this IEnumerable list)
+        {
+            Type listType = list.GetType();
+            Type itemType = null;
+
+            // if it's a generic enumerable, we get the generic type
+
+            // Unfortunately, if data source is fed from a bare IEnumerable, TypeHelper will report an element type of object,
+            // which is not particularly interesting.  We deal with it further on.
+            if (listType.IsEnumerableType())
+            {
+                itemType = listType.GetEnumerableItemType();
+            }
+
+            // Bare IEnumerables mean that result type will be object.  In that case, we try to get something more interesting
+            if (itemType == null || itemType == typeof(object))
+            {
+                // We haven't located a type yet.. try a different approach.
+                // Does the list have anything in it?
+
+                IEnumerator en = list.GetEnumerator();
+                if (en.MoveNext() && en.Current != null)
+                {
+                    return en.Current.GetType();
+                }
+            }
+
+            // if we're null at this point, give up
+            return itemType;
+        }
+    }
+}

+ 60 - 0
src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs

@@ -0,0 +1,60 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Input;
+using Avalonia.VisualTree;
+using Avalonia.Controls;
+
+namespace Avalonia.Controls.Utils
+{
+    internal static class TreeHelper
+    {
+        /// <summary>
+        /// Walks the visual tree to determine if a particular child is contained within a parent Visual.
+        /// </summary>
+        /// <param name="element">Parent Visual</param>
+        /// <param name="child">Child Visual</param>
+        /// <returns>True if the parent element contains the child</returns>
+        internal static bool ContainsChild(this IVisual element, IVisual child)
+        {
+            if (element != null)
+            {
+                while (child != null)
+                {
+                    if (child == element)
+                    {
+                        return true;
+                    }
+
+                    // Walk up the visual tree.  If we hit the root, try using the framework element's
+                    // parent.  We do this because Popups behave differently with respect to the visual tree,
+                    // and it could have a parent even if the VisualTreeHelper doesn't find it.
+                    IVisual parent = child.GetVisualParent();
+                    if (parent == null)
+                    {
+                        if (child is IControl childElement)
+                        {
+                            parent = childElement.Parent;
+                        }
+                    }
+                    child = parent;
+                }
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Walks the visual tree to determine if the currently focused element is contained within
+        /// a parent AvaloniaObject.  The FocusManager's Current property is used to determine
+        /// the currently focused element, which is updated synchronously.
+        /// </summary>
+        /// <param name="element">Parent Visual</param>
+        /// <returns>True if the currently focused element is within the visual tree of the parent</returns>
+        internal static bool ContainsFocusedElement(this IVisual element)
+        {
+            return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current);
+        }
+    }
+}

+ 167 - 0
src/Avalonia.Controls.DataGrid/Utils/ValidationUtil.cs

@@ -0,0 +1,167 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+using System.Threading;
+using System.Linq;
+using Avalonia.Data;
+
+namespace Avalonia.Controls.Utils
+{
+    internal static class ValidationUtil
+    {
+        /// <summary>
+        /// Searches a ValidationResult for the specified target member name.  If the target is null
+        /// or empty, this method will return true if there are no member names at all.
+        /// </summary>
+        /// <param name="validationResult">ValidationResult to search.</param>
+        /// <param name="target">Member name to search for.</param>
+        /// <returns>True if found.</returns>
+        public static bool ContainsMemberName(this ValidationResult validationResult, string target)
+        {
+            int memberNameCount = 0;
+            foreach (string memberName in validationResult.MemberNames)
+            {
+                if (string.Equals(target, memberName))
+                {
+                    return true;
+                }
+                memberNameCount++;
+            }
+            return (memberNameCount == 0 && string.IsNullOrEmpty(target));
+        }
+
+        /// <summary>
+        /// Finds an equivalent ValidationResult if one exists.
+        /// </summary>
+        /// <param name="collection">ValidationResults to search through.</param>
+        /// <param name="target">ValidationResult to find.</param>
+        /// <returns>Equal ValidationResult if found, null otherwise.</returns>
+        public static ValidationResult FindEqualValidationResult(this ICollection<ValidationResult> collection, ValidationResult target)
+        {
+            foreach (ValidationResult oldValidationResult in collection)
+            {
+                if (oldValidationResult.ErrorMessage == target.ErrorMessage)
+                {
+                    bool movedOld = true;
+                    bool movedTarget = true;
+                    IEnumerator<string> oldEnumerator = oldValidationResult.MemberNames.GetEnumerator();
+                    IEnumerator<string> targetEnumerator = target.MemberNames.GetEnumerator();
+                    while (movedOld && movedTarget)
+                    {
+                        movedOld = oldEnumerator.MoveNext();
+                        movedTarget = targetEnumerator.MoveNext();
+
+                        if (!movedOld && !movedTarget)
+                        {
+                            return oldValidationResult;
+                        }
+                        if (movedOld != movedTarget || oldEnumerator.Current != targetEnumerator.Current)
+                        {
+                            break;
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+
+        public static bool IsValid(this ValidationResult result)
+        {
+            return result == null || result == ValidationResult.Success;
+        }
+
+        public static IEnumerable<Exception> UnpackException(Exception exception)
+        {
+            if (exception != null)
+            {
+                var aggregate = exception as AggregateException;
+                var exceptions = aggregate == null ?
+                    (IEnumerable<Exception>)new[] { exception } :
+                    aggregate.InnerExceptions;
+                var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
+
+                if (filtered.Count > 0)
+                {
+                    return filtered;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Determines whether the collection contains an equivalent ValidationResult
+        /// </summary>
+        /// <param name="collection">ValidationResults to search through</param>
+        /// <param name="target">ValidationResult to search for</param>
+        /// <returns></returns>
+        public static bool ContainsEqualValidationResult(this ICollection<ValidationResult> collection, ValidationResult target)
+        {
+            return (collection.FindEqualValidationResult(target) != null);
+        }
+
+        /// <summary>
+        /// Adds a new ValidationResult to the collection if an equivalent does not exist.
+        /// </summary>
+        /// <param name="collection">ValidationResults to search through</param>
+        /// <param name="value">ValidationResult to add</param>
+        public static void AddIfNew(this ICollection<ValidationResult> collection, ValidationResult value)
+        {
+            if (!collection.ContainsEqualValidationResult(value))
+            {
+                collection.Add(value);
+            }
+        }
+
+        private static bool ExceptionsMatch(Exception e1, Exception e2)
+        {
+            return e1.Message == e2.Message;
+        }
+        public static void AddExceptionIfNew(this ICollection<Exception> collection, Exception value)
+        {
+            if(!collection.Any(e => ExceptionsMatch(e, value)))
+            {
+                collection.Add(value);
+            }
+        }
+
+        /// <summary>
+        /// Performs an action and catches any non-critical exceptions.
+        /// </summary>
+        /// <param name="action">Action to perform</param>
+        public static void CatchNonCriticalExceptions(Action action)
+        {
+            try
+            {
+                action();
+            }
+            catch (Exception exception)
+            {
+                if (IsCriticalException(exception))
+                {
+                    throw;
+                }
+                // Catch any non-critical exceptions
+            }
+        }
+
+        /// <summary>
+        /// Determines if the specified exception is un-recoverable.
+        /// </summary>
+        /// <param name="exception">The exception.</param>
+        /// <returns>True if the process cannot be recovered from the exception.</returns>
+        public static bool IsCriticalException(Exception exception)
+        {
+            return (exception is OutOfMemoryException) ||
+                (exception is StackOverflowException) ||
+                (exception is AccessViolationException) ||
+                (exception is ThreadAbortException);
+        }
+    }
+}

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

@@ -58,7 +58,6 @@ namespace Avalonia.Controls
     /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populating" />
     /// event.
     /// </summary>
-    /// <QualityBand>Stable</QualityBand>
     public class PopulatingEventArgs : CancelEventArgs
     {
         /// <summary>
@@ -97,7 +96,6 @@ namespace Avalonia.Controls
     /// <typeparam name="T">The type used for filtering the
     /// <see cref="T:Avalonia.Controls.AutoCompleteBox" />. This type can
     /// be either a string or an object.</typeparam>
-    /// <QualityBand>Stable</QualityBand>
     public delegate bool AutoCompleteFilterPredicate<T>(string search, T item);
 
     /// <summary>
@@ -107,7 +105,6 @@ namespace Avalonia.Controls
     /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
     /// property for display in the drop-down.
     /// </summary>
-    /// <QualityBand>Stable</QualityBand>
     public enum AutoCompleteFilterMode
     {
         /// <summary>

+ 45 - 24
src/Avalonia.Controls/Button.cs

@@ -7,6 +7,7 @@ using System.Windows.Input;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -160,6 +161,40 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc/>
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromVisualTree(e);
+
+            if (IsDefault)
+            {
+                if (e.Root is IInputElement inputElement)
+                {
+                    StopListeningForDefault(inputElement);
+                }
+            }
+        }
+
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged += CanExecuteChanged;
+            }
+        }
+
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged -= CanExecuteChanged;
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {
@@ -195,20 +230,6 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <inheritdoc/>
-        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
-        {
-            base.OnDetachedFromVisualTree(e);
-
-            if (IsDefault)
-            {
-                if (e.Root is IInputElement inputElement)
-                {
-                    StopListeningForDefault(inputElement);
-                }
-            }
-        }
-
         /// <summary>
         /// Invokes the <see cref="Click"/> event.
         /// </summary>
@@ -281,17 +302,17 @@ namespace Avalonia.Controls
         {
             if (e.Sender is Button button)
             {
-                var oldCommand = e.OldValue as ICommand;
-                var newCommand = e.NewValue as ICommand;
-
-                if (oldCommand != null)
-                {
-                    oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
-                }
-
-                if (newCommand != null)
+                if (((ILogical)button).IsAttachedToLogicalTree)
                 {
-                    newCommand.CanExecuteChanged += button.CanExecuteChanged;
+                    if (e.OldValue is ICommand oldCommand)
+                    {
+                        oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
+                    }
+
+                    if (e.NewValue is ICommand newCommand)
+                    {
+                        newCommand.CanExecuteChanged += button.CanExecuteChanged;
+                    }
                 }
 
                 button.CanExecuteChanged(button, EventArgs.Empty);

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

@@ -117,7 +117,6 @@ namespace Avalonia.Controls
     /// <see cref="E:Avalonia.Controls.Calendar.DisplayModeChanged" />
     /// event.
     /// </summary>
-    /// <QualityBand>Mature</QualityBand>
     public class CalendarModeChangedEventArgs : RoutedEventArgs
     {
         /// <summary>

+ 373 - 0
src/Avalonia.Controls/ComboBox.cs

@@ -0,0 +1,373 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Linq;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// A drop-down list control.
+    /// </summary>
+    public class ComboBox : SelectingItemsControl
+    {
+        /// <summary>
+        /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
+        /// </summary>
+        private static readonly FuncTemplate<IPanel> DefaultPanel =
+            new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
+
+        /// <summary>
+        /// Defines the <see cref="IsDropDownOpen"/> property.
+        /// </summary>
+        public static readonly DirectProperty<ComboBox, bool> IsDropDownOpenProperty =
+            AvaloniaProperty.RegisterDirect<ComboBox, bool>(
+                nameof(IsDropDownOpen),
+                o => o.IsDropDownOpen,
+                (o, v) => o.IsDropDownOpen = v);
+
+        /// <summary>
+        /// Defines the <see cref="MaxDropDownHeight"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MaxDropDownHeightProperty =
+            AvaloniaProperty.Register<ComboBox, double>(nameof(MaxDropDownHeight), 200);
+
+        /// <summary>
+        /// Defines the <see cref="SelectionBoxItem"/> property.
+        /// </summary>
+        public static readonly DirectProperty<ComboBox, object> SelectionBoxItemProperty =
+            AvaloniaProperty.RegisterDirect<ComboBox, object>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
+
+        /// <summary>
+        /// Defines the <see cref="VirtualizationMode"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
+            ItemsPresenter.VirtualizationModeProperty.AddOwner<ComboBox>();
+
+        private bool _isDropDownOpen;
+        private Popup _popup;
+        private object _selectionBoxItem;
+        private IDisposable _subscriptionsOnOpen;
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ComboBox"/> class.
+        /// </summary>
+        static ComboBox()
+        {
+            ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
+            FocusableProperty.OverrideDefaultValue<ComboBox>(true);
+            SelectedItemProperty.Changed.AddClassHandler<ComboBox>(x => x.SelectedItemChanged);
+            KeyDownEvent.AddClassHandler<ComboBox>(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the dropdown is currently open.
+        /// </summary>
+        public bool IsDropDownOpen
+        {
+            get { return _isDropDownOpen; }
+            set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the maximum height for the dropdown list.
+        /// </summary>
+        public double MaxDropDownHeight
+        {
+            get { return GetValue(MaxDropDownHeightProperty); }
+            set { SetValue(MaxDropDownHeightProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the item to display as the control's content.
+        /// </summary>
+        protected object SelectionBoxItem
+        {
+            get { return _selectionBoxItem; }
+            set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the virtualization mode for the items.
+        /// </summary>
+        public ItemVirtualizationMode VirtualizationMode
+        {
+            get { return GetValue(VirtualizationModeProperty); }
+            set { SetValue(VirtualizationModeProperty, value); }
+        }
+
+        /// <inheritdoc/>
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new ItemContainerGenerator<ComboBoxItem>(
+                this,
+                ComboBoxItem.ContentProperty,
+                ComboBoxItem.ContentTemplateProperty);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+            this.UpdateSelectionBoxItem(this.SelectedItem);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            base.OnKeyDown(e);
+
+            if (e.Handled)
+                return;
+
+            if (e.Key == Key.F4 ||
+                ((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0)))
+            {
+                IsDropDownOpen = !IsDropDownOpen;
+                e.Handled = true;
+            }
+            else if (IsDropDownOpen && e.Key == Key.Escape)
+            {
+                IsDropDownOpen = false;
+                e.Handled = true;
+            }
+            else if (IsDropDownOpen && e.Key == Key.Enter)
+            {
+                SelectFocusedItem();
+                IsDropDownOpen = false;
+                e.Handled = true;
+            }
+            else if (!IsDropDownOpen)
+            {
+                if (e.Key == Key.Down)
+                {
+                    SelectNext();
+                    e.Handled = true;
+                }
+                else if (e.Key == Key.Up)
+                {
+                    SelectPrev();
+                    e.Handled = true;
+                }
+            }
+            else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
+                      (e.Key == Key.Up || e.Key == Key.Down))
+            {
+                var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
+                if (firstChild != null)
+                {
+                    FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional);
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <inheritdoc/>
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+
+            if (!e.Handled)
+            {
+                if (!IsDropDownOpen)
+                {
+                    if (IsFocused)
+                    {
+                        if (e.Delta.Y < 0)
+                            SelectNext();
+                        else
+                            SelectPrev();
+
+                        e.Handled = true;
+                    }
+                }
+                else
+                {
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <inheritdoc/>
+        protected override void OnPointerPressed(PointerPressedEventArgs e)
+        {
+            if (!e.Handled)
+            {
+                if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
+                {
+                    if (UpdateSelectionFromEventSource(e.Source))
+                    {
+                        _popup?.Close();
+                        e.Handled = true;
+                    }
+                }
+                else
+                {
+                    IsDropDownOpen = !IsDropDownOpen;
+                    e.Handled = true;
+                }
+            }
+
+            base.OnPointerPressed(e);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            if (_popup != null)
+            {
+                _popup.Opened -= PopupOpened;
+                _popup.Closed -= PopupClosed;
+            }
+
+            _popup = e.NameScope.Get<Popup>("PART_Popup");
+            _popup.Opened += PopupOpened;
+            _popup.Closed += PopupClosed;
+
+            base.OnTemplateApplied(e);
+        }
+
+        internal void ItemFocused(ComboBoxItem dropDownItem)
+        {
+            if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
+            {
+                dropDownItem.BringIntoView();
+            }
+        }
+
+        private void PopupClosed(object sender, EventArgs e)
+        {
+            _subscriptionsOnOpen?.Dispose();
+            _subscriptionsOnOpen = null;
+
+            if (CanFocus(this))
+            {
+                Focus();
+            }
+        }
+
+        private void PopupOpened(object sender, EventArgs e)
+        {
+            TryFocusSelectedItem();
+
+            _subscriptionsOnOpen?.Dispose();
+            _subscriptionsOnOpen = null;
+
+            var toplevel = this.GetVisualRoot() as TopLevel;
+            if (toplevel != null)
+            {
+                _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
+                {
+                    //eat wheel scroll event outside dropdown popup while it's open
+                    if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
+                    {
+                        ev.Handled = true;
+                    }
+                }, Interactivity.RoutingStrategies.Tunnel);
+            }
+        }
+
+        private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UpdateSelectionBoxItem(e.NewValue);
+            TryFocusSelectedItem();
+        }
+
+        private void TryFocusSelectedItem()
+        {
+            var selectedIndex = SelectedIndex;
+            if (IsDropDownOpen && selectedIndex != -1)
+            {
+                var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
+
+                if (container == null && SelectedItems.Count > 0)
+                {
+                    ScrollIntoView(SelectedItems[0]);
+                    container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
+                }
+
+                if (container != null && CanFocus(container))
+                {
+                    container.Focus();
+                }
+            }
+        }
+
+        private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible;
+
+        private void UpdateSelectionBoxItem(object item)
+        {
+            var contentControl = item as IContentControl;
+
+            if (contentControl != null)
+            {
+                item = contentControl.Content;
+            }
+
+            var control = item as IControl;
+
+            if (control != null)
+            {
+                control.Measure(Size.Infinity);
+
+                SelectionBoxItem = new Rectangle
+                {
+                    Width = control.DesiredSize.Width,
+                    Height = control.DesiredSize.Height,
+                    Fill = new VisualBrush
+                    {
+                        Visual = control,
+                        Stretch = Stretch.None,
+                        AlignmentX = AlignmentX.Left,
+                    }
+                };
+            }
+            else
+            {
+                var selector = MemberSelector;
+                SelectionBoxItem = selector != null ? selector.Select(item) : item;
+            }
+        }
+
+        private void SelectFocusedItem()
+        {
+            foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
+            {
+                if (dropdownItem.ContainerControl.IsFocused)
+                {
+                    SelectedIndex = dropdownItem.Index;
+                    break;
+                }
+            }
+        }
+
+        private void SelectNext()
+        {
+            int next = SelectedIndex + 1;
+
+            if (next >= ItemCount)
+                next = 0;
+
+            SelectedIndex = next;
+        }
+
+        private void SelectPrev()
+        {
+            int prev = SelectedIndex - 1;
+
+            if (prev < 0)
+                prev = ItemCount - 1;
+
+            SelectedIndex = prev;
+        }
+    }
+}

+ 5 - 5
src/Avalonia.Controls/DropDownItem.cs → src/Avalonia.Controls/ComboBoxItem.cs

@@ -7,14 +7,14 @@ using System.Reactive.Linq;
 namespace Avalonia.Controls
 {
     /// <summary>
-    /// A selectable item in a <see cref="DropDown"/>.
+    /// A selectable item in a <see cref="ComboBox"/>.
     /// </summary>
-    public class DropDownItem : ListBoxItem
+    public class ComboBoxItem : ListBoxItem
     {
-        public DropDownItem()
+        public ComboBoxItem()
         {
-            this.GetObservable(DropDownItem.IsFocusedProperty).Where(focused => focused)
-                .Subscribe(_ => (Parent as DropDown)?.ItemFocused(this));
+            this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused)
+                .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this));
         }
     }
 }

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

@@ -15,7 +15,7 @@ namespace Avalonia.Controls
     public static class ControlExtensions
     {
         /// <summary>
-        /// Tries to being the control into view.
+        /// Tries to bring the control into view.
         /// </summary>
         /// <param name="control">The control.</param>
         public static void BringIntoView(this IControl control)
@@ -26,7 +26,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Tries to being the control into view.
+        /// Tries to bring the control into view.
         /// </summary>
         /// <param name="control">The control.</param>
         /// <param name="rect">The area of the control to being into view.</param>

+ 15 - 360
src/Avalonia.Controls/DropDown.cs

@@ -1,373 +1,28 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Linq;
-using Avalonia.Controls.Generators;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Shapes;
-using Avalonia.Controls.Templates;
-using Avalonia.Input;
-using Avalonia.LogicalTree;
-using Avalonia.Media;
-using Avalonia.VisualTree;
+using System;
+using Avalonia.Logging;
+using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
-    /// <summary>
-    /// A drop-down list control.
-    /// </summary>
-    public class DropDown : SelectingItemsControl
+    [Obsolete("Use ComboBox")]
+    public class DropDown : ComboBox, IStyleable
     {
-        /// <summary>
-        /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
-        /// </summary>
-        private static readonly FuncTemplate<IPanel> DefaultPanel =
-            new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
-
-        /// <summary>
-        /// Defines the <see cref="IsDropDownOpen"/> property.
-        /// </summary>
-        public static readonly DirectProperty<DropDown, bool> IsDropDownOpenProperty =
-            AvaloniaProperty.RegisterDirect<DropDown, bool>(
-                nameof(IsDropDownOpen),
-                o => o.IsDropDownOpen,
-                (o, v) => o.IsDropDownOpen = v);
-
-        /// <summary>
-        /// Defines the <see cref="MaxDropDownHeight"/> property.
-        /// </summary>
-        public static readonly StyledProperty<double> MaxDropDownHeightProperty =
-            AvaloniaProperty.Register<DropDown, double>(nameof(MaxDropDownHeight), 200);
-
-        /// <summary>
-        /// Defines the <see cref="SelectionBoxItem"/> property.
-        /// </summary>
-        public static readonly DirectProperty<DropDown, object> SelectionBoxItemProperty =
-            AvaloniaProperty.RegisterDirect<DropDown, object>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
-
-        /// <summary>
-        /// Defines the <see cref="VirtualizationMode"/> property.
-        /// </summary>
-        public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
-            ItemsPresenter.VirtualizationModeProperty.AddOwner<DropDown>();
-
-        private bool _isDropDownOpen;
-        private Popup _popup;
-        private object _selectionBoxItem;
-        private IDisposable _subscriptionsOnOpen;
-
-        /// <summary>
-        /// Initializes static members of the <see cref="DropDown"/> class.
-        /// </summary>
-        static DropDown()
-        {
-            ItemsPanelProperty.OverrideDefaultValue<DropDown>(DefaultPanel);
-            FocusableProperty.OverrideDefaultValue<DropDown>(true);
-            SelectedItemProperty.Changed.AddClassHandler<DropDown>(x => x.SelectedItemChanged);
-            KeyDownEvent.AddClassHandler<DropDown>(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel);
-        }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the dropdown is currently open.
-        /// </summary>
-        public bool IsDropDownOpen
-        {
-            get { return _isDropDownOpen; }
-            set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
-        }
-
-        /// <summary>
-        /// Gets or sets the maximum height for the dropdown list.
-        /// </summary>
-        public double MaxDropDownHeight
-        {
-            get { return GetValue(MaxDropDownHeightProperty); }
-            set { SetValue(MaxDropDownHeightProperty, value); }
-        }
-
-        /// <summary>
-        /// Gets or sets the item to display as the control's content.
-        /// </summary>
-        protected object SelectionBoxItem
-        {
-            get { return _selectionBoxItem; }
-            set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); }
-        }
-
-        /// <summary>
-        /// Gets or sets the virtualization mode for the items.
-        /// </summary>
-        public ItemVirtualizationMode VirtualizationMode
-        {
-            get { return GetValue(VirtualizationModeProperty); }
-            set { SetValue(VirtualizationModeProperty, value); }
-        }
-
-        /// <inheritdoc/>
-        protected override IItemContainerGenerator CreateItemContainerGenerator()
-        {
-            return new ItemContainerGenerator<DropDownItem>(
-                this,
-                DropDownItem.ContentProperty,
-                DropDownItem.ContentTemplateProperty);
-        }
-
-        /// <inheritdoc/>
-        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-        {
-            base.OnAttachedToLogicalTree(e);
-            this.UpdateSelectionBoxItem(this.SelectedItem);
-        }
-
-        /// <inheritdoc/>
-        protected override void OnKeyDown(KeyEventArgs e)
-        {
-            base.OnKeyDown(e);
-
-            if (e.Handled)
-                return;
-
-            if (e.Key == Key.F4 ||
-                ((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0)))
-            {
-                IsDropDownOpen = !IsDropDownOpen;
-                e.Handled = true;
-            }
-            else if (IsDropDownOpen && e.Key == Key.Escape)
-            {
-                IsDropDownOpen = false;
-                e.Handled = true;
-            }
-            else if (IsDropDownOpen && e.Key == Key.Enter)
-            {
-                SelectFocusedItem();
-                IsDropDownOpen = false;
-                e.Handled = true;
-            }
-            else if (!IsDropDownOpen)
-            {
-                if (e.Key == Key.Down)
-                {
-                    SelectNext();
-                    e.Handled = true;
-                }
-                else if (e.Key == Key.Up)
-                {
-                    SelectPrev();
-                    e.Handled = true;
-                }
-            }
-            else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
-                      (e.Key == Key.Up || e.Key == Key.Down))
-            {
-                var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
-                if (firstChild != null)
-                {
-                    FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional);
-                    e.Handled = true;
-                }
-            }
-        }
-
-        /// <inheritdoc/>
-        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
-        {
-            base.OnPointerWheelChanged(e);
-
-            if (!e.Handled)
-            {
-                if (!IsDropDownOpen)
-                {
-                    if (IsFocused)
-                    {
-                        if (e.Delta.Y < 0)
-                            SelectNext();
-                        else
-                            SelectPrev();
-
-                        e.Handled = true;
-                    }
-                }
-                else
-                {
-                    e.Handled = true;
-                }
-            }
-        }
-
-        /// <inheritdoc/>
-        protected override void OnPointerPressed(PointerPressedEventArgs e)
-        {
-            if (!e.Handled)
-            {
-                if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
-                {
-                    if (UpdateSelectionFromEventSource(e.Source))
-                    {
-                        _popup?.Close();
-                        e.Handled = true;
-                    }
-                }
-                else
-                {
-                    IsDropDownOpen = !IsDropDownOpen;
-                    e.Handled = true;
-                }
-            }
-
-            base.OnPointerPressed(e);
-        }
-
-        /// <inheritdoc/>
-        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
-        {
-            if (_popup != null)
-            {
-                _popup.Opened -= PopupOpened;
-                _popup.Closed -= PopupClosed;
-            }
-
-            _popup = e.NameScope.Get<Popup>("PART_Popup");
-            _popup.Opened += PopupOpened;
-            _popup.Closed += PopupClosed;
-
-            base.OnTemplateApplied(e);
-        }
-
-        internal void ItemFocused(DropDownItem dropDownItem)
+        public DropDown()
         {
-            if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
-            {
-                dropDownItem.BringIntoView();
-            }
+            Logger.Warning(LogArea.Control, this, "DropDown is deprecated: Use ComboBox");
         }
 
-        private void PopupClosed(object sender, EventArgs e)
-        {
-            _subscriptionsOnOpen?.Dispose();
-            _subscriptionsOnOpen = null;
-
-            if (CanFocus(this))
-            {
-                Focus();
-            }
-        }
-
-        private void PopupOpened(object sender, EventArgs e)
-        {
-            TryFocusSelectedItem();
-
-            _subscriptionsOnOpen?.Dispose();
-            _subscriptionsOnOpen = null;
-
-            var toplevel = this.GetVisualRoot() as TopLevel;
-            if (toplevel != null)
-            {
-                _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
-                {
-                    //eat wheel scroll event outside dropdown popup while it's open
-                    if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
-                    {
-                        ev.Handled = true;
-                    }
-                }, Interactivity.RoutingStrategies.Tunnel);
-            }
-        }
-
-        private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
-        {
-            UpdateSelectionBoxItem(e.NewValue);
-            TryFocusSelectedItem();
-        }
-
-        private void TryFocusSelectedItem()
-        {
-            var selectedIndex = SelectedIndex;
-            if (IsDropDownOpen && selectedIndex != -1)
-            {
-                var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
-
-                if (container == null && SelectedItems.Count > 0)
-                {
-                    ScrollIntoView(SelectedItems[0]);
-                    container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
-                }
-
-                if (container != null && CanFocus(container))
-                {
-                    container.Focus();
-                }
-            }
-        }
-
-        private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible;
-
-        private void UpdateSelectionBoxItem(object item)
-        {
-            var contentControl = item as IContentControl;
-
-            if (contentControl != null)
-            {
-                item = contentControl.Content;
-            }
-
-            var control = item as IControl;
-
-            if (control != null)
-            {
-                control.Measure(Size.Infinity);
-
-                SelectionBoxItem = new Rectangle
-                {
-                    Width = control.DesiredSize.Width,
-                    Height = control.DesiredSize.Height,
-                    Fill = new VisualBrush
-                    {
-                        Visual = control,
-                        Stretch = Stretch.None,
-                        AlignmentX = AlignmentX.Left,
-                    }
-                };
-            }
-            else
-            {
-                var selector = MemberSelector;
-                SelectionBoxItem = selector != null ? selector.Select(item) : item;
-            }
-        }
-
-        private void SelectFocusedItem()
-        {
-            foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
-            {
-                if (dropdownItem.ContainerControl.IsFocused)
-                {
-                    SelectedIndex = dropdownItem.Index;
-                    break;
-                }
-            }
-        }
+        Type IStyleable.StyleKey => typeof(ComboBox);
+    }
 
-        private void SelectNext()
+    [Obsolete("Use ComboBoxItem")]
+    public class DropDownItem : ComboBoxItem, IStyleable
+    {
+        public DropDownItem()
         {
-            int next = SelectedIndex + 1;
-
-            if (next >= ItemCount)
-                next = 0;
-
-            SelectedIndex = next;
+            Logger.Warning(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem");
         }
 
-        private void SelectPrev()
-        {
-            int prev = SelectedIndex - 1;
-
-            if (prev < 0)
-                prev = ItemCount - 1;
-
-            SelectedIndex = prev;
-        }
+        Type IStyleable.StyleKey => typeof(ComboBoxItem);
     }
 }

+ 29 - 6
src/Avalonia.Controls/MenuItem.cs

@@ -286,6 +286,26 @@ namespace Avalonia.Controls
             return new MenuItemContainerGenerator(this);
         }
 
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged += CanExecuteChanged;
+            }
+        }
+
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged -= CanExecuteChanged;
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="MenuItem"/> is clicked.
         /// </summary>
@@ -399,14 +419,17 @@ namespace Avalonia.Controls
         {
             if (e.Sender is MenuItem menuItem)
             {
-                if (e.OldValue is ICommand oldCommand)
+                if (((ILogical)menuItem).IsAttachedToLogicalTree)
                 {
-                    oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
-                }
+                    if (e.OldValue is ICommand oldCommand)
+                    {
+                        oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
+                    }
 
-                if (e.NewValue is ICommand newCommand)
-                {
-                    newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                    if (e.NewValue is ICommand newCommand)
+                    {
+                        newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                    }
                 }
 
                 menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);

+ 5 - 3
src/Avalonia.Controls/Platform/IScreenImpl.cs

@@ -1,9 +1,11 @@
-namespace Avalonia.Platform
+using System.Collections.Generic;
+
+namespace Avalonia.Platform
 {
     public interface IScreenImpl
     {
         int ScreenCount { get; }
 
-        Screen[] AllScreens { get; }
+        IReadOnlyList<Screen> AllScreens { get; }
     }
-}
+}

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

@@ -91,12 +91,12 @@ namespace Avalonia.Controls.Primitives
 
                 if (screenX > screen.Bounds.Width)
                 {
-                    Position = Position.WithX(Position.X - screenX - bounds.Width);
+                    Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
                 }
 
                 if (screenY > screen.Bounds.Height)
                 {
-                    Position = Position.WithY(Position.Y - screenY - bounds.Height);
+                    Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
                 }
             }
         }

+ 3 - 2
src/Avalonia.Controls/Screens.cs

@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Platform;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
@@ -10,7 +11,7 @@ namespace Avalonia.Controls
         private readonly IScreenImpl _iScreenImpl;
 
         public int ScreenCount => _iScreenImpl.ScreenCount;
-        public Screen[] All => _iScreenImpl?.AllScreens;
+        public IReadOnlyList<Screen> All => _iScreenImpl?.AllScreens;
         public Screen Primary => All.FirstOrDefault(x => x.Primary);
 
         public Screens(IScreenImpl iScreenImpl)

+ 2 - 2
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -156,7 +156,7 @@ namespace Avalonia.DesignerSupport.Remote
     {
         public int ScreenCount => 1;
 
-        public Screen[] AllScreens { get; } =
-            {new Screen(new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true)};
+        public IReadOnlyList<Screen> AllScreens { get; } =
+            new Screen[] { new Screen(new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
     }
 }

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

@@ -67,8 +67,7 @@ namespace Avalonia.Native
             if (_factory.MacOptions != null)
             {
                 var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
-                if (macOpts != null)
-                    _factory.MacOptions.ShowInDock = macOpts.ShowInDock ? 1 : 0;
+                _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0;
             }
 
             AvaloniaLocator.CurrentMutable

+ 2 - 1
src/Avalonia.Native/ScreenImpl.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
 
@@ -18,7 +19,7 @@ namespace Avalonia.Native
 
         public int ScreenCount => _native.GetScreenCount();
 
-        public Screen[] AllScreens
+        public IReadOnlyList<Screen> AllScreens
         {
             get
             {

+ 2 - 2
src/Avalonia.Themes.Default/DropDown.xaml → src/Avalonia.Themes.Default/ComboBox.xaml

@@ -1,5 +1,5 @@
 <Styles xmlns="https://github.com/avaloniaui">
-  <Style Selector="DropDown">
+  <Style Selector="ComboBox">
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
     <Setter Property="Padding" Value="4"/>
@@ -57,7 +57,7 @@
       </ControlTemplate>
     </Setter>
   </Style>
-  <Style Selector="DropDown:pointerover /template/ Border#border">
+  <Style Selector="ComboBox:pointerover /template/ Border#border">
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
   </Style>
 </Styles>

+ 6 - 6
src/Avalonia.Themes.Default/DropDownItem.xaml → src/Avalonia.Themes.Default/ComboBoxItem.xaml

@@ -1,5 +1,5 @@
 <Styles xmlns="https://github.com/avaloniaui">
-  <Style Selector="DropDownItem">
+  <Style Selector="ComboBoxItem">
     <Setter Property="Background" Value="Transparent"/>
     <Setter Property="Padding" Value="2"/>
     <Setter Property="HorizontalAlignment" Value="Stretch"/>
@@ -19,23 +19,23 @@
     </Setter>
   </Style>
     
-  <Style Selector="DropDownItem:pointerover /template/ ContentPresenter">
+  <Style Selector="ComboBoxItem:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
   </Style>
     
-  <Style Selector="DropDownItem:selected /template/ ContentPresenter">
+  <Style Selector="ComboBoxItem:selected /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
   </Style>
     
-  <Style Selector="DropDownItem:selected:focus /template/ ContentPresenter">
+  <Style Selector="ComboBoxItem:selected:focus /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/>
   </Style>
     
-  <Style Selector="DropDownItem:selected:pointerover /template/ ContentPresenter">
+  <Style Selector="ComboBoxItem:selected:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/>
   </Style>
 
-  <Style Selector="DropDownItem:selected:focus:pointerover /template/ ContentPresenter">
+  <Style Selector="ComboBoxItem:selected:focus:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
   </Style>
 </Styles>

+ 2 - 2
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -7,9 +7,9 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.Button.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.Carousel.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.CheckBox.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.ComboBox.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.ComboBoxItem.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ContentControl.xaml?assembly=Avalonia.Themes.Default"/>
-  <StyleInclude Source="resm:Avalonia.Themes.Default.DropDown.xaml?assembly=Avalonia.Themes.Default"/>
-  <StyleInclude Source="resm:Avalonia.Themes.Default.DropDownItem.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.GridSplitter.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ItemsControl.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ListBox.xaml?assembly=Avalonia.Themes.Default"/>

+ 12 - 0
src/Avalonia.X11/NativeDialogs/Gtk.cs

@@ -190,6 +190,18 @@ namespace Avalonia.X11.NativeDialogs
         [DllImport(GtkName)]
         public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file);
 
+        [DllImport(GtkName)]
+        public static extern IntPtr gtk_file_filter_new();
+        
+        [DllImport(GtkName)]
+        public static extern IntPtr gtk_file_filter_set_name(IntPtr filter, Utf8Buffer name);
+        
+        [DllImport(GtkName)]
+        public static extern IntPtr gtk_file_filter_add_pattern(IntPtr filter, Utf8Buffer pattern);
+        
+        [DllImport(GtkName)]
+        public static extern IntPtr gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter);
+        
         [DllImport(GtkName)]
         public static extern void gtk_widget_realize(IntPtr gtkWidget);
 

+ 21 - 4
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs
     {
         private Task<bool> _initialized;
         private unsafe  Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
-            bool multiSelect, string initialFileName)
+            bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> filters)
         {
             IntPtr dlg;
             using (var name = new Utf8Buffer(title))
@@ -35,6 +35,20 @@ namespace Avalonia.X11.NativeDialogs
                 foreach (var d in disposables) d.Dispose();
                 disposables.Clear();
             }
+            
+            if(filters != null)
+                foreach (var f in filters)
+                {
+                    var filter = gtk_file_filter_new();
+                    using (var b = new Utf8Buffer(f.Name))
+                        gtk_file_filter_set_name(filter, b);
+                    
+                    foreach (var e in f.Extensions)
+                        using (var b = new Utf8Buffer("*." + e))
+                            gtk_file_filter_add_pattern(filter, b);
+
+                    gtk_file_chooser_add_filter(dlg, filter);
+                }
 
             disposables = new List<IDisposable>
             {
@@ -68,7 +82,10 @@ namespace Avalonia.X11.NativeDialogs
                     return false;
                 })
             };
-            using (var open = new Utf8Buffer("Open"))
+            using (var open = new Utf8Buffer(
+                action == GtkFileChooserAction.Save ? "Save"
+                : action == GtkFileChooserAction.SelectFolder ? "Select"
+                : "Open"))
                 gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
             using (var open = new Utf8Buffer("Cancel"))
                 gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
@@ -87,7 +104,7 @@ namespace Avalonia.X11.NativeDialogs
                     dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
                     (dialog as OpenFileDialog)?.AllowMultiple ?? false,
                     Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
-                        string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName)));
+                        string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters));
         }
 
         public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
@@ -96,7 +113,7 @@ namespace Avalonia.X11.NativeDialogs
             return await await RunOnGlibThread(async () =>
             {
                 var res = await ShowDialog(dialog.Title, parent,
-                    GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory);
+                    GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null);
                 return res?.FirstOrDefault();
             });
         }

+ 5 - 0
src/Avalonia.X11/X11Platform.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Reflection;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
@@ -24,6 +25,7 @@ namespace Avalonia.X11
         public X11Info Info { get; private set; }
         public IX11Screens X11Screens { get; private set; }
         public IScreenImpl Screens { get; private set; }
+        public X11PlatformOptions Options { get; private set; }
         public void Initialize(X11PlatformOptions options)
         {
             XInitThreads();
@@ -63,6 +65,8 @@ namespace Avalonia.X11
                 else
                     GlxGlPlatformFeature.TryInitialize(Info);
             }
+
+            Options = options;
         }
 
         public IntPtr DeferredDisplay { get; set; }
@@ -91,6 +95,7 @@ namespace Avalonia
     {
         public bool UseEGL { get; set; }
         public bool UseGpu { get; set; } = true;
+        public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
     }
     public static class AvaloniaX11PlatformExtensions
     {

+ 1 - 1
src/Avalonia.X11/X11Screens.cs

@@ -156,7 +156,7 @@ namespace Avalonia.X11
 
         public int ScreenCount => _impl.Screens.Length;
 
-        public Screen[] AllScreens =>
+        public IReadOnlyList<Screen> AllScreens =>
             _impl.Screens.Select(s => new Screen(s.Bounds, s.WorkingArea, s.Primary)).ToArray();
     }
 

+ 12 - 0
src/Avalonia.X11/X11Window.cs

@@ -128,6 +128,8 @@ namespace Avalonia.X11
             XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM,
                 32, PropertyMode.Replace, new[] {_x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL}, 1);
 
+            if (platform.Options.WmClass != null)
+                SetWmClass(platform.Options.WmClass);
 
             var surfaces = new List<object>
             {
@@ -873,6 +875,16 @@ namespace Avalonia.X11
             }
         }
 
+        public void SetWmClass(string wmClass)
+        {
+            var data = Encoding.ASCII.GetBytes(wmClass);
+            fixed (void* pdata = data)
+            {
+                XChangeProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_CLASS, _x11.Atoms.XA_STRING, 8,
+                    PropertyMode.Replace, pdata, data.Length);
+            }
+        }
+
         public void SetMinMaxSize(Size minSize, Size maxSize)
         {
             _scaledMinMaxSize = (minSize, maxSize);

+ 3 - 2
src/Gtk/Avalonia.Gtk3/ScreenImpl.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Gtk3.Interop;
 using Avalonia.Platform;
 
@@ -8,11 +9,11 @@ namespace Avalonia.Gtk3
     {
         public int ScreenCount
         {
-            get => AllScreens.Length;
+            get => _allScreens.Length;
         }
         
         private Screen[] _allScreens;
-        public Screen[] AllScreens
+        public IReadOnlyList<Screen> AllScreens
         {
             get
             {

+ 2 - 1
src/Windows/Avalonia.Win32/ScreenImpl.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
 using Avalonia.Platform;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
 
@@ -15,7 +16,7 @@ namespace Avalonia.Win32
         }
 
         private Screen[] _allScreens;
-        public  Screen[] AllScreens
+        public IReadOnlyList<Screen> AllScreens
         {
             get
             {

+ 44 - 2
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -5,6 +5,7 @@ using Avalonia.Input;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Moq;
 using Xunit;
@@ -21,6 +22,7 @@ namespace Avalonia.Controls.UnitTests
             {
                 Command = command,
             };
+            var root = new TestRoot { Child = target };
 
             Assert.False(target.IsEnabled);
             command.IsEnabled = true;
@@ -215,6 +217,39 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(clicked);
         }
 
+        [Fact]
+        public void Button_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button
+            {
+                Command = command,
+            };
+
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void Button_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button { Command = command };
+            var root = new TestRoot { Child = target };
+
+            Assert.Equal(1, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void Button_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button { Command = command };
+            var root = new TestRoot { Child = target };
+
+            root.Child = null;
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
         private class TestButton : Button, IRenderRoot
         {
             public TestButton()
@@ -298,6 +333,7 @@ namespace Avalonia.Controls.UnitTests
 
         private class TestCommand : ICommand
         {
+            private EventHandler _canExecuteChanged;
             private bool _enabled;
 
             public TestCommand(bool enabled)
@@ -313,12 +349,18 @@ namespace Avalonia.Controls.UnitTests
                     if (_enabled != value)
                     {
                         _enabled = value;
-                        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+                        _canExecuteChanged?.Invoke(this, EventArgs.Empty);
                     }
                 }
             }
 
-            public event EventHandler CanExecuteChanged;
+            public int SubscriptionCount { get; private set; }
+
+            public event EventHandler CanExecuteChanged
+            {
+                add { _canExecuteChanged += value; ++SubscriptionCount; }
+                remove { _canExecuteChanged -= value; --SubscriptionCount; }
+            }
 
             public bool CanExecute(object parameter) => _enabled;
 

+ 9 - 9
tests/Avalonia.Controls.UnitTests/DropDownTests.cs → tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -13,12 +13,12 @@ using Xunit;
 
 namespace Avalonia.Controls.UnitTests
 {
-    public class DropDownTests
+    public class ComboBoxTests
     {
         [Fact]
         public void Clicking_On_Control_Toggles_IsDropDownOpen()
         {
-            var target = new DropDown
+            var target = new ComboBox
             {
                 Items = new[] { "Foo", "Bar" },
             };
@@ -42,13 +42,13 @@ namespace Avalonia.Controls.UnitTests
         public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control()
         {
             var items = new[] { new Canvas() };
-            var target = new DropDown
+            var target = new ComboBox
             {
                 Items = items,
                 SelectedIndex = 0,
             };
 
-            var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle;
+            var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
             Assert.NotNull(rectangle);
 
             var brush = rectangle.Fill as VisualBrush;
@@ -59,7 +59,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void SelectionBoxItem_Rectangle_Is_Removed_From_Logical_Tree()
         {
-            var target = new DropDown
+            var target = new ComboBox
             {
                 Items = new[] { new Canvas() },
                 SelectedIndex = 0,
@@ -70,7 +70,7 @@ namespace Avalonia.Controls.UnitTests
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle;
+            var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
             Assert.True(((ILogical)target).IsAttachedToLogicalTree);
             Assert.True(((ILogical)rectangle).IsAttachedToLogicalTree);
 
@@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests
 
         private FuncControlTemplate GetTemplate()
         {
-            return new FuncControlTemplate<DropDown>(parent =>
+            return new FuncControlTemplate<ComboBox>(parent =>
             {
                 return new Panel
                 {
@@ -93,7 +93,7 @@ namespace Avalonia.Controls.UnitTests
                     {
                         new ContentControl
                         {
-                            [!ContentControl.ContentProperty] = parent[!DropDown.SelectionBoxItemProperty],
+                            [!ContentControl.ContentProperty] = parent[!ComboBox.SelectionBoxItemProperty],
                         },
                         new ToggleButton
                         {
@@ -105,7 +105,7 @@ namespace Avalonia.Controls.UnitTests
                             Child = new ItemsPresenter
                             {
                                 Name = "PART_ItemsPresenter",
-                                [!ItemsPresenter.ItemsProperty] = parent[!DropDown.ItemsProperty],
+                                [!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty],
                             }
                         }
                     }

+ 54 - 0
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
+using System.Windows.Input;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -22,5 +24,57 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.False(target.Focusable);
         }
+
+        [Fact]
+        public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem
+            {
+                Command = command,
+            };
+
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void MenuItem_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem { Command = command };
+            var root = new TestRoot { Child = target };
+
+            Assert.Equal(1, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void MenuItem_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem { Command = command };
+            var root = new TestRoot { Child = target };
+
+            root.Child = null;
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        private class TestCommand : ICommand
+        {
+            private EventHandler _canExecuteChanged;
+
+            public int SubscriptionCount { get; private set; }
+
+            public event EventHandler CanExecuteChanged
+            {
+                add { _canExecuteChanged += value; ++SubscriptionCount; }
+                remove { _canExecuteChanged -= value; --SubscriptionCount; }
+            }
+
+            public bool CanExecute(object parameter) => true;
+
+            public void Execute(object parameter)
+            {
+            }
+        }
     }
 }