diff --git a/.editorconfig b/.editorconfig index b7a03207a4..5f08d1e940 100644 --- a/.editorconfig +++ b/.editorconfig @@ -132,6 +132,9 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# Wrapping preferences +csharp_wrap_before_ternary_opsigns = false + # Xaml files [*.xaml] indent_size = 4 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 78b9cff039..acff8cc117 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,18 @@ ## What does the pull request do? + -Give a bit of background on the PR here, together with links to with related issues etc. ## What is the current behavior? + -If the PR is a fix, describe the current incorrect behavior, otherwise delete this section. ## What is the updated/expected behavior with this PR? + -Describe how to test the PR. ## How was the solution implemented (if it's not obvious)? + -Include any information that might be of use to a reviewer here. ## Checklist @@ -21,12 +21,11 @@ Include any information that might be of use to a reviewer here. - [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation ## Breaking changes + -List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes ## Fixed issues - -If the pull request fixes issue(s) list them like this: - + diff --git a/Avalonia.sln b/Avalonia.sln index d6472503fe..484d7a4cde 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -202,6 +202,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1845,6 +1847,30 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhone.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhone.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.Build.0 = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.ActiveCfg = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/SharedVersion.props b/build/SharedVersion.props index b46ac16a79..4f0b1f0a5b 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,8 +2,8 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.7.1 - Copyright 2018 © The AvaloniaUI Project + 0.8.1 + Copyright 2019 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ https://github.com/AvaloniaUI/Avalonia/ @@ -11,4 +11,4 @@ CS1591 latest - \ No newline at end of file + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bb31034299..84092d52eb 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -122,6 +122,14 @@ partial class Build : NukeBuild foreach(var fw in frameworks) { + if (fw.StartsWith("net4") + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") + { + Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + continue; + } + Information("Running for " + fw); DotNetTest(c => { diff --git a/nukebuild/Numerge b/nukebuild/Numerge index 4464343aef..aef10ae67d 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8 +Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 diff --git a/samples/BindingDemo/App.xaml.cs b/samples/BindingDemo/App.xaml.cs index 01c52a2a49..f2f44cd502 100644 --- a/samples/BindingDemo/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; using Serilog; namespace BindingDemo diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index 2ab6c26e68..df80931367 100644 --- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels } else { - throw new ArgumentOutOfRangeException("Value must be less than 10."); + throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10."); } } } diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 329b2ab5a3..b7aa34f5ba 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; using Avalonia.Platform; +using Avalonia.ReactiveUI; using Serilog; namespace ControlCatalog @@ -22,7 +23,11 @@ namespace ControlCatalog /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().LogToDebug().UsePlatformDetect().UseReactiveUI(); + => AppBuilder.Configure() + .LogToDebug() + .UsePlatformDetect() + .UseReactiveUI() + .UseDataGrid(); private static void ConfigureAssetAssembly(AppBuilder builder) { diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 57c8b700df..c8f3fb9921 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,12 +4,13 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Skia; +using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { static class Program { - + static void Main(string[] args) { Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); @@ -43,7 +44,11 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); + => AppBuilder.Configure() + .UsePlatformDetect() + .UseSkia() + .UseReactiveUI() + .UseDataGrid(); static void ConsoleSilencer() { diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 67220e6dd0..d20e0100a0 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -4,6 +4,7 @@ + + + + + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs index dc73bef07a..96e8b49f89 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -8,6 +9,7 @@ namespace ControlCatalog.Pages public ContextMenuPage() { this.InitializeComponent(); + DataContext = new ContextMenuPageViewModel(); } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml new file mode 100644 index 0000000000..268b06f756 --- /dev/null +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + DataGrid + A control for displaying and interacting with a data source. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs new file mode 100644 index 0000000000..b8f63cf3e3 --- /dev/null +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.Models; +using Avalonia.Collections; + +namespace ControlCatalog.Pages +{ + public class DataGridPage : UserControl + { + public DataGridPage() + { + this.InitializeComponent(); + var dg1 = this.FindControl("dataGrid1"); + dg1.IsReadOnly = true; + + var collectionView1 = new DataGridCollectionView(Countries.All); + //collectionView.GroupDescriptions.Add(new PathGroupDescription("Region")); + + dg1.Items = collectionView1; + + var dg2 = this.FindControl("dataGridGrouping"); + dg2.IsReadOnly = true; + + var collectionView2 = new DataGridCollectionView(Countries.All); + collectionView2.GroupDescriptions.Add(new DataGridPathGroupDescription("Region")); + + dg2.Items = collectionView2; + + var dg3 = this.FindControl("dataGridEdit"); + dg3.IsReadOnly = false; + + var items = new List + { + new Person { FirstName = "John", LastName = "Doe" }, + new Person { FirstName = "Elizabeth", LastName = "Thomas" }, + new Person { FirstName = "Zack", LastName = "Ward" } + }; + var collectionView3 = new DataGridCollectionView(items); + + dg3.Items = collectionView3; + + var addButton = this.FindControl("btnAdd"); + addButton.Click += (a, b) => collectionView3.AddNew(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 2bd9a39300..60f8e3656e 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.DialogsPage"> + Use filters Open File Save File Select Folder diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e2e9fbd21c..d207689223 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Markup.Xaml; #pragma warning disable 4014 @@ -9,18 +10,39 @@ namespace ControlCatalog.Pages public DialogsPage() { this.InitializeComponent(); + + List GetFilters() + { + if (this.FindControl("UseFilters").IsChecked != true) + return null; + return new List + { + new FileDialogFilter + { + Name = "Text files (.txt)", Extensions = new List {"txt"} + }, + new FileDialogFilter + { + Name = "All files", + Extensions = new List {"*"} + } + }; + } + this.FindControl("OpenFile").Click += delegate { new OpenFileDialog() { - Title = "Open file" + Title = "Open file", + Filters = GetFilters() }.ShowAsync(GetWindow()); }; this.FindControl("SaveFile").Click += delegate { new SaveFileDialog() { - Title = "Save file" + Title = "Save file", + Filters = GetFilters() }.ShowAsync(GetWindow()); }; this.FindControl("SelectFolder").Click += delegate diff --git a/samples/ControlCatalog/Pages/DropDownPage.xaml b/samples/ControlCatalog/Pages/DropDownPage.xaml deleted file mode 100644 index 7673294e46..0000000000 --- a/samples/ControlCatalog/Pages/DropDownPage.xaml +++ /dev/null @@ -1,42 +0,0 @@ - - - DropDown - A drop-down list. - - - - Inline Items - Inline Item 2 - Inline Item 3 - Inline Item 4 - - - - - - - Control Items - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 5a07cae89f..0a77607719 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; using ReactiveUI; namespace ControlCatalog.Pages @@ -13,51 +14,7 @@ namespace ControlCatalog.Pages public MenuPage() { this.InitializeComponent(); - var vm = new MenuPageViewModel(); - - vm.MenuItems = new[] - { - new MenuItemViewModel - { - Header = "_File", - Items = new[] - { - new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand }, - new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand }, - new MenuItemViewModel { Header = "-" }, - new MenuItemViewModel - { - Header = "Recent", - Items = new[] - { - new MenuItemViewModel - { - Header = "File1.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File1.txt" - }, - new MenuItemViewModel - { - Header = "File2.txt", - Command = vm.OpenRecentCommand, - CommandParameter = @"c:\foo\File2.txt" - }, - } - }, - } - }, - new MenuItemViewModel - { - Header = "_Edit", - Items = new[] - { - new MenuItemViewModel { Header = "_Copy" }, - new MenuItemViewModel { Header = "_Paste" }, - } - } - }; - - DataContext = vm; + DataContext = new MenuPageViewModel(); } private void InitializeComponent() @@ -65,51 +22,4 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } } - - public class MenuPageViewModel - { - public MenuPageViewModel() - { - OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); - OpenRecentCommand = ReactiveCommand.Create(OpenRecent); - } - - public IReadOnlyList MenuItems { get; set; } - public ReactiveCommand OpenCommand { get; } - public ReactiveCommand SaveCommand { get; } - public ReactiveCommand OpenRecentCommand { get; } - - public async Task Open() - { - var dialog = new OpenFileDialog(); - var result = await dialog.ShowAsync(App.Current.MainWindow); - - if (result != null) - { - foreach (var path in result) - { - System.Diagnostics.Debug.WriteLine($"Opened: {path}"); - } - } - } - - public void Save() - { - System.Diagnostics.Debug.WriteLine("Save"); - } - - public void OpenRecent(string path) - { - System.Diagnostics.Debug.WriteLine($"Open recent: {path}"); - } - } - - public class MenuItemViewModel - { - public string Header { get; set; } - public ICommand Command { get; set; } - public object CommandParameter { get; set; } - public IList Items { get; set; } - } } diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index 07e5581304..e605a92da0 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -23,9 +23,9 @@ FormatString: - - + @@ -33,15 +33,15 @@ - - + + ButtonSpinnerLocation: - CultureInfo: - Watermark: diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b5c6892885..b9b384e8fe 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -23,7 +23,7 @@ namespace ControlCatalog.Pages { base.Render(context); Window w = (Window)VisualRoot; - Screen[] screens = w.Screens.All; + var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; Pen p = new Pen(Brushes.Black); diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml index 01ddc0ddca..e700146a5b 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml @@ -88,12 +88,12 @@ HorizontalAlignment="Center" VerticalAlignment="Center"> Tab Placement: - - Left - Bottom - Right - Top - + + Left + Bottom + Right + Top + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index f8f3cd5848..c03edb8b03 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -9,7 +9,7 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 625b344b8c..fea55bcb07 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,6 +1,14 @@ + + + + + + + + - + \ No newline at end of file diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index 730b61ed54..1c485eb69c 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -7,9 +7,9 @@ Margin="16 0 0 0" MinWidth="150" Spacing="4"> - - Horiz. ScrollBar - Vert. ScrollBar - - \ No newline at end of file + diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index 98f1f08d6c..9d8f7c1a3d 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -5,6 +5,7 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; +using Avalonia.ReactiveUI; using Serilog; namespace VirtualizationDemo diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs index d5de5ccb4e..21302fa68a 100644 --- a/samples/interop/Direct3DInteropSample/Program.cs +++ b/samples/interop/Direct3DInteropSample/Program.cs @@ -11,7 +11,9 @@ namespace Direct3DInteropSample { static void Main(string[] args) { - AppBuilder.Configure().UseWin32(deferredRendering: false).UseDirect2D1().Start(); + AppBuilder.Configure() + .With(new Win32PlatformOptions {UseDeferredRendering = false}) + .UseWin32().UseDirect2D1().Start(); } } } diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index 6de50f978d..70f5eaa40b 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,6 +1,5 @@ -copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 52d1609cf9..7da7a9382b 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static object Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo culture) { string v = value; @@ -70,7 +70,7 @@ namespace Avalonia.Animation } } - public class CueTypeConverter : TypeConverter + public class CueTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7e8d733f1b..7601b64ce9 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,27 +22,11 @@ namespace Avalonia /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { - /// - /// The parent object that inherited values are inherited from. - /// private IAvaloniaObject _inheritanceParent; - - /// - /// Maintains a list of direct property binding subscriptions so that the binding source - /// doesn't get collected. - /// private List _directBindings; - - /// - /// Event handler for implementation. - /// private PropertyChangedEventHandler _inpcChanged; - - /// - /// Event handler for implementation. - /// private EventHandler _propertyChanged; - + private EventHandler _inheritablePropertyChanged; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -52,32 +36,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - - void Notify(AvaloniaProperty property) - { - object value = property.IsDirect ? - ((IDirectPropertyAccessor)property).GetValue(this) : - ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - - var e = new AvaloniaPropertyChangedEventArgs( - this, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this)) - { - Notify(property); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())) - { - Notify(property); - } + AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -98,6 +57,15 @@ namespace Avalonia remove { _inpcChanged -= value; } } + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler IAvaloniaObject.InheritablePropertyChanged + { + add { _inheritablePropertyChanged += value; } + remove { _inheritablePropertyChanged -= value; } + } + /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -118,8 +86,9 @@ namespace Avalonia { if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged -= ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } + var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); var inherited = (from property in properties @@ -144,7 +113,7 @@ namespace Avalonia if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged += ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; } } } @@ -421,6 +390,7 @@ namespace Avalonia internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { + LogIfError(property, notification); UpdateDataValidation(property, notification); } @@ -452,6 +422,23 @@ namespace Avalonia }); } + /// + /// Logs a binding error for a property. + /// + /// The property that the error occurred on. + /// The binding error. + protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) + { + Logger.Log( + LogEventLevel.Warning, + LogArea.Binding, + this, + "Error in binding to {Target}.{Property}: {Message}", + this, + property, + e.Message); + } + /// /// Called to update the validation state for properties for which data validation is /// enabled. @@ -509,6 +496,11 @@ namespace Avalonia PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); _inpcChanged(this, e2); } + + if (property.Inherits) + { + _inheritablePropertyChanged?.Invoke(this, e); + } } finally { @@ -628,7 +620,7 @@ namespace Avalonia /// /// The property. /// The default value. - internal object GetDefaultValue(AvaloniaProperty property) + private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValue(property); @@ -648,7 +640,7 @@ namespace Avalonia if (notification != null) { - notification.LogIfError(this, property); + LogIfError(property, notification); value = notification.Value; } @@ -780,6 +772,29 @@ namespace Avalonia return description?.Description ?? o.ToString(); } + /// + /// Logs a mesage if the notification represents a binding error. + /// + /// The property being bound. + /// The binding notification. + private void LogIfError(AvaloniaProperty property, BindingNotification notification) + { + if (notification.ErrorType == BindingErrorType.Error) + { + if (notification.Error is AggregateException aggregate) + { + foreach (var inner in aggregate.InnerExceptions) + { + LogBindingError(property, inner); + } + } + else + { + LogBindingError(property, notification.Error); + } + } + } + /// /// Logs a property set message. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 11b1096052..037e0dd72e 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Data; namespace Avalonia { @@ -13,8 +14,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly IList _properties = - new List(); + private readonly Dictionary _properties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -23,6 +24,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); + private readonly Dictionary>> _initializedCache = + new Dictionary>>(); /// /// Gets the instance @@ -30,6 +33,11 @@ namespace Avalonia public static AvaloniaPropertyRegistry Instance { get; } = new AvaloniaPropertyRegistry(); + /// + /// Gets a list of all registered properties. + /// + internal IReadOnlyCollection Properties => _properties.Values; + /// /// Gets all non-attached s registered on a type. /// @@ -215,8 +223,13 @@ namespace Avalonia inner.Add(property.Id, property); } - _properties.Add(property); + if (!_properties.ContainsKey(property.Id)) + { + _properties.Add(property.Id, property); + } + _registeredCache.Clear(); + _initializedCache.Clear(); } /// @@ -250,9 +263,59 @@ namespace Avalonia { inner.Add(property.Id, property); } - - _properties.Add(property); + _attachedCache.Clear(); + _initializedCache.Clear(); + } + + internal void NotifyInitialized(AvaloniaObject o) + { + Contract.Requires(o != null); + + var type = o.GetType(); + + void Notify(AvaloniaProperty property, object value) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + property, + AvaloniaProperty.UnsetValue, + value, + BindingPriority.Unset); + + property.NotifyInitialized(e); + } + + if (!_initializedCache.TryGetValue(type, out var items)) + { + var build = new Dictionary(); + + foreach (var property in GetRegistered(type)) + { + var value = !property.IsDirect ? + ((IStyledPropertyAccessor)property).GetDefaultValue(type) : + null; + build.Add(property, value); + } + + foreach (var property in GetRegisteredAttached(type)) + { + if (!build.ContainsKey(property)) + { + var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); + build.Add(property, value); + } + } + + items = build.ToList(); + _initializedCache.Add(type, items); + } + + foreach (var i in items) + { + var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; + Notify(i.Key, value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index c4ffa839e0..f1717bde3b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -177,7 +177,7 @@ namespace Avalonia.Data.Core protected override void Subscribed(IObserver observer, bool first) { - if (!first && _value != null && _value.TryGetTarget(out var val) == true) + if (!first && _value != null && _value.TryGetTarget(out var val)) { observer.OnNext(val); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 135935498c..e48c671a13 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins { if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method)); + var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11f8ada7e..5a3829167a 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -16,6 +16,11 @@ namespace Avalonia /// event EventHandler PropertyChanged; + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler InheritablePropertyChanged; + /// /// Gets a value. /// @@ -97,4 +102,4 @@ namespace Avalonia IObservable source, BindingPriority priority = BindingPriority.LocalValue); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 8cbf212381..540b1bf19b 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Data; using Avalonia.Utilities; @@ -28,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Logs a binding error. + /// + /// The property the error occurred on. + /// The binding error. + void LogError(AvaloniaProperty property, Exception e); + /// /// Ensures that the current thread is the UI thread. /// diff --git a/src/Avalonia.Base/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs deleted file mode 100644 index 04e3d72e6c..0000000000 --- a/src/Avalonia.Base/ISupportInitialize.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia -{ - /// - /// Specifies that this object supports a simple, transacted notification for batch - /// initialization. - /// - public interface ISupportInitialize - { - /// - /// Signals the object that initialization is starting. - /// - void BeginInit(); - - /// - /// Signals the object that initialization is complete. - /// - void EndInit(); - } -} diff --git a/src/Avalonia.Base/Logging/LoggerExtensions.cs b/src/Avalonia.Base/Logging/LoggerExtensions.cs deleted file mode 100644 index 24e44bf9de..0000000000 --- a/src/Avalonia.Base/Logging/LoggerExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Avalonia.Data; - -namespace Avalonia.Logging -{ - internal static class LoggerExtensions - { - public static void LogIfError( - this BindingNotification notification, - object source, - AvaloniaProperty property) - { - if (notification.ErrorType == BindingErrorType.Error) - { - if (notification.Error is AggregateException aggregate) - { - foreach (var inner in aggregate.InnerExceptions) - { - LogError(source, property, inner); - } - } - else - { - LogError(source, property, notification.Error); - } - } - } - - private static void LogError(object source, AvaloniaProperty property, Exception e) - { - var level = LogEventLevel.Warning; - - if (e is BindingChainException b && - !string.IsNullOrEmpty(b.Expression) && - string.IsNullOrEmpty(b.ExpressionErrorPoint)) - { - // The error occurred at the root of the binding chain: it's possible that the - // DataContext isn't set up yet, so log at Information level instead of Warning - // to prevent spewing hundreds of errors. - level = LogEventLevel.Information; - } - - Logger.Log( - level, - LogArea.Binding, - source, - "Error in binding to {Target}.{Property}: {Message}", - source, - property, - e.Message); - } - } -} diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index c8b434c6f9..89a893577f 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -197,7 +197,7 @@ namespace Avalonia /// The binding error. public void LevelError(PriorityLevel level, BindingNotification error) { - error.LogIfError(Owner, Property); + Owner.LogError(Property, error.Error); } /// diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 22e5c952bf..66024236da 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -4,6 +4,8 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; +using System.Xml.Linq; +using System.Linq; // ReSharper disable AssignNullToNotNullAttribute @@ -19,10 +21,20 @@ namespace Avalonia.Utilities { var ver = new BinaryReader(stream).ReadInt32(); if (ver > LastKnownVersion) - throw new Exception("Resources index format version is not known"); - var index = (AvaloniaResourcesIndex) - new DataContractSerializer(typeof(AvaloniaResourcesIndex)).ReadObject(stream); - return index.Entries; + throw new Exception("Resources index format version is not known"); + + var assetDoc = XDocument.Load(stream); + XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; + List entries= + (from entry in assetDoc.Root.Element(assetNs + "Entries").Elements(assetNs + "AvaloniaResourcesIndexEntry") + select new AvaloniaResourcesIndexEntry + { + Path = entry.Element(assetNs + "Path").Value, + Offset = int.Parse(entry.Element(assetNs + "Offset").Value), + Size = int.Parse(entry.Element(assetNs + "Size").Value) + }).ToList(); + + return entries; } public static void Write(Stream stream, List entries) diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs new file mode 100644 index 0000000000..b59ed166bc --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -0,0 +1,221 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// Manages subscriptions to events using weak listeners. + /// + public static class WeakEventHandlerManager + { + /// + /// Subscribes to an event on an object using a weak subscription. + /// + /// The type of the target. + /// The type of the event arguments. + /// The type of the subscriber. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Subscribe(TTarget target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + var dic = SubscriptionTypeStorage.Subscribers.GetOrCreateValue(target); + Subscription sub; + + if (!dic.TryGetValue(eventName, out sub)) + { + dic[eventName] = sub = new Subscription(dic, typeof(TTarget), target, eventName); + } + + sub.Add(subscriber); + } + + /// + /// Unsubscribes from an event. + /// + /// The type of the event arguments. + /// The type of the subscriber. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Unsubscribe(object target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + SubscriptionDic dic; + + if (SubscriptionTypeStorage.Subscribers.TryGetValue(target, out dic)) + { + Subscription sub; + + if (dic.TryGetValue(eventName, out sub)) + { + sub.Remove(subscriber); + } + } + } + + private static class SubscriptionTypeStorage + where TArgs : EventArgs where TSubscriber : class + { + public static readonly ConditionalWeakTable> Subscribers + = new ConditionalWeakTable>(); + } + + private class SubscriptionDic : Dictionary> + where T : EventArgs where TSubscriber : class + { + } + + private static readonly Dictionary> Accessors + = new Dictionary>(); + + private class Subscription where T : EventArgs where TSubscriber : class + { + private readonly EventInfo _info; + private readonly SubscriptionDic _sdic; + private readonly object _target; + private readonly string _eventName; + private readonly Delegate _delegate; + + private Descriptor[] _data = new Descriptor[2]; + private int _count = 0; + + delegate void CallerDelegate(TSubscriber s, object sender, T args); + + struct Descriptor + { + public WeakReference Subscriber; + public CallerDelegate Caller; + } + + private static Dictionary s_Callers = + new Dictionary(); + + public Subscription(SubscriptionDic sdic, Type targetType, object target, string eventName) + { + _sdic = sdic; + _target = target; + _eventName = eventName; + Dictionary evDic; + if (!Accessors.TryGetValue(targetType, out evDic)) + Accessors[targetType] = evDic = new Dictionary(); + + if (!evDic.TryGetValue(eventName, out _info)) + { + var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName); + + if (ev == null) + { + throw new ArgumentException( + $"The event {eventName} was not found on {target.GetType()}."); + } + + evDic[eventName] = _info = ev; + } + + var del = new Action(OnEvent); + _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType, del.Target); + _info.AddMethod.Invoke(target, new[] { _delegate }); + } + + void Destroy() + { + _info.RemoveMethod.Invoke(_target, new[] { _delegate }); + _sdic.Remove(_eventName); + } + + public void Add(EventHandler s) + { + Compact(true); + if (_count == _data.Length) + { + //Extend capacity + var ndata = new Descriptor[_data.Length*2]; + Array.Copy(_data, ndata, _data.Length); + _data = ndata; + } + + var subscriber = (TSubscriber)s.Target; + if (!s_Callers.TryGetValue(s.Method, out var caller)) + s_Callers[s.Method] = caller = + (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, s.Method); + _data[_count] = new Descriptor + { + Caller = caller, + Subscriber = new WeakReference(subscriber) + }; + _count++; + } + + public void Remove(EventHandler s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c].Subscriber; + TSubscriber instance; + + if (reference != null && reference.TryGetTarget(out instance) && instance == s) + { + _data[c] = default; + removed = true; + } + } + + if (removed) + { + Compact(); + } + } + + void Compact(bool preventDestroy = false) + { + int empty = -1; + for (int c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r.Subscriber == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r.Subscriber != null && empty != -1) + { + _data[c] = default; + _data[empty] = r; + empty++; + } + } + if (empty != -1) + _count = empty; + if (_count == 0 && !preventDestroy) + Destroy(); + } + + void OnEvent(object sender, T eventArgs) + { + var needCompact = false; + for(var c=0; c<_count; c++) + { + var r = _data[c].Subscriber; + TSubscriber sub; + if (r.TryGetTarget(out sub)) + { + _data[c].Caller(sub, sender, eventArgs); + } + else + needCompact = true; + } + if (needCompact) + Compact(); + } + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index d520e2b80a..24f85ea6b1 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -118,6 +118,10 @@ namespace Avalonia return dict; } + public void LogError(AvaloniaProperty property, Exception e) + { + _owner.LogBindingError(property, e); + } public object GetValue(AvaloniaProperty property) { diff --git a/src/Avalonia.Controls.DataGrid/AppBuilderExtensions.cs b/src/Avalonia.Controls.DataGrid/AppBuilderExtensions.cs new file mode 100644 index 0000000000..bdb9bf182c --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/AppBuilderExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Threading; + +namespace Avalonia +{ + public static class AppBuilderExtensions + { + public static TAppBuilder UseDataGrid(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + // Portable.Xaml doesn't correctly load referenced assemblies and so doesn't + // find `DataGrid` when loading XAML. Call this method from AppBuilder as a + // temporary workaround until we fix XAML. + return builder; + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj new file mode 100644 index 0000000000..889ed84993 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs new file mode 100644 index 0000000000..4b4203ba40 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -0,0 +1,4315 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Utils; +using Avalonia.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Avalonia.Collections +{ + /// + /// 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. + internal 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, 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("PageSize cannot have a negative value."); + } + + // if the Refresh is currently deferred, cache the desired PageSize + // and set the flag so that once the defer is over, we can then + // update the PageSize. + if (IsRefreshDeferred) + { + // set cached value and flag so that we update the PageSize on EndDefer + _cachedPageSize = value; + SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, true); + return; + } + + // to see whether or not to fire an OnPropertyChanged + int oldCount = Count; + + if (_pageSize != value) + { + // Remember current currency values for upcoming OnPropertyChanged notifications + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // Check if there is a current edited or new item so changes can be committed first. + if (CurrentAddItem != null || CurrentEditItem != null) + { + // Check with the ICollectionView.CurrentChanging listeners if it's OK to + // change the currency. If not, then we can't fire the event to allow them to + // commit their changes. So, we will not be able to change the PageSize. + if (!OkToChangeCurrent()) + { + throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); + } + + // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or + // cancelling an item that is no longer on the current page. That's acceptable and means that + // the potential _newItem or _editItem needs to be committed before this PageSize change. + // The reason why we temporarily reset currency here is to give a chance to the bound + // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit() + // for example as a result of changing currency. + SetCurrentToPosition(-1); + RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // If the bound controls did not successfully end their potential item editing/addition, we + // need to throw an exception to show that the PageSize change failed. + if (CurrentAddItem != null || CurrentEditItem != null) + { + throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); + } + } + + _pageSize = value; + OnPropertyChanged(nameof(PageSize)); + + if (_pageSize == 0) + { + // update the groups for the current page + //*************************************** + PrepareGroups(); + + // if we are not paging + MoveToPage(-1); + } + else if (_pageIndex != 0) + { + if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) + { + // if the temporaryGroup was not created yet and is out of sync + // then create it so that we can use it as a refernce while paging. + if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) + { + PrepareTemporaryGroups(); + } + + MoveToFirstPage(); + } + } + else if (IsGrouping) + { + // if the temporaryGroup was not created yet and is out of sync + // then create it so that we can use it as a refernce while paging. + if (_temporaryGroup.ItemCount != InternalList.Count) + { + // update the groups that get created for the + // entire collection as well as the current page + PrepareTemporaryGroups(); + } + + // update the groups for the current page + PrepareGroupsForCurrentPage(); + } + + // if the count has changed + if (Count != oldCount) + { + OnPropertyChanged(nameof(Count)); + } + + // reset currency values + ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // send a notification that our collection has been updated + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Reset)); + + // now raise currency changes at the end + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + } + } + + /// + /// 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); } + } + + /// + /// 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 indicies larger than the count + if (index >= Count || index < 0) + { + throw new ArgumentOutOfRangeException("index"); + } + + if (IsGrouping) + { + return RootGroup?.LeafAt(_isUsingTemporaryGroup ? ConvertToInternalIndex(index) : index); + } + + if (IsAddingNew && UsesLocalArray && index == Count - 1) + { + return CurrentAddItem; + } + + return InternalItemAt(ConvertToInternalIndex(index)); + } + + /// + /// 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 & flags) != 0; + } + + /// + /// 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)); + } + } + + 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; + } + + object addedItem = args.NewItems?[0]; + object removedItem = args.OldItems?[0]; + + // fire notifications for removes + if (args.Action == NotifyCollectionChangedAction.Remove || + args.Action == NotifyCollectionChangedAction.Replace) + { + ProcessRemoveEvent(removedItem, args.Action == NotifyCollectionChangedAction.Replace); + } + + // fire notifications for adds + if ((args.Action == NotifyCollectionChangedAction.Add || + args.Action == NotifyCollectionChangedAction.Replace) && + (Filter == null || PassesFilter(addedItem))) + { + ProcessAddEvent(addedItem, args.NewStartingIndex); + } + if (args.Action != NotifyCollectionChangedAction.Replace) + { + OnPropertyChanged(nameof(ItemCount)); + } + } + + /// + /// 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."); + } + } + + /// + /// 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; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs new file mode 100644 index 0000000000..9d8ebbfac1 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -0,0 +1,1366 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Utils; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public abstract class DataGridGroupDescription : INotifyPropertyChanged + { + public AvaloniaList 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; + + if (_valueConverter != null) + key = _valueConverter.Convert(key, typeof(object), level, culture); + + return key; + } + public override bool KeysMatch(object groupKey, object itemKey) + { + if(groupKey is string k1 && itemKey is string k2) + { + return String.Equals(k1, k2, _stringComparison); + } + else + return base.KeysMatch(groupKey, itemKey); + } + public override string PropertyName => _propertyPath; + + private Type GetPropertyType(object o) + { + return o.GetType().GetNestedPropertyType(_propertyPath); + } + private static object InvokePath(object item, string propertyPath, Type propertyType) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); + if (exception != null) + { + throw exception; + } + return propertyValue; + } + } + + public abstract class DataGridCollectionViewGroup : INotifyPropertyChanged + { + private int _itemCount; + + public object Key { get; } + public int ItemCount => _itemCount; + public IAvaloniaReadOnlyList 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 new file mode 100644 index 0000000000..86113da87e --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -0,0 +1,259 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Utils; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public abstract class DataGridSortDescription + { + public virtual string PropertyPath => null; + public virtual bool Descending => false; + public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); + public abstract IComparer 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); + } + + internal virtual DataGridSortDescription SwitchSortDirection() + { + return this; + } + + internal virtual void Initialize(Type itemType) + { } + + private static object InvokePath(object item, string propertyPath, Type propertyType) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); + if (exception != null) + { + throw exception; + } + return propertyValue; + } + + /// + /// 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 bool _descending; + 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 (_internalComparerTyped 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 bool Descending => _descending; + + public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture) + { + _propertyPath = propertyPath; + _descending = descending; + _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); + _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); + } + private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending) + { + _propertyPath = inner._propertyPath; + _descending = descending; + _propertyType = inner._propertyType; + _cultureSensitiveComparer = inner._cultureSensitiveComparer; + _internalComparer = inner._internalComparer; + _internalComparerTyped = inner._internalComparerTyped; + + _comparer = new Lazy>(() => 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 (typeof(Comparer<>).MakeGenericType(type).GetProperty("Default")).GetValue(null, null) as IComparer; + } + private Type GetPropertyType(object o) + { + return o.GetType().GetNestedPropertyType(_propertyPath); + } + + private int Compare(object x, object y) + { + int result = 0; + + if(_propertyType == null) + { + if(x != null) + { + _propertyType = GetPropertyType(x); + } + if(_propertyType == null && y != null) + { + _propertyType = GetPropertyType(y); + } + } + + object v1 = GetValue(x); + object v2 = GetValue(y); + + if (_propertyType != null && _internalComparer == null) + _internalComparer = GetComparerForType(_propertyType); + + result = _internalComparer?.Compare(v1, v2) ?? 0; + + if (_descending) + return -result; + else + return result; + } + + internal override void Initialize(Type itemType) + { + base.Initialize(itemType); + + if(_propertyType == null) + _propertyType = itemType.GetNestedPropertyType(_propertyPath); + if (_internalComparer == null && _propertyType != null) + _internalComparer = GetComparerForType(_propertyType); + } + public override IOrderedEnumerable OrderBy(IEnumerable seq) + { + if(_descending) + { + return seq.OrderByDescending(o => GetValue(o), InternalComparer); + } + else + { + return seq.OrderBy(o => GetValue(o), InternalComparer); + } + } + public override IOrderedEnumerable ThenBy(IOrderedEnumerable seq) + { + if (_descending) + { + return seq.ThenByDescending(o => GetValue(o), InternalComparer); + } + else + { + return seq.ThenByDescending(o => GetValue(o), InternalComparer); + } + } + + internal override DataGridSortDescription SwitchSortDirection() + { + return new DataGridPathSortDescription(this, !_descending); + } + } + + public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) + { + return new DataGridPathSortDescription(propertyPath, descending, culture); + } + } + + public class DataGridSortDescriptionCollection : AvaloniaList + { } +} diff --git a/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs new file mode 100644 index 0000000000..12f44fc4f8 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs @@ -0,0 +1,233 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Text; + +namespace Avalonia.Collections +{ + /// 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. + internal 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 new file mode 100644 index 0000000000..bcd12fbfbb --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -0,0 +1,5953 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Collections; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.VisualTree; +using Avalonia.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Linq; +using Avalonia.Input.Platform; +using System.ComponentModel.DataAnnotations; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls +{ + /// + /// Displays data in a customizable grid. + /// + public partial class DataGrid : TemplatedControl + { + private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; + private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; + private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; + private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; + private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter"; + private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; + private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; + private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary"; + private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar"; + + private const bool DATAGRID_defaultAutoGenerateColumns = true; + internal const bool DATAGRID_defaultCanUserReorderColumns = true; + internal const bool DATAGRID_defaultCanUserResizeColumns = true; + internal const bool DATAGRID_defaultCanUserSortColumns = true; + private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected; + private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended; + + /// + /// 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 = 48.0; + private const double DATAGRID_maxHeadersThickness = 32768; + + private const double DATAGRID_defaultRowHeight = 22; + internal const double DATAGRID_defaultRowGroupSublevelIndent = 20; + private const double DATAGRID_defaultMinColumnWidth = 20; + private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; + + private List _validationErrors; + private List _bindingValidationErrors; + private IDisposable _validationSubscription; + + private INotifyCollectionChanged _topLevelGroup; + private ContentControl _clipboardContentControl; + + private DataGridColumnHeadersPresenter _columnHeadersPresenter; + private DataGridRowsPresenter _rowsPresenter; + private ScrollBar _vScrollBar; + private ScrollBar _hScrollBar; + + private ContentControl _topLeftCornerHeader; + private ContentControl _topRightCornerHeader; + private Control _frozenColumnScrollBarSpacer; + + // the sum of the widths in pixels of the scrolling columns preceding + // the first displayed scrolling column + private double _horizontalOffset; + + // the number of pixels of the firstDisplayedScrollingCol which are not displayed + private double _negHorizontalOffset; + private byte _autoGeneratingColumnOperationCount; + private bool _areHandlersSuspended; + private bool _autoSizingColumns; + private IndexToValueTable _collapsedSlotsTable; + private DataGridCellCoordinates _currentCellCoordinates; + private Control _clickedElement; + + // used to store the current column during a Reset + private int _desiredCurrentColumnIndex; + private int _editingColumnIndex; + + // this is a workaround only for the scenarios where we need it, it is not all encompassing nor always updated + private RoutedEventArgs _editingEventArgs; + private bool _executingLostFocusActions; + private bool _flushCurrentCellChanged; + private bool _focusEditingControl; + private IVisual _focusedObject; + private byte _horizontalScrollChangesIgnored; + private DataGridRow _focusedRow; + private bool _ignoreNextScrollBarsLayout; + + // Nth row of rows 0..N that make up the RowHeightEstimate + private int _lastEstimatedRow; + private List _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. + private ICellEditBinding _currentCellEditBinding; + + // An approximation of the sum of the heights in pixels of the scrolling rows preceding + // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid + // does not know their actual height. The heights used for the approximation are the ones + // set as the rows were scrolled off. + private double _verticalOffset; + private byte _verticalScrollChangesIgnored; + + private IEnumerable _items; + + /// + /// 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: ValidateColumnHeaderHeight); + + private static double ValidateColumnHeaderHeight(DataGrid grid, double value) + { + if (value < DATAGRID_minimumColumnHeaderHeight) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight); + } + if (value > DATAGRID_maxHeadersThickness) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness); + } + + return value; + } + + /// + /// 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); + + /// + /// 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 AlternatingRowBackgroundProperty = + AvaloniaProperty.Register(nameof(AlternatingRowBackground)); + + /// + /// Gets or sets the that is used to paint the background of odd-numbered rows. + /// + /// + /// The brush that is used to paint the background of odd-numbered rows. The default is a + /// with a + /// value of white (ARGB value #00FFFFFF). + /// + public IBrush AlternatingRowBackground + { + get { return GetValue(AlternatingRowBackgroundProperty); } + set { SetValue(AlternatingRowBackgroundProperty, 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 int ValidateFrozenColumnCount(DataGrid grid, int value) + { + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); + } + + return value; + } + + 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(); + } + } + } + } + } + + 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); } + } + + public static readonly StyledProperty MaxColumnWidthProperty = + AvaloniaProperty.Register( + nameof(MaxColumnWidth), + defaultValue: DATAGRID_defaultMaxColumnWidth, + validate: ValidateMaxColumnWidth); + + private static double ValidateMaxColumnWidth(DataGrid grid, double value) + { + if (double.IsNaN(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth)); + } + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0); + } + if (grid.MinColumnWidth > value) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth)); + } + + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); + } + + return value; + } + + /// + /// 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: ValidateMinColumnWidth); + + private static double ValidateMinColumnWidth(DataGrid grid, double value) + { + if (double.IsNaN(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth)); + } + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0); + } + if (double.IsPositiveInfinity(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth)); + } + if (grid.MaxColumnWidth < value) + { + throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth)); + } + + return value; + } + + /// + /// 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: ValidateRowHeight); + private static double ValidateRowHeight(DataGrid grid, double value) + { + if (value < DataGridRow.DATAGRIDROW_minimumHeight) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0); + } + if (value > DataGridRow.DATAGRIDROW_maximumHeight) + { + throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight); + } + + return value; + } + + /// + /// 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: ValidateRowHeaderWidth); + private static double ValidateRowHeaderWidth(DataGrid grid, double value) + { + if (value < DATAGRID_minimumRowHeaderWidth) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth); + } + if (value > DATAGRID_maxHeadersThickness) + { + throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness); + } + + return value; + } + + /// + /// 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); + + /// + /// 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); + + /// + /// 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 dependency property. + /// + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); + + /// + /// Gets or sets a collection that is used to generate the content of the control. + /// + public IEnumerable Items + { + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, 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); } + } + + static DataGrid() + { + AffectsMeasure( + ColumnHeaderHeightProperty, + HorizontalScrollBarVisibilityProperty, + VerticalScrollBarVisibilityProperty); + + PseudoClass(IsValidProperty, x => !x, ":invalid"); + + ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); + CanUserResizeColumnsProperty.Changed.AddClassHandler(x => x.OnCanUserResizeColumnsChanged); + ColumnWidthProperty.Changed.AddClassHandler(x => x.OnColumnWidthChanged); + RowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); + AlternatingRowBackgroundProperty.Changed.AddClassHandler(x => x.OnRowBackgroundChanged); + FrozenColumnCountProperty.Changed.AddClassHandler(x => x.OnFrozenColumnCountChanged); + GridLinesVisibilityProperty.Changed.AddClassHandler(x => x.OnGridLinesVisibilityChanged); + HeadersVisibilityProperty.Changed.AddClassHandler(x => x.OnHeadersVisibilityChanged); + HorizontalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnHorizontalGridLinesBrushChanged); + IsReadOnlyProperty.Changed.AddClassHandler(x => x.OnIsReadOnlyChanged); + MaxColumnWidthProperty.Changed.AddClassHandler(x => x.OnMaxColumnWidthChanged); + MinColumnWidthProperty.Changed.AddClassHandler(x => x.OnMinColumnWidthChanged); + RowHeightProperty.Changed.AddClassHandler(x => x.OnRowHeightChanged); + RowHeaderWidthProperty.Changed.AddClassHandler(x => x.OnRowHeaderWidthChanged); + SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); + VerticalGridLinesBrushProperty.Changed.AddClassHandler(x => x.OnVerticalGridLinesBrushChanged); + SelectedIndexProperty.Changed.AddClassHandler(x => x.OnSelectedIndexChanged); + SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemChanged); + IsEnabledProperty.Changed.AddClassHandler(x => x.DataGrid_IsEnabledChanged); + AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler(x => x.OnAreRowGroupHeadersFrozenChanged); + RowDetailsTemplateProperty.Changed.AddClassHandler(x => x.OnRowDetailsTemplateChanged); + RowDetailsVisibilityModeProperty.Changed.AddClassHandler(x => x.OnRowDetailsVisibilityModeChanged); + AutoGenerateColumnsProperty.Changed.AddClassHandler(x => x.OnAutoGenerateColumnsChanged); + } + + /// + /// 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(); + + 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; + } + + 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(); + } + + /// + /// ItemsProperty property changed handler. + /// + /// AvaloniaPropertyChangedEventArgs. + private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + Debug.Assert(DataConnection != null); + + var oldValue = (IEnumerable)e.OldValue; + var newItemsSource = (IEnumerable)e.NewValue; + + if (LoadingOrUnloadingRow) + { + SetValueNoCallback(ItemsProperty, oldValue); + throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows(); + } + + // Try to commit edit on the old DataSource, but force a cancel if it fails + if (!CommitEdit()) + { + CancelEdit(DataGridEditingUnit.Row, false); + } + + DataConnection.UnWireEvents(DataConnection.DataSource); + DataConnection.ClearDataProperties(); + ClearRowGroupHeadersTable(); + + // The old selected indexes are no longer relevant. There's a perf benefit from + // updating the selected indexes with a null DataSource, because we know that all + // of the previously selected indexes have been removed from selection + DataConnection.DataSource = null; + _selectedItems.UpdateIndexes(); + CoerceSelectedItem(); + + // Wrap an IEnumerable in an ICollectionView if it's not already one + bool setDefaultSelection = false; + if (newItemsSource != null && !(newItemsSource is IDataGridCollectionView)) + { + DataConnection.DataSource = DataGridDataConnection.CreateView(newItemsSource); + } + else + { + DataConnection.DataSource = newItemsSource; + setDefaultSelection = true; + } + + if (DataConnection.DataSource != null) + { + // Setup the column headers + if (DataConnection.DataType != null) + { + foreach (var column in ColumnsInternal.GetDisplayedColumns()) + { + if (column is DataGridBoundColumn boundColumn) + { + boundColumn.SetHeaderFromBinding(); + } + } + } + DataConnection.WireEvents(DataConnection.DataSource); + } + + // Wait for the current cell to be set before we raise any SelectionChanged events + _makeFirstDisplayedCellCurrentCellPending = true; + + // Clear out the old rows and remove the generated columns + ClearRows(false); //recycle + RemoveAutoGeneratedColumns(); + + // Set the SlotCount (from the data count and number of row group headers) before we make the default selection + PopulateRowGroupHeadersTable(); + SelectedItem = null; + if (DataConnection.CollectionView != null && setDefaultSelection) + { + SelectedItem = DataConnection.CollectionView.CurrentItem; + } + + // Treat this like the DataGrid has never been measured because all calculations at + // this point are invalid until the next layout cycle. For instance, the ItemsSource + // can be set when the DataGrid is not part of the visual tree + _measured = false; + InvalidateMeasure(); + } + } + + private void OnSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + int index = (int)e.NewValue; + + // GetDataItem returns null if index is >= Count, we do not check newValue + // against Count here to avoid enumerating through an Enumerable twice + // Setting SelectedItem coerces the finally value of the SelectedIndex + object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index); + SelectedItem = newSelectedItem; + if (SelectedItem != newSelectedItem) + { + SetValueNoCallback(SelectedIndexProperty, (int)e.OldValue); + } + } + } + + private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + int rowIndex = (e.NewValue == null) ? -1 : DataConnection.IndexOf(e.NewValue); + if (rowIndex == -1) + { + // If the Item is null or it's not found, clear the Selection + if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) + { + // Edited value couldn't be committed or aborted + SetValueNoCallback(SelectedItemProperty, e.OldValue); + return; + } + + // Clear all row selections + ClearRowSelection(resetAnchorSlot: true); + } + else + { + int slot = SlotFromRowIndex(rowIndex); + if (slot != CurrentSlot) + { + if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) + { + // Edited value couldn't be committed or aborted + SetValueNoCallback(SelectedItemProperty, e.OldValue); + return; + } + if (slot >= SlotCount || slot < -1) + { + if (DataConnection.CollectionView != null) + { + DataConnection.CollectionView.MoveCurrentToPosition(rowIndex); + } + } + } + + int oldSelectedIndex = SelectedIndex; + SetValueNoCallback(SelectedIndexProperty, rowIndex); + try + { + _noSelectionChangeCount++; + int columnIndex = CurrentColumnIndex; + + if (columnIndex == -1) + { + columnIndex = FirstDisplayedNonFillerColumnIndex; + } + if (IsSlotOutOfSelectionBounds(slot)) + { + ClearRowSelection(slotException: slot, setAnchorSlot: true); + return; + } + + UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); + } + finally + { + NoSelectionChangeCount--; + } + + if (!_successfullyUpdatedSelection) + { + SetValueNoCallback(SelectedIndexProperty, oldSelectedIndex); + SetValueNoCallback(SelectedItemProperty, e.OldValue); + } + } + } + } + + private void OnVerticalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + row.EnsureGridLines(); + } + } + } + + private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + ClearRowSelection(resetAnchorSlot: true); + } + } + + private void OnRowHeaderWidthChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + EnsureRowHeaderWidth(); + } + } + + private void OnRowHeightChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + InvalidateRowHeightEstimate(); + // Re-measure all the rows due to the Height change + InvalidateRowsMeasure(invalidateIndividualElements: true); + // DataGrid needs to update the layout information and the ScrollBars + InvalidateMeasure(); + } + } + + private void OnMinColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + double oldValue = (double)e.OldValue; + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) + { + OnColumnMinWidthChanged(column, Math.Max(column.MinWidth, oldValue)); + } + } + } + + private void OnMaxColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + var oldValue = (double)e.OldValue; + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) + { + OnColumnMaxWidthChanged(column, Math.Min(column.MaxWidth, oldValue)); + } + } + } + + private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended) + { + var value = (bool)e.NewValue; + if (value && !CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) + { + CancelEdit(DataGridEditingUnit.Row, raiseEvents: false); + } + } + } + + private void OnHorizontalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_areHandlersSuspended && _rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + row.EnsureGridLines(); + } + } + } + + private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + { + var oldValue = (DataGridHeadersVisibility)e.OldValue; + var newValue = (DataGridHeadersVisibility)e.NewValue; + bool hasFlags(DataGridHeadersVisibility value, DataGridHeadersVisibility flags) => ((value & flags) == flags); + + bool newValueCols = hasFlags(newValue, DataGridHeadersVisibility.Column); + bool newValueRows = hasFlags(newValue, DataGridHeadersVisibility.Row); + bool oldValueCols = hasFlags(oldValue, DataGridHeadersVisibility.Column); + bool oldValueRows = hasFlags(oldValue, DataGridHeadersVisibility.Row); + + // Columns + if (newValueCols != oldValueCols) + { + if (_columnHeadersPresenter != null) + { + EnsureColumnHeadersVisibility(); + if (!newValueCols) + { + _columnHeadersPresenter.Measure(Size.Empty); + } + else + { + EnsureVerticalGridLines(); + } + InvalidateMeasure(); + } + } + + // Rows + if (newValueRows != oldValueRows) + { + if (_rowsPresenter != null) + { + foreach (Control element in _rowsPresenter.Children) + { + if (element is DataGridRow row) + { + row.EnsureHeaderStyleAndVisibility(null); + if (newValueRows) + { + row.UpdatePseudoClasses(); + row.EnsureHeaderVisibility(); + } + } + else if (element is DataGridRowGroupHeader rowGroupHeader) + { + rowGroupHeader.EnsureHeaderVisibility(); + } + } + InvalidateRowHeightEstimate(); + InvalidateRowsMeasure(invalidateIndividualElements: true); + } + } + + if (_topLeftCornerHeader != null) + { + _topLeftCornerHeader.IsVisible = newValueRows && newValueCols; + if (_topLeftCornerHeader.IsVisible) + { + _topLeftCornerHeader.Measure(Size.Empty); + } + } + + } + + private void OnGridLinesVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + { + foreach (DataGridRow row in GetAllRows()) + { + row.EnsureGridLines(); + row.InvalidateHorizontalArrange(); + } + } + + private void OnFrozenColumnCountChanged(AvaloniaPropertyChangedEventArgs e) + { + ProcessFrozenColumnCount(); + } + + private void ProcessFrozenColumnCount() + { + CorrectColumnFrozenStates(); + ComputeScrollBarsLayout(); + + InvalidateColumnHeadersArrange(); + InvalidateCellsArrange(); + } + + private void OnRowBackgroundChanged(AvaloniaPropertyChangedEventArgs e) + { + foreach (DataGridRow row in GetAllRows()) + { + row.EnsureBackground(); + } + } + + private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) + { + var value = (DataGridLength)e.NewValue; + + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) + { + if (column.InheritsWidth) + { + column.SetWidthInternalNoCallback(value); + } + } + + EnsureHorizontalLayout(); + } + + private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e) + { + EnsureHorizontalLayout(); + } + + /// + /// 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
+ /// 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. + ///