Browse Source

Merge remote-tracking branch 'upstream/master' into skia-renderinterface2

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

26
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

6
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)
{

8
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
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
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" />

11
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="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>

2
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
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
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
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
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;
}
}
}

18
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>

8
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
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>

10
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
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
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
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>

26
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

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>

12
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>

2
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);

12
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>

10
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>

4
samples/interop/Direct3DInteropSample/Program.cs

@ -11,7 +11,9 @@ namespace Direct3DInteropSample
{
static void Main(string[] args)
{
AppBuilder.Configure<App>().UseWin32(deferredRendering: false).UseDirect2D1().Start<MainWindow>();
AppBuilder.Configure<App>()
.With(new Win32PlatformOptions {UseDeferredRendering = false})
.UseWin32().UseDirect2D1().Start<MainWindow>();
}
}
}

20
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
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
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

File diff suppressed because it is too large

1366
src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs

File diff suppressed because it is too large

259
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
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
src/Avalonia.Controls.DataGrid/DataGrid.cs

File diff suppressed because it is too large

145
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
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
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
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
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
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
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

File diff suppressed because it is too large

586
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
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
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

File diff suppressed because it is too large

696
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
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
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
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
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
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
src/Avalonia.Controls.DataGrid/DataGridRow.cs

File diff suppressed because it is too large

449
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
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
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
src/Avalonia.Controls.DataGrid/DataGridRows.cs

File diff suppressed because it is too large

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

21
src/Avalonia.Controls/AppBuilderBase.cs

@ -15,6 +15,7 @@ namespace Avalonia.Controls
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
private static bool s_setupWasAlreadyCalled;
private Action _optionsInitializers;
/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
@ -249,6 +250,24 @@ namespace Avalonia.Controls
Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
}
/// <summary>
/// Configures platform-specific options
/// </summary>
public TAppBuilder With<T>(T options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>
public TAppBuilder With<T>(Func<T> options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self;
}
/// <summary>
/// Sets up the platform-speciic services for the <see cref="Application"/>.
/// </summary>
@ -280,7 +299,7 @@ namespace Avalonia.Controls
}
s_setupWasAlreadyCalled = true;
_optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();

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>

69
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);

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

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

4
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>

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

35
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);

8
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; }
}
}
}

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

5
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)

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

64
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -16,6 +16,7 @@ namespace Avalonia.Native
class AvaloniaNativePlatform : IPlatformSettings, IWindowingPlatform
{
private readonly IAvaloniaNativeFactory _factory;
private AvaloniaNativePlatformOptions _options;
[DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative();
@ -27,29 +28,31 @@ namespace Avalonia.Native
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO
public static void Initialize(IntPtr factory, Action<AvaloniaNativeOptions> configure)
public static void Initialize(IntPtr factory, AvaloniaNativePlatformOptions options)
{
new AvaloniaNativePlatform(new IAvaloniaNativeFactory(factory))
.DoInitialize(configure);
.DoInitialize(options);
}
delegate IntPtr CreateAvaloniaNativeDelegate();
public static void Initialize(string library, Action<AvaloniaNativeOptions> configure)
public static void Initialize(AvaloniaNativePlatformOptions options)
{
var loader = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? (IDynLoader)new Win32Loader() : new UnixLoader();
var lib = loader.LoadLibrary(library);
var proc = loader.GetProcAddress(lib, "CreateAvaloniaNative", false);
var d = Marshal.GetDelegateForFunctionPointer<CreateAvaloniaNativeDelegate>(proc);
if (options.AvaloniaNativeLibraryPath != null)
{
var loader = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
(IDynLoader)new Win32Loader() :
new UnixLoader();
var lib = loader.LoadLibrary(options.AvaloniaNativeLibraryPath);
var proc = loader.GetProcAddress(lib, "CreateAvaloniaNative", false);
var d = Marshal.GetDelegateForFunctionPointer<CreateAvaloniaNativeDelegate>(proc);
Initialize(d(), configure);
}
public static void Initialize(Action<AvaloniaNativeOptions> configure)
{
Initialize(CreateAvaloniaNative(), configure);
Initialize(d(), options);
}
else
Initialize(CreateAvaloniaNative(), options);
}
private AvaloniaNativePlatform(IAvaloniaNativeFactory factory)
@ -57,14 +60,19 @@ namespace Avalonia.Native
_factory = factory;
}
void DoInitialize(Action<AvaloniaNativeOptions> configure)
void DoInitialize(AvaloniaNativePlatformOptions options)
{
var opts = new AvaloniaNativeOptions(_factory);
configure?.Invoke(opts);
_options = options;
_factory.Initialize();
if (_factory.MacOptions != null)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
_factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0;
}
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
.Bind<IPlatformThreadingInterface>()
.ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
@ -76,13 +84,13 @@ namespace Avalonia.Native
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<IWindowingPlatformGlFeature>().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows))
.Bind<AvaloniaNativeOptions>().ToConstant(opts);
.Bind<PlatformHotkeyConfiguration>()
.ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows));
}
public IWindowImpl CreateWindow()
{
return new WindowImpl(_factory);
return new WindowImpl(_factory, _options);
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
@ -92,7 +100,7 @@ namespace Avalonia.Native
public IPopupImpl CreatePopup()
{
return new PopupImpl(_factory);
return new PopupImpl(_factory, _options);
}
}
@ -116,18 +124,4 @@ namespace Avalonia.Native
}
}
}
public class AvaloniaNativeOptions
{
public AvaloniaNativeMacOptions MacOptions { get; set; }
public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true;
internal AvaloniaNativeOptions(IAvaloniaNativeFactory factory)
{
var mac = factory.GetMacOptions();
if (mac != null)
MacOptions = new AvaloniaNativeMacOptions(mac);
}
}
}

