diff --git a/.gitmodules b/.gitmodules index 2d11fdfa9e..79694800b2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git +[submodule "src/Avalonia.Controls.DataGrid"] + path = src/Avalonia.Controls.DataGrid + url = https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid.git diff --git a/Avalonia.sln b/Avalonia.sln index cd7e56e9d4..9d60fe2e45 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -115,7 +115,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SkiaSharp.props = build\SkiaSharp.props build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props - build\System.Memory.props = build\System.Memory.props build\TargetFrameworks.props = build\TargetFrameworks.props build\TrimmingEnable.props = build\TrimmingEnable.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props @@ -171,14 +170,10 @@ 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" @@ -302,6 +297,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}" EndProject Global @@ -498,10 +494,6 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|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}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.Build.0 = Release|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -510,10 +502,6 @@ Global {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -755,7 +743,6 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} - {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 6b0f1c59cc..08c27e16f2 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -61,6 +61,12 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.Platform.IClipboard.FlushAsync + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) diff --git a/build/Base.props b/build/Base.props index 6b7ae5d677..0e667f105b 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,12 @@ + + + + + + diff --git a/build/System.Memory.props b/build/System.Memory.props deleted file mode 100644 index 35a87a3a2f..0000000000 --- a/build/System.Memory.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj index 2e959798d0..303daa451f 100644 --- a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj +++ b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj @@ -9,7 +9,6 @@ - diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index faeb643d8a..33c6aa1440 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -2,6 +2,7 @@ Exe $(AvsCurrentTargetFramework) + diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index ed23f68d91..9d68c8da8a 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -3,6 +3,7 @@ x:Class="BindingDemo.MainWindow" xmlns:vm="using:BindingDemo.ViewModels" xmlns:local="using:BindingDemo" + xmlns:system="clr-namespace:System;assembly=System.Runtime" Title="AvaloniaUI Bindings Test" Width="800" Height="600" @@ -29,7 +30,7 @@ - + Shuffle @@ -51,9 +52,9 @@ + Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/> + Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/> @@ -67,8 +68,8 @@ - - + + @@ -81,7 +82,7 @@ - + diff --git a/samples/BindingDemo/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs index 57466fe581..36d4feb108 100644 --- a/samples/BindingDemo/MainWindow.xaml.cs +++ b/samples/BindingDemo/MainWindow.xaml.cs @@ -9,7 +9,7 @@ namespace BindingDemo { public MainWindow() { - Resources["SharedItem"] = new MainWindowViewModel.TestItem() { StringValue = "shared" }; + Resources["SharedItem"] = new MainWindowViewModel.TestItem() { Value = "shared" }; this.InitializeComponent(); this.DataContext = new MainWindowViewModel(); this.AttachDevTools(); diff --git a/samples/BindingDemo/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml index 6eb59ffc83..fcf3a80ceb 100644 --- a/samples/BindingDemo/TestItemView.xaml +++ b/samples/BindingDemo/TestItemView.xaml @@ -2,9 +2,9 @@ xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:viewModels="using:BindingDemo.ViewModels" x:Class="BindingDemo.TestItemView" - x:DataType="viewModels:MainWindowViewModel+TestItem"> + x:DataType="{x:Type viewModels:MainWindowViewModel+TestItem, x:TypeArguments=x:String}"> - + diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index f5f4f3531f..eb1a007695 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -23,14 +23,14 @@ namespace BindingDemo.ViewModels public MainWindowViewModel() { - Items = new ObservableCollection( - Enumerable.Range(0, 20).Select(x => new TestItem + Items = new ObservableCollection>( + Enumerable.Range(0, 20).Select(x => new TestItem { - StringValue = "Item " + x, + Value = "Item " + x, Detail = "Item " + x + " details", })); - Selection = new SelectionModel { SingleSelect = false }; + Selection = new SelectionModel> { SingleSelect = false }; ShuffleItems = MiniCommand.Create(() => { @@ -58,8 +58,8 @@ namespace BindingDemo.ViewModels .Select(x => DateTimeOffset.Now); } - public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public ObservableCollection> Items { get; } + public SelectionModel> Selection { get; } public MiniCommand ShuffleItems { get; } public string BooleanString @@ -117,15 +117,15 @@ namespace BindingDemo.ViewModels } // Nested class, jsut so we can test it in XAML - public class TestItem : ViewModelBase + public class TestItem : ViewModelBase { - private string _stringValue = "String Value"; + private T _value; private string _detail; - public string StringValue + public T Value { - get { return _stringValue; } - set { this.RaiseAndSetIfChanged(ref this._stringValue, value); } + get { return _value; } + set { this.RaiseAndSetIfChanged(ref this._value, value); } } public string Detail diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 157009a5e3..022118d3ab 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -28,11 +28,8 @@ - - + - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 2b55c5bad8..a29a23ff61 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -18,7 +18,6 @@ namespace ControlCatalog private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; private IStyle? _colorPickerFluent, _colorPickerSimple; - private IStyle? _dataGridFluent, _dataGridSimple; public App() { @@ -35,8 +34,6 @@ namespace ControlCatalog _simpleTheme = (SimpleTheme)Resources["SimpleTheme"]!; _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; - _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; - _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; SetCatalogThemes(CatalogTheme.Fluent); } @@ -83,13 +80,11 @@ namespace ControlCatalog { app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; } else if (theme == CatalogTheme.Simple) { app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; } if (shouldReopenWindow) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index edcbcbe988..8ef96169df 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -26,7 +26,6 @@ - diff --git a/samples/ControlCatalog/Models/Countries.cs b/samples/ControlCatalog/Models/Countries.cs deleted file mode 100644 index 20b6a85137..0000000000 --- a/samples/ControlCatalog/Models/Countries.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; - -namespace ControlCatalog.Models -{ - public static class Countries - { - static IEnumerable 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? _all; - - public static IReadOnlyList All - { - get - { - if(_all == null) - { - _all = GetCountries().ToList().AsReadOnly(); - } - - return _all; - } - - } - } -} diff --git a/samples/ControlCatalog/Models/Country.cs b/samples/ControlCatalog/Models/Country.cs deleted file mode 100644 index 072383a31c..0000000000 --- a/samples/ControlCatalog/Models/Country.cs +++ /dev/null @@ -1,41 +0,0 @@ -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; - } - } -} diff --git a/samples/ControlCatalog/Models/GDPValueConverter.cs b/samples/ControlCatalog/Models/GDPValueConverter.cs deleted file mode 100644 index cad1574e7a..0000000000 --- a/samples/ControlCatalog/Models/GDPValueConverter.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 new SolidColorBrush(Colors.Orange, 0.6); - else if (gdp <= 10000) - return new SolidColorBrush(Colors.Yellow, 0.6); - else - return new SolidColorBrush(Colors.LightGreen, 0.6); - } - - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } -} diff --git a/samples/ControlCatalog/Models/GDPdLengthConverter.cs b/samples/ControlCatalog/Models/GDPdLengthConverter.cs deleted file mode 100644 index 034e664305..0000000000 --- a/samples/ControlCatalog/Models/GDPdLengthConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace ControlCatalog.Models; - -internal class GDPdLengthConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double d) - { - return new Avalonia.Controls.DataGridLength(d,Avalonia.Controls.DataGridLengthUnitType.Pixel,d,d); - } - else if (value is decimal d2) - { - var dv =System.Convert.ToDouble(d2); - return new Avalonia.Controls.DataGridLength(dv, Avalonia.Controls.DataGridLengthUnitType.Pixel, dv, dv); - } - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Avalonia.Controls.DataGridLength width) - { - return System.Convert.ToDecimal(width.DisplayValue); - } - return value; - } -} diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index a968b50666..3187e13f9f 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -1,141 +1,11 @@ - + x:Class="ControlCatalog.Pages.DataGridPage"> + + + + + - - - - - - - - - - - - - - - A control for displaying and interacting with a data source. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 149b36fc3f..a4fe9619fb 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -1,101 +1,17 @@ -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; +using System.Diagnostics; using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using ControlCatalog.Models; -using Avalonia.Collections; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Threading; +using Avalonia.Interactivity; -namespace ControlCatalog.Pages +namespace ControlCatalog.Pages; + +public class DataGridPage : UserControl { - public class DataGridPage : UserControl + private void OnLinkClicked(object? sender, RoutedEventArgs e) { - public DataGridPage() - { - this.InitializeComponent(); - - var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer()); - var collectionView1 = new DataGridCollectionView(Countries.All); - collectionView1.SortDescriptions.Add(dataGridSortDescription); - var dg1 = this.Get("dataGrid1"); - dg1.IsReadOnly = true; - dg1.Sorting += (s, a) => - { - var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding; - - if (binding?.Path is string property - && property == dataGridSortDescription.PropertyPath - && !collectionView1.SortDescriptions.Contains(dataGridSortDescription)) - { - collectionView1.SortDescriptions.Add(dataGridSortDescription); - } - }; - dg1.ItemsSource = collectionView1; - - var dg2 = this.Get("dataGridGrouping"); - dg2.IsReadOnly = true; - - var collectionView2 = new DataGridCollectionView(Countries.All); - collectionView2.GroupDescriptions.Add(new DataGridPathGroupDescription("Region")); - - dg2.ItemsSource = collectionView2; - - var dg3 = this.Get("dataGridEdit"); - dg3.IsReadOnly = false; - - var list = new ObservableCollection - { - new Person { FirstName = "John", LastName = "Doe" , Age = 30}, - new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 }, - new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 } - }; - DataGrid3Source = list; - - var addButton = this.Get("btnAdd"); - addButton.Click += (a, b) => list.Add(new Person()); - - DataContext = this; - } - - public IEnumerable DataGrid3Source { get; } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private class ReversedStringComparer : IComparer, IComparer - { - public int Compare(object? x, object? y) - { - if (x is string left && y is string right) - { - var reversedLeft = new string(left.Reverse().ToArray()); - var reversedRight = new string(right.Reverse().ToArray()); - return reversedLeft.CompareTo(reversedRight); - } - - return Comparer.Default.Compare(x, y); - } - } - - private void NumericUpDown_OnTemplateApplied(object sender, TemplateAppliedEventArgs e) + Process.Start(new ProcessStartInfo { - // We want to focus the TextBox of the NumericUpDown. To do so we search for this control when the template - // is applied, but we postpone the action until the control is actually loaded. - if (e.NameScope.Find("PART_TextBox") is {} textBox) - { - Dispatcher.UIThread.InvokeAsync(() => - { - textBox.Focus(); - textBox.SelectAll(); - }, DispatcherPriority.Loaded); - } - } + FileName = "https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid", + UseShellExecute = true + }); } } diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs index be5d920b89..b076f5b489 100644 --- a/samples/GpuInterop/DrawingSurfaceDemoBase.cs +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -33,7 +33,11 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_initialized) + { + Surface?.Dispose(); FreeGraphicsResources(); + } + _initialized = false; base.OnDetachedFromLogicalTree(e); } diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index 5fbef07abc..d8fd135ebd 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -14,7 +14,6 @@ - diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index d1a654e1ba..571149950f 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -11,7 +11,6 @@ - diff --git a/samples/SingleProjectSandbox/SingleProjectSandbox.csproj b/samples/SingleProjectSandbox/SingleProjectSandbox.csproj index dd3b9f170b..bbf83d66fa 100644 --- a/samples/SingleProjectSandbox/SingleProjectSandbox.csproj +++ b/samples/SingleProjectSandbox/SingleProjectSandbox.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 8be8cd45e0..7012ad1878 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -55,7 +55,11 @@ namespace Avalonia.Android.Platform public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); - + public Task TryGetInProcessDataObjectAsync() => Task.FromResult(null); + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index c782bff011..3e89984a38 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 748882a450..ded0a815a5 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -40,7 +40,8 @@ namespace Avalonia.Controls set { Inner[key] = value; - Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } @@ -150,6 +151,19 @@ namespace Avalonia.Controls public void AddNotSharedDeferred(object key, IDeferredContent deferredContent) => Add(key, new NotSharedDeferredItem(deferredContent)); + public void SetItems(IEnumerable> values) + { + try + { + foreach (var value in values) + Inner[value.Key] = value.Value; + } + finally + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + public void Clear() { if (_inner?.Count > 0) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index c307b709fe..f1a4a90864 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.Reactive; using Avalonia.Styling; @@ -75,12 +76,17 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceHost? current = control; + using var activity = Diagnostic.FindingResource()? + .AddTag(Diagnostic.Tags.Key, key) + .AddTag(Diagnostic.Tags.ThemeVariant, theme); + + var current = control; while (current != null) { if (current.TryGetResource(key, theme, out value)) { + activity?.AddTag(Diagnostic.Tags.Result, true); return true; } diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs new file mode 100644 index 0000000000..b6b2cad5da --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +// ReSharper disable ExplicitCallerInfoArgument + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + private static ActivitySource? s_activitySource; + + public static void InitActivitySource() + { + s_activitySource = new("Avalonia.Diagnostic.Source"); + } + + private static Activity? StartActivity(string name) => s_activitySource?.StartActivity(name); + + public static Activity? AttachingStyle() => StartActivity("Avalonia.AttachingStyle"); + public static Activity? FindingResource() => StartActivity("Avalonia.FindingResource"); + public static Activity? EvaluatingStyle() => StartActivity("Avalonia.EvaluatingStyle"); + public static Activity? MeasuringLayoutable() => StartActivity("Avalonia.MeasuringLayoutable"); + public static Activity? ArrangingLayoutable() => StartActivity("Avalonia.ArrangingLayoutable"); + public static Activity? PerformingHitTest() => StartActivity("Avalonia.PerformingHitTest"); + public static Activity? RaisingRoutedEvent() => StartActivity("Avalonia.RaisingRoutedEvent"); +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs new file mode 100644 index 0000000000..e89aef4bf0 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs @@ -0,0 +1,51 @@ +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + public static class Meters + { + public const string SecondsUnit = "s"; + public const string MillisecondsUnit = "ms"; + + public const string CompositorRenderPassName = "avalonia.comp.render.time"; + public const string CompositorRenderPassDescription = "Duration of the compositor render pass on render thread"; + public const string CompositorUpdatePassName = "avalonia.comp.update.time"; + public const string CompositorUpdatePassDescription = "Duration of the compositor update pass on render thread"; + + public const string LayoutMeasurePassName = "avalonia.ui.measure.time"; + public const string LayoutMeasurePassDescription = "Duration of layout measurement pass on UI thread"; + public const string LayoutArrangePassName = "avalonia.ui.arrange.time"; + public const string LayoutArrangePassDescription = "Duration of layout arrangement pass on UI thread"; + public const string LayoutRenderPassName = "avalonia.ui.render.time"; + public const string LayoutRenderPassDescription = "Duration of render recording pass on UI thread"; + public const string LayoutInputPassName = "avalonia.ui.input.time"; + public const string LayoutInputPassDescription = "Duration of input processing on UI thread"; + + public const string TotalEventHandleCountName = "avalonia.ui.event.handler.count"; + public const string TotalEventHandleCountDescription = "Number of event handlers currently registered in the application"; + public const string TotalEventHandleCountUnit = "{handler}"; + public const string TotalVisualCountName = "avalonia.ui.visual.count"; + public const string TotalVisualCountDescription = "Number of visual elements currently present in the visual tree"; + public const string TotalVisualCountUnit = "{visual}"; + public const string TotalDispatcherTimerCountName = "avalonia.ui.dispatcher.timer.count"; + public const string TotalDispatcherTimerCountDescription = "Number of active dispatcher timers in the application"; + public const string TotalDispatcherTimerCountUnit = "{timer}"; + } + + public static class Tags + { + public const string Style = nameof(Style); + public const string SelectorResult = nameof(SelectorResult); + + public const string Key = nameof(Key); + public const string ThemeVariant = nameof(ThemeVariant); + public const string Result = nameof(Result); + + public const string Activator = nameof(Activator); + public const string IsActive = nameof(IsActive); + public const string Selector = nameof(Selector); + public const string Control = nameof(Control); + + public const string RoutedEvent = nameof(RoutedEvent); + } +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs new file mode 100644 index 0000000000..5881d39131 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs @@ -0,0 +1,94 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + private static Histogram? s_compositorRender; + private static Histogram? s_compositorUpdate; + private static Histogram? s_layoutMeasure; + private static Histogram? s_layoutArrange; + private static Histogram? s_layoutRender; + private static Histogram? s_layoutInput; + + public static void InitMetrics() + { + // Metrics + var meter = new Meter("Avalonia.Diagnostic.Meter"); + s_compositorRender = meter.CreateHistogram( + Meters.CompositorRenderPassName, + Meters.MillisecondsUnit, + Meters.CompositorRenderPassDescription); + s_compositorUpdate = meter.CreateHistogram( + Meters.CompositorUpdatePassName, + Meters.MillisecondsUnit, + Meters.CompositorUpdatePassDescription); + s_layoutMeasure = meter.CreateHistogram( + Meters.LayoutMeasurePassName, + Meters.MillisecondsUnit, + Meters.LayoutMeasurePassDescription); + s_layoutArrange = meter.CreateHistogram( + Meters.LayoutArrangePassName, + Meters.MillisecondsUnit, + Meters.LayoutArrangePassDescription); + s_layoutRender = meter.CreateHistogram( + Meters.LayoutRenderPassName, + Meters.MillisecondsUnit, + Meters.LayoutRenderPassDescription); + s_layoutInput = meter.CreateHistogram( + Meters.LayoutInputPassName, + Meters.MillisecondsUnit, + Meters.LayoutInputPassDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalEventHandleCountName, + () => Interactive.TotalHandlersCount, + Meters.TotalEventHandleCountUnit, + Meters.TotalEventHandleCountDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalVisualCountName, + () => Visual.RootedVisualChildrenCount, + Meters.TotalVisualCountUnit, + Meters.TotalVisualCountDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalDispatcherTimerCountName, + () => DispatcherTimer.ActiveTimersCount, + Meters.TotalDispatcherTimerCountUnit, + Meters.TotalDispatcherTimerCountDescription); + } + + public static HistogramReportDisposable BeginCompositorRenderPass() => Begin(s_compositorRender); + public static HistogramReportDisposable BeginCompositorUpdatePass() => Begin(s_compositorUpdate); + public static HistogramReportDisposable BeginLayoutMeasurePass() => Begin(s_layoutMeasure); + public static HistogramReportDisposable BeginLayoutArrangePass() => Begin(s_layoutArrange); + public static HistogramReportDisposable BeginLayoutInputPass() => Begin(s_layoutInput); + public static HistogramReportDisposable BeginLayoutRenderPass() => Begin(s_layoutRender); + + private static HistogramReportDisposable Begin(Histogram? histogram) => histogram is not null ? new(histogram) : default; + + internal readonly ref struct HistogramReportDisposable + { + private readonly Histogram _histogram; + private readonly long _timestamp; + + public HistogramReportDisposable(Histogram histogram) + { + _histogram = histogram; + if (histogram.Enabled) + { + _timestamp = Stopwatch.GetTimestamp(); + } + } + + public void Dispose() + { + if (_timestamp > 0) + { + _histogram.Record(StopwatchHelper.GetElapsedTimeMs(_timestamp)); + } + } + } +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.cs new file mode 100644 index 0000000000..f49676d6b9 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + public static bool IsEnabled { get; } + + private static bool InitializeIsEnabled() => AppContext.TryGetSwitch("Avalonia.Diagnostics.Diagnostic.IsEnabled", out var isEnabled) && isEnabled; + + static Diagnostic() + { + IsEnabled = InitializeIsEnabled(); + if (!IsEnabled) + { + return; + } + + InitActivitySource(); + InitMetrics(); + } +} diff --git a/src/Avalonia.Base/Input/InputMethod.cs b/src/Avalonia.Base/Input/InputMethod.cs index 9df48a7d2e..b8b6c564a9 100644 --- a/src/Avalonia.Base/Input/InputMethod.cs +++ b/src/Avalonia.Base/Input/InputMethod.cs @@ -43,7 +43,7 @@ namespace Avalonia.Input public static void RemoveTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler handler) { - element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler); + element.RemoveHandler(TextInputMethodClientRequeryRequestedEvent, handler); } private InputMethod() diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 122176a127..83d99bf7a9 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -79,15 +79,10 @@ namespace Avalonia.Input { var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); - if (isLast) - { - key = ParseKey(partSpan.ToString()); - } - else + if (!TryParseKey(partSpan.ToString(), out key)) { keyModifiers |= ParseModifier(partSpan); } - cstart = c + 1; } } @@ -151,8 +146,11 @@ namespace Avalonia.Input s.Append(formatInfo.Meta); } - Plus(s); - s.Append(formatInfo.FormatKey(Key)); + if ((Key != Key.None) || (KeyModifiers == KeyModifiers.None)) + { + Plus(s); + s.Append(formatInfo.FormatKey(Key)); + } return StringBuilderCache.GetStringAndRelease(s); } @@ -163,12 +161,16 @@ namespace Avalonia.Input ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); // TODO: Move that to external key parser - private static Key ParseKey(string key) + private static bool TryParseKey(string keyStr, out Key key) { - if (s_keySynonyms.TryGetValue(key.ToLower(CultureInfo.InvariantCulture), out Key rv)) - return rv; + key = Key.None; + if (s_keySynonyms.TryGetValue(keyStr.ToLower(CultureInfo.InvariantCulture), out key)) + return true; + + if (EnumHelper.TryParse(keyStr, true, out key)) + return true; - return EnumHelper.Parse(key, true); + return false; } private static KeyModifiers ParseModifier(ReadOnlySpan modifier) diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index 6621f954ab..55516b2250 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,16 +6,48 @@ namespace Avalonia.Input.Platform [NotClientImplementable] public interface IClipboard { + /// + /// Returns a string containing the text data on the Clipboard. + /// + /// A string containing text data in the specified data format, or an empty string if no corresponding text data is available. Task GetTextAsync(); + /// + /// Stores text data on the Clipboard. The text data to store is specified as a string. + /// + /// A string that contains the UnicodeText data to store on the Clipboard. + /// is null. Task SetTextAsync(string? text); + /// + /// Clears any data from the system Clipboard. + /// Task ClearAsync(); + /// + /// Places a specified non-persistent data object on the system Clipboard. + /// + /// A data object (an object that implements ) to place on the system Clipboard. + /// is null. Task SetDataObjectAsync(IDataObject data); - + + /// + /// Permanently adds the data that is on the Clipboard so that it is available after the data's original application closes. + /// + /// + /// This method works only on Windows platform, on other platforms it does nothing. + Task FlushAsync(); + + /// + /// Get list of available Clipboard format. + /// Task GetFormatsAsync(); - + + /// + /// Retrieves data in a specified format from the Clipboard. + /// + /// A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the class. + /// Task GetDataAsync(string format); /// diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index b989def335..e79e0c3591 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -79,7 +79,9 @@ namespace Avalonia.Input else if (pointerDevice.TryGetPointer(args) is { } pointer && pointer.Type != PointerType.Touch) { - var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor; + var element = GetEffectivePointerOverElement( + args.InputHitTestResult.firstEnabledAncestor, + pointer.Captured); SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), @@ -96,7 +98,10 @@ namespace Avalonia.Input if (dirtyRect.Contains(clientPoint)) { - var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint); + var element = GetEffectivePointerOverElement( + _inputRoot.InputHitTest(clientPoint), + pointer.Captured); + SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint)) @@ -106,6 +111,11 @@ namespace Avalonia.Input } } + private static IInputElement? GetEffectivePointerOverElement(IInputElement? hitTestElement, IInputElement? captured) + => captured is not null && hitTestElement != captured ? + null : + hitTestElement; + private void ClearPointerOver() { if (_currentPointer is (var pointer, var position)) diff --git a/src/Avalonia.Base/Interactivity/EventRoute.cs b/src/Avalonia.Base/Interactivity/EventRoute.cs index d0d82b4884..967c095d98 100644 --- a/src/Avalonia.Base/Interactivity/EventRoute.cs +++ b/src/Avalonia.Base/Interactivity/EventRoute.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; namespace Avalonia.Interactivity { @@ -80,6 +81,10 @@ namespace Avalonia.Interactivity e.Source = source; + using var _ = Diagnostic.RaisingRoutedEvent()? + .AddTag(Diagnostic.Tags.Control, e.Source) + .AddTag(Diagnostic.Tags.RoutedEvent, e.RoutedEvent); + if (_event.RoutingStrategies == RoutingStrategies.Direct) { e.Route = RoutingStrategies.Direct; diff --git a/src/Avalonia.Base/Interactivity/Interactive.cs b/src/Avalonia.Base/Interactivity/Interactive.cs index 0dfaae0fc1..33443af8e5 100644 --- a/src/Avalonia.Base/Interactivity/Interactive.cs +++ b/src/Avalonia.Base/Interactivity/Interactive.cs @@ -12,6 +12,7 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable { + internal static int TotalHandlersCount { get; private set; } private Dictionary>? _eventHandlers; /// @@ -90,6 +91,7 @@ namespace Avalonia.Interactivity if (subscriptions[i].Handler == handler) { subscriptions.RemoveAt(i); + TotalHandlersCount--; } } } @@ -185,6 +187,7 @@ namespace Avalonia.Interactivity } subscriptions.Add(subscription); + TotalHandlersCount++; } private void AddToEventRoute(RoutedEvent routedEvent, EventRoute route) diff --git a/src/Avalonia.Base/Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs index 79a4dc60e4..bc3a2e5015 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEvent.cs @@ -103,6 +103,11 @@ namespace Avalonia.Interactivity { _routeFinished.OnNext(e); } + + public override string ToString() + { + return FormattableString.Invariant($"{OwnerType.Name}.{Name}"); + } } public class RoutedEvent : RoutedEvent diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 79d43239e6..411b747107 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Metadata; @@ -246,6 +247,7 @@ namespace Avalonia.Layout private void ExecuteMeasurePass() { + using var _ = Diagnostic.BeginLayoutMeasurePass(); while (_toMeasure.Count > 0) { var control = _toMeasure.Dequeue(); @@ -261,6 +263,7 @@ namespace Avalonia.Layout private void ExecuteArrangePass() { + using var _ = Diagnostic.BeginLayoutArrangePass(); while (_toArrange.Count > 0) { var control = _toArrange.Dequeue(); diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 53a3c84364..a71ceb76af 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Reactive; using Avalonia.VisualTree; @@ -367,6 +368,9 @@ namespace Avalonia.Layout if (!IsMeasureValid || _previousMeasure != availableSize) { + using var activity = Diagnostic.MeasuringLayoutable()? + .AddTag(Diagnostic.Tags.Control, this); + var previousDesiredSize = DesiredSize; var desiredSize = default(Size); @@ -417,6 +421,9 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { + using var activity = Diagnostic.ArrangingLayoutable()? + .AddTag(Diagnostic.Tags.Control, this); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); IsArrangeValid = true; diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs index 9a4b664f47..c0c1048e51 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs @@ -2,26 +2,25 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts +using System.Collections; using System.Collections.Generic; using System.IO; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts.Tables.Name { - internal class NameTable + internal class NameTable : IEnumerable { internal const string TableName = "name"; internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName); private readonly NameRecord[] _names; - internal NameTable(NameRecord[] names, IReadOnlyList languages) + internal NameTable(NameRecord[] names) { _names = names; - Languages = languages; } - public IReadOnlyList Languages { get; } - /// /// Gets the name of the font. /// @@ -133,22 +132,6 @@ namespace Avalonia.Media.Fonts.Tables.Name } } - //var languageNames = Array.Empty(); - - //if (format == 1) - //{ - // // Format 1 adds language data. - // var langCount = reader.ReadUInt16(); - // languageNames = new StringLoader[langCount]; - - // for (var i = 0; i < langCount; i++) - // { - // languageNames[i] = StringLoader.Create(reader); - - // strings.Add(languageNames[i]); - // } - //} - foreach (var readable in strings) { var readableStartOffset = stringOffset + readable.Offset; @@ -158,22 +141,17 @@ namespace Avalonia.Media.Fonts.Tables.Name readable.LoadValue(reader); } - var cultures = new List(); - - foreach (var nameRecord in names) - { - if (nameRecord.NameID != KnownNameIds.FontFamilyName || nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } + return new NameTable(names); + } - if (!cultures.Contains(nameRecord.LanguageID)) - { - cultures.Add(nameRecord.LanguageID); - } - } + public IEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(_names); + } - return new NameTable(names, cultures); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } } diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs index 84b892dae3..3bd2b1e767 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface2.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface2.cs @@ -29,5 +29,11 @@ namespace Avalonia.Media /// Gets supported font features. /// IReadOnlyList SupportedFeatures { get; } + + /// + /// Gets the localized face names. + /// Keys are culture identifiers. + /// + IReadOnlyDictionary FaceNames { get; } } } diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index ffe73c795c..32ee1ab932 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -98,6 +98,13 @@ partial class MediaContext if (AvaloniaLocator.Current.GetService() == null) return; + using var _ = NonPumpingLockHelper.Use(); + SyncWaitCompositorBatch(compositor, CommitCompositor(compositor), waitFullRender, catchExceptions); + } + + private void SyncWaitCompositorBatch(Compositor compositor, CompositionBatch batch, + bool waitFullRender, bool catchExceptions) + { using var _ = NonPumpingLockHelper.Use(); if (compositor is { @@ -105,12 +112,10 @@ partial class MediaContext Loop.RunsInBackground: true }) { - var batch = CommitCompositor(compositor); (waitFullRender ? batch.Rendered : batch.Processed).Wait(); } else { - CommitCompositor(compositor); compositor.Server.Render(catchExceptions); } } @@ -132,10 +137,15 @@ partial class MediaContext /// public void SyncDisposeCompositionTarget(CompositionTarget compositionTarget) { - compositionTarget.Dispose(); + using var _ = NonPumpingLockHelper.Use(); + + // TODO: We are sending a dispose command outside of the normal commit cycle and we might + // want to ask the compositor to skip any actual rendering and return the control ASAP + // Not sure if we should do that for background thread rendering since it might affect the animation + // smoothness of other windows - // TODO: introduce a way to skip any actual rendering for other targets and only do a dispose? - SyncCommit(compositionTarget.Compositor, false, true); + var oobBatch = compositionTarget.Compositor.OobDispose(compositionTarget); + SyncWaitCompositorBatch(compositionTarget.Compositor, oobBatch, false, true); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 39a8ff870e..590d2f2133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -679,20 +679,47 @@ namespace Avalonia.Media.TextFormatting private void UpdateMetrics(TextLineImpl currentLine, ref bool first) { - _metrics.InkBounds = _metrics.InkBounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.InkBounds.Position, currentLine.InkBounds.Size)); - _metrics.Bounds = _metrics.Bounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.Bounds.Position, currentLine.Bounds.Size)); + // 1) Offset each line’s bounding rectangles by the total height so far, + // so we keep an overall bounding box for the entire text block. + var lineTop = _metrics.Height; - _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Bounds.Width); - _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.InkBounds.Width); + // Offset the line's Bounds + var lineBoundsRect = new Rect( + currentLine.Bounds.X, + lineTop + currentLine.Bounds.Y, + currentLine.Bounds.Width, + currentLine.Bounds.Height); - _metrics.Height = _metrics.Bounds.Height; - _metrics.Width = _metrics.InkBounds.Width; - _metrics.WidthIncludingTrailingWhitespace = _metrics.Bounds.Width; - _metrics.Extent = _metrics.InkBounds.Height; - _metrics.OverhangLeading = Math.Max(0, _metrics.Bounds.Left - _metrics.InkBounds.Left); - _metrics.OverhangTrailing = Math.Max(0, _metrics.InkBounds.Right - _metrics.Bounds.Right); - _metrics.OverhangAfter = Math.Max(0, _metrics.InkBounds.Bottom - _metrics.Bounds.Bottom); + _metrics.Bounds = _metrics.Bounds.Union(lineBoundsRect); + // Offset the line's InkBounds + var lineInkRect = new Rect( + currentLine.InkBounds.X, + lineTop + currentLine.InkBounds.Y, + currentLine.InkBounds.Width, + currentLine.InkBounds.Height); + + _metrics.InkBounds = _metrics.InkBounds.Union(lineInkRect); + + // 2) Accumulate total layout height by adding the line’s Height. + _metrics.Height += currentLine.Height; + + // 3) For the layout’s Width and WidthIncludingTrailingWhitespace, + // use the maximum of the line widths rather than the bounding box. + _metrics.Width = Math.Max(_metrics.Width, currentLine.Width); + _metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace); + + // 4) Extent is the max black-pixel extent among lines. + _metrics.Extent = Math.Max(_metrics.Extent, currentLine.Extent); + + // 5) We can track min-text-width or overhangs similarly if needed. + _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Width); + + _metrics.OverhangLeading = Math.Max(_metrics.OverhangLeading, currentLine.OverhangLeading); + _metrics.OverhangTrailing = Math.Max(_metrics.OverhangTrailing, currentLine.OverhangTrailing); + _metrics.OverhangAfter = Math.Max(_metrics.OverhangAfter, currentLine.OverhangAfter); + + // 6) Capture the baseline from the first line. if (first) { _metrics.Baseline = currentLine.Baseline; diff --git a/src/Avalonia.Base/Platform/Internal/SlicedStream.cs b/src/Avalonia.Base/Platform/Internal/SlicedStream.cs index 124c248aa8..86a8dd5da9 100644 --- a/src/Avalonia.Base/Platform/Internal/SlicedStream.cs +++ b/src/Avalonia.Base/Platform/Internal/SlicedStream.cs @@ -29,7 +29,7 @@ internal class SlicedStream : Stream if (origin == SeekOrigin.Begin) Position = offset; if (origin == SeekOrigin.End) - Position = _from + Length + offset; + Position = Length + offset; if (origin == SeekOrigin.Current) Position = Position + offset; return Position; diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index cf20f20172..04759e314d 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -1,7 +1,7 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Threading; -using Avalonia.Threading; namespace Avalonia.Reactive { @@ -118,32 +118,59 @@ namespace Avalonia.Reactive if (Volatile.Read(ref _observers) != null) { IObserver[]? observers = null; - IObserver? singleObserver = null; + int count = 0; + + // Optimize for the common case of 1/2/3 observers. + IObserver? observer0 = null; + IObserver? observer1 = null; + IObserver? observer2 = null; lock (this) { if (_observers == null) { return; } - if (_observers.Count == 1) - { - singleObserver = _observers[0]; - } - else + + count = _observers.Count; + switch (count) { - observers = _observers.ToArray(); + case 3: + observer0 = _observers[0]; + observer1 = _observers[1]; + observer2 = _observers[2]; + break; + case 2: + observer0 = _observers[0]; + observer1 = _observers[1]; + break; + case 1: + observer0 = _observers[0]; + break; + case 0: + return; + default: + { + observers = ArrayPool>.Shared.Rent(count); + _observers.CopyTo(observers); + break; + } } } - if (singleObserver != null) + + if (observer0 != null) { - singleObserver.OnNext(value); + observer0.OnNext(value); + observer1?.OnNext(value); + observer2?.OnNext(value); } - else + else if (observers != null) { - foreach (var observer in observers!) + for(int i = 0; i < count; i++) { - observer.OnNext(value); + observers[i].OnNext(value); } + + ArrayPool>.Shared.Return(observers); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 9faa3d721f..98fb147c2c 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Threading; @@ -96,6 +97,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester /// public IEnumerable HitTest(Point p, Visual? root, Func? filter) { + using var _ = Diagnostic.PerformingHitTest(); + CompositionVisual? rootVisual = null; if (root != null) { @@ -198,7 +201,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester _updating = true; try { - UpdateCore(); + using (Diagnostic.BeginLayoutRenderPass()) + UpdateCore(); } finally { diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 4190991b25..257e41f2d6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -211,6 +211,28 @@ namespace Avalonia.Rendering.Composition return commit; } } + + /// + /// This method submits a composition with a single dispose command outside the normal + /// commit cycle. This is currently used for disposing CompositionTargets since we need to do that ASAP + /// and without affecting the not yet completed composition batch + /// + internal CompositionBatch OobDispose(CompositionObject obj) + { + using var _ = NonPumpingLockHelper.Use(); + obj.Dispose(); + var batch = new CompositionBatch(); + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + writer.WriteObject(ServerCompositor.RenderThreadDisposeStartMarker); + writer.Write(1); + writer.WriteObject(obj.Server); + } + + batch.CommittedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch; + } internal void RegisterForSerialization(ICompositorSerializable compositionObject) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e942761fcf..90b973c3a8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; @@ -122,22 +123,24 @@ namespace Avalonia.Rendering.Composition.Server Revision++; _overlays.MarkUpdateCallStart(); + using (Diagnostic.BeginCompositorUpdatePass()) + { + var transform = Matrix.CreateScale(Scaling, Scaling); + // Update happens in a separate phase to extend dirty rect if needed + Root.Update(this, transform); - var transform = Matrix.CreateScale(Scaling, Scaling); - // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this, transform); + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this, transform); + } - while (_adornerUpdateQueue.Count > 0) - { - var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this, transform); - } + _updateRequested = false; + Readback.CompleteWrite(Revision); - _updateRequested = false; - Readback.CompleteWrite(Revision); + _overlays.MarkUpdateCallEnd(); + } - _overlays.MarkUpdateCallEnd(); - if (!_redrawRequested) return; @@ -151,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties( this.PixelSize, out var properties)) + using (var renderTiming = Diagnostic.BeginCompositorRenderPass()) { if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)) { diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 6cd09e1808..fbd869a9a7 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -46,12 +47,18 @@ namespace Avalonia.Styling if (TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); + using var activity = Diagnostic.AttachingStyle()? + .AddTag(Diagnostic.Tags.Style, this); + if (HasSettersOrAnimations && TargetType.IsAssignableFrom(StyledElement.GetStyleKey(target))) { Attach(target, null, type, true); + activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.AlwaysThisType); + return SelectorMatchResult.AlwaysThisType; } + activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.NeverThisType); return SelectorMatchResult.NeverThisType; } } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 44ffc22e91..57ce8d7927 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -67,11 +68,16 @@ namespace Avalonia.Styling if (HasSettersOrAnimations) { + using var activity = Diagnostic.AttachingStyle()? + .AddTag(Diagnostic.Tags.Style, this); + var match = Selector?.Match(target, Parent, true) ?? (target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance); + activity?.AddTag(Diagnostic.Tags.SelectorResult, match.Result); + if (match.IsMatch) { Attach(target, match.Activator, type, Selector is not OrSelector); diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index 61cb31c6d0..bfccfa8e83 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Styling.Activators; @@ -100,8 +101,15 @@ namespace Avalonia.Styling _animationTrigger?.OnNext(_activator.GetIsActive()); } + using var activity = _activator is null ? null : Diagnostic.EvaluatingStyle()? + .AddTag(Diagnostic.Tags.Activator, _activator) + .AddTag(Diagnostic.Tags.Selector, (Source as Style)?.Selector) + .AddTag(Diagnostic.Tags.Style, Source as StyleBase); + _isActive = _activator?.GetIsActive() ?? true; hasChanged = _isActive != previous; + + activity?.AddTag(Diagnostic.Tags.IsActive, _isActive); return _isActive; } diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index fd008186fa..ea88109bdf 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -9,6 +9,8 @@ namespace Avalonia.Threading; /// public partial class DispatcherTimer { + internal static int ActiveTimersCount { get; private set; } + /// /// Creates a timer that uses theUI thread's Dispatcher2 to /// process the timer event at background priority. @@ -147,6 +149,7 @@ public partial class DispatcherTimer if (!_isEnabled) { _isEnabled = true; + ActiveTimersCount++; Restart(); } @@ -165,6 +168,7 @@ public partial class DispatcherTimer if (_isEnabled) { _isEnabled = false; + ActiveTimersCount--; updateOSTimer = true; // If the operation is in the queue, abort it. diff --git a/src/Avalonia.Base/Utilities/StopwatchHelper.cs b/src/Avalonia.Base/Utilities/StopwatchHelper.cs index 4719226ea4..622bdc27a0 100644 --- a/src/Avalonia.Base/Utilities/StopwatchHelper.cs +++ b/src/Avalonia.Base/Utilities/StopwatchHelper.cs @@ -10,10 +10,14 @@ namespace Avalonia.Utilities; internal static class StopwatchHelper { private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private static readonly double s_timestampToMs = s_timestampToTicks / TimeSpan.TicksPerMillisecond; public static TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks)); + + public static double GetElapsedTimeMs(long startingTimestamp) + => (Stopwatch.GetTimestamp() - startingTimestamp) * s_timestampToMs; } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index cf9a050185..52e5251add 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; @@ -31,6 +32,8 @@ namespace Avalonia [UsableDuringInitialization] public partial class Visual : StyledElement, IAvaloniaListItemValidator { + internal static int RootedVisualChildrenCount { get; private set; } + /// /// Defines the property. /// @@ -493,6 +496,7 @@ namespace Avalonia Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); _visualRoot = e.Root; + RootedVisualChildrenCount++; if (_visualParent is null) { throw new InvalidOperationException("Visual was attached to the root without being added to the visual parent first."); @@ -541,6 +545,7 @@ namespace Avalonia Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); _visualRoot = null; + RootedVisualChildrenCount--; if (RenderTransform is IMutableTransform mutableTransform) { diff --git a/src/Avalonia.Controls.DataGrid b/src/Avalonia.Controls.DataGrid new file mode 160000 index 0000000000..85a0b32ef6 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid @@ -0,0 +1 @@ +Subproject commit 85a0b32ef6d963c1d67619ca3e2f6da0bc43ac9a diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridAutomationPeer.cs deleted file mode 100644 index 5d181d8567..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridAutomationPeer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Avalonia.Automation.Peers; - -namespace Avalonia.Controls.Automation.Peers; - -public class DataGridAutomationPeer : ControlAutomationPeer -{ - public DataGridAutomationPeer(DataGrid owner) - : base(owner) - { - } - - public new DataGrid Owner => (DataGrid)base.Owner; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.DataGrid; - } -} diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridCellAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridCellAutomationPeer.cs deleted file mode 100644 index f1301be70d..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridCellAutomationPeer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Automation.Peers; - -namespace Avalonia.Controls.Automation.Peers; - -public class DataGridCellAutomationPeer : ContentControlAutomationPeer -{ - public DataGridCellAutomationPeer(DataGridCell owner) - : base(owner) - { - } - - public new DataGridCell Owner => (DataGridCell)base.Owner; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Custom; - } - - protected override bool IsContentElementCore() => true; - - protected override bool IsControlElementCore() => true; -} diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeaderAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeaderAutomationPeer.cs deleted file mode 100644 index bfcf00607f..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeaderAutomationPeer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Automation.Peers; - -namespace Avalonia.Controls.Automation.Peers; - -public class DataGridColumnHeaderAutomationPeer : ContentControlAutomationPeer -{ - public DataGridColumnHeaderAutomationPeer(DataGridColumnHeader owner) - : base(owner) - { - } - - public new DataGridColumnHeader Owner => (DataGridColumnHeader)base.Owner; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.HeaderItem; - } - - protected override bool IsContentElementCore() => false; - - protected override bool IsControlElementCore() => true; -} diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeadersPresenterAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeadersPresenterAutomationPeer.cs deleted file mode 100644 index f84951206e..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeadersPresenterAutomationPeer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Avalonia.Automation.Peers; -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls.Automation.Peers; - -public class DataGridColumnHeadersPresenterAutomationPeer : ControlAutomationPeer -{ - public DataGridColumnHeadersPresenterAutomationPeer(DataGridColumnHeadersPresenter owner) - : base(owner) - { - } - - public new DataGridColumnHeadersPresenter Owner => (DataGridColumnHeadersPresenter)base.Owner; - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.Header; - } - - protected override bool IsContentElementCore() => false; - - protected override bool IsControlElementCore() => true; -} diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridDetailsPresenterAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridDetailsPresenterAutomationPeer.cs deleted file mode 100644 index 58b79541c5..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridDetailsPresenterAutomationPeer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Automation.Peers; -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls.Automation.Peers; - -public class DataGridDetailsPresenterAutomationPeer : ControlAutomationPeer -{ - public DataGridDetailsPresenterAutomationPeer(DataGridDetailsPresenter owner) - : base(owner) - { - } - - public new DataGridDetailsPresenter Owner => (DataGridDetailsPresenter)base.Owner; -} diff --git a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridRowAutomationPeer.cs b/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridRowAutomationPeer.cs deleted file mode 100644 index 4e137a4e74..0000000000 --- a/src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridRowAutomationPeer.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia.Controls; - -namespace Avalonia.Automation.Peers -{ - public class DataGridRowAutomationPeer : ControlAutomationPeer - { - public DataGridRowAutomationPeer(DataGridRow owner) - : base(owner) - { - } - - protected override AutomationControlType GetAutomationControlTypeCore() - { - return AutomationControlType.DataItem; - } - - protected override bool IsContentElementCore() => true; - protected override bool IsControlElementCore() => true; - } -} diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj deleted file mode 100644 index bc7885fa53..0000000000 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - $(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0 - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs deleted file mode 100644 index c329492b5e..0000000000 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ /dev/null @@ -1,4380 +0,0 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using Avalonia.Controls.Utils; -using Avalonia.Utilities; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Avalonia.Collections -{ - /// - /// Event argument used for page index change notifications. The requested page move - /// can be canceled by setting e.Cancel to True. - /// - public sealed class PageChangingEventArgs : CancelEventArgs - { - /// - /// Constructor that takes the target page index - /// - /// Index of the requested page - public PageChangingEventArgs(int newPageIndex) - { - NewPageIndex = newPageIndex; - } - - /// - /// Gets the index of the requested page - /// - public int NewPageIndex - { - get; - private set; - } - } - - /// Defines a method that enables a collection to provide a custom view for specialized sorting, filtering, grouping, and currency. - public interface IDataGridCollectionViewFactory - { - /// Returns a custom view for specialized sorting, filtering, grouping, and currency. - /// A custom view for specialized sorting, filtering, grouping, and currency. - IDataGridCollectionView CreateView(); - } - - /// - /// DataGrid-readable view over an IEnumerable. - /// - public sealed class DataGridCollectionView : IDataGridCollectionView, IDataGridEditableCollectionView, IList, INotifyPropertyChanged - { - /// - /// Since there's nothing in the un-cancelable event args that is mutable, - /// just create one instance to be used universally. - /// - private static readonly DataGridCurrentChangingEventArgs uncancelableCurrentChangingEventArgs = new DataGridCurrentChangingEventArgs(false); - - /// - /// Value that we cache for the PageIndex if we are in a DeferRefresh, - /// and the user has attempted to move to a different page. - /// - private int _cachedPageIndex = -1; - - /// - /// Value that we cache for the PageSize if we are in a DeferRefresh, - /// and the user has attempted to change the PageSize. - /// - private int _cachedPageSize; - - /// - /// CultureInfo used in this DataGridCollectionView - /// - private CultureInfo _culture; - - /// - /// Private accessor for the Monitor we use to prevent recursion - /// - private SimpleMonitor _currentChangedMonitor = new SimpleMonitor(); - - /// - /// Private accessor for the CurrentItem - /// - private object _currentItem; - - /// - /// Private accessor for the CurrentPosition - /// - private int _currentPosition; - - /// - /// The number of requests to defer Refresh() - /// - private int _deferLevel; - - /// - /// The item we are currently editing - /// - private object _editItem; - - /// - /// Private accessor for the Filter - /// - private Func _filter; - - /// - /// Private accessor for the CollectionViewFlags - /// - private CollectionViewFlags _flags = CollectionViewFlags.ShouldProcessCollectionChanged; - - /// - /// Private accessor for the Grouping data - /// - private CollectionViewGroupRoot _group; - - /// - /// Private accessor for the InternalList - /// - private IList _internalList; - - /// - /// Keeps track of whether groups have been applied to the - /// collection already or not. Note that this can still be set - /// to false even though we specify a GroupDescription, as the - /// collection may not have gone through the PrepareGroups function. - /// - private bool _isGrouping; - - /// - /// Private accessor for indicating whether we want to point to the temporary grouping data for calculations - /// - private bool _isUsingTemporaryGroup; - - /// - /// ConstructorInfo obtained from reflection for generating new items - /// - private ConstructorInfo _itemConstructor; - - /// - /// Whether we have the correct ConstructorInfo information for the ItemConstructor - /// - private bool _itemConstructorIsValid; - - /// - /// The new item we are getting ready to add to the collection - /// - private object _newItem; - - /// - /// Private accessor for the PageIndex - /// - private int _pageIndex = -1; - - /// - /// Private accessor for the PageSize - /// - private int _pageSize; - - /// - /// Whether the source needs to poll for changes - /// (if it did not implement INotifyCollectionChanged) - /// - private bool _pollForChanges; - - /// - /// Private accessor for the SortDescriptions - /// - private DataGridSortDescriptionCollection _sortDescriptions; - - /// - /// Private accessor for the SourceCollection - /// - private IEnumerable _sourceCollection; - - /// - /// Private accessor for the Grouping data on the entire collection - /// - private CollectionViewGroupRoot _temporaryGroup; - - /// - /// Timestamp used to see if there was a collection change while - /// processing enumerator changes - /// - private int _timestamp; - - /// - /// Private accessor for the TrackingEnumerator - /// - private IEnumerator _trackingEnumerator; - - /// - /// Helper constructor that sets default values for isDataSorted and isDataInGroupOrder. - /// - /// The source for the collection - public DataGridCollectionView(IEnumerable source) - : this(source, false /*isDataSorted*/, false /*isDataInGroupOrder*/) - { - } - - /// - /// Initializes a new instance of the DataGridCollectionView class. - /// - /// The source for the collection - /// Determines whether the source is already sorted - /// Whether the source is already in the correct order for grouping - public DataGridCollectionView(IEnumerable source, bool isDataSorted, bool isDataInGroupOrder) - { - _sourceCollection = source ?? throw new ArgumentNullException(nameof(source)); - - SetFlag(CollectionViewFlags.IsDataSorted, isDataSorted); - SetFlag(CollectionViewFlags.IsDataInGroupOrder, isDataInGroupOrder); - - _temporaryGroup = new CollectionViewGroupRoot(this, isDataInGroupOrder); - _group = new CollectionViewGroupRoot(this, false); - _group.GroupDescriptionChanged += OnGroupDescriptionChanged; - _group.GroupDescriptions.CollectionChanged += OnGroupByChanged; - - CopySourceToInternalList(); - _trackingEnumerator = source.GetEnumerator(); - - // set currency - if (_internalList.Count > 0) - { - SetCurrent(_internalList[0], 0, 1); - } - else - { - SetCurrent(null, -1, 0); - } - - // Set flag for whether the collection is empty - SetFlag(CollectionViewFlags.CachedIsEmpty, Count == 0); - - // If we implement INotifyCollectionChanged - if (source is INotifyCollectionChanged coll) - { - coll.CollectionChanged += (_, args) => ProcessCollectionChanged(args); - } - else - { - // If the source doesn't raise collection change events, try to - // detect changes by polling the enumerator - _pollForChanges = true; - } - } - - /// - /// Raise this event when the (filtered) view changes - /// - public event NotifyCollectionChangedEventHandler CollectionChanged; - - /// - /// CollectionChanged event (per INotifyCollectionChanged). - /// - event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged - { - add { CollectionChanged += value; } - remove { CollectionChanged -= value; } - } - - /// - /// Raised when the CurrentItem property changed - /// - public event EventHandler CurrentChanged; - - /// - /// Raised when the CurrentItem property is changing - /// - public event EventHandler CurrentChanging; - - /// - /// Raised when a page index change completed - /// - //TODO Paging - public event EventHandler PageChanged; - - /// - /// Raised when a page index change is requested - /// - //TODO Paging - public event EventHandler PageChanging; - - /// - /// PropertyChanged event. - /// - public event PropertyChangedEventHandler PropertyChanged; - - /// - /// PropertyChanged event (per INotifyPropertyChanged) - /// - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged - { - add { PropertyChanged += value; } - remove { PropertyChanged -= value; } - } - - /// - /// Enum for CollectionViewFlags - /// - //TODO Paging - [Flags] - private enum CollectionViewFlags - { - /// - /// Whether the list of items (after applying the sort and filters, if any) - /// is already in the correct order for grouping. - /// - IsDataInGroupOrder = 0x01, - - /// - /// Whether the source collection is already sorted according to the SortDescriptions collection - /// - IsDataSorted = 0x02, - - /// - /// Whether we should process the collection changed event - /// - ShouldProcessCollectionChanged = 0x04, - - /// - /// Whether the current item is before the first - /// - IsCurrentBeforeFirst = 0x08, - - /// - /// Whether the current item is after the last - /// - IsCurrentAfterLast = 0x10, - - /// - /// Whether we need to refresh - /// - NeedsRefresh = 0x20, - - /// - /// Whether we cache the IsEmpty value - /// - CachedIsEmpty = 0x40, - - /// - /// Indicates whether a page index change is in process or not - /// - IsPageChanging = 0x80, - - /// - /// Whether we need to move to another page after EndDefer - /// - IsMoveToPageDeferred = 0x100, - - /// - /// Whether we need to update the PageSize after EndDefer - /// - IsUpdatePageSizeDeferred = 0x200 - } - - private Type _itemType; - private Type ItemType - { - get - { - if (_itemType == null) - _itemType = GetItemType(true); - - return _itemType; - } - } - - /// - /// Gets a value indicating whether the view supports AddNew. - /// - public bool CanAddNew - { - get - { - return !IsEditingItem && - (SourceList != null && !SourceList.IsFixedSize && CanConstructItem); - } - } - - /// - /// Gets a value indicating whether the view supports the notion of "pending changes" - /// on the current edit item. This may vary, depending on the view and the particular - /// item. For example, a view might return true if the current edit item - /// implements IEditableObject, or if the view has special knowledge about - /// the item that it can use to support rollback of pending changes. - /// - public bool CanCancelEdit - { - get { return _editItem is IEditableObject; } - } - - /// - /// Gets a value indicating whether the PageIndex value is allowed to change or not. - /// - //TODO Paging - public bool CanChangePage - { - get { return true; } - } - - /// - /// Gets a value indicating whether we support filtering with this ICollectionView. - /// - public bool CanFilter - { - get { return true; } - } - - /// - /// Gets a value indicating whether this view supports grouping. - /// When this returns false, the rest of the interface is ignored. - /// - public bool CanGroup - { - get { return true; } - } - - /// - /// Gets a value indicating whether the view supports Remove and RemoveAt. - /// - public bool CanRemove - { - get - { - return !IsEditingItem && !IsAddingNew && - (SourceList != null && !SourceList.IsFixedSize); - } - } - - /// - /// Gets a value indicating whether we support sorting with this ICollectionView. - /// - public bool CanSort - { - get { return true; } - } - - /// - /// Gets the number of records in the view after - /// filtering, sorting, and paging. - /// - //TODO Paging - public int Count - { - get - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - - // if we have paging - if (PageSize > 0 && PageIndex > -1) - { - if (IsGrouping && !_isUsingTemporaryGroup) - { - return _group.ItemCount; - } - else - { - return Math.Max(0, Math.Min(PageSize, InternalCount - (_pageSize * PageIndex))); - } - } - else - { - if (IsGrouping) - { - if (_isUsingTemporaryGroup) - { - return _temporaryGroup.ItemCount; - } - else - { - return _group.ItemCount; - } - } - else - { - return InternalCount; - } - } - } - } - - /// - /// Gets or sets Culture to use during sorting. - /// - public CultureInfo Culture - { - get - { - return _culture; - } - - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - if (_culture != value) - { - _culture = value; - OnPropertyChanged(nameof(Culture)); - } - } - } - - /// - /// Gets the new item when an AddNew transaction is in progress - /// Otherwise it returns null. - /// - public object CurrentAddItem - { - get - { - return _newItem; - } - - private set - { - if (_newItem != value) - { - Debug.Assert(value == null || _newItem == null, "Old and new _newItem values are unexpectedly non null"); - _newItem = value; - OnPropertyChanged(nameof(IsAddingNew)); - OnPropertyChanged(nameof(CurrentAddItem)); - } - } - } - - /// - /// Gets the affected item when an EditItem transaction is in progress - /// Otherwise it returns null. - /// - public object CurrentEditItem - { - get - { - return _editItem; - } - - private set - { - if (_editItem != value) - { - Debug.Assert(value == null || _editItem == null, "Old and new _editItem values are unexpectedly non null"); - bool oldCanCancelEdit = CanCancelEdit; - _editItem = value; - OnPropertyChanged(nameof(IsEditingItem)); - OnPropertyChanged(nameof(CurrentEditItem)); - if (oldCanCancelEdit != CanCancelEdit) - { - OnPropertyChanged(nameof(CanCancelEdit)); - } - } - } - } - - /// - /// Gets the "current item" for this view - /// - public object CurrentItem - { - get - { - VerifyRefreshNotDeferred(); - return _currentItem; - } - } - - /// - /// Gets the ordinal position of the CurrentItem within the - /// (optionally sorted and filtered) view. - /// - public int CurrentPosition - { - get - { - VerifyRefreshNotDeferred(); - return _currentPosition; - } - } - - private string GetOperationNotAllowedDuringAddOrEditText(string action) - { - return $"'{action}' is not allowed during an AddNew or EditItem transaction."; - } - private string GetOperationNotAllowedText(string action, string transaction = null) - { - if (String.IsNullOrWhiteSpace(transaction)) - { - return $"'{action}' is not allowed for this view."; - } - else - { - return $"'{action}' is not allowed during a transaction started by '{transaction}'."; - } - } - - /// - /// Gets or sets the Filter, which is a callback set by the consumer of the ICollectionView - /// and used by the implementation of the ICollectionView to determine if an - /// item is suitable for inclusion in the view. - /// - /// - /// Simpler implementations do not support filtering and will throw a NotSupportedException. - /// Use property to test if filtering is supported before - /// assigning a non-null value. - /// - public Func Filter - { - get - { - return _filter; - } - - set - { - if (IsAddingNew || IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Filter))); - } - - if (!CanFilter) - { - throw new NotSupportedException("The Filter property cannot be set when the CanFilter property returns false."); - } - - if (_filter != value) - { - _filter = value; - RefreshOrDefer(); - OnPropertyChanged(nameof(Filter)); - } - } - } - - /// - /// Gets the description of grouping, indexed by level. - /// - public AvaloniaList GroupDescriptions - { - get - { - return _group?.GroupDescriptions; - } - } - - int IDataGridCollectionView.GroupingDepth => GroupDescriptions?.Count ?? 0; - string IDataGridCollectionView.GetGroupingPropertyNameAtDepth(int level) - { - var groups = GroupDescriptions; - if(groups != null && level >= 0 && level < groups.Count) - { - return groups[level].PropertyName; - } - else - { - return String.Empty; - } - } - - /// - /// Gets the top-level groups, constructed according to the descriptions - /// given in GroupDescriptions. - /// - public IAvaloniaReadOnlyList Groups - { - get - { - if (!IsGrouping) - { - return null; - } - - return RootGroup?.Items; - } - } - - /// - /// Gets a value indicating whether an "AddNew" transaction is in progress. - /// - public bool IsAddingNew - { - get { return _newItem != null; } - } - - /// - /// Gets a value indicating whether currency is beyond the end (End-Of-File). - /// - /// Whether IsCurrentAfterLast - public bool IsCurrentAfterLast - { - get - { - VerifyRefreshNotDeferred(); - return CheckFlag(CollectionViewFlags.IsCurrentAfterLast); - } - } - - /// - /// Gets a value indicating whether currency is before the beginning (Beginning-Of-File). - /// - /// Whether IsCurrentBeforeFirst - public bool IsCurrentBeforeFirst - { - get - { - VerifyRefreshNotDeferred(); - return CheckFlag(CollectionViewFlags.IsCurrentBeforeFirst); - } - } - - /// - /// Gets a value indicating whether an EditItem transaction is in progress. - /// - public bool IsEditingItem - { - get { return _editItem != null; } - } - - /// - /// Gets a value indicating whether the resulting (filtered) view is empty. - /// - public bool IsEmpty - { - get - { - EnsureCollectionInSync(); - return InternalCount == 0; - } - } - - /// - /// Gets a value indicating whether a page index change is in process or not. - /// - //TODO Paging - public bool IsPageChanging - { - get - { - return CheckFlag(CollectionViewFlags.IsPageChanging); - } - - private set - { - if (CheckFlag(CollectionViewFlags.IsPageChanging) != value) - { - SetFlag(CollectionViewFlags.IsPageChanging, value); - OnPropertyChanged(nameof(IsPageChanging)); - } - } - } - - /// - /// Gets the minimum number of items known to be in the source collection - /// that verify the current filter if any - /// - public int ItemCount - { - get - { - return InternalList.Count; - } - } - - /// - /// Gets a value indicating whether this view needs to be refreshed. - /// - public bool NeedsRefresh - { - get { return CheckFlag(CollectionViewFlags.NeedsRefresh); } - } - - /// - /// Gets the current page we are on. (zero based) - /// - //TODO Paging - public int PageIndex - { - get - { - return _pageIndex; - } - } - - /// - /// Gets or sets the number of items to display on a page. If the - /// PageSize = 0, then we are not paging, and will display all items - /// in the collection. Otherwise, we will have separate pages for - /// the items to display. - /// - //TODO Paging - public int PageSize - { - get - { - return _pageSize; - } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "PageSize cannot have a negative value."); - } - - // if the Refresh is currently deferred, cache the desired PageSize - // and set the flag so that once the defer is over, we can then - // update the PageSize. - if (IsRefreshDeferred) - { - // set cached value and flag so that we update the PageSize on EndDefer - _cachedPageSize = value; - SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, true); - return; - } - - // to see whether or not to fire an OnPropertyChanged - int oldCount = Count; - - if (_pageSize != value) - { - // Remember current currency values for upcoming OnPropertyChanged notifications - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - // Check if there is a current edited or new item so changes can be committed first. - if (CurrentAddItem != null || CurrentEditItem != null) - { - // Check with the ICollectionView.CurrentChanging listeners if it's OK to - // change the currency. If not, then we can't fire the event to allow them to - // commit their changes. So, we will not be able to change the PageSize. - if (!OkToChangeCurrent()) - { - throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); - } - - // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or - // cancelling an item that is no longer on the current page. That's acceptable and means that - // the potential _newItem or _editItem needs to be committed before this PageSize change. - // The reason why we temporarily reset currency here is to give a chance to the bound - // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit() - // for example as a result of changing currency. - SetCurrentToPosition(-1); - RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // If the bound controls did not successfully end their potential item editing/addition, we - // need to throw an exception to show that the PageSize change failed. - if (CurrentAddItem != null || CurrentEditItem != null) - { - throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); - } - } - - _pageSize = value; - OnPropertyChanged(nameof(PageSize)); - - if (_pageSize == 0) - { - // update the groups for the current page - //*************************************** - PrepareGroups(); - - // if we are not paging - MoveToPage(-1); - } - else if (_pageIndex != 0) - { - if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) - { - // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a reference while paging. - if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) - { - PrepareTemporaryGroups(); - } - - MoveToFirstPage(); - } - } - else if (IsGrouping) - { - // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a reference while paging. - if (_temporaryGroup.ItemCount != InternalList.Count) - { - // update the groups that get created for the - // entire collection as well as the current page - PrepareTemporaryGroups(); - } - - // update the groups for the current page - PrepareGroupsForCurrentPage(); - } - - // if the count has changed - if (Count != oldCount) - { - OnPropertyChanged(nameof(Count)); - } - - // reset currency values - ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // send a notification that our collection has been updated - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Reset)); - - // now raise currency changes at the end - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - } - } - - /// - /// Gets the Sort criteria to sort items in collection. - /// - /// - /// - /// Clear a sort criteria by assigning SortDescription.Empty to this property. - /// One or more sort criteria in form of - /// can be used, each specifying a property and direction to sort by. - /// - /// - /// - /// Simpler implementations do not support sorting and will throw a NotSupportedException. - /// Use property to test if sorting is supported before adding - /// to SortDescriptions. - /// - public DataGridSortDescriptionCollection SortDescriptions - { - get - { - if (_sortDescriptions == null) - { - SetSortDescriptions(new DataGridSortDescriptionCollection()); - } - - return _sortDescriptions; - } - } - - /// - /// Gets the source of the IEnumerable collection we are using for our view. - /// - public IEnumerable SourceCollection - { - get { return _sourceCollection; } - } - - /// - /// Gets the total number of items in the view before paging is applied. - /// - public int TotalItemCount - { - get - { - return InternalList.Count; - } - } - - /// - /// Gets a value indicating whether we have a valid ItemConstructor of the correct type - /// - private bool CanConstructItem - { - get - { - if (!_itemConstructorIsValid) - { - EnsureItemConstructor(); - } - - return _itemConstructor != null; - } - } - - /// - /// Gets the private count without taking paging or - /// placeholders into account - /// - private int InternalCount - { - get { return InternalList.Count; } - } - - /// - /// Gets the InternalList - /// - private IList InternalList - { - get { return _internalList; } - } - - /// - /// Gets a value indicating whether CurrentItem and CurrentPosition are - /// up-to-date with the state and content of the collection. - /// - private bool IsCurrentInSync - { - get - { - if (IsCurrentInView) - { - return GetItemAt(CurrentPosition).Equals(CurrentItem); - } - else - { - return CurrentItem == null; - } - } - } - - /// - /// Gets a value indicating whether the current item is in the view - /// - private bool IsCurrentInView - { - get - { - VerifyRefreshNotDeferred(); - - // Calling IndexOf will check whether the specified currentItem - // is within the (paged) view. - return IndexOf(CurrentItem) >= 0; - } - } - - /// - /// Gets a value indicating whether or not we have grouping - /// taking place in this collection. - /// - private bool IsGrouping - { - get { return _isGrouping; } - } - - bool IDataGridCollectionView.IsGrouping => IsGrouping; - - /// - /// Gets a value indicating whether there - /// is still an outstanding DeferRefresh in - /// use. If at all possible, derived classes - /// should not call Refresh if IsRefreshDeferred - /// is true. - /// - private bool IsRefreshDeferred - { - get { return _deferLevel > 0; } - } - - /// - /// Gets whether the current page is empty and we need - /// to move to a previous page. - /// - //TODO Paging - private bool NeedToMoveToPreviousPage - { - get { return (PageSize > 0 && Count == 0 && PageIndex != 0 && PageCount == PageIndex); } - } - - /// - /// Gets a value indicating whether we are on the last local page - /// - //TODO Paging - private bool OnLastLocalPage - { - get - { - if (PageSize == 0) - { - return false; - } - - Debug.Assert(PageCount > 0, "Unexpected PageCount <= 0"); - - // if we have no items (PageCount==1) or there is just one page - if (PageCount == 1) - { - return true; - } - - return (PageIndex == PageCount - 1); - } - } - - /// - /// Gets the number of pages we currently have - /// - //TODO Paging - private int PageCount - { - get { return (_pageSize > 0) ? Math.Max(1, (int)Math.Ceiling((double)ItemCount / _pageSize)) : 0; } - } - - /// - /// Gets the root of the Group that we expose to the user - /// - private CollectionViewGroupRoot RootGroup - { - get - { - return _isUsingTemporaryGroup ? _temporaryGroup : _group; - } - } - - /// - /// Gets the SourceCollection as an IList - /// - private IList SourceList - { - get { return SourceCollection as IList; } - } - - /// - /// Gets Timestamp used by the NewItemAwareEnumerator to determine if a - /// collection change has occurred since the enumerator began. (If so, - /// MoveNext should throw.) - /// - private int Timestamp - { - get { return _timestamp; } - } - - /// - /// Gets a value indicating whether a private copy of the data - /// is needed for sorting, filtering, and paging. We want any deriving - /// classes to also be able to access this value to see whether or not - /// to use the default source collection, or the internal list. - /// - //TODO Paging - private bool UsesLocalArray - { - get { return SortDescriptions.Count > 0 || Filter != null || _pageSize > 0 || GroupDescriptions.Count > 0; } - } - - /// - /// Return the item at the specified index - /// - /// Index of the item we want to retrieve - /// The item at the specified index - public object this[int index] - { - get { return GetItemAt(index); } - } - - bool IList.IsFixedSize => SourceList?.IsFixedSize ?? true; - bool IList.IsReadOnly => SourceList?.IsReadOnly ?? true; - bool ICollection.IsSynchronized => false; - object ICollection.SyncRoot => this; - - object IList.this[int index] - { - get => this[index]; - set - { - SourceList[index] = value; - if (SourceList is not INotifyCollectionChanged) - { - // TODO: implement Replace - ProcessCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, value)); - } - } - } - - /// - /// Add a new item to the underlying collection. Returns the new item. - /// After calling AddNew and changing the new item as desired, either - /// CommitNew or CancelNew" should be called to complete the transaction. - /// - /// The new item we are adding - //TODO Paging - public object AddNew() - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - - if (IsEditingItem) - { - // Implicitly close a previous EditItem - CommitEdit(); - } - - // Implicitly close a previous AddNew - CommitNew(); - - // Checking CanAddNew will validate that we have the correct itemConstructor - if (!CanAddNew) - { - throw new InvalidOperationException(GetOperationNotAllowedText(nameof(AddNew))); - } - - object newItem = null; - - if (_itemConstructor != null) - { - newItem = _itemConstructor.Invoke(null); - } - - try - { - // temporarily disable the CollectionChanged event - // handler so filtering, sorting, or grouping - // doesn't get applied yet - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); - - if (SourceList != null) - { - SourceList.Add(newItem); - } - } - finally - { - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); - } - - // Modify our _trackingEnumerator so that it shows that our collection is "up to date" - // and will not refresh for now. - _trackingEnumerator = _sourceCollection.GetEnumerator(); - - int addIndex; - int removeIndex = -1; - - // Adjust index based on where it should be displayed in view. - if (PageSize > 0) - { - // if the page is full (Count==PageSize), then replace last item (Count-1). - // otherwise, we just append at end (Count). - addIndex = Count - ((Count == PageSize) ? 1 : 0); - - // if the page is full, remove the last item to make space for the new one. - removeIndex = (Count == PageSize) ? addIndex : -1; - } - else - { - // for non-paged lists, we want to insert the item - // as the last item in the view - addIndex = Count; - } - - // if we need to remove an item from the view due to paging - if (removeIndex > -1) - { - object removeItem = GetItemAt(removeIndex); - if (IsGrouping) - { - _group.RemoveFromSubgroups(removeItem); - } - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - removeItem, - removeIndex)); - } - - // add the new item to the internal list - _internalList.Insert(ConvertToInternalIndex(addIndex), newItem); - OnPropertyChanged(nameof(ItemCount)); - - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForAdd(null, addIndex); - - if (IsGrouping) - { - _group.InsertSpecialItem(_group.Items.Count, newItem, false); - if (PageSize > 0) - { - _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, newItem, false); - } - } - - // fire collection changed. - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - newItem, - addIndex)); - - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // set the current new item - CurrentAddItem = newItem; - - MoveCurrentTo(newItem); - - // if the new item is editable, call BeginEdit on it - if (newItem is IEditableObject editableObject) - { - editableObject.BeginEdit(); - } - - return newItem; - } - - /// - /// Complete the transaction started by . - /// The pending changes (if any) to the item are discarded. - /// - public void CancelEdit() - { - if (IsAddingNew) - { - throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelEdit), nameof(AddNew))); - } - else if (!CanCancelEdit) - { - throw new InvalidOperationException("CancelEdit is not supported for the current edit item."); - } - - VerifyRefreshNotDeferred(); - - if (CurrentEditItem == null) - { - return; - } - - object editItem = CurrentEditItem; - CurrentEditItem = null; - - if (editItem is IEditableObject ieo) - { - ieo.CancelEdit(); - } - else - { - throw new InvalidOperationException("CancelEdit is not supported for the current edit item."); - } - } - - /// - /// Complete the transaction started by AddNew. The new - /// item is removed from the collection. - /// - //TODO Paging - public void CancelNew() - { - if (IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelNew), nameof(EditItem))); - } - - VerifyRefreshNotDeferred(); - - if (CurrentAddItem == null) - { - return; - } - - // get index of item before it is removed - int index = IndexOf(CurrentAddItem); - - // remove the new item from the underlying collection - try - { - // temporarily disable the CollectionChanged event - // handler so filtering, sorting, or grouping - // doesn't get applied yet - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); - - if (SourceList != null) - { - SourceList.Remove(CurrentAddItem); - } - } - finally - { - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); - } - - // Modify our _trackingEnumerator so that it shows that our collection is "up to date" - // and will not refresh for now. - _trackingEnumerator = _sourceCollection.GetEnumerator(); - - // fire the correct events - if (CurrentAddItem != null) - { - object newItem = EndAddNew(true); - - int addIndex = -1; - - // Adjust index based on where it should be displayed in view. - if (PageSize > 0 && !OnLastLocalPage) - { - // if there is paging and we are not on the last page, we need - // to bring in an item from the next page. - addIndex = Count - 1; - } - - // remove the new item from the internal list - InternalList.Remove(newItem); - - if (IsGrouping) - { - _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false); - if (PageSize > 0) - { - _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false); - } - } - - OnPropertyChanged(nameof(ItemCount)); - - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForRemove(index); - - // fire collection changed. - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - newItem, - index)); - - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // if we need to add an item into the view due to paging - if (addIndex > -1) - { - int internalIndex = ConvertToInternalIndex(addIndex); - object addItem = null; - if (IsGrouping) - { - addItem = _temporaryGroup.LeafAt(internalIndex); - _group.AddToSubgroups(addItem, loading: false); - } - else - { - addItem = InternalItemAt(internalIndex); - } - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - addItem, - IndexOf(addItem))); - } - } - } - - /// - /// Complete the transaction started by . - /// The pending changes (if any) to the item are committed. - /// - //TODO Paging - public void CommitEdit() - { - if (IsAddingNew) - { - throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitEdit), nameof(AddNew))); - } - - VerifyRefreshNotDeferred(); - - if (CurrentEditItem == null) - { - return; - } - - object editItem = CurrentEditItem; - CurrentEditItem = null; - - if (editItem is IEditableObject ieo) - { - ieo.EndEdit(); - } - - if (UsesLocalArray) - { - // first remove the item from the array so that we can insert into the correct position - int removeIndex = IndexOf(editItem); - int internalRemoveIndex = InternalIndexOf(editItem); - _internalList.Remove(editItem); - - // check whether to restore currency to the item being edited - object restoreCurrencyTo = (editItem == CurrentItem) ? editItem : null; - - if (removeIndex >= 0 && IsGrouping) - { - // we can't just call RemoveFromSubgroups, as the group name - // for the item may have changed during the edit. - _group.RemoveItemFromSubgroupsByExhaustiveSearch(editItem); - if (PageSize > 0) - { - _temporaryGroup.RemoveItemFromSubgroupsByExhaustiveSearch(editItem); - } - } - - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - // only adjust currency and fire the event if we actually removed the item - if (removeIndex >= 0) - { - AdjustCurrencyForRemove(removeIndex); - - // raise the remove event so we can next insert it into the correct place - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - editItem, - removeIndex)); - } - - // check to see that the item will be added back in - bool passedFilter = PassesFilter(editItem); - - // if we removed all items from the current page, - // move to the previous page. we do not need to - // fire additional notifications, as moving the page will - // trigger a reset. - if (NeedToMoveToPreviousPage && !passedFilter) - { - MoveToPreviousPage(); - return; - } - - // next process adding it into the correct location - ProcessInsertToCollection(editItem, internalRemoveIndex); - - int pageStartIndex = PageIndex * PageSize; - int nextPageStartIndex = pageStartIndex + PageSize; - - if (IsGrouping) - { - int leafIndex = -1; - if (passedFilter && PageSize > 0) - { - _temporaryGroup.AddToSubgroups(editItem, false /*loading*/); - leafIndex = _temporaryGroup.LeafIndexOf(editItem); - } - - // if we are not paging, we should just be able to add the item. - // otherwise, we need to validate that it is within the current page. - if (passedFilter && (PageSize == 0 || - (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex))) - { - _group.AddToSubgroups(editItem, false /*loading*/); - int addIndex = IndexOf(editItem); - AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - editItem, - addIndex)); - } - else if (PageSize > 0) - { - int addIndex = -1; - if (passedFilter && leafIndex < pageStartIndex) - { - // if the item was added to an earlier page, then we need to bring - // in the item that would have been pushed down to this page - addIndex = pageStartIndex; - } - else if (!OnLastLocalPage && removeIndex >= 0) - { - // if the item was added to a later page, then we need to bring in the - // first item from the next page - addIndex = nextPageStartIndex - 1; - } - - object addItem = _temporaryGroup.LeafAt(addIndex); - if (addItem != null) - { - _group.AddToSubgroups(addItem, false /*loading*/); - addIndex = IndexOf(addItem); - AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - addItem, - addIndex)); - } - } - } - else - { - // if we are still within the view - int addIndex = IndexOf(editItem); - if (addIndex >= 0) - { - AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - editItem, - addIndex)); - } - else if (PageSize > 0) - { - // calculate whether the item was inserted into the previous page - bool insertedToPreviousPage = PassesFilter(editItem) && - (InternalIndexOf(editItem) < ConvertToInternalIndex(0)); - addIndex = insertedToPreviousPage ? 0 : Count - 1; - - // don't fire the event if we are on the last page - // and we don't have any items to bring in. - if (insertedToPreviousPage || (!OnLastLocalPage && removeIndex >= 0)) - { - AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - GetItemAt(addIndex), - addIndex)); - } - } - } - - // now raise currency changes at the end - RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - else if (!Contains(editItem)) - { - // if the item did not belong to the collection, add it - InternalList.Add(editItem); - } - } - - /// - /// Complete the transaction started by AddNew. We follow the WPF - /// convention in that the view's sort, filter, and paging - /// specifications (if any) are applied to the new item. - /// - //TODO Paging - public void CommitNew() - { - if (IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitNew), nameof(EditItem))); - } - - VerifyRefreshNotDeferred(); - - if (CurrentAddItem == null) - { - return; - } - - // End the AddNew transaction - object newItem = EndAddNew(false); - - // keep track of the current item - object previousCurrentItem = CurrentItem; - - // Modify our _trackingEnumerator so that it shows that our collection is "up to date" - // and will not refresh for now. - _trackingEnumerator = _sourceCollection.GetEnumerator(); - - if (UsesLocalArray) - { - // first remove the item from the array so that we can insert into the correct position - int removeIndex = Count - 1; - int internalIndex = _internalList.IndexOf(newItem); - _internalList.Remove(newItem); - - if (IsGrouping) - { - _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false); - if (PageSize > 0) - { - _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false); - } - } - - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForRemove(removeIndex); - - // raise the remove event so we can next insert it into the correct place - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - newItem, - removeIndex)); - - // check to see that the item will be added back in - bool passedFilter = PassesFilter(newItem); - - // next process adding it into the correct location - ProcessInsertToCollection(newItem, internalIndex); - - int pageStartIndex = PageIndex * PageSize; - int nextPageStartIndex = pageStartIndex + PageSize; - - if (IsGrouping) - { - int leafIndex = -1; - if (passedFilter && PageSize > 0) - { - _temporaryGroup.AddToSubgroups(newItem, false /*loading*/); - leafIndex = _temporaryGroup.LeafIndexOf(newItem); - } - - // if we are not paging, we should just be able to add the item. - // otherwise, we need to validate that it is within the current page. - if (passedFilter && (PageSize == 0 || - (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex))) - { - _group.AddToSubgroups(newItem, false /*loading*/); - int addIndex = IndexOf(newItem); - - // adjust currency to either the previous current item if possible - // or to the item at the end of the list where the new item was. - if (previousCurrentItem != null) - { - if (Contains(previousCurrentItem)) - { - AdjustCurrencyForAdd(previousCurrentItem, addIndex); - } - else - { - AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex); - } - } - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - newItem, - addIndex)); - } - else - { - if (!passedFilter && (PageSize == 0 || OnLastLocalPage)) - { - AdjustCurrencyForRemove(removeIndex); - } - else if (PageSize > 0) - { - int addIndex = -1; - if (passedFilter && leafIndex < pageStartIndex) - { - // if the item was added to an earlier page, then we need to bring - // in the item that would have been pushed down to this page - addIndex = pageStartIndex; - } - else if (!OnLastLocalPage) - { - // if the item was added to a later page, then we need to bring in the - // first item from the next page - addIndex = nextPageStartIndex - 1; - } - - object addItem = _temporaryGroup.LeafAt(addIndex); - if (addItem != null) - { - _group.AddToSubgroups(addItem, false /*loading*/); - addIndex = IndexOf(addItem); - - // adjust currency to either the previous current item if possible - // or to the item at the end of the list where the new item was. - if (previousCurrentItem != null) - { - if (Contains(previousCurrentItem)) - { - AdjustCurrencyForAdd(previousCurrentItem, addIndex); - } - else - { - AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex); - } - } - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - addItem, - addIndex)); - } - } - } - } - else - { - // if we are still within the view - int addIndex = IndexOf(newItem); - if (addIndex >= 0) - { - AdjustCurrencyForAdd(newItem, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - newItem, - addIndex)); - } - else - { - if (!passedFilter && (PageSize == 0 || OnLastLocalPage)) - { - AdjustCurrencyForRemove(removeIndex); - } - else if (PageSize > 0) - { - bool insertedToPreviousPage = InternalIndexOf(newItem) < ConvertToInternalIndex(0); - addIndex = insertedToPreviousPage ? 0 : Count - 1; - - // don't fire the event if we are on the last page - // and we don't have any items to bring in. - if (insertedToPreviousPage || !OnLastLocalPage) - { - AdjustCurrencyForAdd(null, addIndex); - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - GetItemAt(addIndex), - addIndex)); - } - } - } - } - - // we want to fire the current changed event, even if we kept - // the same current item and position, since the item was - // removed/added back to the collection - RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - } - - /// - /// Return true if the item belongs to this view. No assumptions are - /// made about the item. This method will behave similarly to IList.Contains(). - /// If the caller knows that the item belongs to the - /// underlying collection, it is more efficient to call PassesFilter. - /// - /// The item we are checking to see whether it is within the collection - /// Boolean value of whether or not the collection contains the item - public bool Contains(object item) - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - return IndexOf(item) >= 0; - } - - /// - /// Enter a Defer Cycle. - /// Defer cycles are used to coalesce changes to the ICollectionView. - /// - /// IDisposable used to notify that we no longer need to defer, when we dispose - public IDisposable DeferRefresh() - { - if (IsAddingNew || IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(DeferRefresh))); - } - - ++_deferLevel; - return new DeferHelper(this); - } - - /// - /// Begins an editing transaction on the given item. The transaction is - /// completed by calling either CommitEdit or CancelEdit. Any changes made - /// to the item during the transaction are considered "pending", provided - /// that the view supports the notion of "pending changes" for the given item. - /// - /// Item we want to edit - public void EditItem(object item) - { - VerifyRefreshNotDeferred(); - - if (IsAddingNew) - { - if (Object.Equals(item, CurrentAddItem)) - { - // EditItem(newItem) is a no-op - return; - } - - // implicitly close a previous AddNew - CommitNew(); - } - - // implicitly close a previous EditItem transaction - CommitEdit(); - - CurrentEditItem = item; - - if (item is IEditableObject ieo) - { - ieo.BeginEdit(); - } - } - - /// - /// Implementation of IEnumerable.GetEnumerator(). - /// This provides a way to enumerate the members of the collection - /// without changing the currency. - /// - /// IEnumerator for the collection - //TODO Paging - public IEnumerator GetEnumerator() - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - - if (IsGrouping) - { - return RootGroup?.GetLeafEnumerator(); - } - - // if we are paging - if (PageSize > 0) - { - List list = new List(); - - // if we are in the middle of asynchronous load - if (PageIndex < 0) - { - return list.GetEnumerator(); - } - - for (int index = _pageSize * PageIndex; - index < (int)Math.Min(_pageSize * (PageIndex + 1), InternalList.Count); - index++) - { - list.Add(InternalList[index]); - } - - return new NewItemAwareEnumerator(this, list.GetEnumerator(), CurrentAddItem); - } - else - { - return new NewItemAwareEnumerator(this, InternalList.GetEnumerator(), CurrentAddItem); - } - } - - /// - /// Interface Implementation for GetEnumerator() - /// - /// IEnumerator that we get from our internal collection - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Retrieve item at the given zero-based index in this DataGridCollectionView, after the source collection - /// is filtered, sorted, and paged. - /// - /// - /// Thrown if index is out of range - /// - /// Index of the item we want to retrieve - /// Item at specified index - public object GetItemAt(int index) - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - - // for indices larger than the count - if (index >= Count || index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (IsGrouping) - { - return RootGroup?.LeafAt(_isUsingTemporaryGroup ? ConvertToInternalIndex(index) : index); - } - - if (IsAddingNew && UsesLocalArray && index == Count - 1) - { - return CurrentAddItem; - } - - return InternalItemAt(ConvertToInternalIndex(index)); - } - - /// - /// Return the index where the given item appears, or -1 if doesn't appear. - /// - /// Item we are searching for - /// Index of specified item - //TODO Paging - public int IndexOf(object item) - { - EnsureCollectionInSync(); - VerifyRefreshNotDeferred(); - - if (IsGrouping) - { - return RootGroup?.LeafIndexOf(item) ?? -1; - } - if (IsAddingNew && Object.Equals(item, CurrentAddItem) && UsesLocalArray) - { - return Count - 1; - } - - int internalIndex = InternalIndexOf(item); - - if (PageSize > 0 && internalIndex != -1) - { - if ((internalIndex >= (PageIndex * _pageSize)) && - (internalIndex < ((PageIndex + 1) * _pageSize))) - { - return internalIndex - (PageIndex * _pageSize); - } - else - { - return -1; - } - } - else - { - return internalIndex; - } - } - - /// - /// Move to the given item. - /// - /// Item we want to move the currency to - /// Whether the operation was successful - public bool MoveCurrentTo(object item) - { - VerifyRefreshNotDeferred(); - - // if already on item, don't do anything - if (Object.Equals(CurrentItem, item)) - { - // also check that we're not fooled by a false null currentItem - if (item != null || IsCurrentInView) - { - return IsCurrentInView; - } - } - - // if the item is not found IndexOf() will return -1, and - // the MoveCurrentToPosition() below will move current to BeforeFirst - // The IndexOf function takes into account paging, filtering, and sorting - return MoveCurrentToPosition(IndexOf(item)); - } - - /// - /// Move to the first item. - /// - /// Whether the operation was successful - public bool MoveCurrentToFirst() - { - VerifyRefreshNotDeferred(); - - return MoveCurrentToPosition(0); - } - - /// - /// Move to the last item. - /// - /// Whether the operation was successful - public bool MoveCurrentToLast() - { - VerifyRefreshNotDeferred(); - - int index = Count - 1; - - return MoveCurrentToPosition(index); - } - - /// - /// Move to the next item. - /// - /// Whether the operation was successful - public bool MoveCurrentToNext() - { - VerifyRefreshNotDeferred(); - - int index = CurrentPosition + 1; - - if (index <= Count) - { - return MoveCurrentToPosition(index); - } - else - { - return false; - } - } - - /// - /// Move CurrentItem to this index - /// - /// Position we want to move the currency to - /// True if the resulting CurrentItem is an item within the view; otherwise False - public bool MoveCurrentToPosition(int position) - { - VerifyRefreshNotDeferred(); - - // We want to allow the user to set the currency to just - // beyond the last item. EnumerableCollectionView in WPF - // also checks (position > Count) though the ListCollectionView - // looks for (position >= Count). - if (position < -1 || position > Count) - { - throw new ArgumentOutOfRangeException(nameof(position)); - } - - if ((position != CurrentPosition || !IsCurrentInSync) - && OkToChangeCurrent()) - { - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - SetCurrentToPosition(position); - OnCurrentChanged(); - - if (IsCurrentAfterLast != oldIsCurrentAfterLast) - { - OnPropertyChanged(nameof(IsCurrentAfterLast)); - } - - if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) - { - OnPropertyChanged(nameof(IsCurrentBeforeFirst)); - } - - OnPropertyChanged(nameof(CurrentPosition)); - OnPropertyChanged(nameof(CurrentItem)); - } - - return IsCurrentInView; - } - - /// - /// Move to the previous item. - /// - /// Whether the operation was successful - public bool MoveCurrentToPrevious() - { - VerifyRefreshNotDeferred(); - - int index = CurrentPosition - 1; - - if (index >= -1) - { - return MoveCurrentToPosition(index); - } - else - { - return false; - } - } - - /// - /// Moves to the first page. - /// - /// Whether or not the move was successful. - //TODO Paging - public bool MoveToFirstPage() - { - return MoveToPage(0); - } - - /// - /// Moves to the last page. - /// The move is only attempted when TotalItemCount is known. - /// - /// Whether or not the move was successful. - //TODO Paging - public bool MoveToLastPage() - { - if (TotalItemCount != -1 && PageSize > 0) - { - return MoveToPage(PageCount - 1); - } - else - { - return false; - } - } - - /// - /// Moves to the page after the current page we are on. - /// - /// Whether or not the move was successful. - //TODO Paging - public bool MoveToNextPage() - { - return MoveToPage(_pageIndex + 1); - } - - /// - /// Requests a page move to page . - /// - /// Index of the target page - /// Whether or not the move was successfully initiated. - //TODO Paging - public bool MoveToPage(int pageIndex) - { - // Boundary checks for negative pageIndex - if (pageIndex < -1) - { - return false; - } - - // if the Refresh is deferred, cache the requested PageIndex so that we - // can move to the desired page when EndDefer is called. - if (IsRefreshDeferred) - { - // set cached value and flag so that we move to the page on EndDefer - _cachedPageIndex = pageIndex; - SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); - return false; - } - - // check for invalid pageIndex - if (pageIndex == -1 && PageSize > 0) - { - return false; - } - - // Check if the target page is out of bound, or equal to the current page - if (pageIndex >= PageCount || _pageIndex == pageIndex) - { - return false; - } - - // Check with the ICollectionView.CurrentChanging listeners if it's OK to move - // on to another page - if (!OkToChangeCurrent()) - { - return false; - } - - if (RaisePageChanging(pageIndex) && pageIndex != -1) - { - // Page move was cancelled. Abort the move, but only if the target index isn't -1. - return false; - } - - // Check if there is a current edited or new item so changes can be committed first. - if (CurrentAddItem != null || CurrentEditItem != null) - { - // Remember current currency values for upcoming OnPropertyChanged notifications - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or - // cancelling an item that is no longer on the current page. That's acceptable and means that - // the potential _newItem or _editItem needs to be committed before this page move. - // The reason why we temporarily reset currency here is to give a chance to the bound - // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit() - // for example as a result of changing currency. - SetCurrentToPosition(-1); - RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // If the bound controls did not successfully end their potential item editing/addition, the - // page move needs to be aborted. - if (CurrentAddItem != null || CurrentEditItem != null) - { - // Since PageChanging was raised and not cancelled, a PageChanged notification needs to be raised - // even though the PageIndex actually did not change. - RaisePageChanged(); - - // Restore original currency - Debug.Assert(CurrentItem == null, "Unexpected CurrentItem != null"); - Debug.Assert(CurrentPosition == -1, "Unexpected CurrentPosition != -1"); - Debug.Assert(IsCurrentBeforeFirst, "Unexpected IsCurrentBeforeFirst == false"); - Debug.Assert(!IsCurrentAfterLast, "Unexpected IsCurrentAfterLast == true"); - - SetCurrentToPosition(oldCurrentPosition); - RaiseCurrencyChanges(false /*fireChangedEvent*/, null /*oldCurrentItem*/, -1 /*oldCurrentPosition*/, - true /*oldIsCurrentBeforeFirst*/, false /*oldIsCurrentAfterLast*/); - - return false; - } - - // Finally raise a CurrentChanging notification for the upcoming currency change - // that will occur in CompletePageMove(pageIndex). - OnCurrentChanging(); - } - - IsPageChanging = true; - CompletePageMove(pageIndex); - - return true; - } - - /// - /// Moves to the page before the current page we are on. - /// - /// Whether or not the move was successful. - //TODO Paging - public bool MoveToPreviousPage() - { - return MoveToPage(_pageIndex - 1); - } - - /// - /// Return true if the item belongs to this view. The item is assumed to belong to the - /// underlying DataCollection; this method merely takes filters into account. - /// It is commonly used during collection-changed notifications to determine if the added/removed - /// item requires processing. - /// Returns true if no filter is set on collection view. - /// - /// The item to compare against the Filter - /// Whether the item passes the filter - public bool PassesFilter(object item) - { - if (Filter != null) - { - return Filter(item); - } - - return true; - } - - /// - /// Re-create the view, using any SortDescriptions and/or Filters. - /// - public void Refresh() - { - if (this is IDataGridEditableCollectionView ecv && (ecv.IsAddingNew || ecv.IsEditingItem)) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Refresh))); - } - - RefreshInternal(); - } - - /// - /// Remove the given item from the underlying collection. It - /// needs to be in the current filtered, sorted, and paged view - /// to call - /// - /// Item we want to remove - public void Remove(object item) - { - int index = IndexOf(item); - if (index >= 0) - { - RemoveAt(index); - } - } - - /// - /// Remove the item at the given index from the underlying collection. - /// The index is interpreted with respect to the view (filtered, sorted, - /// and paged list). - /// - /// Index of the item we want to remove - //TODO Paging - public void RemoveAt(int index) - { - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the collection."); - } - - if (IsEditingItem || IsAddingNew) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(RemoveAt))); - } - else if (!CanRemove) - { - throw new InvalidOperationException("Remove/RemoveAt is not supported."); - } - - VerifyRefreshNotDeferred(); - - // convert the index from "view-relative" to "list-relative" - object item = GetItemAt(index); - - // before we remove the item, see if we are not on the last page - // and will have to bring in a new item to replace it - bool replaceItem = PageSize > 0 && !OnLastLocalPage; - - try - { - // temporarily disable the CollectionChanged event - // handler so filtering, sorting, or grouping - // doesn't get applied yet - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); - - if (SourceList != null) - { - SourceList.Remove(item); - } - } - finally - { - SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); - } - - // Modify our _trackingEnumerator so that it shows that our collection is "up to date" - // and will not refresh for now. - _trackingEnumerator = _sourceCollection.GetEnumerator(); - - Debug.Assert(index == IndexOf(item), "IndexOf returned unexpected value"); - - // remove the item from the internal list - _internalList.Remove(item); - - if (IsGrouping) - { - if (PageSize > 0) - { - _temporaryGroup.RemoveFromSubgroups(item); - } - _group.RemoveFromSubgroups(item); - } - - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForRemove(index); - - // fire remove notification - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - item, - index)); - - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // if we removed all items from the current page, - // move to the previous page. we do not need to - // fire additional notifications, as moving the page will - // trigger a reset. - if (NeedToMoveToPreviousPage) - { - MoveToPreviousPage(); - return; - } - - // if we are paging, we may have to fire another notification for the item - // that needs to replace the one we removed on this page. - if (replaceItem) - { - // we first need to add the item into the current group - if (IsGrouping) - { - object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1); - if (newItem != null) - { - _group.AddToSubgroups(newItem, loading: false); - } - } - - // fire the add notification - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - GetItemAt(PageSize - 1), - PageSize - 1)); - } - } - - /// - /// Helper for SortList to handle nested properties (e.g. Address.Street) - /// - /// parent object - /// property names path - /// property type that we want to check for - /// child object - 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; - } - - /// - /// Fix up CurrentPosition and CurrentItem after a collection change - /// - /// Item that we want to set currency to - /// Index of item involved in the collection change - private void AdjustCurrencyForAdd(object newCurrentItem, int index) - { - if (newCurrentItem != null) - { - int newItemIndex = IndexOf(newCurrentItem); - - // if we already have the correct currency set, we don't - // want to unnecessarily fire events - if (newItemIndex >= 0 && (newItemIndex != CurrentPosition || !IsCurrentInSync)) - { - OnCurrentChanging(); - SetCurrent(newCurrentItem, newItemIndex); - } - return; - } - - if (Count == 1) - { - if (CurrentItem != null || CurrentPosition != -1) - { - // fire current changing notification - OnCurrentChanging(); - } - - // added first item; set current at BeforeFirst - SetCurrent(null, -1); - } - else if (index <= CurrentPosition) - { - // fire current changing notification - OnCurrentChanging(); - - // adjust current index if insertion is earlier - int newPosition = CurrentPosition + 1; - if (newPosition >= Count) - { - // if currency was on last item and it got shifted up, - // keep currency on last item. - newPosition = Count - 1; - } - SetCurrent(GetItemAt(newPosition), newPosition); - } - } - - /// - /// Fix up CurrentPosition and CurrentItem after a collection change - /// - /// Item that we want to set currency to - /// Index of item involved in the collection change - private void AdjustCurrencyForEdit(object newCurrentItem, int index) - { - if (newCurrentItem != null && IndexOf(newCurrentItem) >= 0) - { - OnCurrentChanging(); - SetCurrent(newCurrentItem, IndexOf(newCurrentItem)); - return; - } - - if (index <= CurrentPosition) - { - // fire current changing notification - OnCurrentChanging(); - - // adjust current index if insertion is earlier - int newPosition = CurrentPosition + 1; - if (newPosition < Count) - { - // CurrentItem might be out of sync if underlying list is not INCC - // or if this Add is the result of a Replace (Rem + Add) - SetCurrent(GetItemAt(newPosition), newPosition); - } - else - { - SetCurrent(null, Count); - } - } - } - - /// - /// Fix up CurrentPosition and CurrentItem after a collection change - /// The index can be -1 if the item was removed from a previous page - /// - /// Index of item involved in the collection change - private void AdjustCurrencyForRemove(int index) - { - // adjust current index if deletion is earlier - if (index < CurrentPosition) - { - // fire current changing notification - OnCurrentChanging(); - - SetCurrent(CurrentItem, CurrentPosition - 1); - } - - // adjust current index if > Count - if (CurrentPosition >= Count) - { - // fire current changing notification - OnCurrentChanging(); - - SetCurrentToPosition(Count - 1); - } - - // make sure that current position and item are in sync - if (!IsCurrentInSync) - { - // fire current changing notification - OnCurrentChanging(); - - SetCurrentToPosition(CurrentPosition); - } - } - - /// - /// Returns true if specified flag in flags is set. - /// - /// Flag we are checking for - /// Whether the specified flag is set - private bool CheckFlag(CollectionViewFlags flags) - { - return _flags.HasAllFlags(flags); - } - - /// - /// Called to complete the page move operation to set the - /// current page index. - /// - /// Final page index - //TODO Paging - private void CompletePageMove(int pageIndex) - { - Debug.Assert(_pageIndex != pageIndex, "Unexpected _pageIndex == pageIndex"); - - // to see whether or not to fire an OnPropertyChanged - int oldCount = Count; - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - _pageIndex = pageIndex; - - // update the groups - if (IsGrouping && PageSize > 0) - { - PrepareGroupsForCurrentPage(); - } - - // update currency - if (Count >= 1) - { - SetCurrent(GetItemAt(0), 0); - } - else - { - SetCurrent(null, -1); - } - - IsPageChanging = false; - OnPropertyChanged(nameof(PageIndex)); - RaisePageChanged(); - - // if the count has changed - if (Count != oldCount) - { - OnPropertyChanged(nameof(Count)); - } - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Reset)); - - // Always raise CurrentChanged since the calling method MoveToPage(pageIndex) raised CurrentChanging. - RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - - /// - /// Convert a value for the index passed in to the index it would be - /// relative to the InternalIndex property. - /// - /// Index to convert - /// Value for the InternalIndex - //TODO Paging - private int ConvertToInternalIndex(int index) - { - Debug.Assert(index > -1, "Unexpected index == -1"); - if (PageSize > 0) - { - return (_pageSize * PageIndex) + index; - } - else - { - return index; - } - } - - /// - /// Copy all items from the source collection to the internal list for processing. - /// - private void CopySourceToInternalList() - { - _internalList = new List(); - - IEnumerator enumerator = SourceCollection.GetEnumerator(); - - while (enumerator.MoveNext()) - { - _internalList.Add(enumerator.Current); - } - } - - /// - /// Common functionality used by CommitNew, CancelNew, and when the - /// new item is removed by Remove or Refresh. - /// - /// Whether we canceled the add - /// The new item we ended adding - private object EndAddNew(bool cancel) - { - object newItem = CurrentAddItem; - - CurrentAddItem = null; // leave "adding-new" mode - - if (newItem is IEditableObject ieo) - { - if (cancel) - { - ieo.CancelEdit(); - } - else - { - ieo.EndEdit(); - } - } - - return newItem; - } - - /// - /// Subtracts from the deferLevel counter and calls Refresh() if there are no other defers - /// - private void EndDefer() - { - --_deferLevel; - - if (_deferLevel == 0) - { - if (CheckFlag(CollectionViewFlags.IsUpdatePageSizeDeferred)) - { - SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, false); - PageSize = _cachedPageSize; - } - - if (CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) - { - SetFlag(CollectionViewFlags.IsMoveToPageDeferred, false); - MoveToPage(_cachedPageIndex); - _cachedPageIndex = -1; - } - - if (CheckFlag(CollectionViewFlags.NeedsRefresh)) - { - Refresh(); - } - } - } - - /// - /// Makes sure that the ItemConstructor is set for the correct type - /// - private void EnsureItemConstructor() - { - if (!_itemConstructorIsValid) - { - Type itemType = ItemType; - if (itemType != null) - { - _itemConstructor = itemType.GetConstructor(Type.EmptyTypes); - _itemConstructorIsValid = true; - } - } - } - - /// - /// If the IEnumerable has changed, bring the collection up to date. - /// (This isn't necessary if the IEnumerable is also INotifyCollectionChanged - /// because we keep the collection in sync incrementally.) - /// - private void EnsureCollectionInSync() - { - // if the IEnumerable is not a INotifyCollectionChanged - if (_pollForChanges) - { - try - { - _trackingEnumerator.MoveNext(); - } - catch (InvalidOperationException) - { - // When the collection has been modified, calling MoveNext() - // on the enumerator throws an InvalidOperationException, stating - // that the collection has been modified. Therefore, we know when - // to update our internal collection. - _trackingEnumerator = SourceCollection.GetEnumerator(); - RefreshOrDefer(); - } - } - } - - /// - /// Helper function used to determine the type of an item - /// - /// Whether we should use a representative item - /// The type of the items in the collection - private Type GetItemType(bool useRepresentativeItem) - { - Type collectionType = SourceCollection.GetType(); - Type[] interfaces = collectionType.GetInterfaces(); - - // Look for IEnumerable. All generic collections should implement - // We loop through the interface list, rather than call - // GetInterface(IEnumerableT), so that we handle an ambiguous match - // (by using the first match) without an exception. - for (int i = 0; i < interfaces.Length; ++i) - { - Type interfaceType = interfaces[i]; - if (interfaceType.Name == typeof(IEnumerable<>).Name) - { - // found IEnumerable<>, extract T - Type[] typeParameters = interfaceType.GetGenericArguments(); - if (typeParameters.Length == 1) - { - return typeParameters[0]; - } - } - } - - // No generic information found. Use a representative item instead. - if (useRepresentativeItem) - { - // get type of a representative item - object item = GetRepresentativeItem(); - if (item != null) - { - return item.GetType(); - } - } - - return null; - } - - /// - /// Gets a representative item from the collection - /// - /// An item that can represent the collection - private object GetRepresentativeItem() - { - if (IsEmpty) - { - return null; - } - - IEnumerator enumerator = GetEnumerator(); - while (enumerator.MoveNext()) - { - object item = enumerator.Current; - // Since this collection view does not support a NewItemPlaceholder, - // simply return the first non-null item. - if (item != null) - { - return item; - } - } - - return null; - } - - /// - /// Return index of item in the internal list. - /// - /// The item we are checking - /// Integer value on where in the InternalList the object is located - private int InternalIndexOf(object item) - { - return InternalList.IndexOf(item); - } - - /// - /// Return item at the given index in the internal list. - /// - /// The index we are checking - /// The item at the specified index - private object InternalItemAt(int index) - { - if (index >= 0 && index < InternalList.Count) - { - return InternalList[index]; - } - else - { - return null; - } - } - - /// - /// Ask listeners (via ICollectionView.CurrentChanging event) if it's OK to change currency - /// - /// False if a listener cancels the change, True otherwise - private bool OkToChangeCurrent() - { - DataGridCurrentChangingEventArgs args = new DataGridCurrentChangingEventArgs(); - OnCurrentChanging(args); - return !args.Cancel; - } - - /// - /// Notify listeners that this View has changed - /// - /// - /// CollectionViews (and sub-classes) should take their filter/sort/grouping/paging - /// into account before calling this method to forward CollectionChanged events. - /// - /// - /// The NotifyCollectionChangedEventArgs to be passed to the EventHandler - /// - //TODO Paging - private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - - unchecked - { - // invalidate enumerators because of a change - ++_timestamp; - } - - if (CollectionChanged != null) - { - if (args.Action != NotifyCollectionChangedAction.Add || PageSize == 0 || args.NewStartingIndex < Count) - { - CollectionChanged(this, args); - } - } - - // Collection changes change the count unless an item is being - // replaced within the collection. - if (args.Action != NotifyCollectionChangedAction.Replace) - { - OnPropertyChanged(nameof(Count)); - } - - bool listIsEmpty = IsEmpty; - if (listIsEmpty != CheckFlag(CollectionViewFlags.CachedIsEmpty)) - { - SetFlag(CollectionViewFlags.CachedIsEmpty, listIsEmpty); - OnPropertyChanged(nameof(IsEmpty)); - } - } - - /// - /// Raises the CurrentChanged event - /// - private void OnCurrentChanged() - { - if (CurrentChanged != null && _currentChangedMonitor.Enter()) - { - using (_currentChangedMonitor) - { - CurrentChanged(this, EventArgs.Empty); - } - } - } - - /// - /// Raise a CurrentChanging event that is not cancelable. - /// This is called by CollectionChanges (Add, Remove, and Refresh) that - /// affect the CurrentItem. - /// - /// - /// This CurrentChanging event cannot be canceled. - /// - private void OnCurrentChanging() - { - OnCurrentChanging(uncancelableCurrentChangingEventArgs); - } - - /// - /// Raises the CurrentChanging event - /// - /// - /// CancelEventArgs used by the consumer of the event. args.Cancel will - /// be true after this call if the CurrentItem should not be changed for - /// any reason. - /// - /// - /// This CurrentChanging event cannot be canceled. - /// - private void OnCurrentChanging(DataGridCurrentChangingEventArgs args) - { - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - - if (_currentChangedMonitor.Busy) - { - if (args.IsCancelable) - { - args.Cancel = true; - } - - return; - } - - CurrentChanging?.Invoke(this, args); - } - - /// - /// GroupBy changed handler - /// - /// CollectionViewGroup whose GroupBy has changed - /// Arguments for the NotifyCollectionChanged event - private void OnGroupByChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (IsAddingNew || IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping")); - } - - RefreshOrDefer(); - } - - /// - /// GroupDescription changed handler - /// - /// CollectionViewGroup whose GroupDescription has changed - /// Arguments for the GroupDescriptionChanged event - //TODO Paging - private void OnGroupDescriptionChanged(object sender, EventArgs e) - { - if (IsAddingNew || IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping")); - } - - // we want to make sure that the data is refreshed before we try to move to a page - // since the refresh would take care of the filtering, sorting, and grouping. - RefreshOrDefer(); - - if (PageSize > 0) - { - if (IsRefreshDeferred) - { - // set cached value and flag so that we move to first page on EndDefer - _cachedPageIndex = 0; - SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); - } - else - { - MoveToFirstPage(); - } - } - } - - /// - /// Raises a PropertyChanged event. - /// - /// PropertyChangedEventArgs for this change - private void OnPropertyChanged(PropertyChangedEventArgs e) - { - PropertyChanged?.Invoke(this, e); - } - - /// - /// Helper to raise a PropertyChanged event. - /// - /// Property name for the property that changed - private void OnPropertyChanged(string propertyName) - { - OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Sets up the ActiveComparer for the CollectionViewGroupRoot specified - /// - /// The CollectionViewGroupRoot - private void PrepareGroupingComparer(CollectionViewGroupRoot groupRoot) - { - if (groupRoot == _temporaryGroup || PageSize == 0) - { - if (groupRoot.ActiveComparer is DataGridCollectionViewGroupInternal.ListComparer listComparer) - { - listComparer.ResetList(InternalList); - } - else - { - groupRoot.ActiveComparer = new DataGridCollectionViewGroupInternal.ListComparer(InternalList); - } - } - else if (groupRoot == _group) - { - // create the new comparer based on the current _temporaryGroup - groupRoot.ActiveComparer = new DataGridCollectionViewGroupInternal.CollectionViewGroupComparer(_temporaryGroup); - } - } - - /// - /// Use the GroupDescriptions to place items into their respective groups. - /// This assumes that there is no paging, so we just group the entire collection - /// of items that the CollectionView holds. - /// - private void PrepareGroups() - { - // we should only use this method if we aren't paging - Debug.Assert(PageSize == 0, "Unexpected PageSize != 0"); - - _group.Clear(); - _group.Initialize(); - - _group.IsDataInGroupOrder = CheckFlag(CollectionViewFlags.IsDataInGroupOrder); - - // set to false so that we access internal collection items - // instead of the group items, as they have been cleared - _isGrouping = false; - - if (_group.GroupDescriptions.Count > 0) - { - for (int num = 0, count = _internalList.Count; num < count; ++num) - { - object item = _internalList[num]; - if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) - { - _group.AddToSubgroups(item, loading: true); - } - } - if (IsAddingNew) - { - _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true); - } - } - - _isGrouping = _group.GroupBy != null; - - // now we set the value to false, so that subsequent adds will insert - // into the correct groups. - _group.IsDataInGroupOrder = false; - - // reset the grouping comparer - PrepareGroupingComparer(_group); - } - - /// - /// Use the GroupDescriptions to place items into their respective groups. - /// Because of the fact that we have paging, it is possible that we are only - /// going to need a subset of the items to be displayed. However, before we - /// actually group the entire collection, we can't display the items in the - /// correct order. We therefore want to just create a temporary group with - /// the entire collection, and then using this data we can create the group - /// that is exposed with just the items we need. - /// - private void PrepareTemporaryGroups() - { - _temporaryGroup = new CollectionViewGroupRoot(this, CheckFlag(CollectionViewFlags.IsDataInGroupOrder)); - - foreach (var gd in _group.GroupDescriptions) - { - _temporaryGroup.GroupDescriptions.Add(gd); - } - - _temporaryGroup.Initialize(); - - // set to false so that we access internal collection items - // instead of the group items, as they have been cleared - _isGrouping = false; - - if (_temporaryGroup.GroupDescriptions.Count > 0) - { - for (int num = 0, count = _internalList.Count; num < count; ++num) - { - object item = _internalList[num]; - if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) - { - _temporaryGroup.AddToSubgroups(item, loading: true); - } - } - if (IsAddingNew) - { - _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, CurrentAddItem, true); - } - } - - _isGrouping = _temporaryGroup.GroupBy != null; - - // reset the grouping comparer - PrepareGroupingComparer(_temporaryGroup); - } - - /// - /// Update our Groups private accessor to point to the subset of data - /// covered by the current page, or to display the entire group if paging is not - /// being used. - /// - //TODO Paging - private void PrepareGroupsForCurrentPage() - { - _group.Clear(); - _group.Initialize(); - - // set to indicate that we will be pulling data from the temporary group data - _isUsingTemporaryGroup = true; - - // since we are getting our data from the temporary group, it should - // already be in group order - _group.IsDataInGroupOrder = true; - _group.ActiveComparer = null; - - if (GroupDescriptions.Count > 0) - { - for (int num = 0, count = Count; num < count; ++num) - { - object item = GetItemAt(num); - if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) - { - _group.AddToSubgroups(item, loading: true); - } - } - if (IsAddingNew) - { - _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true); - } - } - - // set flag to indicate that we do not need to access the temporary data any longer - _isUsingTemporaryGroup = false; - - // now we set the value to false, so that subsequent adds will insert - // into the correct groups. - _group.IsDataInGroupOrder = false; - - // reset the grouping comparer - PrepareGroupingComparer(_group); - - _isGrouping = _group.GroupBy != null; - } - - /// - /// Create, filter and sort the local index array. - /// called from Refresh(), override in derived classes as needed. - /// - /// new IEnumerable to associate this view with - /// new local array to use for this view - private IList PrepareLocalArray(IEnumerable enumerable) - { - Debug.Assert(enumerable != null, "Input list to filter/sort should not be null"); - - // filter the collection's array into the local array - List localList = new List(); - - foreach (object item in enumerable) - { - if (Filter == null || PassesFilter(item)) - { - localList.Add(item); - } - } - - // sort the local array - if (!CheckFlag(CollectionViewFlags.IsDataSorted) && SortDescriptions.Count > 0) - { - localList = SortList(localList); - } - - return localList; - } - - /// - /// Process an Add operation from an INotifyCollectionChanged event handler. - /// - /// Item added to the source collection - /// Index item was added into - //TODO Paging - private void ProcessAddEvent(object addedItem, int addIndex) - { - // item to fire remove notification for if necessary - object removeNotificationItem = null; - if (PageSize > 0 && !IsGrouping) - { - removeNotificationItem = (Count == PageSize) ? - GetItemAt(PageSize - 1) : null; - } - - // process the add by filtering and sorting the item - ProcessInsertToCollection( - addedItem, - addIndex); - - // next check if we need to add an item into the current group - bool needsGrouping = false; - if (Count == 1 && GroupDescriptions.Count > 0) - { - // if this is the first item being added - // we want to setup the groups with the - // correct element type comparer - if (PageSize > 0) - { - PrepareGroupingComparer(_temporaryGroup); - } - PrepareGroupingComparer(_group); - } - - if (IsGrouping) - { - int leafIndex = -1; - - if (PageSize > 0) - { - _temporaryGroup.AddToSubgroups(addedItem, false /*loading*/); - leafIndex = _temporaryGroup.LeafIndexOf(addedItem); - } - - // if we are not paging, we should just be able to add the item. - // otherwise, we need to validate that it is within the current page. - if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex) - { - needsGrouping = true; - - int pageStartIndex = PageIndex * PageSize; - - // if the item was inserted on a previous page - if (pageStartIndex > leafIndex && PageSize > 0) - { - addedItem = _temporaryGroup.LeafAt(pageStartIndex); - } - - // if we're grouping and have more items than the - // PageSize will allow, remove the last item - if (PageSize > 0 && _group.ItemCount == PageSize) - { - removeNotificationItem = _group.LeafAt(PageSize - 1); - _group.RemoveFromSubgroups(removeNotificationItem); - } - } - } - - // if we are paging, we may have to fire another notification for the item - // that needs to be removed for the one we added on this page. - if (PageSize > 0 && !OnLastLocalPage && - (((IsGrouping && removeNotificationItem != null) || - (!IsGrouping && (PageIndex + 1) * PageSize > InternalIndexOf(addedItem))))) - { - if (removeNotificationItem != null && removeNotificationItem != addedItem) - { - AdjustCurrencyForRemove(PageSize - 1); - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - removeNotificationItem, - PageSize - 1)); - } - } - - // if we need to add the item into the current group - // that will be displayed - if (needsGrouping) - { - this._group.AddToSubgroups(addedItem, false /*loading*/); - } - - int addedIndex = IndexOf(addedItem); - - // if the item is within the current page - if (addedIndex >= 0) - { - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForAdd(null, addedIndex); - - // fire add notification - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - addedItem, - addedIndex)); - - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - else if (PageSize > 0) - { - // otherwise if the item was added into a previous page - int internalIndex = InternalIndexOf(addedItem); - - if (internalIndex < ConvertToInternalIndex(0)) - { - // fire add notification for item pushed in - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - GetItemAt(0), - 0)); - } - } - } - - /// - /// Process CollectionChanged event on source collection - /// that implements INotifyCollectionChanged. - /// - /// - /// The NotifyCollectionChangedEventArgs to be processed. - /// - private void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) - { - // if we do not want to handle the CollectionChanged event, return - if (!CheckFlag(CollectionViewFlags.ShouldProcessCollectionChanged)) - { - return; - } - - if (args.Action == NotifyCollectionChangedAction.Reset) - { - // if we have no items now, clear our own internal list - if (!SourceCollection.GetEnumerator().MoveNext()) - { - _internalList.Clear(); - } - - // calling Refresh, will fire the collectionchanged event - RefreshOrDefer(); - return; - } - - // fire notifications for removes - if (args.OldItems != null && - (args.Action == NotifyCollectionChangedAction.Remove || - args.Action == NotifyCollectionChangedAction.Replace)) - { - foreach (var removedItem in args.OldItems) - { - ProcessRemoveEvent(removedItem, args.Action == NotifyCollectionChangedAction.Replace); - } - } - - // fire notifications for adds - if (args.NewItems != null && - (args.Action == NotifyCollectionChangedAction.Add || - args.Action == NotifyCollectionChangedAction.Replace)) - { - for (var i = 0; i < args.NewItems.Count; i++) - { - if (Filter == null || PassesFilter(args.NewItems[i])) - { - ProcessAddEvent(args.NewItems[i], args.NewStartingIndex + i); - } - } - } - if (args.Action != NotifyCollectionChangedAction.Replace) - { - OnPropertyChanged(nameof(ItemCount)); - } - } - - /// - /// Process a Remove operation from an INotifyCollectionChanged event handler. - /// - /// Item removed from the source collection - /// Whether this was part of a Replace operation - //TODO Paging - private void ProcessRemoveEvent(object removedItem, bool isReplace) - { - int internalRemoveIndex = -1; - - if (IsGrouping) - { - internalRemoveIndex = PageSize > 0 ? _temporaryGroup.LeafIndexOf(removedItem) : - _group.LeafIndexOf(removedItem); - } - else - { - internalRemoveIndex = InternalIndexOf(removedItem); - } - - int removeIndex = IndexOf(removedItem); - - // remove the item from the collection - _internalList.Remove(removedItem); - - // only fire the remove if it was removed from either the current page, or a previous page - bool needToRemove = (PageSize == 0 && removeIndex >= 0) || (internalRemoveIndex < (PageIndex + 1) * PageSize); - - if (IsGrouping) - { - if (PageSize > 0) - { - _temporaryGroup.RemoveFromSubgroups(removedItem); - } - - if (needToRemove) - { - _group.RemoveFromSubgroups(removeIndex >= 0 ? removedItem : _group.LeafAt(0)); - } - } - - if (needToRemove) - { - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - AdjustCurrencyForRemove(removeIndex); - - // fire remove notification - // if we removed from current page, remove from removeIndex, - // if we removed from previous page, remove first item (index=0) - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - removedItem, - Math.Max(0, removeIndex))); - - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - // if we removed all items from the current page, - // move to the previous page. we do not need to - // fire additional notifications, as moving the page will - // trigger a reset. - if (NeedToMoveToPreviousPage && !isReplace) - { - MoveToPreviousPage(); - return; - } - - // if we are paging, we may have to fire another notification for the item - // that needs to replace the one we removed on this page. - if (PageSize > 0 && Count == PageSize) - { - // we first need to add the item into the current group - if (IsGrouping) - { - object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1); - if (newItem != null) - { - _group.AddToSubgroups(newItem, false /*loading*/); - } - } - - // fire the add notification - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - GetItemAt(PageSize - 1), - PageSize - 1)); - } - } - } - - /// - /// Handles adding an item into the collection, and applying sorting, filtering, grouping, paging. - /// - /// Item to insert in the collection - /// Index to insert item into - private void ProcessInsertToCollection(object item, int index) - { - // first check to see if it passes the filter - if (Filter == null || PassesFilter(item)) - { - if (SortDescriptions.Count > 0) - { - var itemType = ItemType; - foreach (var sort in SortDescriptions) - sort.Initialize(itemType); - - // create the SortFieldComparer to use - var sortFieldComparer = new MergedComparer(this); - - // check if the item would be in sorted order if inserted into the specified index - // otherwise, calculate the correct sorted index - if (index < 0 || /* if item was not originally part of list */ - (index > 0 && (sortFieldComparer.Compare(item, InternalItemAt(index - 1)) < 0)) || /* item has moved up in the list */ - ((index < InternalList.Count - 1) && (sortFieldComparer.Compare(item, InternalItemAt(index)) > 0))) /* item has moved down in the list */ - { - index = sortFieldComparer.FindInsertIndex(item, _internalList); - } - } - - // make sure that the specified insert index is within the valid range - // otherwise, just add it to the end. the index can be set to an invalid - // value if the item was originally not in the collection, on a different - // page, or if it had been previously filtered out. - if (index < 0 || index > _internalList.Count) - { - index = _internalList.Count; - } - - _internalList.Insert(index, item); - } - } - - /// - /// Raises Currency Change events - /// - /// Whether to fire the CurrentChanged event even if the parameters have not changed - /// CurrentItem before processing changes - /// CurrentPosition before processing changes - /// IsCurrentBeforeFirst before processing changes - /// IsCurrentAfterLast before processing changes - private void RaiseCurrencyChanges(bool fireChangedEvent, object oldCurrentItem, int oldCurrentPosition, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast) - { - // fire events for currency changes - if (fireChangedEvent || CurrentItem != oldCurrentItem || CurrentPosition != oldCurrentPosition) - { - OnCurrentChanged(); - } - if (CurrentItem != oldCurrentItem) - { - OnPropertyChanged(nameof(CurrentItem)); - } - if (CurrentPosition != oldCurrentPosition) - { - OnPropertyChanged(nameof(CurrentPosition)); - } - if (IsCurrentAfterLast != oldIsCurrentAfterLast) - { - OnPropertyChanged(nameof(IsCurrentAfterLast)); - } - if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) - { - OnPropertyChanged(nameof(IsCurrentBeforeFirst)); - } - } - - /// - /// Raises the PageChanged event - /// - private void RaisePageChanged() - { - PageChanged?.Invoke(this, EventArgs.Empty); - } - - /// - /// Raises the PageChanging event - /// - /// Index of the requested page - /// True if the event is cancelled (e.Cancel was set to True), False otherwise - private bool RaisePageChanging(int newPageIndex) - { - EventHandler handler = PageChanging; - if (handler != null) - { - PageChangingEventArgs pageChangingEventArgs = new PageChangingEventArgs(newPageIndex); - handler(this, pageChangingEventArgs); - return pageChangingEventArgs.Cancel; - } - - return false; - } - - /// - /// Will call RefreshOverride and clear the NeedsRefresh flag - /// - private void RefreshInternal() - { - RefreshOverride(); - SetFlag(CollectionViewFlags.NeedsRefresh, false); - } - - /// - /// Refresh, or mark that refresh is needed when defer cycle completes. - /// - private void RefreshOrDefer() - { - if (IsRefreshDeferred) - { - SetFlag(CollectionViewFlags.NeedsRefresh, true); - } - else - { - RefreshInternal(); - } - } - - /// - /// Re-create the view, using any SortDescriptions. - /// Also updates currency information. - /// - //TODO Paging - private void RefreshOverride() - { - object oldCurrentItem = CurrentItem; - int oldCurrentPosition = CurrentPosition; - bool oldIsCurrentAfterLast = IsCurrentAfterLast; - bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; - - // set IsGrouping to false - _isGrouping = false; - - // force currency off the collection (gives user a chance to save dirty information) - OnCurrentChanging(); - - // if there's no sort/filter/paging/grouping, just use the collection's array - if (UsesLocalArray) - { - try - { - // apply filtering/sorting through the PrepareLocalArray method - _internalList = PrepareLocalArray(_sourceCollection); - - // apply grouping - if (PageSize == 0) - { - PrepareGroups(); - } - else - { - PrepareTemporaryGroups(); - PrepareGroupsForCurrentPage(); - } - } - catch (TargetInvocationException e) - { - // If there's an exception while invoking PrepareLocalArray, - // we want to unwrap it and throw its inner exception - if (e.InnerException != null) - { - throw e.InnerException; - } - else - { - throw; - } - } - } - else - { - CopySourceToInternalList(); - } - - // check if PageIndex is still valid after filter/sort - if (PageSize > 0 && - PageIndex > 0 && - PageIndex >= PageCount) - { - MoveToPage(PageCount - 1); - } - - // reset currency values - ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - - OnCollectionChanged( - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Reset)); - - // now raise currency changes at the end - RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); - } - - /// - /// Set currency back to the previous value it had if possible. If the item is no longer in view - /// then either use the first item in the view, or if the list is empty, use null. - /// - /// CurrentItem before processing changes - /// IsCurrentBeforeFirst before processing changes - /// IsCurrentAfterLast before processing changes - private void ResetCurrencyValues(object oldCurrentItem, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast) - { - if (oldIsCurrentBeforeFirst || IsEmpty) - { - SetCurrent(null, -1); - } - else if (oldIsCurrentAfterLast) - { - SetCurrent(null, Count); - } - else - { - // try to set currency back to old current item - // if there are duplicates, use the position of the first matching item - int newPosition = IndexOf(oldCurrentItem); - - // if the old current item is no longer in view - if (newPosition < 0) - { - // if we are adding a new item, set it as the current item, otherwise, set it to null - newPosition = 0; - - if (newPosition < Count) - { - SetCurrent(GetItemAt(newPosition), newPosition); - } - else if (!IsEmpty) - { - SetCurrent(GetItemAt(0), 0); - } - else - { - SetCurrent(null, -1); - } - } - else - { - SetCurrent(oldCurrentItem, newPosition); - } - } - } - - /// - /// Set CurrentItem and CurrentPosition, no questions asked! - /// - /// - /// CollectionViews (and sub-classes) should use this method to update - /// the Current values. - /// - /// New CurrentItem - /// New CurrentPosition - private void SetCurrent(object newItem, int newPosition) - { - int count = (newItem != null) ? 0 : (IsEmpty ? 0 : Count); - SetCurrent(newItem, newPosition, count); - } - - /// - /// Set CurrentItem and CurrentPosition, no questions asked! - /// - /// - /// This method can be called from a constructor - it does not call - /// any virtuals. The 'count' parameter is substitute for the real Count, - /// used only when newItem is null. - /// In that case, this method sets IsCurrentAfterLast to true if and only - /// if newPosition >= count. This distinguishes between a null belonging - /// to the view and the dummy null when CurrentPosition is past the end. - /// - /// New CurrentItem - /// New CurrentPosition - /// Numbers of items in the collection - private void SetCurrent(object newItem, int newPosition, int count) - { - if (newItem != null) - { - // non-null item implies position is within range. - // We ignore count - it's just a placeholder - SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, false); - SetFlag(CollectionViewFlags.IsCurrentAfterLast, false); - } - else if (count == 0) - { - // empty collection - by convention both flags are true and position is -1 - SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); - SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); - newPosition = -1; - } - else - { - // null item, possibly within range. - SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, newPosition < 0); - SetFlag(CollectionViewFlags.IsCurrentAfterLast, newPosition >= count); - } - - _currentItem = newItem; - _currentPosition = newPosition; - } - - /// - /// Just move it. No argument check, no events, just move current to position. - /// - /// Position to move the current item to - private void SetCurrentToPosition(int position) - { - if (position < 0) - { - SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); - SetCurrent(null, -1); - } - else if (position >= Count) - { - SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); - SetCurrent(null, Count); - } - else - { - SetFlag(CollectionViewFlags.IsCurrentBeforeFirst | CollectionViewFlags.IsCurrentAfterLast, false); - SetCurrent(GetItemAt(position), position); - } - } - - /// - /// Sets the specified Flag(s) - /// - /// Flags we want to set - /// Value we want to set these flags to - private void SetFlag(CollectionViewFlags flags, bool value) - { - if (value) - { - _flags = _flags | flags; - } - else - { - _flags = _flags & ~flags; - } - } - - /// - /// Set new SortDescription collection; re-hook collection change notification handler - /// - /// SortDescriptionCollection to set the property value to - private void SetSortDescriptions(DataGridSortDescriptionCollection descriptions) - { - if (_sortDescriptions != null) - { - _sortDescriptions.CollectionChanged -= SortDescriptionsChanged; - } - - _sortDescriptions = descriptions; - - if (_sortDescriptions != null) - { - Debug.Assert(_sortDescriptions.Count == 0, "must be empty SortDescription collection"); - _sortDescriptions.CollectionChanged += SortDescriptionsChanged; - } - } - - /// - /// SortDescription was added/removed, refresh DataGridCollectionView - /// - /// Sender that triggered this handler - /// NotifyCollectionChangedEventArgs for this change - private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (IsAddingNew || IsEditingItem) - { - throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Sorting")); - } - - // we want to make sure that the data is refreshed before we try to move to a page - // since the refresh would take care of the filtering, sorting, and grouping. - RefreshOrDefer(); - - if (PageSize > 0) - { - if (IsRefreshDeferred) - { - // set cached value and flag so that we move to first page on EndDefer - _cachedPageIndex = 0; - SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); - } - else - { - MoveToFirstPage(); - } - } - - OnPropertyChanged("SortDescriptions"); - } - - /// - /// Sort the List based on the SortDescriptions property. - /// - /// List of objects to sort - /// The sorted list - private List SortList(List list) - { - Debug.Assert(list != null, "Input list to sort should not be null"); - - IEnumerable seq = (IEnumerable)list; - IComparer comparer = new CultureSensitiveComparer(Culture); - var itemType = ItemType; - - foreach (DataGridSortDescription sort in SortDescriptions) - { - sort.Initialize(itemType); - - if (seq is IOrderedEnumerable orderedEnum) - { - seq = sort.ThenBy(orderedEnum); - } - else - { - seq = sort.OrderBy(seq); - } - } - - return seq.ToList(); - } - - /// - /// Helper to validate that we are not in the middle of a DeferRefresh - /// and throw if that is the case. - /// - private void VerifyRefreshNotDeferred() - { - // If the Refresh is being deferred to change filtering or sorting of the - // data by this DataGridCollectionView, then DataGridCollectionView will not reflect the correct - // state of the underlying data. - if (IsRefreshDeferred) - { - throw new InvalidOperationException("Cannot change or check the contents or current position of the CollectionView while Refresh is being deferred."); - } - } - - int IList.Add(object value) - { - var index = SourceList.Add(value); - if (SourceList is not INotifyCollectionChanged) - { - ProcessCollectionChanged( - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value)); - } - return index; - } - - void IList.Clear() - { - SourceList.Clear(); - if (SourceList is not INotifyCollectionChanged) - { - ProcessCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - void IList.Insert(int index, object value) - { - SourceList.Insert(index, value); - if (SourceList is not INotifyCollectionChanged) - { - // TODO: implement Insert - ProcessCollectionChanged( - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, value)); - } - } - void ICollection.CopyTo(Array array, int index) => InternalList.CopyTo(array, index); - - /// - /// Creates a comparer class that takes in a CultureInfo as a parameter, - /// which it will use when comparing strings. - /// - private class CultureSensitiveComparer : IComparer - { - /// - /// Private accessor for the CultureInfo of our comparer - /// - private CultureInfo _culture; - - /// - /// Creates a comparer which will respect the CultureInfo - /// that is passed in when comparing strings. - /// - /// The CultureInfo to use in string comparisons - public CultureSensitiveComparer(CultureInfo culture) - : base() - { - _culture = culture ?? CultureInfo.InvariantCulture; - } - - /// - /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. - /// - /// first item to compare - /// second item to compare - /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y - /// - /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects. - /// - public int Compare(object x, object y) - { - if (x == null) - { - if (y != null) - { - return -1; - } - return 0; - } - if (y == null) - { - return 1; - } - - // at this point x and y are not null - if (x.GetType() == typeof(string) && y.GetType() == typeof(string)) - { - return _culture.CompareInfo.Compare((string)x, (string)y); - } - else - { - return Comparer.Default.Compare(x, y); - } - } - } - - /// - /// Used to keep track of Defer calls on the DataGridCollectionView, which - /// will prevent the user from calling Refresh() on the view. In order - /// to allow refreshes again, the user will have to call IDisposable.Dispose, - /// to end the Defer operation. - /// - private class DeferHelper : IDisposable - { - /// - /// Private reference to the CollectionView that created this DeferHelper - /// - private DataGridCollectionView collectionView; - - /// - /// Initializes a new instance of the DeferHelper class - /// - /// CollectionView that created this DeferHelper - public DeferHelper(DataGridCollectionView collectionView) - { - this.collectionView = collectionView; - } - - /// - /// Cleanup method called when done using this class - /// - public void Dispose() - { - if (collectionView != null) - { - collectionView.EndDefer(); - collectionView = null; - } - GC.SuppressFinalize(this); - } - } - - /// - /// A simple monitor class to help prevent re-entrant calls - /// - private class SimpleMonitor : IDisposable - { - /// - /// Whether the monitor is entered - /// - private bool entered; - - /// - /// Gets a value indicating whether we have been entered or not - /// - public bool Busy - { - get { return entered; } - } - - /// - /// Sets a value indicating that we have been entered - /// - /// Boolean value indicating whether we were already entered - public bool Enter() - { - if (entered) - { - return false; - } - - entered = true; - return true; - } - - /// - /// Cleanup method called when done using this class - /// - public void Dispose() - { - entered = false; - GC.SuppressFinalize(this); - } - } - - /// - /// IEnumerator generated using the new item taken into account - /// - private class NewItemAwareEnumerator : IEnumerator - { - private enum Position - { - /// - /// Whether the position is before the new item - /// - BeforeNewItem, - - /// - /// Whether the position is on the new item that is being created - /// - OnNewItem, - - /// - /// Whether the position is after the new item - /// - AfterNewItem - } - - /// - /// Initializes a new instance of the NewItemAwareEnumerator class. - /// - /// The DataGridCollectionView we are creating the enumerator for - /// The baseEnumerator that we pass in - /// The new item we are adding to the collection - public NewItemAwareEnumerator(DataGridCollectionView collectionView, IEnumerator baseEnumerator, object newItem) - { - _collectionView = collectionView; - _timestamp = collectionView.Timestamp; - _baseEnumerator = baseEnumerator; - _newItem = newItem; - } - - /// - /// Implements the MoveNext function for IEnumerable - /// - /// Whether we can move to the next item - public bool MoveNext() - { - if (_timestamp != _collectionView.Timestamp) - { - throw new InvalidOperationException("Collection was modified; enumeration operation cannot execute."); - } - - switch (_position) - { - case Position.BeforeNewItem: - if (_baseEnumerator.MoveNext() && - (_newItem == null || _baseEnumerator.Current != _newItem - || _baseEnumerator.MoveNext())) - { - // advance base, skipping the new item - } - else if (_newItem != null) - { - // if base has reached the end, move to new item - _position = Position.OnNewItem; - } - else - { - return false; - } - return true; - } - - // in all other cases, simply advance base, skipping the new item - _position = Position.AfterNewItem; - return _baseEnumerator.MoveNext() && - (_newItem == null - || _baseEnumerator.Current != _newItem - || _baseEnumerator.MoveNext()); - } - - /// - /// Gets the Current value for IEnumerable - /// - public object Current - { - get - { - return (_position == Position.OnNewItem) ? _newItem : _baseEnumerator.Current; - } - } - - /// - /// Implements the Reset function for IEnumerable - /// - public void Reset() - { - _position = Position.BeforeNewItem; - _baseEnumerator.Reset(); - } - - /// - /// CollectionView that we are creating the enumerator for - /// - private DataGridCollectionView _collectionView; - - /// - /// The Base Enumerator that we are passing in - /// - private IEnumerator _baseEnumerator; - - /// - /// The position we are appending items to the enumerator - /// - private Position _position; - - /// - /// Reference to any new item that we want to add to the collection - /// - private object _newItem; - - /// - /// Timestamp to let us know whether there have been updates to the collection - /// - private int _timestamp; - } - - internal class MergedComparer - { - private readonly IComparer[] _comparers; - - public MergedComparer(DataGridSortDescriptionCollection coll) - { - _comparers = MakeComparerArray(coll); - } - public MergedComparer(DataGridCollectionView collectionView) - : this(collectionView.SortDescriptions) - { } - - private static IComparer[] MakeComparerArray(DataGridSortDescriptionCollection coll) - { - return - coll.Select(c => c.Comparer) - .ToArray(); - } - - /// - /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. - /// - /// first item to compare - /// second item to compare - /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y - /// - /// Compares the 2 items using the list of property names and directions. - /// - public int Compare(object x, object y) - { - int result = 0; - - // compare both objects by each of the properties until property values don't match - for (int k = 0; k < _comparers.Length; ++k) - { - var comparer = _comparers[k]; - result = comparer.Compare(x, y); - - if (result != 0) - { - break; - } - } - - return result; - } - - /// - /// Steps through the given list using the comparer to find where - /// to insert the specified item to maintain sorted order - /// - /// Item to insert into the list - /// List where we want to insert the item - /// Index where we should insert into - public int FindInsertIndex(object x, IList list) - { - int min = 0; - int max = list.Count - 1; - int index; - - // run a binary search to find the right index - // to insert into. - while (min <= max) - { - index = (min + max) / 2; - - int result = Compare(x, list[index]); - if (result == 0) - { - return index; - } - else if (result > 0) - { - min = index + 1; - } - else - { - max = index - 1; - } - } - - return min; - } - } - } -} diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs deleted file mode 100644 index 587dd228a3..0000000000 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ /dev/null @@ -1,1369 +0,0 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.Text; -using Avalonia.Controls; -using Avalonia.Controls.Utils; -using Avalonia.Data; -using Avalonia.Data.Converters; -using Avalonia.Utilities; - -namespace Avalonia.Collections -{ - public abstract class DataGridGroupDescription : INotifyPropertyChanged - { - public AvaloniaList GroupKeys { get; } - - public DataGridGroupDescription() - { - GroupKeys = new AvaloniaList(); - GroupKeys.CollectionChanged += (sender, e) => OnPropertyChanged(new PropertyChangedEventArgs(nameof(GroupKeys))); - } - - protected virtual event PropertyChangedEventHandler PropertyChanged; - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged - { - add - { - PropertyChanged += value; - } - - remove - { - PropertyChanged -= value; - } - } - protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) - { - PropertyChanged?.Invoke(this, e); - } - - public virtual string PropertyName => String.Empty; - public abstract object GroupKeyFromItem(object item, int level, CultureInfo culture); - public virtual bool KeysMatch(object groupKey, object itemKey) - { - return object.Equals(groupKey, itemKey); - } - } - public class DataGridPathGroupDescription : DataGridGroupDescription - { - private string _propertyPath; - private Type _propertyType; - private IValueConverter _valueConverter; - private StringComparison _stringComparison = StringComparison.Ordinal; - - public DataGridPathGroupDescription(string propertyPath) - { - _propertyPath = propertyPath; - } - - public override object GroupKeyFromItem(object item, int level, CultureInfo culture) - { - object GetKey(object o) - { - if(o == null) - return null; - - if (_propertyType == null) - _propertyType = GetPropertyType(o); - - return InvokePath(o, _propertyPath, _propertyType); - } - - var key = GetKey(item); - if (key == null) - key = item; - - var valueConverter = ValueConverter; - if (valueConverter != null) - key = valueConverter.Convert(key, typeof(object), level, culture); - - return key; - } - public override bool KeysMatch(object groupKey, object itemKey) - { - if(groupKey is string k1 && itemKey is string k2) - { - return String.Equals(k1, k2, _stringComparison); - } - else - return base.KeysMatch(groupKey, itemKey); - } - public override string PropertyName => _propertyPath; - - public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; } - - private Type GetPropertyType(object o) - { - return o.GetType().GetNestedPropertyType(_propertyPath); - } - private static object InvokePath(object item, string propertyPath, Type propertyType) - { - object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); - if (exception != null) - { - throw exception; - } - return propertyValue; - } - } - - public abstract class DataGridCollectionViewGroup : INotifyPropertyChanged - { - private int _itemCount; - - public object Key { get; } - public int ItemCount => _itemCount; - public IAvaloniaReadOnlyList Items => ProtectedItems; - - protected AvaloniaList ProtectedItems { get; } - protected int ProtectedItemCount - { - get { return _itemCount; } - set - { - _itemCount = value; - OnPropertyChanged(new PropertyChangedEventArgs(nameof(ItemCount))); - } - } - - protected DataGridCollectionViewGroup(object key) - { - Key = key; - ProtectedItems = new AvaloniaList(); - } - - public abstract bool IsBottomLevel { get; } - - protected virtual event PropertyChangedEventHandler PropertyChanged; - event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged - { - add - { - PropertyChanged += value; - } - - remove - { - PropertyChanged -= value; - } - } - protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) - { - PropertyChanged?.Invoke(this, e); - } - } - internal class DataGridCollectionViewGroupInternal : DataGridCollectionViewGroup - { - /// - /// GroupDescription used to define how to group the items - /// - private DataGridGroupDescription _groupBy; - - /// - /// Parent group of this CollectionViewGroupInternal - /// - private readonly DataGridCollectionViewGroupInternal _parentGroup; - - /// - /// Used for detecting stale enumerators - /// - private int _version; - - public DataGridCollectionViewGroupInternal(object key, DataGridCollectionViewGroupInternal parent) - : base(key) - { - _parentGroup = parent; - } - - public override bool IsBottomLevel => _groupBy == null; - - internal int FullCount { get; set; } - - internal DataGridGroupDescription GroupBy - { - get { return _groupBy; } - set - { - bool oldIsBottomLevel = IsBottomLevel; - - if (_groupBy != null) - { - ((INotifyPropertyChanged)_groupBy).PropertyChanged -= OnGroupByChanged; - } - - _groupBy = value; - - if (_groupBy != null) - { - ((INotifyPropertyChanged)_groupBy).PropertyChanged += OnGroupByChanged; - } - - if (oldIsBottomLevel != IsBottomLevel) - { - OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBottomLevel))); - } - } - } - - private void OnGroupByChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { - OnGroupByChanged(); - } - protected virtual void OnGroupByChanged() - { - _parentGroup?.OnGroupByChanged(); - } - - /// - /// Gets or sets the most recent index where activity took place - /// - internal int LastIndex { get; set; } - - /// - /// Gets the first item (leaf) added to this group. If this can't be determined, - /// DependencyProperty.UnsetValue. - /// - internal object SeedItem - { - get - { - if (ItemCount > 0 && (GroupBy == null || GroupBy.GroupKeys.Count == 0)) - { - // look for first item, child by child - for (int k = 0, n = Items.Count; k < n; ++k) - { - if (!(Items[k] is DataGridCollectionViewGroupInternal subgroup)) - { - // child is an item - return it - return Items[k]; - } - else if (subgroup.ItemCount > 0) - { - // child is a nonempty subgroup - ask it - return subgroup.SeedItem; - } - //// otherwise child is an empty subgroup - go to next child - } - - // we shouldn't get here, but just in case... - - return AvaloniaProperty.UnsetValue; - } - else - { - // the group is empty, or it has explicit subgroups. - // In either case, we cannot determine the first item - - // it could have gone into any of the subgroups. - return AvaloniaProperty.UnsetValue; - } - } - } - - private DataGridCollectionViewGroupInternal Parent => _parentGroup; - - /// - /// Adds the specified item to the collection - /// - /// Item to add - internal void Add(object item) - { - ChangeCounts(item, +1); - ProtectedItems.Add(item); - } - - /// - /// Clears the collection of items - /// - internal void Clear() - { - ProtectedItems.Clear(); - FullCount = 1; - ProtectedItemCount = 0; - } - - /// - /// Finds the index of the specified item - /// - /// Item we are looking for - /// Seed of the item we are looking for - /// Comparer used to find the item - /// Low range of item index - /// High range of item index - /// Index of the specified item - protected virtual int FindIndex(object item, object seed, IComparer comparer, int low, int high) - { - int index; - - if (comparer != null) - { - if (comparer is ListComparer listComparer) - { - // reset the IListComparer before each search. This cannot be done - // any less frequently (e.g. in Root.AddToSubgroups), due to the - // possibility that the item may appear in more than one subgroup. - listComparer.Reset(); - } - - if (comparer is CollectionViewGroupComparer groupComparer) - { - // reset the CollectionViewGroupComparer before each search. This cannot be done - // any less frequently (e.g. in Root.AddToSubgroups), due to the - // possibility that the item may appear in more than one subgroup. - groupComparer.Reset(); - } - - for (index = low; index < high; ++index) - { - object seed1 = (ProtectedItems[index] is DataGridCollectionViewGroupInternal subgroup) ? subgroup.SeedItem : ProtectedItems[index]; - if (seed1 == AvaloniaProperty.UnsetValue) - { - continue; - } - if (comparer.Compare(seed, seed1) < 0) - { - break; - } - } - } - else - { - index = high; - } - - return index; - } - - /// - /// Returns an enumerator over the leaves governed by this group - /// - /// Enumerator of leaves - internal IEnumerator GetLeafEnumerator() - { - return new LeafEnumerator(this); - } - - /// - /// Insert a new item or subgroup and return its index. Seed is a - /// representative from the subgroup (or the item itself) that - /// is used to position the new item/subgroup w.r.t. the order given - /// by the comparer. (If comparer is null, just add at the end). - /// - /// Item we are looking for - /// Seed of the item we are looking for - /// Comparer used to find the item - /// The index where the item was inserted - internal int Insert(object item, object seed, IComparer comparer) - { - // never insert the new item/group before the explicit subgroups - int low = (GroupBy == null) ? 0 : GroupBy.GroupKeys.Count; - int index = FindIndex(item, seed, comparer, low, ProtectedItems.Count); - - // now insert the item - ChangeCounts(item, +1); - ProtectedItems.Insert(index, item); - - return index; - } - - /// - /// Return the item at the given index within the list of leaves governed - /// by this group - /// - /// Index of the leaf - /// Item at given index - internal object LeafAt(int index) - { - for (int k = 0, n = Items.Count; k < n; ++k) - { - if (Items[k] is DataGridCollectionViewGroupInternal subgroup) - { - // current item is a group - either drill in, or skip over - if (index < subgroup.ItemCount) - { - return subgroup.LeafAt(index); - } - else - { - index -= subgroup.ItemCount; - } - } - else - { - // current item is a leaf - see if we're done - if (index == 0) - { - return Items[k]; - } - else - { - index -= 1; - } - } - } - - return null; - } - - /// - /// Returns the index of the given item within the list of leaves governed - /// by the full group structure. The item must be a (direct) child of this - /// group. The caller provides the index of the item within this group, - /// if known, or -1 if not. - /// - /// Item we are looking for - /// Index of the leaf - /// Number of items under that leaf - internal int LeafIndexFromItem(object item, int index) - { - int result = 0; - - // accumulate the number of predecessors at each level - for (DataGridCollectionViewGroupInternal group = this; - group != null; - item = group, group = group.Parent, index = -1) - { - // accumulate the number of predecessors at the level of item - for (int k = 0, n = group.Items.Count; k < n; ++k) - { - // if we've reached the item, move up to the next level - if ((index < 0 && Object.Equals(item, group.Items[k])) || - index == k) - { - break; - } - - // accumulate leaf count - DataGridCollectionViewGroupInternal subgroup = group.Items[k] as DataGridCollectionViewGroupInternal; - result += subgroup?.ItemCount ?? 1; - } - } - - return result; - } - - /// - /// Returns the index of the given item within the list of leaves governed - /// by this group - /// - /// Item we are looking for - /// Number of items under that leaf - internal int LeafIndexOf(object item) - { - int leaves = 0; // number of leaves we've passed over so far - for (int k = 0, n = Items.Count; k < n; ++k) - { - if (Items[k] is DataGridCollectionViewGroupInternal subgroup) - { - int subgroupIndex = subgroup.LeafIndexOf(item); - if (subgroupIndex < 0) - { - leaves += subgroup.ItemCount; // item not in this subgroup - } - else - { - return leaves + subgroupIndex; // item is in this subgroup - } - } - else - { - // current item is a leaf - compare it directly - if (Object.Equals(item, Items[k])) - { - return leaves; - } - else - { - leaves += 1; - } - } - } - - // item not found - return -1; - } - - /// - /// Removes the specified item from the collection - /// - /// Item to remove - /// Whether we want to return the leaf index - /// Leaf index where item was removed, if value was specified. Otherwise '-1' - internal int Remove(object item, bool returnLeafIndex) - { - int index = -1; - int localIndex = ProtectedItems.IndexOf(item); - - if (localIndex >= 0) - { - if (returnLeafIndex) - { - index = LeafIndexFromItem(null, localIndex); - } - - ChangeCounts(item, -1); - ProtectedItems.RemoveAt(localIndex); - } - - return index; - } - - /// - /// Removes an empty group from the PagedCollectionView grouping - /// - /// Empty subgroup to remove - private static void RemoveEmptyGroup(DataGridCollectionViewGroupInternal group) - { - DataGridCollectionViewGroupInternal parent = group.Parent; - - if (parent != null) - { - DataGridGroupDescription groupBy = parent.GroupBy; - int index = parent.ProtectedItems.IndexOf(group); - - // remove the subgroup unless it is one of the explicit groups - if (index >= groupBy.GroupKeys.Count) - { - parent.Remove(group, false); - } - } - } - - /// - /// Update the item count of the CollectionViewGroup - /// - /// CollectionViewGroup to update - /// Delta to change count by - protected void ChangeCounts(object item, int delta) - { - bool changeLeafCount = !(item is DataGridCollectionViewGroup); - - for (DataGridCollectionViewGroupInternal group = this; - group != null; - group = group._parentGroup) - { - group.FullCount += delta; - if (changeLeafCount) - { - group.ProtectedItemCount += delta; - - if (group.ProtectedItemCount == 0) - { - RemoveEmptyGroup(group); - } - } - } - - unchecked - { - // this invalidates enumerators - ++_version; - } - } - - /// - /// Enumerator for the leaves in the CollectionViewGroupInternal class. - /// - private class LeafEnumerator : IEnumerator - { - private object _current; // current item - private DataGridCollectionViewGroupInternal _group; // parent group - private int _index; // current index into Items - private IEnumerator _subEnum; // enumerator over current subgroup - private int _version; // parent group's version at ctor - - /// - /// Initializes a new instance of the LeafEnumerator class. - /// - /// CollectionViewGroupInternal that uses the enumerator - public LeafEnumerator(DataGridCollectionViewGroupInternal group) - { - _group = group; - DoReset(); // don't call virtual Reset in ctor - } - - /// - /// Private helper to reset the enumerator - /// - private void DoReset() - { - Debug.Assert(_group != null, "_group should have been initialized in constructor"); - _version = _group._version; - _index = -1; - _subEnum = null; - } - - /// - /// Reset implementation for IEnumerator - /// - void IEnumerator.Reset() - { - DoReset(); - } - - /// - /// MoveNext implementation for IEnumerator - /// - /// Returns whether the MoveNext operation was successful - bool IEnumerator.MoveNext() - { - Debug.Assert(_group != null, "_group should have been initialized in constructor"); - - // check for invalidated enumerator - if (_group._version != _version) - { - throw new InvalidOperationException(); - } - - // move forward to the next leaf - while (_subEnum == null || !_subEnum.MoveNext()) - { - // done with the current top-level item. Move to the next one. - ++_index; - if (_index >= _group.Items.Count) - { - return false; - } - - DataGridCollectionViewGroupInternal subgroup = _group.Items[_index] as DataGridCollectionViewGroupInternal; - if (subgroup == null) - { - // current item is a leaf - it's the new Current - _current = _group.Items[_index]; - _subEnum = null; - return true; - } - else - { - // current item is a subgroup - get its enumerator - _subEnum = subgroup.GetLeafEnumerator(); - } - } - - // the loop terminates only when we have a subgroup enumerator - // positioned at the new Current item - _current = _subEnum.Current; - return true; - } - - /// - /// Gets the current implementation for IEnumerator - /// - object IEnumerator.Current - { - get - { - Debug.Assert(_group != null, "_group should have been initialized in constructor"); - - if (_index < 0 || _index >= _group.Items.Count) - { - throw new InvalidOperationException(); - } - - return _current; - } - } - - } - - // / - // / This comparer is used to insert an item into a group in a position consistent - // / with a given IList. It only works when used in the pattern that FindIndex - // / uses, namely first call Reset(), then call Compare(item, itemSequence) any number of - // / times with the same item (the new item) as the first argument, and a sequence - // / of items as the second argument that appear in the IList in the same sequence. - // / This makes the total search time linear in the size of the IList. (To give - // / the correct answer regardless of the sequence of arguments would involve - // / calling IndexOf and leads to O(N^2) total search time.) - // / - internal class ListComparer : IComparer - { - /// - /// Constructor for the ListComparer that takes - /// in an IList. - /// - /// IList used to compare on - internal ListComparer(IList list) - { - ResetList(list); - } - - /// - /// Sets the index that we start comparing - /// from to 0. - /// - internal void Reset() - { - _index = 0; - } - - /// - /// Sets our IList to a new instance - /// of a list being passed in and resets - /// the index. - /// - /// IList used to compare on - internal void ResetList(IList list) - { - _list = list; - _index = 0; - } - - /// - /// Compares objects x and y to see which one - /// should appear first. - /// - /// The first object - /// The second object - /// -1 if x is less than y, +1 otherwise - public int Compare(object x, object y) - { - if (Object.Equals(x, y)) - { - return 0; - } - - // advance the index until seeing one x or y - int n = (_list != null) ? _list.Count : 0; - for (; _index < n; ++_index) - { - object z = _list[_index]; - if (Object.Equals(x, z)) - { - return -1; // x occurs first, so x < y - } - else if (Object.Equals(y, z)) - { - return +1; // y occurs first, so x > y - } - } - - // if we don't see either x or y, declare x > y. - // This has the effect of putting x at the end of the list. - return +1; - } - - private int _index; - private IList _list; - } - - // / - // / This comparer is used to insert an item into a group in a position consistent - // / with a given CollectionViewGroupRoot. We will only use this when dealing with - // / a temporary CollectionViewGroupRoot that points to the correct grouping of the - // / entire collection, and we have paging that requires us to keep the paged group - // / consistent with the order of items in the temporary group. - // / - internal class CollectionViewGroupComparer : IComparer - { - /// - /// Constructor for the CollectionViewGroupComparer that takes - /// in an CollectionViewGroupRoot. - /// - /// CollectionViewGroupRoot used to compare on - internal CollectionViewGroupComparer(CollectionViewGroupRoot group) - { - ResetGroup(group); - } - - /// - /// Sets the index that we start comparing - /// from to 0. - /// - internal void Reset() - { - _index = 0; - } - - /// - /// Sets our group to a new instance of a - /// CollectionViewGroupRoot being passed in - /// and resets the index. - /// - /// CollectionViewGroupRoot used to compare on - internal void ResetGroup(CollectionViewGroupRoot group) - { - _group = group; - _index = 0; - } - - /// - /// Compares objects x and y to see which one - /// should appear first. - /// - /// The first object - /// The second object - /// -1 if x is less than y, +1 otherwise - public int Compare(object x, object y) - { - if (Object.Equals(x, y)) - { - return 0; - } - - // advance the index until seeing one x or y - int n = (_group != null) ? _group.ItemCount : 0; - for (; _index < n; ++_index) - { - object z = _group.LeafAt(_index); - if (Object.Equals(x, z)) - { - return -1; // x occurs first, so x < y - } - else if (Object.Equals(y, z)) - { - return +1; // y occurs first, so x > y - } - } - - // if we don't see either x or y, declare x > y. - // This has the effect of putting x at the end of the list. - return +1; - } - - private int _index; - private CollectionViewGroupRoot _group; - } - - } - - internal class CollectionViewGroupRoot : DataGridCollectionViewGroupInternal, INotifyCollectionChanged - { - /// - /// String constant used for the Root Name - /// - private const string RootName = "Root"; - - /// - /// Private accessor for empty object instance - /// - private static readonly object UseAsItemDirectly = new object(); - - /// - /// Private accessor for the top level GroupDescription - /// - private static DataGridGroupDescription topLevelGroupDescription; - - /// - /// Private accessor for an ObservableCollection containing group descriptions - /// - private readonly AvaloniaList _groupBy = new AvaloniaList(); - - /// - /// Indicates whether the list of items (after applying the sort and filters, if any) - /// is already in the correct order for grouping. - /// - private bool _isDataInGroupOrder; - - /// - /// Private accessor for the owning ICollectionView - /// - private readonly IDataGridCollectionView _view; - - /// - /// Raise this event when the (grouped) view changes - /// - public event NotifyCollectionChangedEventHandler CollectionChanged; - - /// - /// Raise this event when the GroupDescriptions change - /// - internal event EventHandler GroupDescriptionChanged; - - /// - /// Initializes a new instance of the CollectionViewGroupRoot class. - /// - /// CollectionView that contains this grouping - /// True if items are already in correct order for grouping - internal CollectionViewGroupRoot(IDataGridCollectionView view, bool isDataInGroupOrder) - : base(RootName, null) - { - _view = view; - _isDataInGroupOrder = isDataInGroupOrder; - } - - /// - /// Gets the description of grouping, indexed by level. - /// - public virtual AvaloniaList GroupDescriptions => _groupBy; - - /// - /// Gets or sets the current IComparer being used - /// - internal IComparer ActiveComparer { get; set; } - - /// - /// Gets the culture to use during sorting. - /// - internal CultureInfo Culture - { - get - { - Debug.Assert(_view != null, "this._view should have been set from the constructor"); - return _view.Culture; - } - } - - /// - /// Gets or sets a value indicating whether the data is in group order - /// - internal bool IsDataInGroupOrder - { - get { return _isDataInGroupOrder; } - set { _isDataInGroupOrder = value; } - } - - /// - /// Finds the index of the specified item - /// - /// Item we are looking for - /// Seed of the item we are looking for - /// Comparer used to find the item - /// Low range of item index - /// High range of item index - /// Index of the specified item - protected override int FindIndex(object item, object seed, IComparer comparer, int low, int high) - { - // root group needs to adjust the bounds of the search to exclude the new item (if any) - if (_view is IDataGridEditableCollectionView iecv && iecv.IsAddingNew) - { - --high; - } - - return base.FindIndex(item, seed, comparer, low, high); - } - - /// - /// Initializes the group descriptions - /// - internal void Initialize() - { - if (topLevelGroupDescription == null) - { - topLevelGroupDescription = new TopLevelGroupDescription(); - } - - InitializeGroup(this, 0, null); - } - - /// - /// Inserts specified item into the collection - /// - /// Index to insert into - /// Item to insert - /// Whether we are currently loading - internal void InsertSpecialItem(int index, object item, bool loading) - { - ChangeCounts(item, +1); - ProtectedItems.Insert(index, item); - - if (!loading) - { - int globalIndex = LeafIndexFromItem(item, index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, globalIndex)); - } - } - - /// - /// Notify listeners that this View has changed - /// - /// - /// CollectionViews (and sub-classes) should take their filter/sort/grouping - /// into account before calling this method to forward CollectionChanged events. - /// - /// The NotifyCollectionChangedEventArgs to be passed to the EventHandler - public void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - Debug.Assert(args != null, "Arguments passed in should not be null"); - CollectionChanged?.Invoke(this, args); - } - - /// - /// Notify host that a group description has changed somewhere in the tree - /// - protected override void OnGroupByChanged() - { - GroupDescriptionChanged?.Invoke(this, EventArgs.Empty); - } - - /// - /// Remove specified item from subgroups - /// - /// Item to remove - /// Whether the operation was successful - internal bool RemoveFromSubgroups(object item) - { - return RemoveFromSubgroups(item, this, 0); - } - - /// - /// Remove specified item from subgroups using an exhaustive search - /// - /// Item to remove - internal void RemoveItemFromSubgroupsByExhaustiveSearch(object item) - { - RemoveItemFromSubgroupsByExhaustiveSearch(this, item); - } - - /// - /// Removes specified item into the collection - /// - /// Index to remove from - /// Item to remove - /// Whether we are currently loading - internal void RemoveSpecialItem(int index, object item, bool loading) - { - Debug.Assert(Object.Equals(item, ProtectedItems[index]), "RemoveSpecialItem finds inconsistent data"); - int globalIndex = -1; - - if (!loading) - { - globalIndex = LeafIndexFromItem(item, index); - } - - ChangeCounts(item, -1); - ProtectedItems.RemoveAt(index); - - if (!loading) - { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, globalIndex)); - } - } - - /// - /// Adds specified item to subgroups - /// - /// Item to add - /// Whether we are currently loading - internal void AddToSubgroups(object item, bool loading) - { - AddToSubgroups(item, this, 0, loading); - } - - /// - /// Add an item to the subgroup with the given name - /// - /// Item to add - /// Group to add item to - /// The level of grouping. - /// Name of subgroup to add to - /// Whether we are currently loading - private void AddToSubgroup(object item, DataGridCollectionViewGroupInternal group, int level, object key, bool loading) - { - DataGridCollectionViewGroupInternal subgroup; - int index = (_isDataInGroupOrder) ? group.LastIndex : 0; - - // find the desired subgroup - for (int n = group.Items.Count; index < n; ++index) - { - subgroup = group.Items[index] as DataGridCollectionViewGroupInternal; - if (subgroup == null) - { - continue; // skip children that are not groups - } - - if (group.GroupBy.KeysMatch(subgroup.Key, key)) - { - group.LastIndex = index; - AddToSubgroups(item, subgroup, level + 1, loading); - return; - } - } - - // the item didn't match any subgroups. Create a new subgroup and add the item. - subgroup = new DataGridCollectionViewGroupInternal(key, group); - InitializeGroup(subgroup, level + 1, item); - - if (loading) - { - group.Add(subgroup); - group.LastIndex = index; - } - else - { - // using insert will find the correct sort index to - // place the subgroup, and will default to the last - // position if no ActiveComparer is specified - group.Insert(subgroup, item, ActiveComparer); - } - - AddToSubgroups(item, subgroup, level + 1, loading); - } - - /// - /// Add an item to the desired subgroup(s) of the given group - /// - /// Item to add - /// Group to add item to - /// The level of grouping - /// Whether we are currently loading - private void AddToSubgroups(object item, DataGridCollectionViewGroupInternal group, int level, bool loading) - { - object key = GetGroupKey(item, group.GroupBy, level); - - if (key == UseAsItemDirectly) - { - // the item belongs to the group itself (not to any subgroups) - if (loading) - { - group.Add(item); - } - else - { - int localIndex = group.Insert(item, item, ActiveComparer); - int index = group.LeafIndexFromItem(item, localIndex); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); - } - } - else if(key is ICollection keyList) - { - // the item belongs to multiple subgroups - foreach (object o in keyList) - { - AddToSubgroup(item, group, level, o, loading); - } - } - else - { - // the item belongs to one subgroup - AddToSubgroup(item, group, level, key, loading); - } - } - - public virtual Func GroupBySelector { get; set; } - - /// - /// Returns the description of how to divide the given group into subgroups - /// - /// CollectionViewGroup to get group description from - /// The level of grouping - /// GroupDescription of how to divide the given group - private DataGridGroupDescription GetGroupDescription(DataGridCollectionViewGroup group, int level) - { - DataGridGroupDescription result = null; - if (group == this) - { - group = null; - } - - if (result == null && GroupBySelector != null) - { - result = GroupBySelector?.Invoke(group, level); - } - - if (result == null && level < GroupDescriptions.Count) - { - result = GroupDescriptions[level]; - } - - return result; - } - - /// - /// Get the group name(s) for the given item - /// - /// Item to get group name for - /// GroupDescription for the group - /// The level of grouping - /// Group names for the specified item - private object GetGroupKey(object item, DataGridGroupDescription groupDescription, int level) - { - if (groupDescription != null) - { - return groupDescription.GroupKeyFromItem(item, level, Culture); - } - else - { - return UseAsItemDirectly; - } - } - - /// - /// Initialize the given group - /// - /// Group to initialize - /// The level of grouping - /// The seed item to compare with to see where to insert - private void InitializeGroup(DataGridCollectionViewGroupInternal group, int level, object seedItem) - { - // set the group description for dividing the group into subgroups - DataGridGroupDescription groupDescription = GetGroupDescription(group, level); - group.GroupBy = groupDescription; - - // create subgroups for each of the explicit names - var keys = groupDescription?.GroupKeys; - if (keys != null) - { - for (int k = 0, n = keys.Count; k < n; ++k) - { - DataGridCollectionViewGroupInternal subgroup = new DataGridCollectionViewGroupInternal(keys[k], group); - InitializeGroup(subgroup, level + 1, seedItem); - group.Add(subgroup); - } - } - - group.LastIndex = 0; - } - - /// - /// Remove an item from the direct children of a group. - /// - /// Group to remove item from - /// Item to remove - /// True if item could not be removed - private bool RemoveFromGroupDirectly(DataGridCollectionViewGroupInternal group, object item) - { - int leafIndex = group.Remove(item, true); - if (leafIndex >= 0) - { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, leafIndex)); - return false; - } - else - { - return true; - } - } - - /// - /// Remove an item from the subgroup with the given name. - /// - /// Item to remove - /// Group to remove item from - /// The level of grouping - /// Name of item to remove - /// Return true if the item was not in one of the subgroups it was supposed to be. - private bool RemoveFromSubgroup(object item, DataGridCollectionViewGroupInternal group, int level, object key) - { - bool itemIsMissing = false; - DataGridCollectionViewGroupInternal subgroup; - - // find the desired subgroup - for (int index = 0, n = group.Items.Count; index < n; ++index) - { - subgroup = group.Items[index] as DataGridCollectionViewGroupInternal; - if (subgroup == null) - { - continue; // skip children that are not groups - } - - if (group.GroupBy.KeysMatch(subgroup.Key, key)) - { - if (RemoveFromSubgroups(item, subgroup, level + 1)) - { - itemIsMissing = true; - } - - return itemIsMissing; - } - } - - // the item didn't match any subgroups. It should have. - return true; - } - - /// - /// Remove an item from the desired subgroup(s) of the given group. - /// - /// Item to remove - /// Group to remove item from - /// The level of grouping - /// Return true if the item was not in one of the subgroups it was supposed to be. - private bool RemoveFromSubgroups(object item, DataGridCollectionViewGroupInternal group, int level) - { - bool itemIsMissing = false; - object key = GetGroupKey(item, group.GroupBy, level); - - if (key == UseAsItemDirectly) - { - // the item belongs to the group itself (not to any subgroups) - itemIsMissing = RemoveFromGroupDirectly(group, item); - } - else if (key is ICollection keyList) - { - // the item belongs to multiple subgroups - foreach (object o in keyList) - { - if (RemoveFromSubgroup(item, group, level, o)) - { - itemIsMissing = true; - } - } - } - else - { - // the item belongs to one subgroup - if (RemoveFromSubgroup(item, group, level, key)) - { - itemIsMissing = true; - } - } - - return itemIsMissing; - } - - /// - /// The item did not appear in one or more of the subgroups it - /// was supposed to. This can happen if the item's properties - /// change so that the group names we used to insert it are - /// different from the names used to remove it. If this happens, - /// remove the item the hard way. - /// - /// Group to remove item from - /// Item to remove - private void RemoveItemFromSubgroupsByExhaustiveSearch(DataGridCollectionViewGroupInternal group, object item) - { - // try to remove the item from the direct children - // this function only returns true if it failed to remove from group directly - // in which case we will step through and search exhaustively - if (RemoveFromGroupDirectly(group, item)) - { - // if that didn't work, recurse into each subgroup - // (loop runs backwards in case an entire group is deleted) - for (int k = group.Items.Count - 1; k >= 0; --k) - { - if (group.Items[k] is DataGridCollectionViewGroupInternal subgroup) - { - RemoveItemFromSubgroupsByExhaustiveSearch(subgroup, item); - } - } - } - } - - /// - /// TopLevelGroupDescription class - /// - private class TopLevelGroupDescription : DataGridGroupDescription - { - /// - /// Initializes a new instance of the TopLevelGroupDescription class. - /// - public TopLevelGroupDescription() - { - } - - /// - /// We have to implement this abstract method, but it should never be called - /// - /// Item to get group name from - /// The level of grouping - /// Culture used for sorting - /// We do not return a value here - public override object GroupKeyFromItem(object item, int level, CultureInfo culture) - { - Debug.Assert(true, "We have to implement this abstract method, but it should never be called"); - return null; - } - } - } - -} diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs deleted file mode 100644 index 6503e9e825..0000000000 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ /dev/null @@ -1,324 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using Avalonia.Controls.Utils; - -namespace Avalonia.Collections -{ - public abstract class DataGridSortDescription - { - public virtual string PropertyPath => null; - - public virtual ListSortDirection Direction => ListSortDirection.Ascending; - public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); - public abstract IComparer Comparer { get; } - - public virtual IOrderedEnumerable OrderBy(IEnumerable seq) - { - return seq.OrderBy(o => o, Comparer); - } - public virtual IOrderedEnumerable ThenBy(IOrderedEnumerable seq) - { - return seq.ThenBy(o => o, Comparer); - } - - public 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; - } - - /// - /// Creates a comparer class that takes in a CultureInfo as a parameter, - /// which it will use when comparing strings. - /// - private class CultureSensitiveComparer : Comparer - { - /// - /// Private accessor for the CultureInfo of our comparer - /// - private CultureInfo _culture; - - /// - /// Creates a comparer which will respect the CultureInfo - /// that is passed in when comparing strings. - /// - /// The CultureInfo to use in string comparisons - public CultureSensitiveComparer(CultureInfo culture) - : base() - { - _culture = culture ?? CultureInfo.InvariantCulture; - } - - /// - /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. - /// - /// first item to compare - /// second item to compare - /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y - /// - /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects. - /// - 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.Default.Compare(x, y); - } - } - - } - - private class DataGridPathSortDescription : DataGridSortDescription - { - private readonly ListSortDirection _direction; - private readonly string _propertyPath; - private readonly Lazy _cultureSensitiveComparer; - private readonly Lazy> _comparer; - private Type _propertyType; - private IComparer _internalComparer; - private IComparer _internalComparerTyped; - private IComparer InternalComparer - { - get - { - if (_internalComparerTyped == null && _internalComparer != null) - { - if (_internalComparer is IComparer c) - _internalComparerTyped = c; - else - _internalComparerTyped = Comparer.Create((x, y) => _internalComparer.Compare(x, y)); - } - - return _internalComparerTyped; - } - } - - public override string PropertyPath => _propertyPath; - public override IComparer Comparer => _comparer.Value; - public override ListSortDirection Direction => _direction; - - public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture) - { - _propertyPath = propertyPath; - _direction = direction; - _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); - _internalComparer = internalComparer; - _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); - } - private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction) - { - _propertyPath = inner._propertyPath; - _direction = direction; - _propertyType = inner._propertyType; - _cultureSensitiveComparer = inner._cultureSensitiveComparer; - _internalComparer = inner._internalComparer; - _internalComparerTyped = inner._internalComparerTyped; - - _comparer = new Lazy>(() => Comparer.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 GetComparerForNotStringType(type); - } - - internal static IComparer GetComparerForNotStringType(Type type) - { -#if NET6_0_OR_GREATER - if(System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported == false) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && type.GetGenericArguments()[0].IsAssignableTo(typeof(IComparable))) - return Comparer.Create((x, y) => - { - if (x == null) - return y == null ? 0 : -1; - else - return (x as IComparable)!.CompareTo(y); - }); - else if (type.IsAssignableTo(typeof(IComparable))) //enum should be here - return Comparer.Create((x, y) => (x as IComparable)!.CompareTo(y)); - else - return Comparer.Create((x, y) => 0); //avoid using reflection to avoid crash on AOT - } - else -#endif - 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 (Direction == ListSortDirection.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 OrderBy(IEnumerable seq) - { - if (Direction == ListSortDirection.Descending) - { - return seq.OrderByDescending(o => GetValue(o), InternalComparer); - } - else - { - return seq.OrderBy(o => GetValue(o), InternalComparer); - } - } - public override IOrderedEnumerable ThenBy(IOrderedEnumerable seq) - { - if (Direction == ListSortDirection.Descending) - { - return seq.ThenByDescending(o => GetValue(o), InternalComparer); - } - else - { - return seq.ThenBy(o => GetValue(o), InternalComparer); - } - } - - public override DataGridSortDescription SwitchSortDirection() - { - var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; - return new DataGridPathSortDescription(this, newDirection); - } - } - - public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null) - { - return new DataGridPathSortDescription(propertyPath, direction, null, culture); - } - - public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer) - { - return new DataGridPathSortDescription(propertyPath, direction, comparer, null); - } - - public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending) - { - return new DataGridComparerSortDescription(comparer, direction); - } - } - - public class DataGridComparerSortDescription : DataGridSortDescription - { - private readonly IComparer _innerComparer; - private readonly ListSortDirection _direction; - private readonly IComparer _comparer; - - public IComparer SourceComparer => _innerComparer; - public override IComparer Comparer => _comparer; - public override ListSortDirection Direction => _direction; - public DataGridComparerSortDescription(IComparer comparer, ListSortDirection direction) - { - _innerComparer = comparer; - _direction = direction; - _comparer = Comparer.Create((x, y) => Compare(x, y)); - } - - private int Compare(object x, object y) - { - int result = _innerComparer.Compare(x, y); - - if (Direction == ListSortDirection.Descending) - return -result; - else - return result; - } - public override DataGridSortDescription SwitchSortDirection() - { - var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; - return new DataGridComparerSortDescription(_innerComparer, newDirection); - } - } - - public class DataGridSortDescriptionCollection : AvaloniaList - { } -} diff --git a/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs deleted file mode 100644 index 07da567849..0000000000 --- a/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs +++ /dev/null @@ -1,233 +0,0 @@ -// (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 -{ - /// Provides data for the event. - public class DataGridCurrentChangingEventArgs : EventArgs - { - private bool _cancel; - private bool _isCancelable; - - /// Initializes a new instance of the class and sets the property to true. - public DataGridCurrentChangingEventArgs() - { - Initialize(true); - } - - /// Initializes a new instance of the class and sets the property to the specified value. - /// true to disable the ability to cancel a change; false to enable cancellation. - public DataGridCurrentChangingEventArgs(bool isCancelable) - { - Initialize(isCancelable); - } - - private void Initialize(bool isCancelable) - { - _isCancelable = isCancelable; - } - - /// Gets a value that indicates whether the change can be canceled. - /// true if the event can be canceled; false if the event cannot be canceled. - public bool IsCancelable - { - get - { - return _isCancelable; - } - } - - /// Gets or sets a value that indicates whether the change should be canceled. - /// true if the event should be canceled; otherwise, false. The default is false. - /// The property value is false. - public bool Cancel - { - get - { - return _cancel; - } - set - { - if (IsCancelable) - _cancel = value; - else if (value) - throw new InvalidOperationException("CurrentChanging Cannot Be Canceled"); - } - } - } - - /// Enables collections to have the functionalities of current record management, custom sorting, filtering, and grouping. - public interface IDataGridCollectionView : IEnumerable, INotifyCollectionChanged - { - /// Gets or sets the cultural information for any operations of the view that may differ by culture, such as sorting. - /// The culture information to use during culture-sensitive operations. - CultureInfo Culture { get; set; } - - /// Indicates whether the specified item belongs to this collection view. - /// true if the item belongs to this collection view; otherwise, false. - /// The object to check. - bool Contains(object item); - - /// Gets the underlying collection. - /// The underlying collection. - IEnumerable SourceCollection { get; } - - /// Gets or sets a callback that is used to determine whether an item is appropriate for inclusion in the view. - /// A method that is used to determine whether an item is appropriate for inclusion in the view. - Func Filter { get; set; } - - /// Gets a value that indicates whether this view supports filtering by way of the property. - /// true if this view supports filtering; otherwise, false. - bool CanFilter { get; } - - /// Gets a collection of instances that describe how the items in the collection are sorted in the view. - /// A collection of values that describe how the items in the collection are sorted in the view. - DataGridSortDescriptionCollection SortDescriptions { get; } - - /// Gets a value that indicates whether this view supports sorting by way of the property. - /// true if this view supports sorting; otherwise, false. - bool CanSort { get; } - - /// Gets a value that indicates whether this view supports grouping by way of the property. - /// true if this view supports grouping; otherwise, false. - bool CanGroup { get; } - - /// Gets a collection of objects that describe how the items in the collection are grouped in the view. - /// A collection of objects that describe how the items in the collection are grouped in the view. - //ObservableCollection GroupDescriptions { get; } - - bool IsGrouping { get; } - int GroupingDepth { get; } - string GetGroupingPropertyNameAtDepth(int level); - - /// Gets the top-level groups. - /// A read-only collection of the top-level groups or null if there are no groups. - IAvaloniaReadOnlyList Groups { get; } - - /// Gets a value that indicates whether the view is empty. - /// true if the view is empty; otherwise, false. - bool IsEmpty { get; } - - /// Recreates the view. - void Refresh(); - - /// Enters a defer cycle that you can use to merge changes to the view and delay automatic refresh. - /// 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. - IDisposable DeferRefresh(); - - /// Gets the current item in the view. - /// The current item in the view or null if there is no current item. - object CurrentItem { get; } - - /// Gets the ordinal position of the in the view. - /// The ordinal position of the in the view. - int CurrentPosition { get; } - - /// Gets a value that indicates whether the of the view is beyond the end of the collection. - /// true if the of the view is beyond the end of the collection; otherwise, false. - bool IsCurrentAfterLast { get; } - - /// Gets a value that indicates whether the of the view is beyond the start of the collection. - /// true if the of the view is beyond the start of the collection; otherwise, false. - bool IsCurrentBeforeFirst { get; } - - /// Sets the first item in the view as the . - /// true if the resulting is an item in the view; otherwise, false. - bool MoveCurrentToFirst(); - - /// Sets the last item in the view as the . - /// true if the resulting is an item in the view; otherwise, false. - bool MoveCurrentToLast(); - - /// Sets the item after the in the view as the . - /// true if the resulting is an item in the view; otherwise, false. - bool MoveCurrentToNext(); - - /// Sets the item before the in the view to the . - /// true if the resulting is an item in the view; otherwise, false. - bool MoveCurrentToPrevious(); - - /// Sets the specified item in the view as the . - /// true if the resulting is an item in the view; otherwise, false. - /// The item to set as the current item. - bool MoveCurrentTo(object item); - - /// Sets the item at the specified index to be the in the view. - /// true if the resulting is an item in the view; otherwise, false. - /// The index to set the to. - bool MoveCurrentToPosition(int position); - - /// Occurs before the current item changes. - event EventHandler CurrentChanging; - - /// Occurs after the current item has been changed. - event EventHandler CurrentChanged; - } - internal interface IDataGridEditableCollectionView - { - /// Gets a value that indicates whether a new item can be added to the collection. - /// true if a new item can be added to the collection; otherwise, false. - bool CanAddNew { get; } - - /// Adds a new item to the underlying collection. - /// The new item that is added to the collection. - object AddNew(); - - /// Ends the add transaction and saves the pending new item. - void CommitNew(); - - /// Ends the add transaction and discards the pending new item. - void CancelNew(); - - /// Gets a value that indicates whether an add transaction is in progress. - /// true if an add transaction is in progress; otherwise, false. - bool IsAddingNew { get; } - - /// Gets the item that is being added during the current add transaction. - /// The item that is being added if is true; otherwise, null. - object CurrentAddItem { get; } - - /// Gets a value that indicates whether an item can be removed from the collection. - /// true if an item can be removed from the collection; otherwise, false. - bool CanRemove { get; } - - /// Removes the item at the specified position from the collection. - /// Index of item to remove. - void RemoveAt(int index); - - /// Removes the specified item from the collection. - /// The item to remove. - void Remove(object item); - - /// Begins an edit transaction on the specified item. - /// The item to edit. - void EditItem(object item); - - /// Ends the edit transaction and saves the pending changes. - void CommitEdit(); - - /// Ends the edit transaction and, if possible, restores the original value of the item. - void CancelEdit(); - - /// Gets a value that indicates whether the collection view can discard pending changes and restore the original values of an edited object. - /// true if the collection view can discard pending changes and restore the original values of an edited object; otherwise, false. - bool CanCancelEdit { get; } - - /// Gets a value that indicates whether an edit transaction is in progress. - /// true if an edit transaction is in progress; otherwise, false. - bool IsEditingItem { get; } - - /// Gets the item in the collection that is being edited. - /// The item that is being edited if is true; otherwise, null. - object CurrentEditItem { get; } - } -} diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs deleted file mode 100644 index 2542b98df3..0000000000 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ /dev/null @@ -1,6236 +0,0 @@ -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using Avalonia.Collections; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; -using Avalonia.VisualTree; -using Avalonia.Utilities; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Text; -using System.Linq; -using Avalonia.Input.Platform; -using System.ComponentModel.DataAnnotations; -using Avalonia.Automation.Peers; -using Avalonia.Controls.Automation.Peers; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.Controls.Metadata; -using Avalonia.Input.GestureRecognizers; -using Avalonia.Styling; -using Avalonia.Reactive; - -namespace Avalonia.Controls -{ - /// - /// Displays data in a customizable grid. - /// - [TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(Visual))] - [TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))] - [TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))] - [TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))] - [TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))] - [TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))] - [TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))] - [TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))] - [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")] - public partial class DataGrid : TemplatedControl - { - private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; - private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; - private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; - private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; - private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; - private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; - private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner"; - private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar"; - internal const bool DATAGRID_defaultCanUserReorderColumns = true; - internal const bool DATAGRID_defaultCanUserResizeColumns = true; - internal const bool DATAGRID_defaultCanUserSortColumns = true; - - /// - /// The default order to use for columns when there is no - /// value available for the property. - /// - /// - /// The value of 10,000 comes from the DataAnnotations spec, allowing - /// some properties to be ordered at the beginning and some at the end. - /// - private const int DATAGRID_defaultColumnDisplayOrder = 10000; - - private const double DATAGRID_horizontalGridLinesThickness = 1; - private const double DATAGRID_minimumRowHeaderWidth = 4; - private const double DATAGRID_minimumColumnHeaderHeight = 4; - internal const double DATAGRID_maximumStarColumnWidth = 10000; - internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 50.0; - private const double DATAGRID_maxHeadersThickness = 32768; - - private const double DATAGRID_defaultRowHeight = 22; - internal const double DATAGRID_defaultRowGroupSublevelIndent = 20; - private const double DATAGRID_defaultMinColumnWidth = 20; - private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; - - private List _bindingValidationErrors; - private IDisposable _validationSubscription; - - private INotifyCollectionChanged _topLevelGroup; - private ContentControl _clipboardContentControl; - - private Visual _bottomRightCorner; - private DataGridColumnHeadersPresenter _columnHeadersPresenter; - private DataGridRowsPresenter _rowsPresenter; - private ScrollBar _vScrollBar; - private ScrollBar _hScrollBar; - - private ContentControl _topLeftCornerHeader; - private ContentControl _topRightCornerHeader; - private Control _frozenColumnScrollBarSpacer; - - // the sum of the widths in pixels of the scrolling columns preceding - // the first displayed scrolling column - private double _horizontalOffset; - - // the number of pixels of the firstDisplayedScrollingCol which are not displayed - private double _negHorizontalOffset; - private byte _autoGeneratingColumnOperationCount; - private bool _areHandlersSuspended; - private bool _autoSizingColumns; - private IndexToValueTable _collapsedSlotsTable; - private Control _clickedElement; - - // used to store the current column during a Reset - private int _desiredCurrentColumnIndex; - private int _editingColumnIndex; - - // this is a workaround only for the scenarios where we need it, it is not all encompassing nor always updated - private RoutedEventArgs _editingEventArgs; - private bool _executingLostFocusActions; - private bool _flushCurrentCellChanged; - private bool _focusEditingControl; - private Visual _focusedObject; - private byte _horizontalScrollChangesIgnored; - private DataGridRow _focusedRow; - private bool _ignoreNextScrollBarsLayout; - - // Nth row of rows 0..N that make up the RowHeightEstimate - private int _lastEstimatedRow; - private List _loadedRows; - - // prevents reentry into the VerticalScroll event handler - private Queue _lostFocusActions; - private int _noSelectionChangeCount; - private int _noCurrentCellChangeCount; - private bool _makeFirstDisplayedCellCurrentCellPending; - private bool _measured; - private int? _mouseOverRowIndex; // -1 is used for the 'new row' - private DataGridColumn _previousCurrentColumn; - private object _previousCurrentItem; - private double[] _rowGroupHeightsByLevel; - private double _rowHeaderDesiredWidth; - private Size? _rowsPresenterAvailableSize; - private bool _scrollingByHeight; - private IndexToValueTable _showDetailsTable; - private bool _successfullyUpdatedSelection; - private DataGridSelectedItemsCollection _selectedItems; - private bool _temporarilyResetCurrentCell; - private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode. - - // An approximation of the sum of the heights in pixels of the scrolling rows preceding - // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid - // does not know their actual height. The heights used for the approximation are the ones - // set as the rows were scrolled off. - private double _verticalOffset; - private byte _verticalScrollChangesIgnored; - - public event EventHandler HorizontalScroll; - public event EventHandler VerticalScroll; - - /// - /// Identifies the CanUserReorderColumns dependency property. - /// - public static readonly StyledProperty CanUserReorderColumnsProperty = - AvaloniaProperty.Register(nameof(CanUserReorderColumns)); - - /// - /// Gets or sets a value that indicates whether the user can change - /// the column display order by dragging column headers with the mouse. - /// - public bool CanUserReorderColumns - { - get { return GetValue(CanUserReorderColumnsProperty); } - set { SetValue(CanUserReorderColumnsProperty, value); } - } - - /// - /// Identifies the CanUserResizeColumns dependency property. - /// - public static readonly StyledProperty CanUserResizeColumnsProperty = - AvaloniaProperty.Register(nameof(CanUserResizeColumns)); - - /// - /// Gets or sets a value that indicates whether the user can adjust column widths using the mouse. - /// - public bool CanUserResizeColumns - { - get { return GetValue(CanUserResizeColumnsProperty); } - set { SetValue(CanUserResizeColumnsProperty, value); } - } - - /// - /// Identifies the CanUserSortColumns dependency property. - /// - public static readonly StyledProperty CanUserSortColumnsProperty = - AvaloniaProperty.Register(nameof(CanUserSortColumns), true); - - /// - /// Gets or sets a value that indicates whether the user can sort columns by clicking the column header. - /// - public bool CanUserSortColumns - { - get { return GetValue(CanUserSortColumnsProperty); } - set { SetValue(CanUserSortColumnsProperty, value); } - } - - /// - /// Identifies the ColumnHeaderHeight dependency property. - /// - public static readonly StyledProperty ColumnHeaderHeightProperty = - AvaloniaProperty.Register( - nameof(ColumnHeaderHeight), - defaultValue: double.NaN, - validate: IsValidColumnHeaderHeight); - - private static bool IsValidColumnHeaderHeight(double value) - { - return double.IsNaN(value) || - (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness); - } - - /// - /// Gets or sets the height of the column headers row. - /// - public double ColumnHeaderHeight - { - get { return GetValue(ColumnHeaderHeightProperty); } - set { SetValue(ColumnHeaderHeightProperty, value); } - } - - /// - /// Identifies the ColumnWidth dependency property. - /// - public static readonly StyledProperty ColumnWidthProperty = - AvaloniaProperty.Register(nameof(ColumnWidth), defaultValue: DataGridLength.Auto); - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty RowThemeProperty = - AvaloniaProperty.Register(nameof(RowTheme)); - - /// - /// Gets or sets the theme applied to all rows. - /// - public ControlTheme RowTheme - { - get { return GetValue(RowThemeProperty); } - set { SetValue(RowThemeProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty CellThemeProperty = - AvaloniaProperty.Register(nameof(CellTheme)); - - /// - /// Gets or sets the theme applied to all cells. - /// - public ControlTheme CellTheme - { - get { return GetValue(CellThemeProperty); } - set { SetValue(CellThemeProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty ColumnHeaderThemeProperty = - AvaloniaProperty.Register(nameof(ColumnHeaderTheme)); - - /// - /// Gets or sets the theme applied to all column headers. - /// - public ControlTheme ColumnHeaderTheme - { - get { return GetValue(ColumnHeaderThemeProperty); } - set { SetValue(ColumnHeaderThemeProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty RowGroupThemeProperty = - AvaloniaProperty.Register(nameof(RowGroupTheme)); - - /// - /// Gets or sets the theme applied to all row groups. - /// - public ControlTheme RowGroupTheme - { - get { return GetValue(RowGroupThemeProperty); } - set { SetValue(RowGroupThemeProperty, value); } - } - - /// - /// Gets or sets the standard width or automatic sizing mode of columns in the control. - /// - public DataGridLength ColumnWidth - { - get { return GetValue(ColumnWidthProperty); } - set { SetValue(ColumnWidthProperty, value); } - } - - public static readonly StyledProperty FrozenColumnCountProperty = - AvaloniaProperty.Register( - nameof(FrozenColumnCount), - validate: ValidateFrozenColumnCount); - - /// - /// Gets or sets the number of columns that the user cannot scroll horizontally. - /// - public int FrozenColumnCount - { - get { return GetValue(FrozenColumnCountProperty); } - set { SetValue(FrozenColumnCountProperty, value); } - } - - private static bool ValidateFrozenColumnCount(int value) => value >= 0; - - public static readonly StyledProperty GridLinesVisibilityProperty = - AvaloniaProperty.Register(nameof(GridLinesVisibility)); - - /// - /// Gets or sets a value that indicates which grid lines separating inner cells are shown. - /// - public DataGridGridLinesVisibility GridLinesVisibility - { - get { return GetValue(GridLinesVisibilityProperty); } - set { SetValue(GridLinesVisibilityProperty, value); } - } - - public static readonly StyledProperty HeadersVisibilityProperty = - AvaloniaProperty.Register(nameof(HeadersVisibility)); - - /// - /// Gets or sets a value that indicates the visibility of row and column headers. - /// - public DataGridHeadersVisibility HeadersVisibility - { - get { return GetValue(HeadersVisibilityProperty); } - set { SetValue(HeadersVisibilityProperty, value); } - } - - public static readonly StyledProperty HorizontalGridLinesBrushProperty = - AvaloniaProperty.Register(nameof(HorizontalGridLinesBrush)); - - /// - /// Gets or sets the that is used to paint grid lines separating rows. - /// - public IBrush HorizontalGridLinesBrush - { - get { return GetValue(HorizontalGridLinesBrushProperty); } - set { SetValue(HorizontalGridLinesBrushProperty, value); } - } - - public static readonly StyledProperty HorizontalScrollBarVisibilityProperty = - AvaloniaProperty.Register(nameof(HorizontalScrollBarVisibility)); - - /// - /// Gets or sets a value that indicates how the horizontal scroll bar is displayed. - /// - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return GetValue(HorizontalScrollBarVisibilityProperty); } - set { SetValue(HorizontalScrollBarVisibilityProperty, value); } - } - - public static readonly StyledProperty IsReadOnlyProperty = - AvaloniaProperty.Register(nameof(IsReadOnly)); - - /// - /// Gets or sets a value that indicates whether the user can edit the values in the control. - /// - public bool IsReadOnly - { - get { return GetValue(IsReadOnlyProperty); } - set { SetValue(IsReadOnlyProperty, value); } - } - - public static readonly StyledProperty AreRowGroupHeadersFrozenProperty = - AvaloniaProperty.Register( - nameof(AreRowGroupHeadersFrozen), - defaultValue: true); - - /// - /// Gets or sets a value that indicates whether the row group header sections - /// remain fixed at the width of the display area or can scroll horizontally. - /// - public bool AreRowGroupHeadersFrozen - { - get { return GetValue(AreRowGroupHeadersFrozenProperty); } - set { SetValue(AreRowGroupHeadersFrozenProperty, value); } - } - - private void OnAreRowGroupHeadersFrozenChanged(AvaloniaPropertyChangedEventArgs e) - { - var value = (bool)e.NewValue; - ProcessFrozenColumnCount(); - - // Update elements in the RowGroupHeader that were previously frozen - if (value) - { - if (_rowsPresenter != null) - { - foreach (Control element in _rowsPresenter.Children) - { - if (element is DataGridRowGroupHeader groupHeader) - { - groupHeader.ClearFrozenStates(); - } - } - } - } - } - - /// - /// Defines the property. - /// - public static readonly AttachedProperty IsScrollInertiaEnabledProperty = - ScrollViewer.IsScrollInertiaEnabledProperty.AddOwner(); - - /// - /// Gets or sets whether scroll gestures should include inertia in their behavior and value. - /// - public bool IsScrollInertiaEnabled - { - get => GetValue(IsScrollInertiaEnabledProperty); - set => SetValue(IsScrollInertiaEnabledProperty, value); - } - - private bool _isValid = true; - - public static readonly DirectProperty IsValidProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsValid), - o => o.IsValid); - - public bool IsValid - { - get { return _isValid; } - internal set - { - SetAndRaise(IsValidProperty, ref _isValid, value); - PseudoClasses.Set(":invalid", !value); - } - } - - public static readonly StyledProperty MaxColumnWidthProperty = - AvaloniaProperty.Register( - nameof(MaxColumnWidth), - defaultValue: DATAGRID_defaultMaxColumnWidth, - validate: IsValidColumnWidth); - - private static bool IsValidColumnWidth(double value) - { - return !double.IsNaN(value) && value > 0; - } - - /// - /// Gets or sets the maximum width of columns in the . - /// - public double MaxColumnWidth - { - get { return GetValue(MaxColumnWidthProperty); } - set { SetValue(MaxColumnWidthProperty, value); } - } - - public static readonly StyledProperty MinColumnWidthProperty = - AvaloniaProperty.Register( - nameof(MinColumnWidth), - defaultValue: DATAGRID_defaultMinColumnWidth, - validate: IsValidMinColumnWidth); - - private static bool IsValidMinColumnWidth(double value) - { - return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0; - } - - /// - /// Gets or sets the minimum width of columns in the . - /// - public double MinColumnWidth - { - get { return GetValue(MinColumnWidthProperty); } - set { SetValue(MinColumnWidthProperty, value); } - } - - public static readonly StyledProperty RowBackgroundProperty = - AvaloniaProperty.Register(nameof(RowBackground)); - - /// - /// Gets or sets the that is used to paint row backgrounds. - /// - public IBrush RowBackground - { - get { return GetValue(RowBackgroundProperty); } - set { SetValue(RowBackgroundProperty, value); } - } - - public static readonly StyledProperty RowHeightProperty = - AvaloniaProperty.Register( - nameof(RowHeight), - defaultValue: double.NaN, - validate: IsValidRowHeight); - private static bool IsValidRowHeight(double value) - { - return double.IsNaN(value) || - (value >= DataGridRow.DATAGRIDROW_minimumHeight && - value <= DataGridRow.DATAGRIDROW_maximumHeight); - } - - /// - /// Gets or sets the standard height of rows in the control. - /// - public double RowHeight - { - get { return GetValue(RowHeightProperty); } - set { SetValue(RowHeightProperty, value); } - } - - public static readonly StyledProperty RowHeaderWidthProperty = - AvaloniaProperty.Register( - nameof(RowHeaderWidth), - defaultValue: double.NaN, - validate: IsValidRowHeaderWidth); - private static bool IsValidRowHeaderWidth(double value) - { - return double.IsNaN(value) || - (value >= DATAGRID_minimumRowHeaderWidth && - value <= DATAGRID_maxHeadersThickness); - } - - /// - /// Gets or sets the width of the row header column. - /// - public double RowHeaderWidth - { - get { return GetValue(RowHeaderWidthProperty); } - set { SetValue(RowHeaderWidthProperty, value); } - } - - public static readonly StyledProperty SelectionModeProperty = - AvaloniaProperty.Register(nameof(SelectionMode)); - - /// - /// Gets or sets the selection behavior of the data grid. - /// - public DataGridSelectionMode SelectionMode - { - get { return GetValue(SelectionModeProperty); } - set { SetValue(SelectionModeProperty, value); } - } - - public static readonly StyledProperty VerticalGridLinesBrushProperty = - AvaloniaProperty.Register(nameof(VerticalGridLinesBrush)); - - /// - /// Gets or sets the that is used to paint grid lines separating columns. - /// - public IBrush VerticalGridLinesBrush - { - get { return GetValue(VerticalGridLinesBrushProperty); } - set { SetValue(VerticalGridLinesBrushProperty, value); } - } - - public static readonly StyledProperty VerticalScrollBarVisibilityProperty = - AvaloniaProperty.Register(nameof(VerticalScrollBarVisibility)); - - /// - /// Gets or sets a value that indicates how the vertical scroll bar is displayed. - /// - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return GetValue(VerticalScrollBarVisibilityProperty); } - set { SetValue(VerticalScrollBarVisibilityProperty, value); } - } - - public static readonly StyledProperty> DropLocationIndicatorTemplateProperty = - AvaloniaProperty.Register>(nameof(DropLocationIndicatorTemplate)); - - /// - /// Gets or sets the template that is used when rendering the column headers. - /// - public ITemplate DropLocationIndicatorTemplate - { - get { return GetValue(DropLocationIndicatorTemplateProperty); } - set { SetValue(DropLocationIndicatorTemplateProperty, value); } - } - - private int _selectedIndex = -1; - private object _selectedItem; - - public static readonly DirectProperty SelectedIndexProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedIndex), - o => o.SelectedIndex, - (o, v) => o.SelectedIndex = v, - defaultBindingMode: BindingMode.TwoWay); - - /// - /// Gets or sets the index of the current selection. - /// - /// - /// The index of the current selection, or -1 if the selection is empty. - /// - public int SelectedIndex - { - get { return _selectedIndex; } - set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); } - } - - public static readonly DirectProperty SelectedItemProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedItem), - o => o.SelectedItem, - (o, v) => o.SelectedItem = v, - defaultBindingMode: BindingMode.TwoWay); - - /// - /// Gets or sets the data item corresponding to the selected row. - /// - public object SelectedItem - { - get { return _selectedItem; } - set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } - } - - public static readonly StyledProperty ClipboardCopyModeProperty = - AvaloniaProperty.Register( - nameof(ClipboardCopyMode), - defaultValue: DataGridClipboardCopyMode.ExcludeHeader); - - /// - /// The property which determines how DataGrid content is copied to the Clipboard. - /// - public DataGridClipboardCopyMode ClipboardCopyMode - { - get { return GetValue(ClipboardCopyModeProperty); } - set { SetValue(ClipboardCopyModeProperty, value); } - } - - public static readonly StyledProperty AutoGenerateColumnsProperty = - AvaloniaProperty.Register(nameof(AutoGenerateColumns)); - - /// - /// Gets or sets a value that indicates whether columns are created - /// automatically when the property is set. - /// - public bool AutoGenerateColumns - { - get { return GetValue(AutoGenerateColumnsProperty); } - set { SetValue(AutoGenerateColumnsProperty, value); } - } - - private void OnAutoGenerateColumnsChanged(AvaloniaPropertyChangedEventArgs e) - { - var value = (bool)e.NewValue; - if (value) - { - InitializeElements(recycleRows: false); - } - else - { - RemoveAutoGeneratedColumns(); - } - } - - /// - /// Identifies the ItemsSource property. - /// - public static readonly StyledProperty ItemsSourceProperty = - AvaloniaProperty.Register(nameof(ItemsSource)); - - /// - /// Gets or sets a collection that is used to generate the content of the control. - /// - public IEnumerable ItemsSource - { - get => GetValue(ItemsSourceProperty); - set => SetValue(ItemsSourceProperty, value); - } - - public static readonly StyledProperty AreRowDetailsFrozenProperty = - AvaloniaProperty.Register(nameof(AreRowDetailsFrozen)); - - /// - /// Gets or sets a value that indicates whether the row details sections remain - /// fixed at the width of the display area or can scroll horizontally. - /// - public bool AreRowDetailsFrozen - { - get { return GetValue(AreRowDetailsFrozenProperty); } - set { SetValue(AreRowDetailsFrozenProperty, value); } - } - - public static readonly StyledProperty RowDetailsTemplateProperty = - AvaloniaProperty.Register(nameof(RowDetailsTemplate)); - - /// - /// Gets or sets the template that is used to display the content of the details section of rows. - /// - public IDataTemplate RowDetailsTemplate - { - get { return GetValue(RowDetailsTemplateProperty); } - set { SetValue(RowDetailsTemplateProperty, value); } - } - - public static readonly StyledProperty RowDetailsVisibilityModeProperty = - AvaloniaProperty.Register(nameof(RowDetailsVisibilityMode)); - - /// - /// Gets or sets a value that indicates when the details sections of rows are displayed. - /// - public DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode - { - get { return GetValue(RowDetailsVisibilityModeProperty); } - set { SetValue(RowDetailsVisibilityModeProperty, value); } - } - - - public static readonly DirectProperty CollectionViewProperty = - AvaloniaProperty.RegisterDirect(nameof(CollectionView), - o => o.CollectionView); - - /// - /// Gets current . - /// - public IDataGridCollectionView CollectionView => - DataConnection.CollectionView; - - static DataGrid() - { - AffectsMeasure( - ColumnHeaderHeightProperty, - HorizontalScrollBarVisibilityProperty, - VerticalScrollBarVisibilityProperty); - - ItemsSourceProperty.Changed.AddClassHandler((x, e) => x.OnItemsSourcePropertyChanged(e)); - CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); - ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); - FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); - GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e)); - HeadersVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnHeadersVisibilityChanged(e)); - HorizontalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnHorizontalGridLinesBrushChanged(e)); - IsReadOnlyProperty.Changed.AddClassHandler((x, e) => x.OnIsReadOnlyChanged(e)); - MaxColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMaxColumnWidthChanged(e)); - MinColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnMinColumnWidthChanged(e)); - RowHeightProperty.Changed.AddClassHandler((x, e) => x.OnRowHeightChanged(e)); - RowHeaderWidthProperty.Changed.AddClassHandler((x, e) => x.OnRowHeaderWidthChanged(e)); - SelectionModeProperty.Changed.AddClassHandler((x, e) => x.OnSelectionModeChanged(e)); - VerticalGridLinesBrushProperty.Changed.AddClassHandler((x, e) => x.OnVerticalGridLinesBrushChanged(e)); - SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.OnSelectedIndexChanged(e)); - SelectedItemProperty.Changed.AddClassHandler((x, e) => x.OnSelectedItemChanged(e)); - IsEnabledProperty.Changed.AddClassHandler((x, e) => x.DataGrid_IsEnabledChanged(e)); - AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler((x, e) => x.OnAreRowGroupHeadersFrozenChanged(e)); - RowDetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsTemplateChanged(e)); - RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsVisibilityModeChanged(e)); - AutoGenerateColumnsProperty.Changed.AddClassHandler((x, e) => x.OnAutoGenerateColumnsChanged(e)); - - FocusableProperty.OverrideDefaultValue(true); - } - - /// - /// Initializes a new instance of the class. - /// - public DataGrid() - { - KeyDown += DataGrid_KeyDown; - KeyUp += DataGrid_KeyUp; - - //TODO: Check if override works - GotFocus += DataGrid_GotFocus; - LostFocus += DataGrid_LostFocus; - - _loadedRows = new List(); - _lostFocusActions = new Queue(); - _selectedItems = new DataGridSelectedItemsCollection(this); - RowGroupHeadersTable = new IndexToValueTable(); - _bindingValidationErrors = new List(); - - DisplayData = new DataGridDisplayData(this); - ColumnsInternal = CreateColumnsInstance(); - ColumnsInternal.CollectionChanged += ColumnsInternal_CollectionChanged; - - RowHeightEstimate = DATAGRID_defaultRowHeight; - RowDetailsHeightEstimate = 0; - _rowHeaderDesiredWidth = 0; - - DataConnection = new DataGridDataConnection(this); - _showDetailsTable = new IndexToValueTable(); - _collapsedSlotsTable = new IndexToValueTable(); - - AnchorSlot = -1; - _lastEstimatedRow = -1; - _editingColumnIndex = -1; - _mouseOverRowIndex = null; - CurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); - - RowGroupHeaderHeightEstimate = DATAGRID_defaultRowHeight; - - UpdatePseudoClasses(); - } - - protected override AutomationPeer OnCreateAutomationPeer() - { - return new DataGridAutomationPeer(this); - } - - private void SetValueNoCallback(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - _areHandlersSuspended = true; - try - { - SetValue(property, value, priority); - } - finally - { - _areHandlersSuspended = false; - } - } - - private void OnRowDetailsVisibilityModeChanged(AvaloniaPropertyChangedEventArgs e) - { - UpdateRowDetailsVisibilityMode((DataGridRowDetailsVisibilityMode)e.NewValue); - } - - private void OnRowDetailsTemplateChanged(AvaloniaPropertyChangedEventArgs e) - { - - // Update the RowDetails templates if necessary - if (_rowsPresenter != null) - { - foreach (DataGridRow row in GetAllRows()) - { - if (GetRowDetailsVisibility(row.Index)) - { - // DetailsPreferredHeight is initialized when the DetailsElement's size changes. - row.ApplyDetailsTemplate(initializeDetailsPreferredHeight: false); - } - } - } - - UpdateRowDetailsHeightEstimate(); - InvalidateMeasure(); - } - - /// - /// ItemsSourceProperty property changed handler. - /// - /// The event arguments. - private void OnItemsSourcePropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - Debug.Assert(DataConnection != null); - - var oldCollectionView = DataConnection.CollectionView; - - var oldValue = (IEnumerable)e.OldValue; - var newItemsSource = (IEnumerable)e.NewValue; - - if (LoadingOrUnloadingRow) - { - SetValueNoCallback(ItemsSourceProperty, oldValue); - throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows(); - } - - // Try to commit edit on the old DataSource, but force a cancel if it fails - if (!CommitEdit()) - { - CancelEdit(DataGridEditingUnit.Row, false); - } - - DataConnection.UnWireEvents(DataConnection.DataSource); - DataConnection.ClearDataProperties(); - ClearRowGroupHeadersTable(); - - // The old selected indexes are no longer relevant. There's a perf benefit from - // updating the selected indexes with a null DataSource, because we know that all - // of the previously selected indexes have been removed from selection - DataConnection.DataSource = null; - _selectedItems.UpdateIndexes(); - CoerceSelectedItem(); - - // Wrap an IEnumerable in an ICollectionView if it's not already one - bool setDefaultSelection = false; - if (newItemsSource is IDataGridCollectionView newCollectionView) - { - setDefaultSelection = true; - } - else - { - newCollectionView = newItemsSource is not null - ? DataGridDataConnection.CreateView(newItemsSource) - : default; - } - - DataConnection.DataSource = newCollectionView; - - if (oldCollectionView != DataConnection.CollectionView) - { - RaisePropertyChanged(CollectionViewProperty, - oldCollectionView, - newCollectionView); - } - - if (DataConnection.DataSource != null) - { - // Setup the column headers - if (DataConnection.DataType != null) - { - foreach (var column in ColumnsInternal.GetDisplayedColumns()) - { - if (column is DataGridBoundColumn boundColumn) - { - boundColumn.SetHeaderFromBinding(); - } - } - } - DataConnection.WireEvents(DataConnection.DataSource); - } - - // Wait for the current cell to be set before we raise any SelectionChanged events - _makeFirstDisplayedCellCurrentCellPending = true; - - // Clear out the old rows and remove the generated columns - ClearRows(false); //recycle - RemoveAutoGeneratedColumns(); - - // Set the SlotCount (from the data count and number of row group headers) before we make the default selection - PopulateRowGroupHeadersTable(); - SelectedItem = null; - if (DataConnection.CollectionView != null && setDefaultSelection) - { - SelectedItem = DataConnection.CollectionView.CurrentItem; - } - - // Treat this like the DataGrid has never been measured because all calculations at - // this point are invalid until the next layout cycle. For instance, the ItemsSource - // can be set when the DataGrid is not part of the visual tree - _measured = false; - InvalidateMeasure(); - - UpdatePseudoClasses(); - } - } - - private void ColumnsInternal_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Add - || e.Action == NotifyCollectionChangedAction.Remove - || e.Action == NotifyCollectionChangedAction.Reset) - { - UpdatePseudoClasses(); - } - } - - internal void UpdatePseudoClasses() - { - PseudoClasses.Set(":empty-columns", !ColumnsInternal.GetVisibleColumns().Any()); - PseudoClasses.Set(":empty-rows", !DataConnection.Any()); - } - - private void OnSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - int index = (int)e.NewValue; - - // GetDataItem returns null if index is >= Count, we do not check newValue - // against Count here to avoid enumerating through an Enumerable twice - // Setting SelectedItem coerces the finally value of the SelectedIndex - object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index); - SelectedItem = newSelectedItem; - if (SelectedItem != newSelectedItem) - { - SetValueNoCallback(SelectedIndexProperty, (int)e.OldValue); - } - } - } - - private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - int rowIndex = (e.NewValue == null) ? -1 : DataConnection.IndexOf(e.NewValue); - if (rowIndex == -1) - { - // If the Item is null or it's not found, clear the Selection - if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) - { - // Edited value couldn't be committed or aborted - SetValueNoCallback(SelectedItemProperty, e.OldValue); - return; - } - - // Clear all row selections - ClearRowSelection(resetAnchorSlot: true); - - if (DataConnection.CollectionView != null) - { - DataConnection.CollectionView.MoveCurrentTo(null); - } - } - else - { - int slot = SlotFromRowIndex(rowIndex); - if (slot != CurrentSlot) - { - if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) - { - // Edited value couldn't be committed or aborted - SetValueNoCallback(SelectedItemProperty, e.OldValue); - return; - } - if (slot >= SlotCount || slot < -1) - { - if (DataConnection.CollectionView != null) - { - DataConnection.CollectionView.MoveCurrentToPosition(rowIndex); - } - } - } - - int oldSelectedIndex = SelectedIndex; - SetValueNoCallback(SelectedIndexProperty, rowIndex); - try - { - _noSelectionChangeCount++; - int columnIndex = CurrentColumnIndex; - - if (columnIndex == -1) - { - columnIndex = FirstDisplayedNonFillerColumnIndex; - } - if (IsSlotOutOfSelectionBounds(slot)) - { - ClearRowSelection(slotException: slot, setAnchorSlot: true); - return; - } - - UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); - } - finally - { - NoSelectionChangeCount--; - } - - if (!_successfullyUpdatedSelection) - { - SetValueNoCallback(SelectedIndexProperty, oldSelectedIndex); - SetValueNoCallback(SelectedItemProperty, e.OldValue); - } - } - } - } - - private void OnVerticalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e) - { - if (_rowsPresenter != null) - { - foreach (DataGridRow row in GetAllRows()) - { - row.EnsureGridLines(); - } - } - } - - private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - ClearRowSelection(resetAnchorSlot: true); - } - } - - private void OnRowHeaderWidthChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - EnsureRowHeaderWidth(); - } - } - - private void OnRowHeightChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - InvalidateRowHeightEstimate(); - // Re-measure all the rows due to the Height change - InvalidateRowsMeasure(invalidateIndividualElements: true); - // DataGrid needs to update the layout information and the ScrollBars - InvalidateMeasure(); - } - } - - private void OnMinColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - double oldValue = (double)e.OldValue; - foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) - { - OnColumnMinWidthChanged(column, Math.Max(column.MinWidth, oldValue)); - } - } - } - - private void OnMaxColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - var oldValue = (double)e.OldValue; - foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) - { - OnColumnMaxWidthChanged(column, Math.Min(column.MaxWidth, oldValue)); - } - } - } - - private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended) - { - var value = (bool)e.NewValue; - if (value && !CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) - { - CancelEdit(DataGridEditingUnit.Row, raiseEvents: false); - } - } - } - - private void OnHorizontalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!_areHandlersSuspended && _rowsPresenter != null) - { - foreach (DataGridRow row in GetAllRows()) - { - row.EnsureGridLines(); - } - } - } - - private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e) - { - var oldValue = (DataGridHeadersVisibility)e.OldValue; - var newValue = (DataGridHeadersVisibility)e.NewValue; - bool hasFlags(DataGridHeadersVisibility value, DataGridHeadersVisibility flags) => ((value & flags) == flags); - - bool newValueCols = hasFlags(newValue, DataGridHeadersVisibility.Column); - bool newValueRows = hasFlags(newValue, DataGridHeadersVisibility.Row); - bool oldValueCols = hasFlags(oldValue, DataGridHeadersVisibility.Column); - bool oldValueRows = hasFlags(oldValue, DataGridHeadersVisibility.Row); - - // Columns - if (newValueCols != oldValueCols) - { - if (_columnHeadersPresenter != null) - { - EnsureColumnHeadersVisibility(); - if (!newValueCols) - { - _columnHeadersPresenter.Measure(default); - } - else - { - EnsureVerticalGridLines(); - } - InvalidateMeasure(); - } - } - - // Rows - if (newValueRows != oldValueRows) - { - if (_rowsPresenter != null) - { - foreach (Control element in _rowsPresenter.Children) - { - if (element is DataGridRow row) - { - row.EnsureHeaderStyleAndVisibility(null); - if (newValueRows) - { - row.ApplyState(); - row.EnsureHeaderVisibility(); - } - } - else if (element is DataGridRowGroupHeader rowGroupHeader) - { - rowGroupHeader.EnsureHeaderVisibility(); - } - } - InvalidateRowHeightEstimate(); - InvalidateRowsMeasure(invalidateIndividualElements: true); - } - } - - if (_topLeftCornerHeader != null) - { - _topLeftCornerHeader.IsVisible = newValueRows && newValueCols; - if (_topLeftCornerHeader.IsVisible) - { - _topLeftCornerHeader.Measure(default); - } - } - - } - - private void OnGridLinesVisibilityChanged(AvaloniaPropertyChangedEventArgs e) - { - foreach (DataGridRow row in GetAllRows()) - { - row.EnsureGridLines(); - row.InvalidateHorizontalArrange(); - } - } - - private void OnFrozenColumnCountChanged(AvaloniaPropertyChangedEventArgs e) - { - ProcessFrozenColumnCount(); - } - - private void ProcessFrozenColumnCount() - { - CorrectColumnFrozenStates(); - ComputeScrollBarsLayout(); - - InvalidateColumnHeadersArrange(); - InvalidateCellsArrange(); - } - - private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) - { - var value = (DataGridLength)e.NewValue; - - foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) - { - if (column.InheritsWidth) - { - column.SetWidthInternalNoCallback(value); - } - } - - EnsureHorizontalLayout(); - } - - private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e) - { - EnsureHorizontalLayout(); - } - - /// - /// Occurs one time for each public, non-static property in the bound data type when the - /// property is changed and the - /// property is true. - /// - public event EventHandler AutoGeneratingColumn; - - /// - /// Occurs before a cell or row enters editing mode. - /// - public event EventHandler BeginningEdit; - - /// - /// Occurs after cell editing has ended. - /// - public event EventHandler CellEditEnded; - - /// - /// Occurs immediately before cell editing has ended. - /// - public event EventHandler CellEditEnding; - - /// - /// Occurs when cell is mouse-pressed. - /// - public event EventHandler CellPointerPressed; - - /// - /// Occurs when the - /// property of a column changes. - /// - public event EventHandler ColumnDisplayIndexChanged; - - /// - /// Raised when column reordering ends, to allow subscribers to clean up. - /// - public event EventHandler ColumnReordered; - - /// - /// Raised when starting a column reordering action. Subscribers to this event can - /// set tooltip and caret UIElements, constrain tooltip position, indicate that - /// a preview should be shown, or cancel reordering. - /// - public event EventHandler ColumnReordering; - - /// - /// Occurs when a different cell becomes the current cell. - /// - public event EventHandler CurrentCellChanged; - - /// - /// Occurs after a - /// is instantiated, so that you can customize it before it is used. - /// - public event EventHandler LoadingRow; - - /// - /// Occurs when a cell in a enters editing mode. - /// - /// - public event EventHandler PreparingCellForEdit; - - /// - /// Occurs when the row has been successfully committed or cancelled. - /// - public event EventHandler RowEditEnded; - - /// - /// Occurs immediately before the row has been successfully committed or cancelled. - /// - public event EventHandler RowEditEnding; - - public static readonly RoutedEvent SelectionChangedEvent = - RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble); - - /// - /// Occurs when the or - /// property value changes. - /// - public event EventHandler SelectionChanged - { - add { AddHandler(SelectionChangedEvent, value); } - remove { RemoveHandler(SelectionChangedEvent, value); } - } - - /// - /// Occurs when the sorting request is triggered. - /// - public event EventHandler Sorting; - - /// - /// Occurs when a
- /// Clear a sort criteria by assigning SortDescription.Empty to this property. - /// One or more sort criteria in form of - /// can be used, each specifying a property and direction to sort by. - ///