32
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -9,21 +9,27 @@ namespace Avalonia
{
public static class AvaloniaNativePlatformExtensions
{
public static T UseAvaloniaNative<T>(this T builder,
string libraryPath = null,
Action<AvaloniaNativeOptions> configure = null)
where T : AppBuilderBase<T>, new()
public static T UseAvaloniaNative<T>(this T builder)
where T : AppBuilderBase<T>, new()
{
if (libraryPath == null)
{
builder.UseWindowingSubsystem(() => AvaloniaNativePlatform.Initialize(configure));
}
else
{
builder.UseWindowingSubsystem(() => AvaloniaNativePlatform.Initialize(libraryPath, configure));
}
builder.UseWindowingSubsystem(() =>
AvaloniaNativePlatform.Initialize(
AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
new AvaloniaNativePlatformOptions()));
return builder;
}
}
public class AvaloniaNativePlatformOptions
{
public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true;
public string AvaloniaNativeLibraryPath { get; set; }
}
// ReSharper disable once InconsistentNaming
public class MacOSPlatformOptions
{
public bool ShowInDock { get; set; } = true;
}
}

2
src/Avalonia.Native/PopupImpl.cs

@ -9,7 +9,7 @@ namespace Avalonia.Native
{
public class PopupImpl : WindowBaseImpl, IPopupImpl
{
public PopupImpl(IAvaloniaNativeFactory factory)
public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts)
{
using (var e = new PopupEvents(this))
{

3
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
src/Avalonia.Native/WindowImpl.cs

@ -12,7 +12,7 @@ namespace Avalonia.Native
public class WindowImpl : WindowBaseImpl, IWindowImpl
{
IAvnWindow _native;
public WindowImpl(IAvaloniaNativeFactory factory)
public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts)
{
using (var e = new WindowEvents(this))
{

4
src/Avalonia.Native/WindowImplBase.cs

@ -31,10 +31,8 @@ namespace Avalonia.Native
private double _savedScaling;
private GlPlatformSurface _glSurface;
public WindowBaseImpl()
public WindowBaseImpl(AvaloniaNativePlatformOptions opts)
{
var opts = AvaloniaLocator.Current.GetService<AvaloniaNativeOptions>();
_gpu = opts.UseGpu;
_deferredRendering = opts.UseDeferredRendering;

4
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>

12
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>

4
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"/>

2
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@ -27,7 +27,7 @@ namespace Avalonia.Rendering.SceneGraph
Point origin,
IFormattedTextImpl text,
IDictionary<IVisual, Scene> childScenes = null)
: base(text.Bounds, transform, null)
: base(text.Bounds.Translate(origin), transform, null)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

12
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);

25
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();
});
}

11
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,12 +95,15 @@ 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
{
public static T UseX11<T>(this T builder, X11PlatformOptions options = null) where T : AppBuilderBase<T>, new()
public static T UseX11<T>(this T builder) where T : AppBuilderBase<T>, new()
{
builder.UseWindowingSubsystem(() => new AvaloniaX11Platform().Initialize(options ?? new X11PlatformOptions()));
builder.UseWindowingSubsystem(() =>
new AvaloniaX11Platform().Initialize(AvaloniaLocator.Current.GetService<X11PlatformOptions>() ??
new X11PlatformOptions()));
return builder;
}

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

Loading…
Cancel
Save