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/samples/BindingDemo/App.xaml.cs b/samples/BindingDemo/App.xaml.cs index d95241372f..88333857b8 100644 --- a/samples/BindingDemo/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -3,6 +3,8 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Serilog; namespace BindingDemo { diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index b809cc8c94..2bfd223bb6 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -4,6 +4,8 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; using Avalonia.Platform; +using Avalonia.ReactiveUI; +using Serilog; namespace ControlCatalog { @@ -26,7 +28,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 42be488569..24f88ecba0 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Threading; using Avalonia; +using Avalonia.Skia; +using Avalonia.ReactiveUI; namespace ControlCatalog.NetCore { @@ -36,7 +38,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 @@ + + + + + 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/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/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 4ac844983b..b758f887df 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace RenderDemo { diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 41164c7780..c15abad188 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -33,6 +33,9 @@ + + + diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs new file mode 100644 index 0000000000..2e59d934a1 --- /dev/null +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -0,0 +1,119 @@ +using System; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using SkiaSharp; + +namespace RenderDemo.Pages +{ + public class CustomSkiaPage : Control + { + public CustomSkiaPage() + { + ClipToBounds = true; + } + + class CustomDrawOp : ICustomDrawOperation + { + private readonly FormattedText _noSkia; + + public CustomDrawOp(Rect bounds, FormattedText noSkia) + { + _noSkia = noSkia; + Bounds = bounds; + } + + public void Dispose() + { + // No-op + } + + public Rect Bounds { get; } + public bool HitTest(Point p) => false; + public bool Equals(ICustomDrawOperation other) => false; + static Stopwatch St = Stopwatch.StartNew(); + public void Render(IDrawingContextImpl context) + { + var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; + if (canvas == null) + context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl); + else + { + canvas.Save(); + // create the first shader + var colors = new SKColor[] { + new SKColor(0, 255, 255), + new SKColor(255, 0, 255), + new SKColor(255, 255, 0), + new SKColor(0, 255, 255) + }; + + var sx = Animate(100, 2, 10); + var sy = Animate(1000, 5, 15); + var lightPosition = new SKPoint( + (float)(Bounds.Width / 2 + Math.Cos(St.Elapsed.TotalSeconds) * Bounds.Width / 4), + (float)(Bounds.Height / 2 + Math.Sin(St.Elapsed.TotalSeconds) * Bounds.Height / 4)); + using (var sweep = + SKShader.CreateSweepGradient(new SKPoint((int)Bounds.Width / 2, (int)Bounds.Height / 2), colors, + null)) + using(var turbulence = SKShader.CreatePerlinNoiseFractalNoise(0.05f, 0.05f, 4, 0)) + using(var shader = SKShader.CreateCompose(sweep, turbulence, SKBlendMode.SrcATop)) + using(var blur = SKImageFilter.CreateBlur(Animate(100, 2, 10), Animate(100, 5, 15))) + using (var paint = new SKPaint + { + Shader = shader, + ImageFilter = blur + }) + canvas.DrawPaint(paint); + + using (var pseudoLight = SKShader.CreateRadialGradient( + lightPosition, + (float) (Bounds.Width/3), + new [] { + new SKColor(255, 200, 200, 100), + SKColors.Transparent, + new SKColor(40,40,40, 220), + new SKColor(20,20,20, (byte)Animate(100, 200,220)) }, + new float[] { 0.3f, 0.3f, 0.8f, 1 }, + SKShaderTileMode.Clamp)) + using (var paint = new SKPaint + { + Shader = pseudoLight + }) + canvas.DrawPaint(paint); + canvas.Restore(); + } + } + static int Animate(int d, int from, int to) + { + var ms = (int)(St.ElapsedMilliseconds / d); + var diff = to - from; + var range = diff * 2; + var v = ms % range; + if (v > diff) + v = range - v; + var rv = v + from; + if (rv < from || rv > to) + throw new Exception("WTF"); + return rv; + } + } + + + + public override void Render(DrawingContext context) + { + var noSkia = new FormattedText() + { + Text = "Current rendering API is not Skia" + }; + context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); + Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); + } + } +} 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/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 0ade1af249..b59ed166bc 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -19,6 +19,7 @@ namespace Avalonia.Utilities /// /// 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. @@ -40,6 +41,7 @@ namespace Avalonia.Utilities /// 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. 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 or + /// property value changes. + /// + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { AddHandler(SelectionChangedEvent, value); } + } + + /// + /// Occurs when a + /// object becomes available for reuse. + /// + public event EventHandler UnloadingRow; + + /// + /// Occurs when a new row details template is applied to a row, so that you can customize + /// the details section before it is used. + /// + public event EventHandler LoadingRowDetails; + + /// + /// Occurs when the + /// property value changes. + /// + public event EventHandler RowDetailsVisibilityChanged; + + /// + /// Occurs when a row details element becomes available for reuse. + /// + public event EventHandler UnloadingRowDetails; + + /// + /// Gets a collection that contains all the columns in the control. + /// + public ObservableCollection Columns + { + get + { + // we use a backing field here because the field's type + // is a subclass of the property's + return ColumnsInternal; + } + } + + /// + /// Gets or sets the column that contains the current cell. + /// + public DataGridColumn CurrentColumn + { + get + { + if (CurrentColumnIndex == -1) + { + return null; + } + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + return ColumnsItemsInternal[CurrentColumnIndex]; + } + set + { + DataGridColumn dataGridColumn = value; + if (dataGridColumn == null) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNull("value", "CurrentColumn"); + } + if (CurrentColumn != dataGridColumn) + { + if (dataGridColumn.OwningGrid != this) + { + // Provided column does not belong to this DataGrid + throw DataGridError.DataGrid.ColumnNotInThisDataGrid(); + } + if (!dataGridColumn.IsVisible) + { + // CurrentColumn cannot be set to an invisible column + throw DataGridError.DataGrid.ColumnCannotBeCollapsed(); + } + if (CurrentSlot == -1) + { + // There is no current row so the current column cannot be set + throw DataGridError.DataGrid.NoCurrentRow(); + } + bool beginEdit = _editingColumnIndex != -1; + + //exitEditingMode, keepFocus, raiseEvents + if (!EndCellEdit(DataGridEditAction.Commit, true, ContainsFocus, true)) + { + // Edited value couldn't be committed or aborted + return; + } + + UpdateSelectionAndCurrency(dataGridColumn.Index, CurrentSlot, DataGridSelectionAction.None, false); //scrollIntoView + Debug.Assert(_successfullyUpdatedSelection); + + if (beginEdit && + _editingColumnIndex == -1 && + CurrentSlot != -1 && + CurrentColumnIndex != -1 && + CurrentColumnIndex == dataGridColumn.Index && + dataGridColumn.OwningGrid == this && + !GetColumnEffectiveReadOnlyState(dataGridColumn)) + { + // Returning to editing mode since the grid was in that mode prior to the EndCellEdit call above. + BeginCellEdit(new RoutedEventArgs()); + } + } + } + } + + /// + /// Gets a list that contains the data items corresponding to the selected rows. + /// + public IList SelectedItems + { + get { return _selectedItems as IList; } + } + + internal DataGridColumnCollection ColumnsInternal + { + get; + private set; + } + + internal int AnchorSlot + { + get; + private set; + } + + internal double ActualRowHeaderWidth + { + get + { + if (!AreRowHeadersVisible) + { + return 0; + } + else + { + return !double.IsNaN(RowHeaderWidth) ? RowHeaderWidth : RowHeadersDesiredWidth; + } + } + } + + internal double ActualRowsPresenterHeight + { + get + { + if (_rowsPresenter != null) + { + return _rowsPresenter.Bounds.Height; + } + return 0; + } + } + + internal bool AreColumnHeadersVisible + { + get + { + return (HeadersVisibility & DataGridHeadersVisibility.Column) == DataGridHeadersVisibility.Column; + } + } + + internal bool AreRowHeadersVisible + { + get + { + return (HeadersVisibility & DataGridHeadersVisibility.Row) == DataGridHeadersVisibility.Row; + } + } + + /// + /// Indicates whether or not at least one auto-sizing column is waiting for all the rows + /// to be measured before its final width is determined. + /// + internal bool AutoSizingColumns + { + get + { + return _autoSizingColumns; + } + set + { + if (_autoSizingColumns && !value && ColumnsInternal != null) + { + double adjustment = CellsWidth - ColumnsInternal.VisibleEdgedColumnsWidth; + AdjustColumnWidths(0, adjustment, false); + foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns()) + { + column.IsInitialDesiredWidthDetermined = true; + } + ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + ComputeScrollBarsLayout(); + InvalidateColumnHeadersMeasure(); + InvalidateRowsMeasure(true); + } + _autoSizingColumns = value; + } + } + + internal double AvailableSlotElementRoom + { + get; + set; + } + + // Height currently available for cells this value is smaller. This height is reduced by the existence of ColumnHeaders + // or a horizontal scrollbar. Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are + // not reflected immediately. + internal double CellsHeight + { + get + { + return RowsPresenterEstimatedAvailableHeight ?? 0; + } + } + + // Width currently available for cells this value is smaller. This width is reduced by the existence of RowHeaders + // or a vertical scrollbar. Layout is asynchronous so changes to the RowHeaders or the vertical scrollbar are + // not reflected immediately + internal double CellsWidth + { + get + { + double rowsWidth = double.PositiveInfinity; + if (RowsPresenterAvailableSize.HasValue) + { + rowsWidth = Math.Max(0, RowsPresenterAvailableSize.Value.Width - ActualRowHeaderWidth); + } + return double.IsPositiveInfinity(rowsWidth) ? ColumnsInternal.VisibleEdgedColumnsWidth : rowsWidth; + } + } + + internal DataGridColumnHeadersPresenter ColumnHeaders => _columnHeadersPresenter; + + internal List ColumnsItemsInternal => ColumnsInternal.ItemsInternal; + + internal bool ContainsFocus + { + get; + private set; + } + + internal int CurrentColumnIndex + { + get + { + return CurrentCellCoordinates.ColumnIndex; + } + + private set + { + CurrentCellCoordinates.ColumnIndex = value; + } + } + + internal int CurrentSlot + { + get + { + return CurrentCellCoordinates.Slot; + } + + private set + { + CurrentCellCoordinates.Slot = value; + } + } + + internal DataGridDataConnection DataConnection + { + get; + private set; + } + + internal DataGridDisplayData DisplayData + { + get; + private set; + } + + internal int EditingColumnIndex + { + get; + private set; + } + + internal DataGridRow EditingRow + { + get; + private set; + } + + internal double FirstDisplayedScrollingColumnHiddenWidth => _negHorizontalOffset; + + // When the RowsPresenter's width increases, the HorizontalOffset will be incorrect until + // the scrollbar's layout is recalculated, which doesn't occur until after the cells are measured. + // This property exists to account for this scenario, and avoid collapsing the incorrect cells. + internal double HorizontalAdjustment + { + get; + private set; + } + + internal static double HorizontalGridLinesThickness => DATAGRID_horizontalGridLinesThickness; + + // the sum of the widths in pixels of the scrolling columns preceding + // the first displayed scrolling column + internal double HorizontalOffset + { + get + { + return _horizontalOffset; + } + set + { + if (value < 0) + { + value = 0; + } + double widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); + if (value > widthNotVisible) + { + value = widthNotVisible; + } + if (value == _horizontalOffset) + { + return; + } + + if (_hScrollBar != null && value != _hScrollBar.Value) + { + _hScrollBar.Value = value; + } + _horizontalOffset = value; + + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + // update the lastTotallyDisplayedScrollingCol + ComputeDisplayedColumns(); + } + } + + internal ScrollBar HorizontalScrollBar => _hScrollBar; + + internal IndexToValueTable RowGroupHeadersTable + { + get; + private set; + } + + internal bool LoadingOrUnloadingRow + { + get; + private set; + } + + internal bool InDisplayIndexAdjustments + { + get; + set; + } + + internal int? MouseOverRowIndex + { + get + { + return _mouseOverRowIndex; + } + set + { + if (_mouseOverRowIndex != value) + { + DataGridRow oldMouseOverRow = null; + if (_mouseOverRowIndex.HasValue) + { + int oldSlot = SlotFromRowIndex(_mouseOverRowIndex.Value); + if (IsSlotVisible(oldSlot)) + { + oldMouseOverRow = DisplayData.GetDisplayedElement(oldSlot) as DataGridRow; + } + } + + _mouseOverRowIndex = value; + + // State for the old row needs to be applied after setting the new value + if (oldMouseOverRow != null) + { + oldMouseOverRow.UpdatePseudoClasses(); + } + + if (_mouseOverRowIndex.HasValue) + { + int newSlot = SlotFromRowIndex(_mouseOverRowIndex.Value); + if (IsSlotVisible(newSlot)) + { + DataGridRow newMouseOverRow = DisplayData.GetDisplayedElement(newSlot) as DataGridRow; + Debug.Assert(newMouseOverRow != null); + if (newMouseOverRow != null) + { + newMouseOverRow.UpdatePseudoClasses(); + } + } + } + } + } + } + + internal double NegVerticalOffset + { + get; + private set; + } + + internal int NoCurrentCellChangeCount + { + get + { + return _noCurrentCellChangeCount; + } + set + { + _noCurrentCellChangeCount = value; + if (value == 0) + { + FlushCurrentCellChanged(); + } + } + } + + internal double RowDetailsHeightEstimate + { + get; + private set; + } + + internal double RowHeadersDesiredWidth + { + get + { + return _rowHeaderDesiredWidth; + } + set + { + // We only auto grow + if (_rowHeaderDesiredWidth < value) + { + double oldActualRowHeaderWidth = ActualRowHeaderWidth; + _rowHeaderDesiredWidth = value; + if (oldActualRowHeaderWidth != ActualRowHeaderWidth) + { + EnsureRowHeaderWidth(); + } + } + } + } + + internal double RowGroupHeaderHeightEstimate + { + get; + private set; + } + + internal double RowHeightEstimate + { + get; + private set; + } + + internal Size? RowsPresenterAvailableSize + { + get + { + return _rowsPresenterAvailableSize; + } + set + { + if (_rowsPresenterAvailableSize.HasValue && value.HasValue && value.Value.Width > RowsPresenterAvailableSize.Value.Width) + { + // When the available cells width increases, the horizontal offset can be incorrect. + // Store away an adjustment to use during the CellsPresenter's measure, so that the + // ShouldDisplayCell method correctly determines if a cell will be in view. + // + // | h. offset | new available cells width | + // |-------------->|----------------------------------------->| + // __________________________________________________ | + // | | | | | | + // | column0 | column1 | column2 | column3 |<----->| + // | | | | | adj. | + // + double adjustment = (_horizontalOffset + value.Value.Width) - ColumnsInternal.VisibleEdgedColumnsWidth; + HorizontalAdjustment = Math.Min(HorizontalOffset, Math.Max(0, adjustment)); + } + else + { + HorizontalAdjustment = 0; + } + _rowsPresenterAvailableSize = value; + } + } + internal double? RowsPresenterEstimatedAvailableHeight + { + get; + set; + } + + internal double[] RowGroupSublevelIndents + { + get; + private set; + } + + // This flag indicates whether selection has actually changed during a selection operation, + // and exists to ensure that FlushSelectionChanged doesn't unnecessarily raise SelectionChanged. + internal bool SelectionHasChanged + { + get; + set; + } + + internal int SlotCount + { + get; + private set; + } + + internal bool UpdatedStateOnMouseLeftButtonDown + { + get; + set; + } + + /// + /// Indicates whether or not to use star-sizing logic. If the DataGrid has infinite available space, + /// then star sizing doesn't make sense. In this case, all star columns grow to a predefined size of + /// 10,000 pixels in order to show the developer that star columns shouldn't be used. + /// + internal bool UsesStarSizing + { + get + { + if (ColumnsInternal != null) + { + return ColumnsInternal.VisibleStarColumnCount > 0 && + (!RowsPresenterAvailableSize.HasValue || !double.IsPositiveInfinity(RowsPresenterAvailableSize.Value.Width)); + } + return false; + } + } + + internal ScrollBar VerticalScrollBar => _vScrollBar; + + internal int VisibleSlotCount + { + get; + set; + } + + /// + /// Gets the data item bound to the row that contains the current cell. + /// + protected object CurrentItem + { + get + { + if (CurrentSlot == -1 || Items == null || RowGroupHeadersTable.Contains(CurrentSlot)) + { + return null; + } + return DataConnection.GetDataItem(RowIndexFromSlot(CurrentSlot)); + } + } + + private DataGridCellCoordinates CurrentCellCoordinates + { + get; + set; + } + + private int FirstDisplayedNonFillerColumnIndex + { + get + { + DataGridColumn column = ColumnsInternal.FirstVisibleNonFillerColumn; + if (column != null) + { + if (column.IsFrozen) + { + return column.Index; + } + else + { + if (DisplayData.FirstDisplayedScrollingCol >= column.Index) + { + return DisplayData.FirstDisplayedScrollingCol; + } + else + { + return column.Index; + } + } + } + return -1; + } + } + + private int NoSelectionChangeCount + { + get + { + return _noSelectionChangeCount; + } + set + { + _noSelectionChangeCount = value; + if (value == 0) + { + FlushSelectionChanged(); + } + } + } + + /// + /// Enters editing mode for the current cell and current row (if they're not already in editing mode). + /// + /// True if operation was successful. False otherwise. + public bool BeginEdit() + { + return BeginEdit(null); + } + + /// + /// Enters editing mode for the current cell and current row (if they're not already in editing mode). + /// + /// Provides information about the user gesture that caused the call to BeginEdit. Can be null. + /// True if operation was successful. False otherwise. + public bool BeginEdit(RoutedEventArgs editingEventArgs) + { + if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot)) + { + return false; + } + + Debug.Assert(CurrentColumnIndex >= 0); + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(CurrentSlot >= -1); + Debug.Assert(CurrentSlot < SlotCount); + Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot); + + if (GetColumnEffectiveReadOnlyState(CurrentColumn)) + { + // Current column is read-only + return false; + } + return BeginCellEdit(editingEventArgs); + } + + /// + /// Cancels editing mode and restores the original value. + /// + /// True if operation was successful. False otherwise. + public bool CancelEdit() + { + return CancelEdit(DataGridEditingUnit.Row); + } + + /// + /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value. + /// + /// Specifies whether to cancel edit for a Cell or Row. + /// True if operation was successful. False otherwise. + public bool CancelEdit(DataGridEditingUnit editingUnit) + { + return CancelEdit(editingUnit, raiseEvents: true); + } + + /// + /// Commits editing mode and pushes changes to the backend. + /// + /// True if operation was successful. False otherwise. + public bool CommitEdit() + { + return CommitEdit(DataGridEditingUnit.Row, true); + } + + /// + /// Commits editing mode for the specified DataGridEditingUnit and pushes changes to the backend. + /// + /// Specifies whether to commit edit for a Cell or Row. + /// Editing mode is left if True. + /// True if operation was successful. False otherwise. + public bool CommitEdit(DataGridEditingUnit editingUnit, bool exitEditingMode) + { + if (!EndCellEdit( + editAction: DataGridEditAction.Commit, + exitEditingMode: editingUnit == DataGridEditingUnit.Cell ? exitEditingMode : true, + keepFocus: ContainsFocus, + raiseEvents: true)) + { + return false; + } + if (editingUnit == DataGridEditingUnit.Row) + { + return EndRowEdit(DataGridEditAction.Commit, exitEditingMode, raiseEvents: true); + } + return true; + } + + /// + /// Scrolls the specified item or RowGroupHeader and/or column into view. + /// If item is not null: scrolls the row representing the item into view; + /// If column is not null: scrolls the column into view; + /// If both item and column are null, the method returns without scrolling. + /// + /// an item from the DataGrid's items source or a CollectionViewGroup from the collection view + /// a column from the DataGrid's columns collection + public void ScrollIntoView(object item, DataGridColumn column) + { + if ((column == null && (item == null || FirstDisplayedNonFillerColumnIndex == -1)) + || (column != null && column.OwningGrid != this)) + { + // no-op + return; + } + if (item == null) + { + // scroll column into view + ScrollSlotIntoView( + column.Index, + DisplayData.FirstScrollingSlot, + forCurrentCellChange: false, + forceHorizontalScroll: true); + } + else + { + int slot = -1; + DataGridRowGroupInfo rowGroupInfo = null; + if (item is DataGridCollectionViewGroup collectionViewGroup) + { + rowGroupInfo = RowGroupInfoFromCollectionViewGroup(collectionViewGroup); + if (rowGroupInfo == null) + { + Debug.Assert(false); + return; + } + slot = rowGroupInfo.Slot; + } + else + { + // the row index will be set to -1 if the item is null or not in the list + int rowIndex = DataConnection.IndexOf(item); + if (rowIndex == -1) + { + return; + } + slot = SlotFromRowIndex(rowIndex); + } + + int columnIndex = (column == null) ? FirstDisplayedNonFillerColumnIndex : column.Index; + + if (_collapsedSlotsTable.Contains(slot)) + { + // We need to expand all parent RowGroups so that the slot is visible + if (rowGroupInfo != null) + { + ExpandRowGroupParentChain(rowGroupInfo.Level - 1, rowGroupInfo.Slot); + } + else + { + rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(slot)); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo != null) + { + ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot); + } + } + + // Update Scrollbar and display information + NegVerticalOffset = 0; + SetVerticalOffset(0); + ResetDisplayedRows(); + DisplayData.FirstScrollingSlot = 0; + ComputeScrollBarsLayout(); + } + + ScrollSlotIntoView( + columnIndex, slot, + forCurrentCellChange: true, + forceHorizontalScroll: true); + } + } + + /// + /// Arranges the content of the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + /// + /// The actual size used by the . + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (_makeFirstDisplayedCellCurrentCellPending) + { + MakeFirstDisplayedCellCurrentCell(); + } + + if (Bounds.Width != finalSize.Width) + { + // If our final width has changed, we might need to update the filler + InvalidateColumnHeadersArrange(); + InvalidateCellsArrange(); + } + + return base.ArrangeOverride(finalSize); + } + + /// + /// Measures the children of a to prepare for + /// arranging them during the + /// pass. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that + /// child elements should not exceed. + /// + protected override Size MeasureOverride(Size availableSize) + { + // Delay layout until after the initial measure to avoid invalid calculations when the + // DataGrid is not part of the visual tree + if (!_measured) + { + _measured = true; + + // We don't need to clear the rows because it was already done when the ItemsSource changed + RefreshRowsAndColumns(clearRows: false); + + //// Update our estimates now that the DataGrid has all of the information necessary + UpdateRowDetailsHeightEstimate(); + + // Update frozen columns to account for columns added prior to loading or autogenerated columns + if (FrozenColumnCountWithFiller > 0) + { + ProcessFrozenColumnCount(); + } + } + + Size desiredSize; + // This is a shortcut to skip layout if we don't have any columns + if (ColumnsInternal.VisibleEdgedColumnsWidth == 0) + { + if (_hScrollBar != null && _hScrollBar.IsVisible) + { + _hScrollBar.IsVisible = false; + } + if (_vScrollBar != null && _vScrollBar.IsVisible) + { + _vScrollBar.IsVisible = false; + } + desiredSize = base.MeasureOverride(availableSize); + } + else + { + if (_rowsPresenter != null) + { + _rowsPresenter.InvalidateMeasure(); + } + + InvalidateColumnHeadersMeasure(); + + desiredSize = base.MeasureOverride(availableSize); + + ComputeScrollBarsLayout(); + } + + return desiredSize; + } + + /// + /// Raises the BeginningEdit event. + /// + protected virtual void OnBeginningEdit(DataGridBeginningEditEventArgs e) + { + BeginningEdit?.Invoke(this, e); + } + + /// + /// Raises the CellEditEnded event. + /// + protected virtual void OnCellEditEnded(DataGridCellEditEndedEventArgs e) + { + CellEditEnded?.Invoke(this, e); + } + + /// + /// Raises the CellEditEnding event. + /// + protected virtual void OnCellEditEnding(DataGridCellEditEndingEventArgs e) + { + CellEditEnding?.Invoke(this, e); + } + + /// + /// Raises the CellPointerPressed event. + /// + internal virtual void OnCellPointerPressed(DataGridCellPointerPressedEventArgs e) + { + CellPointerPressed?.Invoke(this, e); + } + + /// + /// Raises the CurrentCellChanged event. + /// + protected virtual void OnCurrentCellChanged(EventArgs e) + { + CurrentCellChanged?.Invoke(this, e); + } + + /// + /// Raises the LoadingRow event for row preparation. + /// + protected virtual void OnLoadingRow(DataGridRowEventArgs e) + { + EventHandler handler = LoadingRow; + if (handler != null) + { + Debug.Assert(!_loadedRows.Contains(e.Row)); + _loadedRows.Add(e.Row); + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + Debug.Assert(_loadedRows.Contains(e.Row)); + _loadedRows.Remove(e.Row); + } + } + + /// + /// Scrolls the DataGrid according to the direction of the delta. + /// + /// PointerWheelEventArgs + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) + { + double scrollHeight = 0; + if (e.Delta.Y > 0) + { + scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta); + } + else if (e.Delta.Y < 0) + { + if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) + { + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + } + else + { + double maximum = EdgedRowsHeightCalculated - CellsHeight; + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + } + } + if (scrollHeight != 0) + { + DisplayData.PendingVerticalScrollHeight = scrollHeight; + InvalidateRowsMeasure(invalidateIndividualElements: false); + e.Handled = true; + } + } + } + + /// + /// Raises the PreparingCellForEdit event. + /// + protected virtual void OnPreparingCellForEdit(DataGridPreparingCellForEditEventArgs e) + { + PreparingCellForEdit?.Invoke(this, e); + } + + /// + /// Raises the RowEditEnded event. + /// + protected virtual void OnRowEditEnded(DataGridRowEditEndedEventArgs e) + { + RowEditEnded?.Invoke(this, e); + } + + /// + /// Raises the RowEditEnding event. + /// + protected virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e) + { + RowEditEnding?.Invoke(this, e); + } + + /// + /// Raises the SelectionChanged event and clears the _selectionChanged. + /// This event won't get raised again until after _selectionChanged is set back to true. + /// + protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Raises the UnloadingRow event for row recycling. + /// + protected virtual void OnUnloadingRow(DataGridRowEventArgs e) + { + EventHandler handler = UnloadingRow; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Builds the visual tree for the column header when a new template is applied. + /// + //TODO Validation UI + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + // The template has changed, so we need to refresh the visuals + _measured = false; + + if (_columnHeadersPresenter != null) + { + // If we're applying a new template, we want to remove the old column headers first + _columnHeadersPresenter.Children.Clear(); + } + + _columnHeadersPresenter = e.NameScope.Find(DATAGRID_elementColumnHeadersPresenterName); + + if (_columnHeadersPresenter != null) + { + if (ColumnsInternal.FillerColumn != null) + { + ColumnsInternal.FillerColumn.IsRepresented = false; + } + _columnHeadersPresenter.OwningGrid = this; + // Columns were added before before our Template was applied, add the ColumnHeaders now + foreach (DataGridColumn column in ColumnsItemsInternal) + { + InsertDisplayedColumnHeader(column); + } + } + + if (_rowsPresenter != null) + { + // If we're applying a new template, we want to remove the old rows first + UnloadElements(recycle: false); + } + + _rowsPresenter = e.NameScope.Find(DATAGRID_elementRowsPresenterName); + + if (_rowsPresenter != null) + { + _rowsPresenter.OwningGrid = this; + InvalidateRowHeightEstimate(); + UpdateRowDetailsHeightEstimate(); + } + + _frozenColumnScrollBarSpacer = e.NameScope.Find(DATAGRID_elementFrozenColumnScrollBarSpacerName); + + if (_hScrollBar != null) + { + _hScrollBar.Scroll -= HorizontalScrollBar_Scroll; + } + + _hScrollBar = e.NameScope.Find(DATAGRID_elementHorizontalScrollbarName); + + if (_hScrollBar != null) + { + //_hScrollBar.IsTabStop = false; + _hScrollBar.Maximum = 0.0; + _hScrollBar.Orientation = Orientation.Horizontal; + _hScrollBar.IsVisible = false; + _hScrollBar.Scroll += HorizontalScrollBar_Scroll; + } + + if (_vScrollBar != null) + { + _vScrollBar.Scroll -= VerticalScrollBar_Scroll; + } + + _vScrollBar = e.NameScope.Find(DATAGRID_elementVerticalScrollbarName); + + if (_vScrollBar != null) + { + //_vScrollBar.IsTabStop = false; + _vScrollBar.Maximum = 0.0; + _vScrollBar.Orientation = Orientation.Vertical; + _vScrollBar.IsVisible = false; + _vScrollBar.Scroll += VerticalScrollBar_Scroll; + } + + _topLeftCornerHeader = e.NameScope.Find(DATAGRID_elementTopLeftCornerHeaderName); + EnsureTopLeftCornerHeader(); // EnsureTopLeftCornerHeader checks for a null _topLeftCornerHeader; + _topRightCornerHeader = e.NameScope.Find(DATAGRID_elementTopRightCornerHeaderName); + } + + /// + /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value. + /// + /// Specifies whether to cancel edit for a Cell or Row. + /// Specifies whether or not to raise editing events + /// True if operation was successful. False otherwise. + internal bool CancelEdit(DataGridEditingUnit editingUnit, bool raiseEvents) + { + if (!EndCellEdit( + DataGridEditAction.Cancel, + exitEditingMode: true, + keepFocus: ContainsFocus, + raiseEvents: raiseEvents)) + { + return false; + } + + if (editingUnit == DataGridEditingUnit.Row) + { + return EndRowEdit(DataGridEditAction.Cancel, true, raiseEvents); + } + + return true; + } + + /// + /// call when: selection changes or SelectedItems object changes + /// + internal void CoerceSelectedItem() + { + object selectedItem = null; + + if (SelectionMode == DataGridSelectionMode.Extended && + CurrentSlot != -1 && + _selectedItems.ContainsSlot(CurrentSlot)) + { + selectedItem = CurrentItem; + } + else if (_selectedItems.Count > 0) + { + selectedItem = _selectedItems[0]; + } + + SetValueNoCallback(SelectedItemProperty, selectedItem); + + // Update the SelectedIndex + int newIndex = -1; + + if (selectedItem != null) + { + newIndex = DataConnection.IndexOf(selectedItem); + } + + SetValueNoCallback(SelectedIndexProperty, newIndex); + } + + internal static DataGridCell GetOwningCell(Control element) + { + Debug.Assert(element != null); + DataGridCell cell = element as DataGridCell; + while (element != null && cell == null) + { + element = element.Parent as Control; + cell = element as DataGridCell; + } + return cell; + } + + internal IEnumerable GetSelectionInclusive(int startRowIndex, int endRowIndex) + { + int endSlot = SlotFromRowIndex(endRowIndex); + foreach (int slot in _selectedItems.GetSlots(SlotFromRowIndex(startRowIndex))) + { + if (slot > endSlot) + { + break; + } + yield return DataConnection.GetDataItem(RowIndexFromSlot(slot)); + } + } + + internal void InitializeElements(bool recycleRows) + { + try + { + _noCurrentCellChangeCount++; + + // The underlying collection has changed and our editing row (if there is one) + // is no longer relevant, so we should force a cancel edit. + CancelEdit(DataGridEditingUnit.Row, raiseEvents: false); + + // We want to persist selection throughout a reset, so store away the selected items + List selectedItemsCache = new List(_selectedItems.SelectedItemsCache); + + if (recycleRows) + { + RefreshRows(recycleRows, clearRows: true); + } + else + { + RefreshRowsAndColumns(clearRows: true); + } + + // Re-select the old items + _selectedItems.SelectedItemsCache = selectedItemsCache; + CoerceSelectedItem(); + if (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.Collapsed) + { + UpdateRowDetailsVisibilityMode(RowDetailsVisibilityMode); + } + + // The currently displayed rows may have incorrect visual states because of the selection change + ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot); + } + finally + { + NoCurrentCellChangeCount--; + } + } + + internal bool IsDoubleClickRecordsClickOnCall(Control element) + { + if (_clickedElement == element) + { + _clickedElement = null; + return true; + } + else + { + _clickedElement = element; + return false; + } + } + + // Returns the item or the CollectionViewGroup that is used as the DataContext for a given slot. + // If the DataContext is an item, rowIndex is set to the index of the item within the collection + internal object ItemFromSlot(int slot, ref int rowIndex) + { + if (RowGroupHeadersTable.Contains(slot)) + { + return RowGroupHeadersTable.GetValueAt(slot)?.CollectionViewGroup; + } + else + { + rowIndex = RowIndexFromSlot(slot); + return DataConnection.GetDataItem(rowIndex); + } + } + + internal bool ProcessDownKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessDownKeyInternal(shift, ctrl); + } + + internal bool ProcessEndKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessEndKey(shift, ctrl); + } + + internal bool ProcessEnterKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessEnterKey(shift, ctrl); + } + + internal bool ProcessHomeKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessHomeKey(shift, ctrl); + } + + internal void ProcessHorizontalScroll(ScrollEventType scrollEventType) + { + if (_horizontalScrollChangesIgnored > 0) + { + return; + } + + // If the user scrolls with the buttons, we need to update the new value of the scroll bar since we delay + // this calculation. If they scroll in another other way, the scroll bar's correct value has already been set + double scrollBarValueDifference = 0; + if (scrollEventType == ScrollEventType.SmallIncrement) + { + scrollBarValueDifference = GetHorizontalSmallScrollIncrease(); + } + else if (scrollEventType == ScrollEventType.SmallDecrement) + { + scrollBarValueDifference = -GetHorizontalSmallScrollDecrease(); + } + _horizontalScrollChangesIgnored++; + try + { + if (scrollBarValueDifference != 0) + { + Debug.Assert(_horizontalOffset + scrollBarValueDifference >= 0); + _hScrollBar.Value = _horizontalOffset + scrollBarValueDifference; + } + UpdateHorizontalOffset(_hScrollBar.Value); + } + finally + { + _horizontalScrollChangesIgnored--; + } + } + + internal bool ProcessLeftKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessLeftKey(shift, ctrl); + } + + internal bool ProcessNextKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessNextKey(shift, ctrl); + } + + internal bool ProcessPriorKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessPriorKey(shift, ctrl); + } + + internal bool ProcessRightKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessRightKey(shift, ctrl); + } + + /// + /// Selects items and updates currency based on parameters + /// + /// column index to make current + /// data item or CollectionViewGroup to make current + /// slot to use in case the item is no longer valid + /// selection action to perform + /// whether or not the new current item should be scrolled into view + internal void ProcessSelectionAndCurrency(int columnIndex, object item, int backupSlot, DataGridSelectionAction action, bool scrollIntoView) + { + _noSelectionChangeCount++; + _noCurrentCellChangeCount++; + try + { + int slot = -1; + if (item is DataGridCollectionViewGroup group) + { + DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(group); + if (groupInfo != null) + { + slot = groupInfo.Slot; + } + } + else + { + slot = SlotFromRowIndex(DataConnection.IndexOf(item)); + } + if (slot == -1) + { + slot = backupSlot; + } + if (slot < 0 || slot > SlotCount) + { + return; + } + + switch (action) + { + case DataGridSelectionAction.AddCurrentToSelection: + SetRowSelection(slot, isSelected: true, setAnchorSlot: true); + break; + case DataGridSelectionAction.RemoveCurrentFromSelection: + SetRowSelection(slot, isSelected: false, setAnchorSlot: false); + break; + case DataGridSelectionAction.SelectFromAnchorToCurrent: + if (SelectionMode == DataGridSelectionMode.Extended && AnchorSlot != -1) + { + int anchorSlot = AnchorSlot; + ClearRowSelection(slot, setAnchorSlot: false); + if (slot <= anchorSlot) + { + SetRowsSelection(slot, anchorSlot); + } + else + { + SetRowsSelection(anchorSlot, slot); + } + } + else + { + goto case DataGridSelectionAction.SelectCurrent; + } + break; + case DataGridSelectionAction.SelectCurrent: + ClearRowSelection(slot, setAnchorSlot: true); + break; + case DataGridSelectionAction.None: + break; + } + + if (CurrentSlot != slot || (CurrentColumnIndex != columnIndex && columnIndex != -1)) + { + if (columnIndex == -1) + { + if (CurrentColumnIndex != -1) + { + columnIndex = CurrentColumnIndex; + } + else + { + DataGridColumn firstVisibleColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + if (firstVisibleColumn != null) + { + columnIndex = firstVisibleColumn.Index; + } + } + } + if (columnIndex != -1) + { + if (!SetCurrentCellCore( + columnIndex, slot, + commitEdit: true, + endRowEdit: SlotFromRowIndex(SelectedIndex) != slot) + || (scrollIntoView && + !ScrollSlotIntoView( + columnIndex, slot, + forCurrentCellChange: true, + forceHorizontalScroll: false))) + { + return; + } + } + } + _successfullyUpdatedSelection = true; + } + finally + { + NoCurrentCellChangeCount--; + NoSelectionChangeCount--; + } + } + + internal bool ProcessUpKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessUpKey(shift, ctrl); + } + + //internal void ProcessVerticalScroll(double oldValue, double newValue) + internal void ProcessVerticalScroll(ScrollEventType scrollEventType) + { + if (_verticalScrollChangesIgnored > 0) + { + return; + } + Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); + + _verticalScrollChangesIgnored++; + try + { + Debug.Assert(_vScrollBar != null); + if (scrollEventType == ScrollEventType.SmallIncrement) + { + DisplayData.PendingVerticalScrollHeight = GetVerticalSmallScrollIncrease(); + double newVerticalOffset = _verticalOffset + DisplayData.PendingVerticalScrollHeight; + if (newVerticalOffset > _vScrollBar.Maximum) + { + DisplayData.PendingVerticalScrollHeight -= newVerticalOffset - _vScrollBar.Maximum; + } + } + else if (scrollEventType == ScrollEventType.SmallDecrement) + { + if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + { + DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; + } + else + { + int previousScrollingSlot = GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot); + if (previousScrollingSlot >= 0) + { + ScrollSlotIntoView(previousScrollingSlot, scrolledHorizontally: false); + } + return; + } + } + else + { + DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset; + } + + if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + { + // Invalidate so the scroll happens on idle + InvalidateRowsMeasure(invalidateIndividualElements: false); + } + } + finally + { + _verticalScrollChangesIgnored--; + } + } + + internal void RefreshRowsAndColumns(bool clearRows) + { + if (_measured) + { + try + { + _noCurrentCellChangeCount++; + + if (clearRows) + { + ClearRows(false); + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + } + if (AutoGenerateColumns) + { + //Column auto-generation refreshes the rows too + AutoGenerateColumnsPrivate(); + } + foreach (DataGridColumn column in ColumnsItemsInternal) + { + //We don't need to refresh the state of AutoGenerated column headers because they're up-to-date + if (!column.IsAutoGenerated && column.HasHeaderCell) + { + column.HeaderCell.ApplyState(); + } + } + + RefreshRows(recycleRows: false, clearRows: false); + + if (Columns.Count > 0 && CurrentColumnIndex == -1) + { + MakeFirstDisplayedCellCurrentCell(); + } + else + { + _makeFirstDisplayedCellCurrentCellPending = false; + _desiredCurrentColumnIndex = -1; + FlushCurrentCellChanged(); + } + } + finally + { + NoCurrentCellChangeCount--; + } + } + else + { + if (clearRows) + { + ClearRows(recycle: false); + } + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + } + } + + internal bool ScrollSlotIntoView(int columnIndex, int slot, bool forCurrentCellChange, bool forceHorizontalScroll) + { + Debug.Assert(columnIndex >= 0 && columnIndex < ColumnsItemsInternal.Count); + Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= -1 && DisplayData.FirstDisplayedScrollingCol < ColumnsItemsInternal.Count); + Debug.Assert(DisplayData.LastTotallyDisplayedScrollingCol >= -1 && DisplayData.LastTotallyDisplayedScrollingCol < ColumnsItemsInternal.Count); + Debug.Assert(!IsSlotOutOfBounds(slot)); + Debug.Assert(DisplayData.FirstScrollingSlot >= -1 && DisplayData.FirstScrollingSlot < SlotCount); + Debug.Assert(ColumnsItemsInternal[columnIndex].IsVisible); + + if (CurrentColumnIndex >= 0 && + (CurrentColumnIndex != columnIndex || CurrentSlot != slot)) + { + if (!CommitEditForOperation(columnIndex, slot, forCurrentCellChange) || IsInnerCellOutOfBounds(columnIndex, slot)) + { + return false; + } + } + + double oldHorizontalOffset = HorizontalOffset; + + //scroll horizontally unless we're on a RowGroupHeader and we're not forcing horizontal scrolling + if ((forceHorizontalScroll || (slot != -1)) + && !ScrollColumnIntoView(columnIndex)) + { + return false; + } + + //scroll vertically + if (!ScrollSlotIntoView(slot, scrolledHorizontally: oldHorizontalOffset != HorizontalOffset)) + { + return false; + } + + return true; + } + + // Convenient overload that commits the current edit. + internal bool SetCurrentCellCore(int columnIndex, int slot) + { + return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true); + } + + internal void UpdateHorizontalOffset(double newValue) + { + if (HorizontalOffset != newValue) + { + HorizontalOffset = newValue; + + InvalidateColumnHeadersMeasure(); + InvalidateRowsMeasure(true); + } + } + + internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) + { + _successfullyUpdatedSelection = false; + + _noSelectionChangeCount++; + _noCurrentCellChangeCount++; + try + { + if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && + columnIndex == ColumnsInternal.RowGroupSpacerColumn.Index) + { + columnIndex = -1; + } + if (IsSlotOutOfSelectionBounds(slot) || (columnIndex != -1 && IsColumnOutOfBounds(columnIndex))) + { + return false; + } + + int newCurrentPosition = -1; + object item = ItemFromSlot(slot, ref newCurrentPosition); + + if (EditingRow != null && slot != EditingRow.Slot && !CommitEdit(DataGridEditingUnit.Row, true)) + { + return false; + } + + if (DataConnection.CollectionView != null && + DataConnection.CollectionView.CurrentPosition != newCurrentPosition) + { + DataConnection.MoveCurrentTo(item, slot, columnIndex, action, scrollIntoView); + } + else + { + ProcessSelectionAndCurrency(columnIndex, item, slot, action, scrollIntoView); + } + } + finally + { + NoCurrentCellChangeCount--; + NoSelectionChangeCount--; + } + + return _successfullyUpdatedSelection; + } + + internal void UpdateStateOnCurrentChanged(object currentItem, int currentPosition) + { + if (currentItem == CurrentItem && currentItem == SelectedItem && currentPosition == SelectedIndex) + { + // The DataGrid's CurrentItem is already up-to-date, so we don't need to do anything + return; + } + + int columnIndex = CurrentColumnIndex; + if (columnIndex == -1) + { + if (IsColumnOutOfBounds(_desiredCurrentColumnIndex) || + (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && _desiredCurrentColumnIndex == ColumnsInternal.RowGroupSpacerColumn.Index)) + { + columnIndex = FirstDisplayedNonFillerColumnIndex; + } + else + { + columnIndex = _desiredCurrentColumnIndex; + } + } + _desiredCurrentColumnIndex = -1; + + try + { + _noSelectionChangeCount++; + _noCurrentCellChangeCount++; + + if (!CommitEdit()) + { + CancelEdit(DataGridEditingUnit.Row, false); + } + + ClearRowSelection(true); + if (currentItem == null) + { + SetCurrentCellCore(-1, -1); + } + else + { + int slot = SlotFromRowIndex(currentPosition); + ProcessSelectionAndCurrency(columnIndex, currentItem, slot, DataGridSelectionAction.SelectCurrent, false); + } + } + finally + { + NoCurrentCellChangeCount--; + NoSelectionChangeCount--; + } + } + + //TODO: Ensure left button is checked for + internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) + { + KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.InputModifiers, out bool ctrl, out bool shift); + return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); + } + + internal void UpdateVerticalScrollBar() + { + if (_vScrollBar != null && _vScrollBar.IsVisible) + { + double cellsHeight = CellsHeight; + double edgedRowsHeightCalculated = EdgedRowsHeightCalculated; + UpdateVerticalScrollBar( + needVertScrollbar: edgedRowsHeightCalculated > cellsHeight, + forceVertScrollbar: VerticalScrollBarVisibility == ScrollBarVisibility.Visible, + totalVisibleHeight: edgedRowsHeightCalculated, + cellsHeight: cellsHeight); + } + } + + /// + /// If the editing element has focus, this method will set focus to the DataGrid itself + /// in order to force the element to lose focus. It will then wait for the editing element's + /// LostFocus event, at which point it will perform the specified action. + /// + /// NOTE: It is important to understand that the specified action will be performed when the editing + /// element loses focus only if this method returns true. If it returns false, then the action + /// will not be performed later on, and should instead be performed by the caller, if necessary. + /// + /// Action to perform after the editing element loses focus + /// True if the editing element had focus and the action was cached away; false otherwise + //TODO TabStop + internal bool WaitForLostFocus(Action action) + { + if (EditingRow != null && EditingColumnIndex != -1 && !_executingLostFocusActions) + { + DataGridColumn editingColumn = ColumnsItemsInternal[EditingColumnIndex]; + IControl editingElement = editingColumn.GetCellContent(EditingRow); + if (editingElement != null && editingElement.ContainsChild(_focusedObject)) + { + Debug.Assert(_lostFocusActions != null); + _lostFocusActions.Enqueue(action); + editingElement.LostFocus += EditingElement_LostFocus; + //IsTabStop = true; + Focus(); + return true; + } + } + return false; + } + + /// + /// Raises the LoadingRowDetails for row details preparation + /// + protected virtual void OnLoadingRowDetails(DataGridRowDetailsEventArgs e) + { + EventHandler handler = LoadingRowDetails; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Raises the UnloadingRowDetails event + /// + protected virtual void OnUnloadingRowDetails(DataGridRowDetailsEventArgs e) + { + EventHandler handler = UnloadingRowDetails; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + internal void OnRowDetailsChanged() + { + if (!_scrollingByHeight) + { + // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough + // since rows could be added or removed + InvalidateMeasure(); + } + } + + private void UpdateRowDetailsVisibilityMode(DataGridRowDetailsVisibilityMode newDetailsMode) + { + int itemCount = DataConnection.Count; + if (_rowsPresenter != null && itemCount > 0) + { + bool newDetailsVisibility = false; + switch (newDetailsMode) + { + case DataGridRowDetailsVisibilityMode.Visible: + newDetailsVisibility = true; + _showDetailsTable.AddValues(0, itemCount, true); + break; + case DataGridRowDetailsVisibilityMode.Collapsed: + newDetailsVisibility = false; + _showDetailsTable.AddValues(0, itemCount, false); + break; + case DataGridRowDetailsVisibilityMode.VisibleWhenSelected: + _showDetailsTable.Clear(); + break; + } + + bool updated = false; + foreach (DataGridRow row in GetAllRows()) + { + if (row.IsVisible) + { + if (newDetailsMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected) + { + // For VisibleWhenSelected, we need to calculate the value for each individual row + newDetailsVisibility = _selectedItems.ContainsSlot(row.Slot); + } + if (row.AreDetailsVisible != newDetailsVisibility) + { + updated = true; + + row.SetDetailsVisibilityInternal(newDetailsVisibility, raiseNotification: true, animate: false); + } + } + } + if (updated) + { + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight); + InvalidateRowsMeasure(invalidateIndividualElements: false); + } + } + } + + //TODO Styles + private void AddNewCellPrivate(DataGridRow row, DataGridColumn column) + { + DataGridCell newCell = new DataGridCell(); + PopulateCellContent( + isCellEdited: false, + dataGridColumn: column, + dataGridRow: row, + dataGridCell: newCell); + if (row.OwningGrid != null) + { + newCell.OwningColumn = column; + newCell.IsVisible = column.IsVisible; + } + //newCell.EnsureStyle(null); + row.Cells.Insert(column.Index, newCell); + } + + private bool BeginCellEdit(RoutedEventArgs editingEventArgs) + { + if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot)) + { + return false; + } + + Debug.Assert(CurrentColumnIndex >= 0); + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(CurrentSlot >= -1); + Debug.Assert(CurrentSlot < SlotCount); + Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot); + Debug.Assert(!GetColumnEffectiveReadOnlyState(CurrentColumn)); + Debug.Assert(CurrentColumn.IsVisible); + + if (_editingColumnIndex != -1) + { + // Current cell is already in edit mode + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + return true; + } + + // Get or generate the editing row if it doesn't exist + DataGridRow dataGridRow = EditingRow; + if (dataGridRow == null) + { + if (IsSlotVisible(CurrentSlot)) + { + dataGridRow = DisplayData.GetDisplayedElement(CurrentSlot) as DataGridRow; + Debug.Assert(dataGridRow != null); + } + else + { + dataGridRow = GenerateRow(RowIndexFromSlot(CurrentSlot), CurrentSlot); + } + } + Debug.Assert(dataGridRow != null); + + // Cache these to see if they change later + int currentRowIndex = CurrentSlot; + int currentColumnIndex = CurrentColumnIndex; + + // Raise the BeginningEdit event + DataGridCell dataGridCell = dataGridRow.Cells[CurrentColumnIndex]; + DataGridBeginningEditEventArgs e = new DataGridBeginningEditEventArgs(CurrentColumn, dataGridRow, editingEventArgs); + OnBeginningEdit(e); + if (e.Cancel + || currentRowIndex != CurrentSlot + || currentColumnIndex != CurrentColumnIndex + || !GetRowSelection(CurrentSlot) + || (EditingRow == null && !BeginRowEdit(dataGridRow))) + { + // If either BeginningEdit was canceled, currency/selection was changed in the event handler, + // or we failed opening the row for edit, then we can no longer continue BeginCellEdit + return false; + } + Debug.Assert(EditingRow != null); + Debug.Assert(EditingRow.Slot == CurrentSlot); + + // Finally, we can prepare the cell for editing + _editingColumnIndex = CurrentColumnIndex; + _editingEventArgs = editingEventArgs; + EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses(); + PopulateCellContent( + isCellEdited: true, + dataGridColumn: CurrentColumn, + dataGridRow: dataGridRow, + dataGridCell: dataGridCell); + return true; + } + + //TODO Validation + private bool BeginRowEdit(DataGridRow dataGridRow) + { + Debug.Assert(EditingRow == null); + Debug.Assert(dataGridRow != null); + + Debug.Assert(CurrentSlot >= -1); + Debug.Assert(CurrentSlot < SlotCount); + + if (DataConnection.BeginEdit(dataGridRow.DataContext)) + { + EditingRow = dataGridRow; + GenerateEditingElements(); + return true; + } + return false; + } + + private bool CancelRowEdit(bool exitEditingMode) + { + if (EditingRow == null) + { + return true; + } + Debug.Assert(EditingRow != null && EditingRow.Index >= -1); + Debug.Assert(EditingRow.Slot < SlotCount); + Debug.Assert(CurrentColumn != null); + + object dataItem = EditingRow.DataContext; + if (!DataConnection.CancelEdit(dataItem)) + { + return false; + } + foreach (DataGridColumn column in Columns) + { + if (!exitEditingMode && column.Index == _editingColumnIndex && column is DataGridBoundColumn) + { + continue; + } + PopulateCellContent( + isCellEdited: !exitEditingMode && column.Index == _editingColumnIndex, + dataGridColumn: column, + dataGridRow: EditingRow, + dataGridCell: EditingRow.Cells[column.Index]); + } + return true; + } + + private bool CommitEditForOperation(int columnIndex, int slot, bool forCurrentCellChange) + { + if (forCurrentCellChange) + { + if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true)) + { + return false; + } + if (CurrentSlot != slot && + !EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true)) + { + return false; + } + } + + if (IsColumnOutOfBounds(columnIndex)) + { + return false; + } + if (slot >= SlotCount) + { + // Current cell was reset because the commit deleted row(s). + // Since the user wants to change the current cell, we don't + // want to end up with no current cell. We pick the last row + // in the grid which may be the 'new row'. + int lastSlot = LastVisibleSlot; + if (forCurrentCellChange && + CurrentColumnIndex == -1 && + lastSlot != -1) + { + SetAndSelectCurrentCell(columnIndex, lastSlot, forceCurrentCellSelection: false); + } + // Interrupt operation because it has become invalid. + return false; + } + return true; + } + + //TODO Validation + private bool CommitRowEdit(bool exitEditingMode) + { + if (EditingRow == null) + { + return true; + } + Debug.Assert(EditingRow != null && EditingRow.Index >= -1); + Debug.Assert(EditingRow.Slot < SlotCount); + + //if (!ValidateEditingRow(scrollIntoView: true, wireEvents: false)) + if (!EditingRow.IsValid) + { + return false; + } + + DataConnection.EndEdit(EditingRow.DataContext); + + if (!exitEditingMode) + { + DataConnection.BeginEdit(EditingRow.DataContext); + } + return true; + } + + private void CompleteCellsCollection(DataGridRow dataGridRow) + { + Debug.Assert(dataGridRow != null); + int cellsInCollection = dataGridRow.Cells.Count; + if (ColumnsItemsInternal.Count > cellsInCollection) + { + for (int columnIndex = cellsInCollection; columnIndex < ColumnsItemsInternal.Count; columnIndex++) + { + AddNewCellPrivate(dataGridRow, ColumnsItemsInternal[columnIndex]); + } + } + } + + private void ComputeScrollBarsLayout() + { + if (_ignoreNextScrollBarsLayout) + { + _ignoreNextScrollBarsLayout = false; + // + + } + double cellsWidth = CellsWidth; + double cellsHeight = CellsHeight; + + bool allowHorizScrollbar = false; + bool forceHorizScrollbar = false; + double horizScrollBarHeight = 0; + if (_hScrollBar != null) + { + forceHorizScrollbar = HorizontalScrollBarVisibility == ScrollBarVisibility.Visible; + allowHorizScrollbar = forceHorizScrollbar || (ColumnsInternal.VisibleColumnCount > 0 && + HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled && + HorizontalScrollBarVisibility != ScrollBarVisibility.Hidden); + // Compensate if the horizontal scrollbar is already taking up space + if (!forceHorizScrollbar && _hScrollBar.IsVisible) + { + cellsHeight += _hScrollBar.DesiredSize.Height; + } + horizScrollBarHeight = _hScrollBar.Height + _hScrollBar.Margin.Top + _hScrollBar.Margin.Bottom; + } + bool allowVertScrollbar = false; + bool forceVertScrollbar = false; + double vertScrollBarWidth = 0; + if (_vScrollBar != null) + { + forceVertScrollbar = VerticalScrollBarVisibility == ScrollBarVisibility.Visible; + allowVertScrollbar = forceVertScrollbar || (ColumnsItemsInternal.Count > 0 && + VerticalScrollBarVisibility != ScrollBarVisibility.Disabled && + VerticalScrollBarVisibility != ScrollBarVisibility.Hidden); + // Compensate if the vertical scrollbar is already taking up space + if (!forceVertScrollbar && _vScrollBar.IsVisible) + { + cellsWidth += _vScrollBar.DesiredSize.Width; + } + vertScrollBarWidth = _vScrollBar.Width + _vScrollBar.Margin.Left + _vScrollBar.Margin.Right; + } + + // Now cellsWidth is the width potentially available for displaying data cells. + // Now cellsHeight is the height potentially available for displaying data cells. + + bool needHorizScrollbar = false; + bool needVertScrollbar = false; + + double totalVisibleWidth = ColumnsInternal.VisibleEdgedColumnsWidth; + double totalVisibleFrozenWidth = ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); + + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight); + double totalVisibleHeight = EdgedRowsHeightCalculated; + + if (!forceHorizScrollbar && !forceVertScrollbar) + { + bool needHorizScrollbarWithoutVertScrollbar = false; + + if (allowHorizScrollbar && + DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && + DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && + DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + { + double oldDataHeight = cellsHeight; + cellsHeight -= horizScrollBarHeight; + Debug.Assert(cellsHeight >= 0); + needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true; + if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || + DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) + { + // Would we still need a horizontal scrollbar without the vertical one? + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); + if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) + { + needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); + } + } + + if (!needHorizScrollbar) + { + // Restore old data height because turns out a horizontal scroll bar wouldn't make sense + cellsHeight = oldDataHeight; + } + } + + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); + if (allowVertScrollbar && + DoubleUtil.GreaterThan(cellsHeight, 0) && + DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) + { + cellsWidth -= vertScrollBarWidth; + Debug.Assert(cellsWidth >= 0); + needVertScrollbar = true; + } + + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + // we compute the number of visible columns only after we set up the vertical scroll bar. + ComputeDisplayedColumns(); + + if (allowHorizScrollbar && + needVertScrollbar && !needHorizScrollbar && + DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && + DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && + DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + { + cellsWidth += vertScrollBarWidth; + cellsHeight -= horizScrollBarHeight; + Debug.Assert(cellsHeight >= 0); + needVertScrollbar = false; + + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); + if (cellsHeight > 0 && + vertScrollBarWidth <= cellsWidth && + DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) + { + cellsWidth -= vertScrollBarWidth; + Debug.Assert(cellsWidth >= 0); + needVertScrollbar = true; + } + if (needVertScrollbar) + { + needHorizScrollbar = true; + } + else + { + needHorizScrollbar = needHorizScrollbarWithoutVertScrollbar; + } + } + } + else if (forceHorizScrollbar && !forceVertScrollbar) + { + if (allowVertScrollbar) + { + if (cellsHeight > 0 && + DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) + { + cellsWidth -= vertScrollBarWidth; + Debug.Assert(cellsWidth >= 0); + needVertScrollbar = true; + } + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + ComputeDisplayedColumns(); + } + needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth; + } + else if (!forceHorizScrollbar && forceVertScrollbar) + { + if (allowHorizScrollbar) + { + if (cellsWidth > 0 && + DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) && + DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && + DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth)) + { + cellsHeight -= horizScrollBarHeight; + Debug.Assert(cellsHeight >= 0); + needHorizScrollbar = true; + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); + } + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + ComputeDisplayedColumns(); + } + needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount; + } + else + { + Debug.Assert(forceHorizScrollbar && forceVertScrollbar); + Debug.Assert(allowHorizScrollbar && allowVertScrollbar); + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + ComputeDisplayedColumns(); + needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount; + needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth; + } + + UpdateHorizontalScrollBar(needHorizScrollbar, forceHorizScrollbar, totalVisibleWidth, totalVisibleFrozenWidth, cellsWidth); + UpdateVerticalScrollBar(needVertScrollbar, forceVertScrollbar, totalVisibleHeight, cellsHeight); + + if (_topRightCornerHeader != null) + { + // Show the TopRightHeaderCell based on vertical ScrollBar visibility + if (AreColumnHeadersVisible && + _vScrollBar != null && _vScrollBar.IsVisible) + { + _topRightCornerHeader.IsVisible = true; ; + } + else + { + _topRightCornerHeader.IsVisible = false; + } + } + DisplayData.FullyRecycleElements(); + } + + /// + /// Handles the current editing element's LostFocus event by performing any actions that + /// were cached by the WaitForLostFocus method. + /// + /// Editing element + /// RoutedEventArgs + private void EditingElement_LostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control editingElement) + { + editingElement.LostFocus -= EditingElement_LostFocus; + if (EditingRow != null && EditingColumnIndex != -1) + { + FocusEditingCell(true); + } + Debug.Assert(_lostFocusActions != null); + try + { + _executingLostFocusActions = true; + while (_lostFocusActions.Count > 0) + { + _lostFocusActions.Dequeue()(); + } + } + finally + { + _executingLostFocusActions = false; + } + } + } + + // Makes sure horizontal layout is updated to reflect any changes that affect it + private void EnsureHorizontalLayout() + { + ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + InvalidateColumnHeadersMeasure(); + InvalidateRowsMeasure(true); + InvalidateMeasure(); + } + + private void EnsureRowHeaderWidth() + { + if (AreRowHeadersVisible) + { + if (AreColumnHeadersVisible) + { + EnsureTopLeftCornerHeader(); + } + + if (_rowsPresenter != null) + { + + bool updated = false; + + foreach (Control element in _rowsPresenter.Children) + { + if (element is DataGridRow row) + { + // If the RowHeader resulted in a different width the last time it was measured, we need + // to re-measure it + if (row.HeaderCell != null && row.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth) + { + row.HeaderCell.InvalidateMeasure(); + updated = true; + } + } + else if (element is DataGridRowGroupHeader groupHeader && groupHeader.HeaderCell != null && groupHeader.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth) + { + groupHeader.HeaderCell.InvalidateMeasure(); + updated = true; + } + } + + if (updated) + { + // We need to update the width of the horizontal scrollbar if the rowHeaders' width actually changed + InvalidateMeasure(); + } + } + } + } + + private void EnsureRowsPresenterVisibility() + { + if (_rowsPresenter != null) + { + // RowCount doesn't need to be considered, doing so might cause extra Visibility changes + _rowsPresenter.IsVisible = (ColumnsInternal.FirstVisibleNonFillerColumn != null); + } + } + + private void EnsureTopLeftCornerHeader() + { + if (_topLeftCornerHeader != null) + { + _topLeftCornerHeader.IsVisible = (HeadersVisibility == DataGridHeadersVisibility.All); + + if (_topLeftCornerHeader.IsVisible) + { + if (!double.IsNaN(RowHeaderWidth)) + { + // RowHeaderWidth is set explicitly so we should use that + _topLeftCornerHeader.Width = RowHeaderWidth; + } + else if (VisibleSlotCount > 0) + { + // RowHeaders AutoSize and we have at least 1 row so take the desired width + _topLeftCornerHeader.Width = RowHeadersDesiredWidth; + } + } + } + } + + private void InvalidateCellsArrange() + { + foreach (DataGridRow row in GetAllRows()) + { + row.InvalidateHorizontalArrange(); + } + } + + private void InvalidateColumnHeadersArrange() + { + if (_columnHeadersPresenter != null) + { + _columnHeadersPresenter.InvalidateArrange(); + } + } + + private void InvalidateColumnHeadersMeasure() + { + if (_columnHeadersPresenter != null) + { + EnsureColumnHeadersVisibility(); + _columnHeadersPresenter.InvalidateMeasure(); + } + } + + private void InvalidateRowsArrange() + { + if (_rowsPresenter != null) + { + _rowsPresenter.InvalidateArrange(); + } + } + + private void InvalidateRowsMeasure(bool invalidateIndividualElements) + { + if (_rowsPresenter != null) + { + _rowsPresenter.InvalidateMeasure(); + + if (invalidateIndividualElements) + { + foreach (Control element in _rowsPresenter.Children) + { + element.InvalidateMeasure(); + } + } + } + } + + //TODO: Make override? + private void DataGrid_GotFocus(object sender, RoutedEventArgs e) + { + if (!ContainsFocus) + { + ContainsFocus = true; + ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot); + if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot)) + { + if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row) + { + row.Cells[CurrentColumnIndex].UpdatePseudoClasses(); + } + } + } + + // Keep track of which row contains the newly focused element + DataGridRow focusedRow = null; + IVisual focusedElement = e.Source as IVisual; + _focusedObject = focusedElement; + while (focusedElement != null) + { + focusedRow = focusedElement as DataGridRow; + if (focusedRow != null && focusedRow.OwningGrid == this && _focusedRow != focusedRow) + { + ResetFocusedRow(); + _focusedRow = focusedRow.IsVisible ? focusedRow : null; + break; + } + focusedElement = focusedElement.GetVisualParent(); + } + } + + //TODO: Check + private void DataGrid_IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) + { + } + + private void DataGrid_KeyDown(object sender, KeyEventArgs e) + { + if (!e.Handled) + { + e.Handled = ProcessDataGridKey(e); + } + } + + private void DataGrid_KeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Tab && CurrentColumnIndex != -1 && e.Source == this) + { + bool success = + ScrollSlotIntoView( + CurrentColumnIndex, CurrentSlot, + forCurrentCellChange: false, + forceHorizontalScroll: true); + Debug.Assert(success); + if (CurrentColumnIndex != -1 && SelectedItem == null) + { + SetRowSelection(CurrentSlot, isSelected: true, setAnchorSlot: true); + } + } + } + + //TODO: Make override? + private void DataGrid_LostFocus(object sender, RoutedEventArgs e) + { + _focusedObject = null; + if (ContainsFocus) + { + bool focusLeftDataGrid = true; + bool dataGridWillReceiveRoutedEvent = true; + IVisual focusedObject = FocusManager.Instance.Current; + + while (focusedObject != null) + { + if (focusedObject == this) + { + focusLeftDataGrid = false; + break; + } + + // Walk up the visual tree. If we hit the root, try using the framework element's + // parent. We do this because Popups behave differently with respect to the visual tree, + // and it could have a parent even if the VisualTreeHelper doesn't find it. + IVisual parent = focusedObject.GetVisualParent(); + if (parent == null) + { + if (focusedObject is Control element) + { + parent = element.Parent; + if (parent != null) + { + dataGridWillReceiveRoutedEvent = false; + } + } + } + focusedObject = parent; + } + + if (focusLeftDataGrid) + { + ContainsFocus = false; + if (EditingRow != null) + { + CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true); + } + ResetFocusedRow(); + ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot); + if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot)) + { + if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row) + { + row.Cells[CurrentColumnIndex].UpdatePseudoClasses(); + } + } + } + else if (!dataGridWillReceiveRoutedEvent) + { + if (focusedObject is Control focusedElement) + { + focusedElement.LostFocus += ExternalEditingElement_LostFocus; + } + } + } + } + + private void EditingElement_Initialized(object sender, EventArgs e) + { + var element = sender as Control; + if (element != null) + { + element.Initialized -= EditingElement_Initialized; + } + PreparingCellForEditPrivate(element); + } + + //TODO Validation + //TODO Binding + //TODO TabStop + private bool EndCellEdit(DataGridEditAction editAction, bool exitEditingMode, bool keepFocus, bool raiseEvents) + { + if (_editingColumnIndex == -1) + { + return true; + } + + Debug.Assert(EditingRow != null); + Debug.Assert(_editingColumnIndex >= 0); + Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot); + + // Cache these to see if they change later + int currentSlot = CurrentSlot; + int currentColumnIndex = CurrentColumnIndex; + + // We're ready to start ending, so raise the event + DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex]; + var editingElement = editingCell.Content as Control; + if (editingElement == null) + { + return false; + } + if (raiseEvents) + { + DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction); + OnCellEditEnding(e); + if (e.Cancel) + { + // CellEditEnding has been cancelled + return false; + } + + // Ensure that the current cell wasn't changed in the user's CellEditEnding handler + if (_editingColumnIndex == -1 || + currentSlot != CurrentSlot || + currentColumnIndex != CurrentColumnIndex) + { + return true; + } + Debug.Assert(EditingRow != null); + Debug.Assert(EditingRow.Slot == currentSlot); + Debug.Assert(_editingColumnIndex != -1); + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + } + + // If we're canceling, let the editing column repopulate its old value if it wants + if (editAction == DataGridEditAction.Cancel) + { + CurrentColumn.CancelCellEditInternal(editingElement, _uneditedValue); + + // Ensure that the current cell wasn't changed in the user column's CancelCellEdit + if (_editingColumnIndex == -1 || + currentSlot != CurrentSlot || + currentColumnIndex != CurrentColumnIndex) + { + return true; + } + Debug.Assert(EditingRow != null); + Debug.Assert(EditingRow.Slot == currentSlot); + Debug.Assert(_editingColumnIndex != -1); + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + } + + // If we're committing, explicitly update the source but watch out for any validation errors + if (editAction == DataGridEditAction.Commit) + { + void SetValidationStatus(ICellEditBinding binding) + { + if (binding.IsValid) + { + ResetValidationStatus(); + if (editingElement != null) + { + DataValidationErrors.ClearErrors(editingElement); + } + } + else + { + if (EditingRow != null) + { + if (editingCell.IsValid) + { + editingCell.IsValid = false; + editingCell.UpdatePseudoClasses(); + } + + if (EditingRow.IsValid) + { + EditingRow.IsValid = false; + EditingRow.UpdatePseudoClasses(); + } + } + + if (editingElement != null) + { + var errorList = + binding.ValidationErrors + .SelectMany(ex => ValidationUtil.UnpackException(ex)) + .ToList(); + + DataValidationErrors.SetErrors(editingElement, errorList); + } + } + } + + var editBinding = CurrentColumn?.CellEditBinding; + if (editBinding != null && !editBinding.CommitEdit()) + { + SetValidationStatus(editBinding); + _validationSubscription?.Dispose(); + _validationSubscription = editBinding.ValidationChanged.Subscribe(v => SetValidationStatus(editBinding)); + + ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true); + return false; + } + } + + ResetValidationStatus(); + + if (exitEditingMode) + { + _editingColumnIndex = -1; + editingCell.UpdatePseudoClasses(); + + //IsTabStop = true; + if (keepFocus && editingElement.ContainsFocusedElement()) + { + Focus(); + } + + PopulateCellContent( + isCellEdited: !exitEditingMode, + dataGridColumn: CurrentColumn, + dataGridRow: EditingRow, + dataGridCell: editingCell); + } + + // We're done, so raise the CellEditEnded event + if (raiseEvents) + { + OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction)); + } + + // There's a chance that somebody reopened this cell for edit within the CellEditEnded handler, + // so we should return false if we were supposed to exit editing mode, but we didn't + return !(exitEditingMode && currentColumnIndex == _editingColumnIndex); + } + + //TODO Validation + private bool EndRowEdit(DataGridEditAction editAction, bool exitEditingMode, bool raiseEvents) + { + if (EditingRow == null || DataConnection.CommittingEdit) + { + return true; + } + if (_editingColumnIndex != -1 || (editAction == DataGridEditAction.Cancel && raiseEvents && + !((DataConnection.EditableCollectionView != null && DataConnection.EditableCollectionView.CanCancelEdit) || (EditingRow.DataContext is IEditableObject)))) + { + // Ending the row edit will fail immediately under the following conditions: + // 1. We haven't ended the cell edit yet. + // 2. We're trying to cancel edit when the underlying DataType is not an IEditableObject, + // because we have no way to properly restore the old value. We will only allow this to occur + // if raiseEvents == false, which means we're internally forcing a cancel. + return false; + } + DataGridRow editingRow = EditingRow; + + if (raiseEvents) + { + DataGridRowEditEndingEventArgs e = new DataGridRowEditEndingEventArgs(EditingRow, editAction); + OnRowEditEnding(e); + if (e.Cancel) + { + // RowEditEnding has been cancelled + return false; + } + + // Editing states might have been changed in the RowEditEnding handlers + if (_editingColumnIndex != -1) + { + return false; + } + if (editingRow != EditingRow) + { + return true; + } + } + + // Call the appropriate commit or cancel methods + if (editAction == DataGridEditAction.Commit) + { + if (!CommitRowEdit(exitEditingMode)) + { + return false; + } + } + else + { + if (!CancelRowEdit(exitEditingMode) && raiseEvents) + { + // We failed to cancel edit so we should abort unless we're forcing a cancel + return false; + } + } + ResetValidationStatus(); + + // Update the previously edited row's state + if (exitEditingMode && editingRow == EditingRow) + { + RemoveEditingElements(); + ResetEditingRow(); + } + + // Raise the RowEditEnded event + if (raiseEvents) + { + OnRowEditEnded(new DataGridRowEditEndedEventArgs(editingRow, editAction)); + } + + return true; + } + + private void EnsureColumnHeadersVisibility() + { + if (_columnHeadersPresenter != null) + { + _columnHeadersPresenter.IsVisible = AreColumnHeadersVisible; + } + } + + private void EnsureVerticalGridLines() + { + if (AreColumnHeadersVisible) + { + double totalColumnsWidth = 0; + foreach (DataGridColumn column in ColumnsInternal) + { + totalColumnsWidth += column.ActualWidth; + + column.HeaderCell.AreSeparatorsVisible = (column != ColumnsInternal.LastVisibleColumn || totalColumnsWidth < CellsWidth); + } + } + + foreach (DataGridRow row in GetAllRows()) + { + row.EnsureGridLines(); + } + } + + /// + /// Exits editing mode without trying to commit or revert the editing, and + /// without repopulating the edited row's cell. + /// + //TODO TabStop + private void ExitEdit(bool keepFocus) + { + if (EditingRow == null || DataConnection.CommittingEdit) + { + Debug.Assert(_editingColumnIndex == -1); + return; + } + + if (_editingColumnIndex != -1) + { + Debug.Assert(_editingColumnIndex >= 0); + Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot); + + _editingColumnIndex = -1; + EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses(); + } + //IsTabStop = true; + if (IsSlotVisible(EditingRow.Slot)) + { + EditingRow.UpdatePseudoClasses(); + } + ResetEditingRow(); + if (keepFocus) + { + Focus(); + } + } + + private void ExternalEditingElement_LostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control element) + { + element.LostFocus -= ExternalEditingElement_LostFocus; + DataGrid_LostFocus(sender, e); + } + } + + private void FlushCurrentCellChanged() + { + if (_makeFirstDisplayedCellCurrentCellPending) + { + return; + } + if (SelectionHasChanged) + { + // selection is changing, don't raise CurrentCellChanged until it's done + _flushCurrentCellChanged = true; + FlushSelectionChanged(); + return; + } + + // We don't want to expand all intermediate currency positions, so we only expand + // the last current item before we flush the event + if (_collapsedSlotsTable.Contains(CurrentSlot)) + { + DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot)); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo != null) + { + ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot); + } + } + + if (CurrentColumn != _previousCurrentColumn + || CurrentItem != _previousCurrentItem) + { + CoerceSelectedItem(); + _previousCurrentColumn = CurrentColumn; + _previousCurrentItem = CurrentItem; + + OnCurrentCellChanged(EventArgs.Empty); + } + + _flushCurrentCellChanged = false; + } + + private void FlushSelectionChanged() + { + if (SelectionHasChanged && _noSelectionChangeCount == 0 && !_makeFirstDisplayedCellCurrentCellPending) + { + CoerceSelectedItem(); + if (NoCurrentCellChangeCount != 0) + { + // current cell is changing, don't raise SelectionChanged until it's done + return; + } + SelectionHasChanged = false; + + if (_flushCurrentCellChanged) + { + FlushCurrentCellChanged(); + } + + SelectionChangedEventArgs e = _selectedItems.GetSelectionChangedEventArgs(); + if (e.AddedItems.Count > 0 || e.RemovedItems.Count > 0) + { + OnSelectionChanged(e); + } + } + } + + //TODO TabStop + private bool FocusEditingCell(bool setFocus) + { + Debug.Assert(CurrentColumnIndex >= 0); + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(CurrentSlot >= -1); + Debug.Assert(CurrentSlot < SlotCount); + Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot); + Debug.Assert(_editingColumnIndex != -1); + + //IsTabStop = false; + _focusEditingControl = false; + + bool success = false; + DataGridCell dataGridCell = EditingRow.Cells[_editingColumnIndex]; + if (setFocus) + { + if (dataGridCell.ContainsFocusedElement()) + { + success = true; + } + else + { + dataGridCell.Focus(); + success = dataGridCell.ContainsFocusedElement(); + } + //TODO Check + //success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus(); + _focusEditingControl = !success; + } + return success; + } + + // Calculates the amount to scroll for the ScrollLeft button + // This is a method rather than a property to emphasize a calculation + private double GetHorizontalSmallScrollDecrease() + { + // If the first column is covered up, scroll to the start of it when the user clicks the left button + if (_negHorizontalOffset > 0) + { + return _negHorizontalOffset; + } + else + { + // The entire first column is displayed, show the entire previous column when the user clicks + // the left button + DataGridColumn previousColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn( + ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]); + if (previousColumn != null) + { + return GetEdgedColumnWidth(previousColumn); + } + else + { + // There's no previous column so don't move + return 0; + } + } + } + + // Calculates the amount to scroll for the ScrollRight button + // This is a method rather than a property to emphasize a calculation + private double GetHorizontalSmallScrollIncrease() + { + if (DisplayData.FirstDisplayedScrollingCol >= 0) + { + return GetEdgedColumnWidth(ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]) - _negHorizontalOffset; + } + return 0; + } + + // Calculates the amount the ScrollDown button should scroll + // This is a method rather than a property to emphasize that calculations are taking place + private double GetVerticalSmallScrollIncrease() + { + if (DisplayData.FirstScrollingSlot >= 0) + { + return GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) - NegVerticalOffset; + } + return 0; + } + + private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e) + { + ProcessHorizontalScroll(e.ScrollEventType); + } + + private bool IsColumnOutOfBounds(int columnIndex) + { + return columnIndex >= ColumnsItemsInternal.Count || columnIndex < 0; + } + + private bool IsInnerCellOutOfBounds(int columnIndex, int slot) + { + return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfBounds(slot); + } + + private bool IsInnerCellOutOfSelectionBounds(int columnIndex, int slot) + { + return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfSelectionBounds(slot); + } + + private bool IsSlotOutOfBounds(int slot) + { + return slot >= SlotCount || slot < -1 || _collapsedSlotsTable.Contains(slot); + } + + private bool IsSlotOutOfSelectionBounds(int slot) + { + if (RowGroupHeadersTable.Contains(slot)) + { + Debug.Assert(slot >= 0 && slot < SlotCount); + return false; + } + else + { + int rowIndex = RowIndexFromSlot(slot); + return rowIndex < 0 || rowIndex >= DataConnection.Count; + } + } + + private void MakeFirstDisplayedCellCurrentCell() + { + if (CurrentColumnIndex != -1) + { + _makeFirstDisplayedCellCurrentCellPending = false; + _desiredCurrentColumnIndex = -1; + FlushCurrentCellChanged(); + return; + } + if (SlotCount != SlotFromRowIndex(DataConnection.Count)) + { + _makeFirstDisplayedCellCurrentCellPending = true; + return; + } + + // No current cell, therefore no selection either - try to set the current cell to the + // ItemsSource's ICollectionView.CurrentItem if it exists, otherwise use the first displayed cell. + int slot = 0; + if (DataConnection.CollectionView != null) + { + if (DataConnection.CollectionView.IsCurrentBeforeFirst || + DataConnection.CollectionView.IsCurrentAfterLast) + { + slot = RowGroupHeadersTable.Contains(0) ? 0 : -1; + } + else + { + slot = SlotFromRowIndex(DataConnection.CollectionView.CurrentPosition); + } + } + else + { + if (SelectedIndex == -1) + { + // Try to default to the first row + slot = SlotFromRowIndex(0); + if (!IsSlotVisible(slot)) + { + slot = -1; + } + } + else + { + slot = SlotFromRowIndex(SelectedIndex); + } + } + int columnIndex = FirstDisplayedNonFillerColumnIndex; + if (_desiredCurrentColumnIndex >= 0 && _desiredCurrentColumnIndex < ColumnsItemsInternal.Count) + { + columnIndex = _desiredCurrentColumnIndex; + } + + SetAndSelectCurrentCell(columnIndex, slot, forceCurrentCellSelection: false); + AnchorSlot = slot; + _makeFirstDisplayedCellCurrentCellPending = false; + _desiredCurrentColumnIndex = -1; + FlushCurrentCellChanged(); + } + + //TODO Styles + private void PopulateCellContent(bool isCellEdited, + DataGridColumn dataGridColumn, + DataGridRow dataGridRow, + DataGridCell dataGridCell) + { + Debug.Assert(dataGridColumn != null); + Debug.Assert(dataGridRow != null); + Debug.Assert(dataGridCell != null); + + IControl element = null; + DataGridBoundColumn dataGridBoundColumn = dataGridColumn as DataGridBoundColumn; + if (isCellEdited) + { + // Generate EditingElement and apply column style if available + element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); + if (element != null) + { + // Subscribe to the new element's events + element.Initialized += EditingElement_Initialized; + } + } + else + { + // Generate Element and apply column style if available + element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext); + } + + dataGridCell.Content = element; + } + + private void PreparingCellForEditPrivate(Control editingElement) + { + if (_editingColumnIndex == -1 || + CurrentColumnIndex == -1 || + EditingRow.Cells[CurrentColumnIndex].Content != editingElement) + { + // The current cell has changed since the call to BeginCellEdit, so the fact + // that this element has loaded is no longer relevant + return; + } + + Debug.Assert(EditingRow != null); + Debug.Assert(_editingColumnIndex >= 0); + Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(_editingColumnIndex == CurrentColumnIndex); + Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot); + + FocusEditingCell(setFocus: ContainsFocus || _focusEditingControl); + + // Prepare the cell for editing and raise the PreparingCellForEdit event for all columns + DataGridColumn dataGridColumn = CurrentColumn; + _uneditedValue = dataGridColumn.PrepareCellForEditInternal(editingElement, _editingEventArgs); + OnPreparingCellForEdit(new DataGridPreparingCellForEditEventArgs(dataGridColumn, EditingRow, _editingEventArgs, editingElement)); + } + + private bool ProcessAKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift, out bool alt); + + if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended) + { + SelectAll(); + return true; + } + return false; + } + + //TODO TabStop + //TODO FlowDirection + private bool ProcessDataGridKey(KeyEventArgs e) + { + bool focusDataGrid = false; + switch (e.Key) + { + case Key.Tab: + return ProcessTabKey(e); + + case Key.Up: + focusDataGrid = ProcessUpKey(e); + break; + + case Key.Down: + focusDataGrid = ProcessDownKey(e); + break; + + case Key.PageDown: + focusDataGrid = ProcessNextKey(e); + break; + + case Key.PageUp: + focusDataGrid = ProcessPriorKey(e); + break; + + case Key.Left: + focusDataGrid = ProcessLeftKey(e); + break; + + case Key.Right: + focusDataGrid = ProcessRightKey(e); + break; + + case Key.F2: + return ProcessF2Key(e); + + case Key.Home: + focusDataGrid = ProcessHomeKey(e); + break; + + case Key.End: + focusDataGrid = ProcessEndKey(e); + break; + + case Key.Enter: + focusDataGrid = ProcessEnterKey(e); + break; + + case Key.Escape: + return ProcessEscapeKey(); + + case Key.A: + return ProcessAKey(e); + + case Key.C: + return ProcessCopyKey(e.Modifiers); + + case Key.Insert: + return ProcessCopyKey(e.Modifiers); + } + if (focusDataGrid) + { + Focus(); + } + return focusDataGrid; + } + + private bool ProcessDownKeyInternal(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int lastSlot = LastVisibleSlot; + if (firstVisibleColumnIndex == -1 || lastSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessDownKeyInternal(shift, ctrl))) + { + return true; + } + + int nextSlot = -1; + if (CurrentSlot != -1) + { + nextSlot = GetNextVisibleSlot(CurrentSlot); + if (nextSlot >= SlotCount) + { + nextSlot = -1; + } + } + + _noSelectionChangeCount++; + try + { + int desiredSlot; + int columnIndex; + DataGridSelectionAction action; + if (CurrentColumnIndex == -1) + { + desiredSlot = FirstVisibleSlot; + columnIndex = firstVisibleColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + else if (ctrl) + { + if (shift) + { + // Both Ctrl and Shift + desiredSlot = lastSlot; + columnIndex = CurrentColumnIndex; + action = (SelectionMode == DataGridSelectionMode.Extended) + ? DataGridSelectionAction.SelectFromAnchorToCurrent + : DataGridSelectionAction.SelectCurrent; + } + else + { + // Ctrl without Shift + desiredSlot = lastSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + } + else + { + if (nextSlot == -1) + { + return true; + } + if (shift) + { + // Shift without Ctrl + desiredSlot = nextSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectFromAnchorToCurrent; + } + else + { + // Neither Ctrl nor Shift + desiredSlot = nextSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + } + + UpdateSelectionAndCurrency(columnIndex, desiredSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessEndKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn; + int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int firstVisibleSlot = FirstVisibleSlot; + int lastVisibleSlot = LastVisibleSlot; + if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessEndKey(shift, ctrl))) + { + return true; + } + + _noSelectionChangeCount++; + try + { + if (!ctrl) + { + return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot); + } + else + { + DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended) + ? DataGridSelectionAction.SelectFromAnchorToCurrent + : DataGridSelectionAction.SelectCurrent; + + UpdateSelectionAndCurrency(lastVisibleColumnIndex, lastVisibleSlot, action, scrollIntoView: true); + } + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessEnterKey(bool shift, bool ctrl) + { + int oldCurrentSlot = CurrentSlot; + + if (!ctrl) + { + // If Enter was used by a TextBox, we shouldn't handle the key + if (FocusManager.Instance.Current is TextBox focusedTextBox && focusedTextBox.AcceptsReturn) + { + return false; + } + + if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl))) + { + return true; + } + + // Enter behaves like down arrow - it commits the potential editing and goes down one cell. + if (!ProcessDownKeyInternal(false, ctrl)) + { + return false; + } + } + else if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl))) + { + return true; + } + + // Try to commit the potential editing + if (oldCurrentSlot == CurrentSlot && + EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true) && + EditingRow != null) + { + EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true); + ScrollIntoView(CurrentItem, CurrentColumn); + } + + return true; + } + + private bool ProcessEscapeKey() + { + if (WaitForLostFocus(() => ProcessEscapeKey())) + { + return true; + } + + if (_editingColumnIndex != -1) + { + // Revert the potential cell editing and exit cell editing. + EndCellEdit(DataGridEditAction.Cancel, exitEditingMode: true, keepFocus: true, raiseEvents: true); + return true; + } + else if (EditingRow != null) + { + // Revert the potential row editing and exit row editing. + EndRowEdit(DataGridEditAction.Cancel, exitEditingMode: true, raiseEvents: true); + return true; + } + return false; + } + + private bool ProcessF2Key(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + + if (!shift && !ctrl && + _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) && + !GetColumnEffectiveReadOnlyState(CurrentColumn)) + { + if (ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true)) + { + BeginCellEdit(e); + } + return true; + } + + return false; + } + + private bool ProcessHomeKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int firstVisibleSlot = FirstVisibleSlot; + if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessHomeKey(shift, ctrl))) + { + return true; + } + + _noSelectionChangeCount++; + try + { + if (!ctrl) + { + return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot); + } + else + { + DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended) + ? DataGridSelectionAction.SelectFromAnchorToCurrent + : DataGridSelectionAction.SelectCurrent; + + UpdateSelectionAndCurrency(firstVisibleColumnIndex, firstVisibleSlot, action, scrollIntoView: true); + } + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessLeftKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int firstVisibleSlot = FirstVisibleSlot; + if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessLeftKey(shift, ctrl))) + { + return true; + } + + int previousVisibleColumnIndex = -1; + if (CurrentColumnIndex != -1) + { + dataGridColumn = ColumnsInternal.GetPreviousVisibleNonFillerColumn(ColumnsItemsInternal[CurrentColumnIndex]); + if (dataGridColumn != null) + { + previousVisibleColumnIndex = dataGridColumn.Index; + } + } + + _noSelectionChangeCount++; + try + { + if (ctrl) + { + return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot); + } + else + { + if (RowGroupHeadersTable.Contains(CurrentSlot)) + { + CollapseRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, collapseAllSubgroups: false); + } + else if (CurrentColumnIndex == -1) + { + UpdateSelectionAndCurrency( + firstVisibleColumnIndex, + firstVisibleSlot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: true); + } + else + { + if (previousVisibleColumnIndex == -1) + { + return true; + } + + UpdateSelectionAndCurrency( + previousVisibleColumnIndex, + CurrentSlot, + DataGridSelectionAction.None, + scrollIntoView: true); + } + } + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + // Ctrl Left <==> Home + private bool ProcessLeftMost(int firstVisibleColumnIndex, int firstVisibleSlot) + { + _noSelectionChangeCount++; + try + { + int desiredSlot; + DataGridSelectionAction action; + if (CurrentColumnIndex == -1) + { + desiredSlot = firstVisibleSlot; + action = DataGridSelectionAction.SelectCurrent; + Debug.Assert(_selectedItems.Count == 0); + } + else + { + desiredSlot = CurrentSlot; + action = DataGridSelectionAction.None; + } + + UpdateSelectionAndCurrency(firstVisibleColumnIndex, desiredSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessNextKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessNextKey(shift, ctrl))) + { + return true; + } + + int nextPageSlot = CurrentSlot == -1 ? DisplayData.FirstScrollingSlot : CurrentSlot; + Debug.Assert(nextPageSlot != -1); + int slot = GetNextVisibleSlot(nextPageSlot); + + int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements; + while (scrollCount > 0 && slot < SlotCount) + { + nextPageSlot = slot; + scrollCount--; + slot = GetNextVisibleSlot(slot); + } + + _noSelectionChangeCount++; + try + { + DataGridSelectionAction action; + int columnIndex; + if (CurrentColumnIndex == -1) + { + columnIndex = firstVisibleColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + else + { + columnIndex = CurrentColumnIndex; + action = (shift && SelectionMode == DataGridSelectionMode.Extended) + ? action = DataGridSelectionAction.SelectFromAnchorToCurrent + : action = DataGridSelectionAction.SelectCurrent; + } + + UpdateSelectionAndCurrency(columnIndex, nextPageSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessPriorKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessPriorKey(shift, ctrl))) + { + return true; + } + + int previousPageSlot = (CurrentSlot == -1) ? DisplayData.FirstScrollingSlot : CurrentSlot; + Debug.Assert(previousPageSlot != -1); + + int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements; + int slot = GetPreviousVisibleSlot(previousPageSlot); + while (scrollCount > 0 && slot != -1) + { + previousPageSlot = slot; + scrollCount--; + slot = GetPreviousVisibleSlot(slot); + } + Debug.Assert(previousPageSlot != -1); + + _noSelectionChangeCount++; + try + { + int columnIndex; + DataGridSelectionAction action; + if (CurrentColumnIndex == -1) + { + columnIndex = firstVisibleColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + else + { + columnIndex = CurrentColumnIndex; + action = (shift && SelectionMode == DataGridSelectionMode.Extended) + ? DataGridSelectionAction.SelectFromAnchorToCurrent + : DataGridSelectionAction.SelectCurrent; + } + + UpdateSelectionAndCurrency(columnIndex, previousPageSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessRightKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn; + int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int firstVisibleSlot = FirstVisibleSlot; + if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1) + { + return false; + } + + if (WaitForLostFocus(delegate { ProcessRightKey(shift, ctrl); })) + { + return true; + } + + int nextVisibleColumnIndex = -1; + if (CurrentColumnIndex != -1) + { + dataGridColumn = ColumnsInternal.GetNextVisibleColumn(ColumnsItemsInternal[CurrentColumnIndex]); + if (dataGridColumn != null) + { + nextVisibleColumnIndex = dataGridColumn.Index; + } + } + _noSelectionChangeCount++; + try + { + if (ctrl) + { + return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot); + } + else + { + if (RowGroupHeadersTable.Contains(CurrentSlot)) + { + ExpandRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, expandAllSubgroups: false); + } + else if (CurrentColumnIndex == -1) + { + int firstVisibleColumnIndex = ColumnsInternal.FirstVisibleColumn == null ? -1 : ColumnsInternal.FirstVisibleColumn.Index; + + UpdateSelectionAndCurrency( + firstVisibleColumnIndex, + firstVisibleSlot, + DataGridSelectionAction.SelectCurrent, + scrollIntoView: true); + } + else + { + if (nextVisibleColumnIndex == -1) + { + return true; + } + + UpdateSelectionAndCurrency( + nextVisibleColumnIndex, + CurrentSlot, + DataGridSelectionAction.None, + scrollIntoView: true); + } + } + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + // Ctrl Right <==> End + private bool ProcessRightMost(int lastVisibleColumnIndex, int firstVisibleSlot) + { + _noSelectionChangeCount++; + try + { + int desiredSlot; + DataGridSelectionAction action; + if (CurrentColumnIndex == -1) + { + desiredSlot = firstVisibleSlot; + action = DataGridSelectionAction.SelectCurrent; + } + else + { + desiredSlot = CurrentSlot; + action = DataGridSelectionAction.None; + } + + UpdateSelectionAndCurrency(lastVisibleColumnIndex, desiredSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private bool ProcessTabKey(KeyEventArgs e) + { + KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + return ProcessTabKey(e, shift, ctrl); + } + + private bool ProcessTabKey(KeyEventArgs e, bool shift, bool ctrl) + { + if (ctrl || _editingColumnIndex == -1 || IsReadOnly) + { + //Go to the next/previous control on the page when + // - Ctrl key is used + // - Potential current cell is not edited, or the datagrid is read-only. + return false; + } + + // Try to locate a writable cell before/after the current cell + Debug.Assert(CurrentColumnIndex != -1); + Debug.Assert(CurrentSlot != -1); + + int neighborVisibleWritableColumnIndex, neighborSlot; + DataGridColumn dataGridColumn; + if (shift) + { + dataGridColumn = ColumnsInternal.GetPreviousVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]); + neighborSlot = GetPreviousVisibleSlot(CurrentSlot); + if (EditingRow != null) + { + while (neighborSlot != -1 && RowGroupHeadersTable.Contains(neighborSlot)) + { + neighborSlot = GetPreviousVisibleSlot(neighborSlot); + } + } + } + else + { + dataGridColumn = ColumnsInternal.GetNextVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]); + neighborSlot = GetNextVisibleSlot(CurrentSlot); + if (EditingRow != null) + { + while (neighborSlot < SlotCount && RowGroupHeadersTable.Contains(neighborSlot)) + { + neighborSlot = GetNextVisibleSlot(neighborSlot); + } + } + } + neighborVisibleWritableColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + + if (neighborVisibleWritableColumnIndex == -1 && (neighborSlot == -1 || neighborSlot >= SlotCount)) + { + // There is no previous/next row and no previous/next writable cell on the current row + return false; + } + + if (WaitForLostFocus(() => ProcessTabKey(e, shift, ctrl))) + { + return true; + } + + int targetSlot = -1, targetColumnIndex = -1; + + _noSelectionChangeCount++; + try + { + if (neighborVisibleWritableColumnIndex == -1) + { + targetSlot = neighborSlot; + if (shift) + { + Debug.Assert(ColumnsInternal.LastVisibleWritableColumn != null); + targetColumnIndex = ColumnsInternal.LastVisibleWritableColumn.Index; + } + else + { + Debug.Assert(ColumnsInternal.FirstVisibleWritableColumn != null); + targetColumnIndex = ColumnsInternal.FirstVisibleWritableColumn.Index; + } + } + else + { + targetSlot = CurrentSlot; + targetColumnIndex = neighborVisibleWritableColumnIndex; + } + + DataGridSelectionAction action; + if (targetSlot != CurrentSlot || (SelectionMode == DataGridSelectionMode.Extended)) + { + if (IsSlotOutOfBounds(targetSlot)) + { + return true; + } + action = DataGridSelectionAction.SelectCurrent; + } + else + { + action = DataGridSelectionAction.None; + } + + UpdateSelectionAndCurrency(targetColumnIndex, targetSlot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + + if (_successfullyUpdatedSelection && !RowGroupHeadersTable.Contains(targetSlot)) + { + BeginCellEdit(e); + } + + // Return true to say we handled the key event even if the operation was unsuccessful. If we don't + // say we handled this event, the framework will continue to process the tab key and change focus. + return true; + } + + private bool ProcessUpKey(bool shift, bool ctrl) + { + DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index; + int firstVisibleSlot = FirstVisibleSlot; + if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1) + { + return false; + } + + if (WaitForLostFocus(() => ProcessUpKey(shift, ctrl))) + { + return true; + } + + int previousVisibleSlot = (CurrentSlot != -1) ? GetPreviousVisibleSlot(CurrentSlot) : -1; + + _noSelectionChangeCount++; + + try + { + int slot; + int columnIndex; + DataGridSelectionAction action; + if (CurrentColumnIndex == -1) + { + slot = firstVisibleSlot; + columnIndex = firstVisibleColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + else if (ctrl) + { + if (shift) + { + // Both Ctrl and Shift + slot = firstVisibleSlot; + columnIndex = CurrentColumnIndex; + action = (SelectionMode == DataGridSelectionMode.Extended) + ? DataGridSelectionAction.SelectFromAnchorToCurrent + : DataGridSelectionAction.SelectCurrent; + } + else + { + // Ctrl without Shift + slot = firstVisibleSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + } + else + { + if (previousVisibleSlot == -1) + { + return true; + } + if (shift) + { + // Shift without Ctrl + slot = previousVisibleSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectFromAnchorToCurrent; + } + else + { + // Neither Shift nor Ctrl + slot = previousVisibleSlot; + columnIndex = CurrentColumnIndex; + action = DataGridSelectionAction.SelectCurrent; + } + } + UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: true); + } + finally + { + NoSelectionChangeCount--; + } + return _successfullyUpdatedSelection; + } + + private void RemoveDisplayedColumnHeader(DataGridColumn dataGridColumn) + { + if (_columnHeadersPresenter != null) + { + _columnHeadersPresenter.Children.Remove(dataGridColumn.HeaderCell); + } + } + + private void RemoveDisplayedColumnHeaders() + { + if (_columnHeadersPresenter != null) + { + _columnHeadersPresenter.Children.Clear(); + } + ColumnsInternal.FillerColumn.IsRepresented = false; + } + + private bool ResetCurrentCellCore() + { + return (CurrentColumnIndex == -1 || SetCurrentCellCore(-1, -1)); + } + + private void ResetEditingRow() + { + if (EditingRow != null + && EditingRow != _focusedRow + && !IsSlotVisible(EditingRow.Slot)) + { + // Unload the old editing row if it's off screen + EditingRow.Clip = null; + UnloadRow(EditingRow); + DisplayData.FullyRecycleElements(); + } + EditingRow = null; + } + + private void ResetFocusedRow() + { + if (_focusedRow != null + && _focusedRow != EditingRow + && !IsSlotVisible(_focusedRow.Slot)) + { + // Unload the old focused row if it's off screen + _focusedRow.Clip = null; + UnloadRow(_focusedRow); + DisplayData.FullyRecycleElements(); + } + _focusedRow = null; + } + + private void SelectAll() + { + SetRowsSelection(0, SlotCount - 1); + } + + private void SetAndSelectCurrentCell(int columnIndex, + int slot, + bool forceCurrentCellSelection) + { + DataGridSelectionAction action = forceCurrentCellSelection ? DataGridSelectionAction.SelectCurrent : DataGridSelectionAction.None; + UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false); + } + + // columnIndex = 2, rowIndex = -1 --> current cell belongs to the 'new row'. + // columnIndex = 2, rowIndex = 2 --> current cell is an inner cell + // columnIndex = -1, rowIndex = -1 --> current cell is reset + // columnIndex = -1, rowIndex = 2 --> Unexpected + private bool SetCurrentCellCore(int columnIndex, int slot, bool commitEdit, bool endRowEdit) + { + Debug.Assert(columnIndex < ColumnsItemsInternal.Count); + Debug.Assert(slot < SlotCount); + Debug.Assert(columnIndex == -1 || ColumnsItemsInternal[columnIndex].IsVisible); + Debug.Assert(!(columnIndex > -1 && slot == -1)); + + if (columnIndex == CurrentColumnIndex && + slot == CurrentSlot) + { + Debug.Assert(DataConnection != null); + Debug.Assert(_editingColumnIndex == -1 || _editingColumnIndex == CurrentColumnIndex); + Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot || DataConnection.CommittingEdit); + return true; + } + + Control oldDisplayedElement = null; + DataGridCellCoordinates oldCurrentCell = new DataGridCellCoordinates(CurrentCellCoordinates); + + object newCurrentItem = null; + if (!RowGroupHeadersTable.Contains(slot)) + { + int rowIndex = RowIndexFromSlot(slot); + if (rowIndex >= 0 && rowIndex < DataConnection.Count) + { + newCurrentItem = DataConnection.GetDataItem(rowIndex); + } + } + + if (CurrentColumnIndex > -1) + { + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(CurrentSlot < SlotCount); + + if (!IsInnerCellOutOfBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot) && + IsSlotVisible(oldCurrentCell.Slot)) + { + oldDisplayedElement = DisplayData.GetDisplayedElement(oldCurrentCell.Slot); + } + + if (!RowGroupHeadersTable.Contains(oldCurrentCell.Slot) && !_temporarilyResetCurrentCell) + { + bool keepFocus = ContainsFocus; + if (commitEdit) + { + if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: keepFocus, raiseEvents: true)) + { + return false; + } + // Resetting the current cell: setting it to (-1, -1) is not considered setting it out of bounds + if ((columnIndex != -1 && slot != -1 && IsInnerCellOutOfSelectionBounds(columnIndex, slot)) || + IsInnerCellOutOfSelectionBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot)) + { + return false; + } + + if (endRowEdit && !EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true)) + { + return false; + } + } + else + { + CancelEdit(DataGridEditingUnit.Row, false); + ExitEdit(keepFocus); + } + } + } + + if (newCurrentItem != null) + { + slot = SlotFromRowIndex(DataConnection.IndexOf(newCurrentItem)); + } + if (slot == -1 && columnIndex != -1) + { + return false; + } + CurrentColumnIndex = columnIndex; + CurrentSlot = slot; + + if (_temporarilyResetCurrentCell) + { + if (columnIndex != -1) + { + _temporarilyResetCurrentCell = false; + } + } + if (!_temporarilyResetCurrentCell && _editingColumnIndex != -1) + { + _editingColumnIndex = columnIndex; + } + + if (oldDisplayedElement != null) + { + if (oldDisplayedElement is DataGridRow row) + { + // Don't reset the state of the current cell if we're editing it because that would put it in an invalid state + UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, !(_temporarilyResetCurrentCell && row.IsEditing && _editingColumnIndex == oldCurrentCell.ColumnIndex)); + } + else + { + UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, applyCellState: false); + } + } + + if (CurrentColumnIndex > -1) + { + Debug.Assert(CurrentSlot > -1); + Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count); + Debug.Assert(CurrentSlot < SlotCount); + if (IsSlotVisible(CurrentSlot)) + { + UpdateCurrentState(DisplayData.GetDisplayedElement(CurrentSlot), CurrentColumnIndex, applyCellState: true); + } + } + + return true; + } + + private void SetVerticalOffset(double newVerticalOffset) + { + _verticalOffset = newVerticalOffset; + if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value)) + { + _vScrollBar.Value = _verticalOffset; + } + } + + private void UpdateCurrentState(Control displayedElement, int columnIndex, bool applyCellState) + { + if (displayedElement is DataGridRow row) + { + if (AreRowHeadersVisible) + { + row.ApplyHeaderStatus(); + } + DataGridCell cell = row.Cells[columnIndex]; + if (applyCellState) + { + cell.UpdatePseudoClasses(); + } + } + else if (displayedElement is DataGridRowGroupHeader groupHeader) + { + groupHeader.ApplyState(useTransitions: true); + if (AreRowHeadersVisible) + { + groupHeader.ApplyHeaderStatus(); + } + } + } + + private void UpdateHorizontalScrollBar(bool needHorizScrollbar, bool forceHorizScrollbar, double totalVisibleWidth, double totalVisibleFrozenWidth, double cellsWidth) + { + if (_hScrollBar != null) + { + if (needHorizScrollbar || forceHorizScrollbar) + { + // viewportSize + // v---v + //|<|_____|###|>| + // ^ ^ + // min max + + // we want to make the relative size of the thumb reflect the relative size of the viewing area + // viewportSize / (max + viewportSize) = cellsWidth / max + // -> viewportSize = max * cellsWidth / (max - cellsWidth) + + // always zero + _hScrollBar.Minimum = 0; + if (needHorizScrollbar) + { + // maximum travel distance -- not the total width + _hScrollBar.Maximum = totalVisibleWidth - cellsWidth; + Debug.Assert(totalVisibleFrozenWidth >= 0); + if (_frozenColumnScrollBarSpacer != null) + { + _frozenColumnScrollBarSpacer.Width = totalVisibleFrozenWidth; + } + Debug.Assert(_hScrollBar.Maximum >= 0); + + // width of the scrollable viewing area + double viewPortSize = Math.Max(0, cellsWidth - totalVisibleFrozenWidth); + _hScrollBar.ViewportSize = viewPortSize; + _hScrollBar.LargeChange = viewPortSize; + // The ScrollBar should be in sync with HorizontalOffset at this point. There's a resize case + // where the ScrollBar will coerce an old value here, but we don't want that + if (_hScrollBar.Value != _horizontalOffset) + { + _hScrollBar.Value = _horizontalOffset; + } + _hScrollBar.IsEnabled = true; + } + else + { + _hScrollBar.Maximum = 0; + _hScrollBar.ViewportSize = 0; + _hScrollBar.IsEnabled = false; + } + + if (!_hScrollBar.IsVisible) + { + // This will trigger a call to this method via Cells_SizeChanged for + _ignoreNextScrollBarsLayout = true; + // which no processing is needed. + _hScrollBar.IsVisible = true; + if (_hScrollBar.DesiredSize.Height == 0) + { + // We need to know the height for the rest of layout to work correctly so measure it now + _hScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + } + } + else + { + _hScrollBar.Maximum = 0; + if (_hScrollBar.IsVisible) + { + // This will trigger a call to this method via Cells_SizeChanged for + // which no processing is needed. + _hScrollBar.IsVisible = false; + _ignoreNextScrollBarsLayout = true; + } + } + } + } + + private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrollbar, double totalVisibleHeight, double cellsHeight) + { + if (_vScrollBar != null) + { + if (needVertScrollbar || forceVertScrollbar) + { + // viewportSize + // v---v + //|<|_____|###|>| + // ^ ^ + // min max + + // we want to make the relative size of the thumb reflect the relative size of the viewing area + // viewportSize / (max + viewportSize) = cellsWidth / max + // -> viewportSize = max * cellsHeight / (totalVisibleHeight - cellsHeight) + // -> = max * cellsHeight / (totalVisibleHeight - cellsHeight) + // -> = max * cellsHeight / max + // -> = cellsHeight + + // always zero + _vScrollBar.Minimum = 0; + if (needVertScrollbar && !double.IsInfinity(cellsHeight)) + { + // maximum travel distance -- not the total height + _vScrollBar.Maximum = totalVisibleHeight - cellsHeight; + Debug.Assert(_vScrollBar.Maximum >= 0); + + // total height of the display area + _vScrollBar.ViewportSize = cellsHeight; + _vScrollBar.IsEnabled = true; + } + else + { + _vScrollBar.Maximum = 0; + _vScrollBar.ViewportSize = 0; + _vScrollBar.IsEnabled = false; + } + + if (!_vScrollBar.IsVisible) + { + // This will trigger a call to this method via Cells_SizeChanged for + // which no processing is needed. + _vScrollBar.IsVisible = true; ; + if (_vScrollBar.DesiredSize.Width == 0) + { + // We need to know the width for the rest of layout to work correctly so measure it now + _vScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + _ignoreNextScrollBarsLayout = true; + } + } + else + { + _vScrollBar.Maximum = 0; + if (_vScrollBar.IsVisible) + { + // This will trigger a call to this method via Cells_SizeChanged for + // which no processing is needed. + _vScrollBar.IsVisible = false; + _ignoreNextScrollBarsLayout = true; + } + } + } + } + + private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) + { + ProcessVerticalScroll(e.ScrollEventType); + } + + //TODO: Ensure left button is checked for + private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl) + { + bool beginEdit; + + Debug.Assert(slot >= 0); + + // Before changing selection, check if the current cell needs to be committed, and + // check if the current row needs to be committed. If any of those two operations are required and fail, + // do not change selection, and do not change current cell. + + bool wasInEdit = EditingColumnIndex != -1; + + if (IsSlotOutOfBounds(slot)) + { + return true; + } + + if (wasInEdit && (columnIndex != EditingColumnIndex || slot != CurrentSlot) && + WaitForLostFocus(() => UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl))) + { + return true; + } + + try + { + _noSelectionChangeCount++; + + beginEdit = allowEdit && + CurrentSlot == slot && + columnIndex != -1 && + (wasInEdit || CurrentColumnIndex == columnIndex) && + !GetColumnEffectiveReadOnlyState(ColumnsItemsInternal[columnIndex]); + + DataGridSelectionAction action; + if (SelectionMode == DataGridSelectionMode.Extended && shift) + { + // Shift select multiple rows + action = DataGridSelectionAction.SelectFromAnchorToCurrent; + } + else if (GetRowSelection(slot)) // Unselecting single row or Selecting a previously multi-selected row + { + if (!ctrl && SelectionMode == DataGridSelectionMode.Extended && _selectedItems.Count != 0) + { + // Unselect everything except the row that was clicked on + action = DataGridSelectionAction.SelectCurrent; + } + else if (ctrl && EditingRow == null) + { + action = DataGridSelectionAction.RemoveCurrentFromSelection; + } + else + { + action = DataGridSelectionAction.None; + } + } + else // Selecting a single row or multi-selecting with Ctrl + { + if (SelectionMode == DataGridSelectionMode.Single || !ctrl) + { + // Unselect the currectly selected rows except the new selected row + action = DataGridSelectionAction.SelectCurrent; + } + else + { + action = DataGridSelectionAction.AddCurrentToSelection; + } + } + + UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false); + } + finally + { + NoSelectionChangeCount--; + } + + if (_successfullyUpdatedSelection && beginEdit && BeginCellEdit(pointerPressedEventArgs)) + { + FocusEditingCell(setFocus: true); + } + + return true; + } + + /// + /// Returns the Group at the indicated level or null if the item is not in the ItemsSource + /// + /// item + /// groupLevel + /// The group the given item falls under or null if the item is not in the ItemsSource + public DataGridCollectionViewGroup GetGroupFromItem(object item, int groupLevel) + { + int itemIndex = DataConnection.IndexOf(item); + if (itemIndex == -1) + { + return null; + } + int groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(SlotFromRowIndex(itemIndex)); + DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot); + while (rowGroupInfo != null && rowGroupInfo.Level != groupLevel) + { + groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(rowGroupInfo.Slot); + rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot); + } + return rowGroupInfo?.CollectionViewGroup; + } + + /// + /// Raises the LoadingRowGroup event + /// + /// EventArgs + protected virtual void OnLoadingRowGroup(DataGridRowGroupHeaderEventArgs e) + { + EventHandler handler = LoadingRowGroup; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Raises the UnLoadingRowGroup event + /// + /// EventArgs + protected virtual void OnUnloadingRowGroup(DataGridRowGroupHeaderEventArgs e) + { + EventHandler handler = UnloadingRowGroup; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Occurs before a DataGridRowGroupHeader header is used. + /// + public event EventHandler LoadingRowGroup; + + /// + /// Occurs when the DataGridRowGroupHeader is available for reuse. + /// + public event EventHandler UnloadingRowGroup; + + // Recursively expands parent RowGroupHeaders from the top down + private void ExpandRowGroupParentChain(int level, int slot) + { + if (level < 0) + { + return; + } + int previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(slot + 1); + DataGridRowGroupInfo rowGroupInfo = null; + while (previousHeaderSlot >= 0) + { + rowGroupInfo = RowGroupHeadersTable.GetValueAt(previousHeaderSlot); + Debug.Assert(rowGroupInfo != null); + if (level == rowGroupInfo.Level) + { + if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot)) + { + // Keep going up the chain + ExpandRowGroupParentChain(level - 1, rowGroupInfo.Slot - 1); + } + if (!rowGroupInfo.IsVisible) + { + EnsureRowGroupVisibility(rowGroupInfo, true, false); + } + return; + } + else + { + previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(previousHeaderSlot); + } + } + } + + /// + /// This event is raised by OnCopyingRowClipboardContent method after the default row content is prepared. + /// Event listeners can modify or add to the row clipboard content. + /// + public event EventHandler CopyingRowClipboardContent; + + /// + /// This method raises the CopyingRowClipboardContent event. + /// + /// Contains the necessary information for generating the row clipboard content. + protected virtual void OnCopyingRowClipboardContent(DataGridRowClipboardEventArgs e) + { + CopyingRowClipboardContent?.Invoke(this, e); + } + + /// + /// This method formats a row (specified by a DataGridRowClipboardEventArgs) into + /// a single string to be added to the Clipboard when the DataGrid is copying its contents. + /// + /// DataGridRowClipboardEventArgs + /// The formatted string. + private string FormatClipboardContent(DataGridRowClipboardEventArgs e) + { + StringBuilder text = new StringBuilder(); + for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++) + { + DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex]; + if (cellContent != null) + { + text.Append(cellContent.Content); + } + if (cellIndex < e.ClipboardRowContent.Count - 1) + { + text.Append('\t'); + } + else + { + text.Append('\r'); + text.Append('\n'); + } + } + return text.ToString(); + } + + /// + /// Handles the case where a 'Copy' key ('C' or 'Insert') has been pressed. If pressed in combination with + /// the control key, and the necessary prerequisites are met, the DataGrid will copy its contents + /// to the Clipboard as text. + /// + /// Whether or not the DataGrid handled the key press. + private bool ProcessCopyKey(InputModifiers modifiers) + { + KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt); + + if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0) + { + StringBuilder textBuilder = new StringBuilder(); + + if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader) + { + DataGridRowClipboardEventArgs headerArgs = new DataGridRowClipboardEventArgs(null, true); + foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns()) + { + headerArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(null, column, column.Header)); + } + OnCopyingRowClipboardContent(headerArgs); + textBuilder.Append(FormatClipboardContent(headerArgs)); + } + + for (int index = 0; index < SelectedItems.Count; index++) + { + object item = SelectedItems[index]; + DataGridRowClipboardEventArgs itemArgs = new DataGridRowClipboardEventArgs(item, false); + foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns()) + { + object content = column.GetCellValue(item, column.ClipboardContentBinding); + itemArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(item, column, content)); + } + OnCopyingRowClipboardContent(itemArgs); + textBuilder.Append(FormatClipboardContent(itemArgs)); + } + + string text = textBuilder.ToString(); + + if (!string.IsNullOrEmpty(text)) + { + CopyToClipboard(text); + return true; + } + } + return false; + } + + private async void CopyToClipboard(string text) + { + var clipboard = ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); + await clipboard.SetTextAsync(text); + } + + /// + /// This is an empty content control that's used during the DataGrid's copy procedure + /// to determine the value of a ClipboardContentBinding for a particular column and item. + /// + internal ContentControl ClipboardContentControl + { + get + { + if (_clipboardContentControl == null) + { + _clipboardContentControl = new ContentControl(); + } + return _clipboardContentControl; + } + } + + //TODO Validation UI + private void ResetValidationStatus() + { + // Clear the invalid status of the Cell, Row and DataGrid + if (EditingRow != null) + { + EditingRow.IsValid = true; + if (EditingRow.Index != -1) + { + foreach (DataGridCell cell in EditingRow.Cells) + { + if (!cell.IsValid) + { + cell.IsValid = true; + cell.UpdatePseudoClasses(); + } + } + EditingRow.UpdatePseudoClasses(); + } + } + IsValid = true; + + _validationSubscription?.Dispose(); + _validationSubscription = null; + } + + /// + /// Raises the AutoGeneratingColumn event. + /// + protected virtual void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventArgs e) + { + AutoGeneratingColumn?.Invoke(this, e); + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs new file mode 100644 index 0000000000..d213285aff --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -0,0 +1,145 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Data; +using Avalonia.Utilities; +using System; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using Avalonia.Reactive; +using System.Diagnostics; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls +{ + /// + /// Represents a column that can + /// bind to a property in the grid's data source. + /// + public abstract class DataGridBoundColumn : DataGridColumn + { + private IBinding _binding; + + /// + /// Gets or sets the binding that associates the column with a property in the data source. + /// + //TODO Binding + public virtual IBinding Binding + { + get + { + return _binding; + } + set + { + if (_binding != value) + { + if (OwningGrid != null && !OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) + { + // Edited value couldn't be committed, so we force a CancelEdit + OwningGrid.CancelEdit(DataGridEditingUnit.Row, raiseEvents: false); + } + + _binding = value; + + if (_binding != null) + { + if(_binding is Avalonia.Data.Binding binding) + { + // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. + if (!String.IsNullOrEmpty(binding.Path)) + { + binding.Mode = BindingMode.TwoWay; + } + + if (binding.Converter == null) + { + binding.Converter = DataGridValueConverter.Instance; + } + } + + // Apply the new Binding to existing rows in the DataGrid + if (OwningGrid != null) + { + OwningGrid.OnColumnBindingChanged(this); + } + } + + RemoveEditingElement(); + } + } + } + + /// + /// The binding that will be used to get or set cell content for the clipboard. + /// If the base ClipboardContentBinding is not explicitly set, this will return the value of Binding. + /// + public override IBinding ClipboardContentBinding + { + get + { + return base.ClipboardContentBinding ?? Binding; + } + set + { + base.ClipboardContentBinding = value; + } + } + + //TODO Rename + //TODO Validation + protected sealed override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding editBinding) + { + IControl element = GenerateEditingElementDirect(cell, dataItem); + editBinding = null; + + if (Binding != null) + { + editBinding = BindEditingElement(element, BindingTarget, Binding); + } + + return element; + } + + private static ICellEditBinding BindEditingElement(IAvaloniaObject target, AvaloniaProperty property, IBinding binding) + { + var result = binding.Initiate(target, property, enableDataValidation: true); + + if (result != null) + { + if(result.Subject != null) + { + var bindingHelper = new CellEditBinding(result.Subject); + var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); + + BindingOperations.Apply(target, property, instanceBinding, null); + return bindingHelper; + } + + BindingOperations.Apply(target, property, result, null); + } + + return null; + } + + protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); + + internal AvaloniaProperty BindingTarget { get; set; } + + internal void SetHeaderFromBinding() + { + if (OwningGrid != null && OwningGrid.DataConnection.DataType != null + && Header == null && Binding != null && Binding is Binding binding + && !String.IsNullOrWhiteSpace(binding.Path)) + { + string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path); + if (header != null) + { + Header = header; + } + } + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs new file mode 100644 index 0000000000..a21583b38e --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -0,0 +1,222 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Input; + +namespace Avalonia.Controls +{ + /// + /// Represents an individual cell. + /// + public class DataGridCell : ContentControl + { + private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; + + private Rectangle _rightGridLine; + private DataGridColumn _owningColumn; + + bool _isValid; + + public static readonly DirectProperty IsValidProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsValid), + o => o.IsValid); + + static DataGridCell() + { + PointerPressedEvent.AddClassHandler( + x => x.DataGridCell_PointerPressed, handledEventsToo: true); + } + public DataGridCell() + { } + + public bool IsValid + { + get { return _isValid; } + internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } + } + + internal DataGridColumn OwningColumn + { + get => _owningColumn; + set + { + if (_owningColumn != value) + { + _owningColumn = value; + OnOwningColumnSet(value); + } + } + } + internal DataGridRow OwningRow + { + get; + set; + } + + internal DataGrid OwningGrid + { + get { return OwningRow?.OwningGrid ?? OwningColumn?.OwningGrid; } + } + + internal double ActualRightGridLineWidth + { + get { return _rightGridLine?.Bounds.Width ?? 0; } + } + + internal int ColumnIndex + { + get { return OwningColumn?.Index ?? -1; } + } + + internal int RowIndex + { + get { return OwningRow?.Index ?? -1; } + } + + internal bool IsCurrent + { + get + { + return OwningGrid.CurrentColumnIndex == OwningColumn.Index && + OwningGrid.CurrentSlot == OwningRow.Slot; + } + } + + private bool IsEdited + { + get + { + return OwningGrid.EditingRow == OwningRow && + OwningGrid.EditingColumnIndex == ColumnIndex; + } + } + + private bool IsMouseOver + { + get + { + return OwningRow != null && OwningRow.MouseOverColumnIndex == ColumnIndex; + } + set + { + if (value != IsMouseOver) + { + if (value) + { + OwningRow.MouseOverColumnIndex = ColumnIndex; + } + else + { + OwningRow.MouseOverColumnIndex = null; + } + } + } + } + + /// + /// Builds the visual tree for the cell control when a new template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + + UpdatePseudoClasses(); + _rightGridLine = e.NameScope.Find(DATAGRIDCELL_elementRightGridLine); + if (_rightGridLine != null && OwningColumn == null) + { + // Turn off the right GridLine for filler cells + _rightGridLine.IsVisible = false; + } + else + { + EnsureGridLine(null); + } + + } + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + if (OwningRow != null) + { + IsMouseOver = true; + } + } + protected override void OnPointerLeave(PointerEventArgs e) + { + base.OnPointerLeave(e); + + if (OwningRow != null) + { + IsMouseOver = false; + } + } + + //TODO TabStop + private void DataGridCell_PointerPressed(PointerPressedEventArgs e) + { + // OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow + if (OwningGrid != null) + { + OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); + if (e.MouseButton == MouseButton.Left) + { + if (!e.Handled) + //if (!e.Handled && OwningGrid.IsTabStop) + { + OwningGrid.Focus(); + } + if (OwningRow != null) + { + e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; + } + } + } + } + + internal void UpdatePseudoClasses() + { + + } + + // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the + // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column + internal void EnsureGridLine(DataGridColumn lastVisibleColumn) + { + if (OwningGrid != null && _rightGridLine != null) + { + if (OwningGrid.VerticalGridLinesBrush != null && OwningGrid.VerticalGridLinesBrush != _rightGridLine.Fill) + { + _rightGridLine.Fill = OwningGrid.VerticalGridLinesBrush; + } + + bool newVisibility = + (OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Vertical || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All) + && (OwningGrid.ColumnsInternal.FillerColumn.IsActive || OwningColumn != lastVisibleColumn); + + if (newVisibility != _rightGridLine.IsVisible) + { + _rightGridLine.IsVisible = newVisibility; + } + } + } + + private void OnOwningColumnSet(DataGridColumn column) + { + if (column == null) + { + Classes.Clear(); + } + else + { + Classes.Replace(column.CellStyleClasses); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs new file mode 100644 index 0000000000..a4dff8a2d8 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs @@ -0,0 +1,71 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Controls +{ + internal class DataGridCellCollection + { + private List _cells; + private DataGridRow _owningRow; + + internal event EventHandler CellAdded; + internal event EventHandler CellRemoved; + + public DataGridCellCollection(DataGridRow owningRow) + { + _owningRow = owningRow; + _cells = new List(); + } + + public int Count + { + get + { + return _cells.Count; + } + } + + public IEnumerator GetEnumerator() + { + return _cells.GetEnumerator(); + } + + public void Insert(int cellIndex, DataGridCell cell) + { + Debug.Assert(cellIndex >= 0 && cellIndex <= _cells.Count); + Debug.Assert(cell != null); + + cell.OwningRow = _owningRow; + _cells.Insert(cellIndex, cell); + + CellAdded?.Invoke(this, new DataGridCellEventArgs(cell)); + } + + public void RemoveAt(int cellIndex) + { + DataGridCell dataGridCell = _cells[cellIndex]; + _cells.RemoveAt(cellIndex); + dataGridCell.OwningRow = null; + CellRemoved?.Invoke(this, new DataGridCellEventArgs(dataGridCell)); + } + + public DataGridCell this[int index] + { + get + { + if (index < 0 || index >= _cells.Count) + { + throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _cells.Count, false); + } + return _cells[index]; + } + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs new file mode 100644 index 0000000000..2f723154be --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs @@ -0,0 +1,57 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System.Globalization; + +namespace Avalonia.Controls +{ + internal class DataGridCellCoordinates + { + public DataGridCellCoordinates(int columnIndex, int slot) + { + ColumnIndex = columnIndex; + Slot = slot; + } + + public DataGridCellCoordinates(DataGridCellCoordinates dataGridCellCoordinates) : this(dataGridCellCoordinates.ColumnIndex, dataGridCellCoordinates.Slot) + { + } + + public int ColumnIndex + { + get; + set; + } + + public int Slot + { + get; + set; + } + + public override bool Equals(object o) + { + if (o is DataGridCellCoordinates dataGridCellCoordinates) + { + return dataGridCellCoordinates.ColumnIndex == ColumnIndex && dataGridCellCoordinates.Slot == Slot; + } + return false; + } + + // There is build warning if this is missiing + public override int GetHashCode() + { + return base.GetHashCode(); + } + +#if DEBUG + public override string ToString() + { + return "DataGridCellCoordinates {ColumnIndex = " + ColumnIndex.ToString(CultureInfo.CurrentCulture) + + ", Slot = " + Slot.ToString(CultureInfo.CurrentCulture) + "}"; + } +#endif + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs new file mode 100644 index 0000000000..f1bbea9949 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -0,0 +1,316 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls +{ + /// + /// Represents a column that hosts + /// controls in its cells. + /// + public class DataGridCheckBoxColumn : DataGridBoundColumn + { + + private bool _beganEditWithKeyboard; + private bool _isThreeState; + private CheckBox _currentCheckBox; + private DataGrid _owningGrid; + + /// + /// Initializes a new instance of the class. + /// + public DataGridCheckBoxColumn() + { + BindingTarget = CheckBox.IsCheckedProperty; + } + + /// + /// Gets or sets a value that indicates whether the hosted controls allow three states or two. + /// + /// + /// true if the hosted controls support three states; false if they support two states. The default is false. + /// + public bool IsThreeState + { + get + { + return _isThreeState; + } + set + { + if (_isThreeState != value) + { + _isThreeState = value; + NotifyPropertyChanged(nameof(IsThreeState)); + } + } + } + + /// + /// Causes the column cell being edited to revert to the specified value. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// The previous, unedited value in the cell being edited. + /// + protected override void CancelCellEdit(IControl editingElement, object uneditedValue) + { + if (editingElement is CheckBox editingCheckBox) + { + editingCheckBox.IsChecked = (bool?)uneditedValue; + } + } + + /// + /// Gets a control that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new control that is bound to the column's property value. + /// + protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem) + { + var checkBox = new CheckBox + { + Margin = new Thickness(0) + }; + ConfigureCheckBox(checkBox); + return checkBox; + } + + /// + /// Gets a read-only control that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new, read-only control that is bound to the column's property value. + /// + protected override IControl GenerateElement(DataGridCell cell, object dataItem) + { + bool isEnabled = false; + CheckBox checkBoxElement = new CheckBox(); + if (EnsureOwningGrid()) + { + if (cell.RowIndex != -1 && cell.ColumnIndex != -1 && + cell.OwningRow != null && + cell.OwningRow.Slot == this.OwningGrid.CurrentSlot && + cell.ColumnIndex == this.OwningGrid.CurrentColumnIndex) + { + isEnabled = true; + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + _currentCheckBox = checkBoxElement; + } + } + checkBoxElement.IsEnabled = isEnabled; + checkBoxElement.IsHitTestVisible = false; + ConfigureCheckBox(checkBoxElement); + if (Binding != null) + { + checkBoxElement.Bind(BindingTarget, Binding); + } + return checkBoxElement; + } + + /// + /// Called when a cell in the column enters editing mode. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// Information about the user gesture that is causing a cell to enter editing mode. + /// + /// + /// The unedited value. + /// + protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) + { + if (editingElement is CheckBox editingCheckBox) + { + bool? uneditedValue = editingCheckBox.IsChecked; + bool editValue = false; + if(editingEventArgs is PointerPressedEventArgs args) + { + // Editing was triggered by a mouse click + Point position = args.GetPosition(editingCheckBox); + Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height); + editValue = rect.Contains(position); + } + else if (_beganEditWithKeyboard) + { + // Editing began by a user pressing spacebar + editValue = true; + _beganEditWithKeyboard = false; + } + + if (editValue) + { + // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value + if (editingCheckBox.IsThreeState) + { + switch (editingCheckBox.IsChecked) + { + case false: + editingCheckBox.IsChecked = true; + break; + case true: + editingCheckBox.IsChecked = null; + break; + case null: + editingCheckBox.IsChecked = false; + break; + } + } + else + { + editingCheckBox.IsChecked = !editingCheckBox.IsChecked; + } + } + return uneditedValue; + } + return false; + } + + /// + /// Called by the DataGrid control when this column asks for its elements to be + /// updated, because its CheckBoxContent or IsThreeState property changed. + /// + protected internal override void RefreshCellContent(IControl element, string propertyName) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + if(element is CheckBox checkBox) + { + checkBox.IsThreeState = IsThreeState; + } + else + { + throw DataGridError.DataGrid.ValueIsNotAnInstanceOf("element", typeof(CheckBox)); + } + } + + private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems.Contains(this) && _owningGrid != null) + { + _owningGrid.Columns.CollectionChanged -= Columns_CollectionChanged; + _owningGrid.CurrentCellChanged -= OwningGrid_CurrentCellChanged; + _owningGrid.KeyDown -= OwningGrid_KeyDown; + _owningGrid.LoadingRow -= OwningGrid_LoadingRow; + _owningGrid = null; + } + } + + private void ConfigureCheckBox(CheckBox checkBox) + { + checkBox.HorizontalAlignment = HorizontalAlignment.Center; + checkBox.VerticalAlignment = VerticalAlignment.Center; + checkBox.IsThreeState = IsThreeState; + } + + private bool EnsureOwningGrid() + { + if (OwningGrid != null) + { + if (OwningGrid != _owningGrid) + { + _owningGrid = OwningGrid; + _owningGrid.Columns.CollectionChanged += Columns_CollectionChanged; + _owningGrid.CurrentCellChanged += OwningGrid_CurrentCellChanged; + _owningGrid.KeyDown += OwningGrid_KeyDown; + _owningGrid.LoadingRow += OwningGrid_LoadingRow; + } + return true; + } + return false; + } + + private void OwningGrid_CurrentCellChanged(object sender, EventArgs e) + { + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + if (OwningGrid != null && OwningGrid.CurrentColumn == this + && OwningGrid.IsSlotVisible(OwningGrid.CurrentSlot)) + { + if (OwningGrid.DisplayData.GetDisplayedElement(OwningGrid.CurrentSlot) is DataGridRow row) + { + CheckBox checkBox = GetCellContent(row) as CheckBox; + if (checkBox != null) + { + checkBox.IsEnabled = true; + } + _currentCheckBox = checkBox; + } + } + } + + private void OwningGrid_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Space && OwningGrid != null && + OwningGrid.CurrentColumn == this) + { + if (OwningGrid.DisplayData.GetDisplayedElement(OwningGrid.CurrentSlot) is DataGridRow row) + { + CheckBox checkBox = GetCellContent(row) as CheckBox; + if (checkBox == _currentCheckBox) + { + _beganEditWithKeyboard = true; + OwningGrid.BeginEdit(); + return; + } + } + } + _beganEditWithKeyboard = false; + } + + private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) + { + if (OwningGrid != null) + { + if (GetCellContent(e.Row) is CheckBox checkBox) + { + if (OwningGrid.CurrentColumnIndex == Index && OwningGrid.CurrentSlot == e.Row.Slot) + { + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + checkBox.IsEnabled = true; + _currentCheckBox = checkBox; + } + else + { + checkBox.IsEnabled = false; + } + } + } + } + + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs new file mode 100644 index 0000000000..a4bab8b304 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs @@ -0,0 +1,204 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Controls +{ + /// + /// Defines modes that indicates how DataGrid content is copied to the Clipboard. + /// + public enum DataGridClipboardCopyMode + { + /// + /// Disable the DataGrid's ability to copy selected items as text. + /// + None, + + /// + /// Enable the DataGrid's ability to copy selected items as text, but do not include + /// the column header content as the first line in the text that gets copied to the Clipboard. + /// + ExcludeHeader, + + /// + /// Enable the DataGrid's ability to copy selected items as text, and include + /// the column header content as the first line in the text that gets copied to the Clipboard. + /// + IncludeHeader + } + + /// + /// This structure encapsulate the cell information necessary when clipboard content is prepared. + /// + public struct DataGridClipboardCellContent + { + + private DataGridColumn _column; + private object _content; + private object _item; + + /// + /// Creates a new DataGridClipboardCellValue structure containing information about a DataGrid cell. + /// + /// DataGrid row item containing the cell. + /// DataGridColumn containing the cell. + /// DataGrid cell value. + public DataGridClipboardCellContent(object item, DataGridColumn column, object content) + { + this._item = item; + this._column = column; + this._content = content; + } + + /// + /// DataGridColumn containing the cell. + /// + public DataGridColumn Column + { + get + { + return _column; + } + } + + /// + /// Cell content. + /// + public object Content + { + get + { + return _content; + } + } + + /// + /// DataGrid row item containing the cell. + /// + public object Item + { + get + { + return _item; + } + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// DataGridClipboardCellContent to compare. + /// True iff this and data are equal + public override bool Equals(object obj) + { + if(obj is DataGridClipboardCellContent content) + { + return (((_column == content._column) && (_content == content._content)) && (_item == content._item)); + } + else + { + return false; + } + } + + /// + /// Returns a deterministic hash code. + /// + /// Hash value. + public override int GetHashCode() + { + return ((_column.GetHashCode() ^ _content.GetHashCode()) ^ _item.GetHashCode()); + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// The first DataGridClipboardCellContent. + /// The second DataGridClipboardCellContent. + /// True iff clipboardCellContent1 and clipboardCellContent2 are equal. + public static bool operator ==(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2) + { + return (((clipboardCellContent1._column == clipboardCellContent2._column) && (clipboardCellContent1._content == clipboardCellContent2._content)) && (clipboardCellContent1._item == clipboardCellContent2._item)); + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// The first DataGridClipboardCellContent. + /// The second DataGridClipboardCellContent. + /// True iff clipboardCellContent1 and clipboardCellContent2 are NOT equal. + public static bool operator !=(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2) + { + if ((clipboardCellContent1._column == clipboardCellContent2._column) && (clipboardCellContent1._content == clipboardCellContent2._content)) + { + return (clipboardCellContent1._item != clipboardCellContent2._item); + } + return true; + } + + } + + /// + /// This class encapsulates a selected row's information necessary for the CopyingRowClipboardContent event. + /// + public class DataGridRowClipboardEventArgs : EventArgs + { + + private List _clipboardRowContent; + private bool _isColumnHeadersRow; + private object _item; + + /// + /// Creates a DataGridRowClipboardEventArgs object and initializes the properties. + /// + /// The row's associated data item. + /// Whether or not this EventArgs is for the column headers. + internal DataGridRowClipboardEventArgs(object item, bool isColumnHeadersRow) + { + _isColumnHeadersRow = isColumnHeadersRow; + _item = item; + } + + /// + /// This list should be used to modify, add ot remove a cell content before it gets stored into the clipboard. + /// + public List ClipboardRowContent + { + get + { + if (_clipboardRowContent == null) + { + _clipboardRowContent = new List(); + } + return _clipboardRowContent; + } + } + + /// + /// This property is true when the ClipboardRowContent represents column headers, in which case the Item is null. + /// + public bool IsColumnHeadersRow + { + get + { + return _isColumnHeadersRow; + } + } + + /// + /// DataGrid row item used for proparing the ClipboardRowContent. + /// + public object Item + { + get + { + return _item; + } + } + + } + +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs new file mode 100644 index 0000000000..128fbde0c1 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -0,0 +1,1050 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Data; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using Avalonia.Collections; +using Avalonia.Utilities; +using System; +using System.Linq; +using System.Diagnostics; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls +{ + public abstract class DataGridColumn : AvaloniaObject + { + internal const int DATAGRIDCOLUMN_maximumWidth = 65536; + private const bool DATAGRIDCOLUMN_defaultIsReadOnly = false; + + private DataGridLength? _width; // Null by default, null means inherit the Width from the DataGrid + private bool? _isReadOnly; + private double? _maxWidth; + private double? _minWidth; + private bool _settingWidthInternally; + private int _displayIndexWithFiller; + private bool _isVisible; + private object _header; + private DataGridColumnHeader _headerCell; + private IControl _editingElement; + private ICellEditBinding _editBinding; + private IBinding _clipboardContentBinding; + private readonly Classes _cellStyleClasses = new Classes(); + + /// + /// Initializes a new instance of the class. + /// + protected internal DataGridColumn() + { + _isVisible = true; + _displayIndexWithFiller = -1; + IsInitialDesiredWidthDetermined = false; + InheritsWidth = true; + } + + internal DataGrid OwningGrid + { + get; + set; + } + + internal int Index + { + get; + set; + } + + internal bool? CanUserReorderInternal + { + get; + set; + } + + internal bool? CanUserResizeInternal + { + get; + set; + } + + internal bool? CanUserSortInternal + { + get; + set; + } + + internal bool ActualCanUserResize + { + get + { + if (OwningGrid == null || OwningGrid.CanUserResizeColumns == false || this is DataGridFillerColumn) + { + return false; + } + return CanUserResizeInternal ?? true; + } + } + + // MaxWidth from local setting or DataGrid setting + internal double ActualMaxWidth + { + get + { + return _maxWidth ?? OwningGrid?.MaxColumnWidth ?? double.PositiveInfinity; + } + } + + // MinWidth from local setting or DataGrid setting + internal double ActualMinWidth + { + get + { + double minWidth = _minWidth ?? OwningGrid?.MinColumnWidth ?? 0; + if (Width.IsStar) + { + return Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, minWidth); + } + return minWidth; + } + } + + internal bool DisplayIndexHasChanged + { + get; + set; + } + + internal int DisplayIndexWithFiller + { + get { return _displayIndexWithFiller; } + set { _displayIndexWithFiller = value; } + } + + internal bool HasHeaderCell + { + get + { + return _headerCell != null; + } + } + + internal DataGridColumnHeader HeaderCell + { + get + { + if (_headerCell == null) + { + _headerCell = CreateHeader(); + } + return _headerCell; + } + } + + /// + /// Tracks whether or not this column inherits its Width value from the DataGrid. + /// + internal bool InheritsWidth + { + get; + private set; + } + + /// + /// When a column is initially added, we won't know its initial desired value + /// until all rows have been measured. We use this variable to track whether or + /// not the column has been fully measured. + /// + internal bool IsInitialDesiredWidthDetermined + { + get; + set; + } + + internal double LayoutRoundedWidth + { + get; + private set; + } + + internal ICellEditBinding CellEditBinding + { + get => _editBinding; + } + + /// + /// Determines whether or not this column is visible. + /// + public bool IsVisible + { + get + { + return _isVisible; + } + set + { + if (value != IsVisible) + { + OwningGrid?.OnColumnVisibleStateChanging(this); + _isVisible = value; + + if (_headerCell != null) + { + _headerCell.IsVisible = value; + } + + OwningGrid?.OnColumnVisibleStateChanged(this); + } + } + } + + /// + /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level + /// have been taken into account + /// + public double ActualWidth + { + get + { + if (OwningGrid == null || double.IsNaN(Width.DisplayValue)) + { + return ActualMinWidth; + } + return Width.DisplayValue; + } + } + + /// + /// Gets or sets a value that indicates whether the user can change the column display position by + /// dragging the column header. + /// + /// + /// true if the user can drag the column header to a new position; otherwise, false. The default is the current property value. + /// + public bool CanUserReorder + { + get + { + return + CanUserReorderInternal ?? + OwningGrid?.CanUserReorderColumns ?? + DataGrid.DATAGRID_defaultCanUserResizeColumns; + } + set + { + CanUserReorderInternal = value; + } + } + + /// + /// Gets or sets a value that indicates whether the user can adjust the column width using the mouse. + /// + /// + /// true if the user can resize the column; false if the user cannot resize the column. The default is the current property value. + /// + public bool CanUserResize + { + get + { + return + CanUserResizeInternal ?? + OwningGrid?.CanUserResizeColumns ?? + DataGrid.DATAGRID_defaultCanUserResizeColumns; + } + set + { + CanUserResizeInternal = value; + OwningGrid?.OnColumnCanUserResizeChanged(this); + } + } + + /// + /// Gets or sets a value that indicates whether the user can sort the column by clicking the column header. + /// + /// + /// true if the user can sort the column; false if the user cannot sort the column. The default is the current property value. + /// + public bool CanUserSort + { + get + { + if (CanUserSortInternal.HasValue) + { + return CanUserSortInternal.Value; + } + else if (OwningGrid != null) + { + string propertyPath = GetSortPropertyName(); + Type propertyType = OwningGrid.DataConnection.DataType.GetNestedPropertyType(propertyPath); + + // if the type is nullable, then we will compare the non-nullable type + if (TypeHelper.IsNullableType(propertyType)) + { + propertyType = TypeHelper.GetNonNullableType(propertyType); + } + + // return whether or not the property type can be compared + return (typeof(IComparable).IsAssignableFrom(propertyType)) ? true : false; + } + else + { + return DataGrid.DATAGRID_defaultCanUserSortColumns; + } + } + set + { + CanUserSortInternal = value; + } + } + + /// + /// Gets or sets the display position of the column relative to the other columns in the . + /// + /// + /// The zero-based position of the column as it is displayed in the associated . The default is the index of the corresponding in the collection. + /// + /// + /// When setting this property, the specified value is less than -1 or equal to . + /// + /// -or- + /// + /// When setting this property on a column in a , the specified value is less than zero or greater than or equal to the number of columns in the . + /// + /// + /// When setting this property, the is already making adjustments. For example, this exception is thrown when you attempt to set in a event handler. + /// + /// -or- + /// + /// When setting this property, the specified value would result in a frozen column being displayed in the range of unfrozen columns, or an unfrozen column being displayed in the range of frozen columns. + /// + public int DisplayIndex + { + get + { + if (OwningGrid != null && OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + return _displayIndexWithFiller - 1; + } + else + { + return _displayIndexWithFiller; + } + } + set + { + if (value == Int32.MaxValue) + { + throw DataGridError.DataGrid.ValueMustBeLessThan(nameof(value), nameof(DisplayIndex), Int32.MaxValue); + } + if (OwningGrid != null) + { + if (OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + value++; + } + if (_displayIndexWithFiller != value) + { + if (value < 0 || value >= OwningGrid.ColumnsItemsInternal.Count) + { + throw DataGridError.DataGrid.ValueMustBeBetween(nameof(value), nameof(DisplayIndex), 0, true, OwningGrid.Columns.Count, false); + } + // Will throw an error if a visible frozen column is placed inside a non-frozen area or vice-versa. + OwningGrid.OnColumnDisplayIndexChanging(this, value); + _displayIndexWithFiller = value; + try + { + OwningGrid.InDisplayIndexAdjustments = true; + OwningGrid.OnColumnDisplayIndexChanged(this); + OwningGrid.OnColumnDisplayIndexChanged_PostNotification(); + } + finally + { + OwningGrid.InDisplayIndexAdjustments = false; + } + } + } + else + { + if (value < -1) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(DisplayIndex), -1); + } + _displayIndexWithFiller = value; + } + } + } + + public Classes CellStyleClasses + { + get => _cellStyleClasses; + set + { + if(_cellStyleClasses != value) + { + _cellStyleClasses.Replace(value); + } + } + } + + public object Header + { + get + { + return _header; + } + set + { + if (_header != value) + { + _header = value; + if (_headerCell != null) + { + _headerCell.Content = value; + } + } + } + } + + public bool IsAutoGenerated + { + get; + internal set; + } + + public bool IsFrozen + { + get; + internal set; + } + + public bool IsReadOnly + { + get + { + if (OwningGrid == null) + { + return _isReadOnly ?? DATAGRIDCOLUMN_defaultIsReadOnly; + } + if (_isReadOnly != null) + { + return _isReadOnly.Value || OwningGrid.IsReadOnly; + } + return OwningGrid.GetColumnReadOnlyState(this, DATAGRIDCOLUMN_defaultIsReadOnly); + } + set + { + if (value != _isReadOnly) + { + OwningGrid?.OnColumnReadOnlyStateChanging(this, value); + _isReadOnly = value; + } + } + } + + public double MaxWidth + { + get + { + return _maxWidth ?? double.PositiveInfinity; + } + set + { + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", 0); + } + if (value < ActualMinWidth) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", "MinWidth"); + } + if (!_maxWidth.HasValue || _maxWidth.Value != value) + { + double oldValue = ActualMaxWidth; + _maxWidth = value; + if (OwningGrid != null && OwningGrid.ColumnsInternal != null) + { + OwningGrid.OnColumnMaxWidthChanged(this, oldValue); + } + } + } + } + + public double MinWidth + { + get + { + return _minWidth ?? 0; + } + set + { + if (double.IsNaN(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN("MinWidth"); + } + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MinWidth", 0); + } + if (double.IsPositiveInfinity(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("MinWidth"); + } + if (value > ActualMaxWidth) + { + throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo("value", "MinWidth", "MaxWidth"); + } + if (!_minWidth.HasValue || _minWidth.Value != value) + { + double oldValue = ActualMinWidth; + _minWidth = value; + if (OwningGrid != null && OwningGrid.ColumnsInternal != null) + { + OwningGrid.OnColumnMinWidthChanged(this, oldValue); + } + } + } + } + + public DataGridLength Width + { + get + { + return + _width ?? + OwningGrid?.ColumnWidth ?? + // We don't have a good choice here because we don't want to make this property nullable, see DevDiv Bugs 196581 + DataGridLength.Auto; + } + set + { + if (!_width.HasValue || _width.Value != value) + { + if (!_settingWidthInternally) + { + InheritsWidth = false; + } + + if (OwningGrid != null) + { + DataGridLength width = CoerceWidth(value); + if (width.IsStar != Width.IsStar) + { + // If a column has changed either from or to a star value, we want to recalculate all + // star column widths. They are recalculated during Measure based off what the value we set here. + SetWidthInternalNoCallback(width); + IsInitialDesiredWidthDetermined = false; + OwningGrid.OnColumnWidthChanged(this); + } + else + { + // If a column width's value is simply changing, we resize it (to the right only). + Resize(width.Value, width.UnitType, width.DesiredValue, width.DisplayValue, false); + } + } + else + { + SetWidthInternalNoCallback(value); + } + } + } + } + + /// + /// The binding that will be used to get or set cell content for the clipboard. + /// + public virtual IBinding ClipboardContentBinding + { + get + { + return _clipboardContentBinding; + } + set + { + _clipboardContentBinding = value; + } + } + + /// + /// Gets the value of a cell according to the the specified binding. + /// + /// The item associated with a cell. + /// The binding to get the value of. + /// The resultant cell value. + internal object GetCellValue(object item, IBinding binding) + { + Debug.Assert(OwningGrid != null); + + object content = null; + if (binding != null) + { + OwningGrid.ClipboardContentControl.DataContext = item; + var sub = OwningGrid.ClipboardContentControl.Bind(ContentControl.ContentProperty, binding); + content = OwningGrid.ClipboardContentControl.GetValue(ContentControl.ContentProperty); + sub.Dispose(); + } + return content; + } + + public IControl GetCellContent(DataGridRow dataGridRow) + { + Contract.Requires(dataGridRow != null); + if (OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(GetType()); + } + if (dataGridRow.OwningGrid == OwningGrid) + { + DataGridCell dataGridCell = dataGridRow.Cells[Index]; + if (dataGridCell != null) + { + return dataGridCell.Content as IControl; + } + } + return null; + } + + public IControl GetCellContent(object dataItem) + { + Contract.Requires(dataItem != null); + if (OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(GetType()); + } + DataGridRow dataGridRow = OwningGrid.GetRowFromItem(dataItem); + if (dataGridRow == null) + { + return null; + } + return GetCellContent(dataGridRow); + } + + /// + /// Returns the column which contains the given element + /// + /// element contained in a column + /// Column that contains the element, or null if not found + /// + public static DataGridColumn GetColumnContainingElement(IControl element) + { + // Walk up the tree to find the DataGridCell or DataGridColumnHeader that contains the element + IVisual parent = element; + while (parent != null) + { + if (parent is DataGridCell cell) + { + return cell.OwningColumn; + } + if (parent is DataGridColumnHeader columnHeader) + { + return columnHeader.OwningColumn; + } + parent = parent.GetVisualParent(); + } + return null; + } + + /// + /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// The previous, unedited value in the cell being edited. + /// + protected virtual void CancelCellEdit(IControl editingElement, object uneditedValue) + { } + + /// + /// When overridden in a derived class, gets an editing element that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new editing element that is bound to the column's property value. + /// + protected abstract IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding); + + /// + /// When overridden in a derived class, gets a read-only element that is bound to the column's + /// property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new, read-only element that is bound to the column's property value. + /// + protected abstract IControl GenerateElement(DataGridCell cell, object dataItem); + + /// + /// Called by a specific column type when one of its properties changed, + /// and its current cells need to be updated. + /// + /// Indicates which property changed and caused this call + protected void NotifyPropertyChanged(string propertyName) + { + OwningGrid?.RefreshColumnElements(this, propertyName); + } + + /// + /// When overridden in a derived class, called when a cell in the column enters editing mode. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// Information about the user gesture that is causing a cell to enter editing mode. + /// + /// + /// The unedited value. + /// + protected abstract object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs); + + /// + /// Called by the DataGrid control when a column asked for its + /// elements to be refreshed, typically because one of its properties changed. + /// + /// Indicates the element that needs to be refreshed + /// Indicates which property changed and caused this call + protected internal virtual void RefreshCellContent(IControl element, string propertyName) + { } + + internal void CancelCellEditInternal(IControl editingElement, object uneditedValue) + { + CancelCellEdit(editingElement, uneditedValue); + } + + /// + /// Coerces a DataGridLength to a valid value. If any value components are double.NaN, this method + /// coerces them to a proper initial value. For star columns, the desired width is calculated based + /// on the rest of the star columns. For pixel widths, the desired value is based on the pixel value. + /// For auto widths, the desired value is initialized as the column's minimum width. + /// + /// The DataGridLength to coerce. + /// The resultant (coerced) DataGridLength. + internal DataGridLength CoerceWidth(DataGridLength width) + { + double desiredValue = width.DesiredValue; + if (double.IsNaN(desiredValue)) + { + if (width.IsStar && OwningGrid != null && OwningGrid.ColumnsInternal != null) + { + double totalStarValues = 0; + double totalStarDesiredValues = 0; + double totalNonStarDisplayWidths = 0; + foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && c != this && !double.IsNaN(c.Width.DesiredValue))) + { + if (column.Width.IsStar) + { + totalStarValues += column.Width.Value; + totalStarDesiredValues += column.Width.DesiredValue; + } + else + { + totalNonStarDisplayWidths += column.ActualWidth; + } + } + if (totalStarValues == 0) + { + // Compute the new star column's desired value based on the available space if there are no other visible star columns + desiredValue = Math.Max(ActualMinWidth, OwningGrid.CellsWidth - totalNonStarDisplayWidths); + } + else + { + // Otherwise, compute its desired value based on those of other visible star columns + desiredValue = totalStarDesiredValues * width.Value / totalStarValues; + } + } + else if (width.IsAbsolute) + { + desiredValue = width.Value; + } + else + { + desiredValue = ActualMinWidth; + } + } + + double displayValue = width.DisplayValue; + if (double.IsNaN(displayValue)) + { + displayValue = desiredValue; + } + displayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue)); + + return new DataGridLength(width.Value, width.UnitType, desiredValue, displayValue); + } + + /// + /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to + /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal + /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some + /// pixel gaps and/or overlaps between columns. + /// + /// + internal void ComputeLayoutRoundedWidth(double leftEdge) + { + if (OwningGrid != null && OwningGrid.UseLayoutRounding) + { + double roundedLeftEdge = Math.Floor(leftEdge + 0.5); + double roundedRightEdge = Math.Floor(leftEdge + ActualWidth + 0.5); + LayoutRoundedWidth = roundedRightEdge - roundedLeftEdge; + } + else + { + LayoutRoundedWidth = ActualWidth; + } + } + + //TODO Styles + internal virtual DataGridColumnHeader CreateHeader() + { + var result = new DataGridColumnHeader + { + OwningColumn = this, + Content = _header + }; + //result.EnsureStyle(null); + + return result; + } + + /// + /// Ensures that this column's width has been coerced to a valid value. + /// + internal void EnsureWidth() + { + SetWidthInternalNoCallback(CoerceWidth(Width)); + } + + internal IControl GenerateElementInternal(DataGridCell cell, object dataItem) + { + return GenerateElement(cell, dataItem); + } + + internal object PrepareCellForEditInternal(IControl editingElement, RoutedEventArgs editingEventArgs) + { + var result = PrepareCellForEdit(editingElement, editingEventArgs); + editingElement.Focus(); + + return result; + } + + /// + /// Attempts to resize the column's width to the desired DisplayValue, but limits the final size + /// to the column's minimum and maximum values. If star sizing is being used, then the column + /// can only decrease in size by the amount that the columns after it can increase in size. + /// Likewise, the column can only increase in size if other columns can spare the width. + /// + /// The new Value. + /// The new UnitType. + /// The new DesiredValue. + /// The new DisplayValue. + /// Whether or not this resize was initiated by a user action. + internal void Resize(double value, DataGridLengthUnitType unitType, double desiredValue, double displayValue, bool userInitiated) + { + double newValue = value; + double newDesiredValue = desiredValue; + double newDisplayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue)); + DataGridLengthUnitType newUnitType = unitType; + + int starColumnsCount = 0; + double totalDisplayWidth = 0; + foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + column.EnsureWidth(); + totalDisplayWidth += column.ActualWidth; + starColumnsCount += (column != this && column.Width.IsStar) ? 1 : 0; + } + bool hasInfiniteAvailableWidth = !OwningGrid.RowsPresenterAvailableSize.HasValue || double.IsPositiveInfinity(OwningGrid.RowsPresenterAvailableSize.Value.Width); + + // If we're using star sizing, we can only resize the column as much as the columns to the + // right will allow (i.e. until they hit their max or min widths). + if (!hasInfiniteAvailableWidth && (starColumnsCount > 0 || (unitType == DataGridLengthUnitType.Star && Width.IsStar && userInitiated))) + { + double limitedDisplayValue = Width.DisplayValue; + double availableIncrease = Math.Max(0, OwningGrid.CellsWidth - totalDisplayWidth); + double desiredChange = newDisplayValue - Width.DisplayValue; + if (desiredChange > availableIncrease) + { + // The desired change is greater than the amount of available space, + // so we need to decrease the widths of columns to the right to make room. + desiredChange -= availableIncrease; + double actualChange = desiredChange + OwningGrid.DecreaseColumnWidths(DisplayIndex + 1, -desiredChange, userInitiated); + limitedDisplayValue += availableIncrease + actualChange; + } + else if (desiredChange > 0) + { + // The desired change is positive but less than the amount of available space, + // so there's no need to decrease the widths of columns to the right. + limitedDisplayValue += desiredChange; + } + else + { + // The desired change is negative, so we need to increase the widths of columns to the right. + limitedDisplayValue += desiredChange + OwningGrid.IncreaseColumnWidths(DisplayIndex + 1, -desiredChange, userInitiated); + } + if (ActualCanUserResize || (Width.IsStar && !userInitiated)) + { + newDisplayValue = limitedDisplayValue; + } + } + + if (userInitiated) + { + newDesiredValue = newDisplayValue; + if (!Width.IsStar) + { + InheritsWidth = false; + newValue = newDisplayValue; + newUnitType = DataGridLengthUnitType.Pixel; + } + else if (starColumnsCount > 0 && !hasInfiniteAvailableWidth) + { + // Recalculate star weight of this column based on the new desired value + InheritsWidth = false; + newValue = (Width.Value * newDisplayValue) / ActualWidth; + } + } + + DataGridLength oldWidth = Width; + SetWidthInternalNoCallback(new DataGridLength(Math.Min(double.MaxValue, newValue), newUnitType, newDesiredValue, newDisplayValue)); + if (Width != oldWidth) + { + OwningGrid.OnColumnWidthChanged(this); + } + } + + /// + /// Sets the column's Width to a new DataGridLength with a different DesiredValue. + /// + /// The new DesiredValue. + internal void SetWidthDesiredValue(double desiredValue) + { + SetWidthInternalNoCallback(new DataGridLength(Width.Value, Width.UnitType, desiredValue, Width.DisplayValue)); + } + + /// + /// Sets the column's Width to a new DataGridLength with a different DisplayValue. + /// + /// The new DisplayValue. + internal void SetWidthDisplayValue(double displayValue) + { + SetWidthInternalNoCallback(new DataGridLength(Width.Value, Width.UnitType, Width.DesiredValue, displayValue)); + } + + /// + /// Set the column's Width without breaking inheritance. + /// + /// The new Width. + internal void SetWidthInternal(DataGridLength width) + { + bool originalValue = _settingWidthInternally; + _settingWidthInternally = true; + try + { + Width = width; + } + finally + { + _settingWidthInternally = originalValue; + } + } + + /// + /// Sets the column's Width directly, without any callback effects. + /// + /// The new Width. + internal void SetWidthInternalNoCallback(DataGridLength width) + { + _width = width; + } + + /// + /// Set the column's star value. Whenever the star value changes, width inheritance is broken. + /// + /// The new star value. + internal void SetWidthStarValue(double value) + { + InheritsWidth = false; + SetWidthInternalNoCallback(new DataGridLength(value, Width.UnitType, Width.DesiredValue, Width.DisplayValue)); + } + + //TODO Binding + internal IControl GenerateEditingElementInternal(DataGridCell cell, object dataItem) + { + if (_editingElement == null) + { + _editingElement = GenerateEditingElement(cell, dataItem, out _editBinding); + } + + return _editingElement; + } + + /// + /// Clears the cached editing element. + /// + //TODO Binding + internal void RemoveEditingElement() + { + _editingElement = null; + } + + /// + /// Holds the name of the member to use for sorting, if not using the default. + /// + public string SortMemberPath + { + get; + set; + } + + /// + /// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description + /// describes an unchangeable sort that exists on the data. + /// + internal DataGridSortDescription GetSortDescription() + { + if (OwningGrid != null + && OwningGrid.DataConnection != null + && OwningGrid.DataConnection.SortDescriptions != null) + { + string propertyName = GetSortPropertyName(); + + return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName); + } + + return null; + } + + internal string GetSortPropertyName() + { + string result = SortMemberPath; + + if (String.IsNullOrEmpty(result)) + { + + if(this is DataGridBoundColumn boundColumn && + boundColumn.Binding != null && + boundColumn.Binding is Binding binding && + binding.Path != null) + { + result = binding.Path; + } + } + + return result; + } + + } + +} \ No newline at end of file diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs new file mode 100644 index 0000000000..922b1d9c08 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs @@ -0,0 +1,586 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; + +namespace Avalonia.Controls +{ + internal class DataGridColumnCollection : ObservableCollection + { + private DataGrid _owningGrid; + + public DataGridColumnCollection(DataGrid owningGrid) + { + _owningGrid = owningGrid; + ItemsInternal = new List(); + FillerColumn = new DataGridFillerColumn(owningGrid); + RowGroupSpacerColumn = new DataGridFillerColumn(owningGrid); + DisplayIndexMap = new List(); + } + + internal int AutogeneratedColumnCount + { + get; + set; + } + + internal List DisplayIndexMap + { + get; + set; + } + + internal DataGridFillerColumn FillerColumn + { + get; + private set; + } + + internal DataGridColumn FirstColumn + { + get + { + return GetFirstColumn(null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleNonFillerColumn + { + get + { + DataGridColumn dataGridColumn = FirstVisibleColumn; + if (dataGridColumn == RowGroupSpacerColumn) + { + dataGridColumn = GetNextVisibleColumn(dataGridColumn); + } + return dataGridColumn; + } + } + + internal DataGridColumn FirstVisibleWritableColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleScrollingColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal List ItemsInternal + { + get; + private set; + } + + internal DataGridColumn LastVisibleColumn + { + get + { + return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn LastVisibleScrollingColumn + { + get + { + return GetLastColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn LastVisibleWritableColumn + { + get + { + return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + } + + internal DataGridFillerColumn RowGroupSpacerColumn + { + get; + private set; + } + + internal int VisibleColumnCount + { + get + { + int visibleColumnCount = 0; + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) + { + if (ItemsInternal[columnIndex].IsVisible) + { + visibleColumnCount++; + } + } + return visibleColumnCount; + } + } + + internal double VisibleEdgedColumnsWidth + { + get; + private set; + } + + /// + /// The number of star columns that are currently visible. + /// NOTE: Requires that EnsureVisibleEdgedColumnsWidth has been called. + /// + internal int VisibleStarColumnCount + { + get; + private set; + } + + protected override void ClearItems() + { + try + { + _owningGrid.NoCurrentCellChangeCount++; + if (ItemsInternal.Count > 0) + { + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + _owningGrid.OnClearingColumns(); + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) + { + // Detach the column... + ItemsInternal[columnIndex].OwningGrid = null; + } + ItemsInternal.Clear(); + DisplayIndexMap.Clear(); + AutogeneratedColumnCount = 0; + _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/); + base.ClearItems(); + VisibleEdgedColumnsWidth = 0; + _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/); + } + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + + protected override void InsertItem(int columnIndex, DataGridColumn dataGridColumn) + { + try + { + _owningGrid.NoCurrentCellChangeCount++; + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + if (dataGridColumn == null) + { + throw new ArgumentNullException("dataGridColumn"); + } + + int columnIndexWithFiller = columnIndex; + if (dataGridColumn != RowGroupSpacerColumn && RowGroupSpacerColumn.IsRepresented) + { + columnIndexWithFiller++; + } + + // get the new current cell coordinates + DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnInsertingColumn(columnIndex, dataGridColumn); + + // insert the column into our internal list + ItemsInternal.Insert(columnIndexWithFiller, dataGridColumn); + dataGridColumn.Index = columnIndexWithFiller; + dataGridColumn.OwningGrid = _owningGrid; + dataGridColumn.RemoveEditingElement(); + if (dataGridColumn.IsVisible) + { + VisibleEdgedColumnsWidth += dataGridColumn.ActualWidth; + } + + // continue with the base insert + _owningGrid.OnInsertedColumn_PreNotification(dataGridColumn); + _owningGrid.OnColumnCollectionChanged_PreNotification(true /*columnsGrew*/); + + if (dataGridColumn != RowGroupSpacerColumn) + { + base.InsertItem(columnIndex, dataGridColumn); + } + _owningGrid.OnInsertedColumn_PostNotification(newCurrentCellCoordinates, dataGridColumn.DisplayIndex); + _owningGrid.OnColumnCollectionChanged_PostNotification(true /*columnsGrew*/); + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + + protected override void RemoveItem(int columnIndex) + { + RemoveItemPrivate(columnIndex, false /*isSpacer*/); + } + + protected override void SetItem(int columnIndex, DataGridColumn dataGridColumn) + { + throw new NotSupportedException(); + } + + internal bool DisplayInOrder(int columnIndex1, int columnIndex2) + { + int displayIndex1 = ItemsInternal[columnIndex1].DisplayIndexWithFiller; + int displayIndex2 = ItemsInternal[columnIndex2].DisplayIndexWithFiller; + return displayIndex1 < displayIndex2; + } + + internal bool EnsureRowGrouping(bool rowGrouping) + { + // The insert below could cause the first column to be added. That causes a refresh + // which re-enters method so instead of checking RowGroupSpacerColumn.IsRepresented, + // we need to check to see if it's actually in our collection instead. + bool spacerRepresented = (ItemsInternal.Count > 0) && (ItemsInternal[0] == RowGroupSpacerColumn); + if (rowGrouping && !spacerRepresented) + { + Insert(0, RowGroupSpacerColumn); + RowGroupSpacerColumn.IsRepresented = true; + return true; + } + else if (!rowGrouping && spacerRepresented) + { + // We need to set IsRepresented to false before removing the RowGroupSpacerColumn + // otherwise, we'll remove the column after it + RowGroupSpacerColumn.IsRepresented = false; + RemoveItemPrivate(0, true /*isSpacer*/); + return true; + } + return false; + } + + /// + /// In addition to ensuring that column widths are valid, method updates the values of + /// VisibleEdgedColumnsWidth and VisibleStarColumnCount. + /// + internal void EnsureVisibleEdgedColumnsWidth() + { + VisibleStarColumnCount = 0; + VisibleEdgedColumnsWidth = 0; + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) + { + if (ItemsInternal[columnIndex].IsVisible) + { + ItemsInternal[columnIndex].EnsureWidth(); + if (ItemsInternal[columnIndex].Width.IsStar) + { + VisibleStarColumnCount++; + } + VisibleEdgedColumnsWidth += ItemsInternal[columnIndex].ActualWidth; + } + } + } + + internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex) + { + if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count) + { + return null; + } + int columnIndex = DisplayIndexMap[displayIndex]; + return ItemsInternal[columnIndex]; + } + + internal int GetColumnCount(bool isVisible, bool isFrozen, int fromColumnIndex, int toColumnIndex) + { + int columnCount = 0; + DataGridColumn dataGridColumn = ItemsInternal[fromColumnIndex]; + + while (dataGridColumn != ItemsInternal[toColumnIndex]) + { + dataGridColumn = GetNextColumn(dataGridColumn, isVisible, isFrozen, null /*isReadOnly*/); + columnCount++; + } + return columnCount; + } + + internal IEnumerable GetDisplayedColumns() + { + foreach (int columnIndex in DisplayIndexMap) + { + yield return ItemsInternal[columnIndex]; + } + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// + /// Criteria for inclusion. + /// Columns that meet the criteria, in ascending DisplayIndex order. + internal IEnumerable GetDisplayedColumns(Predicate filter) + { + Debug.Assert(filter != null); + Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count); + foreach (int columnIndex in DisplayIndexMap) + { + DataGridColumn column = ItemsInternal[columnIndex]; + if (filter(column)) + { + yield return column; + } + } + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// The columns are returned in the order specified by the reverse flag. + /// + /// Whether or not to return the columns in descending DisplayIndex order. + /// Criteria for inclusion. + /// Columns that meet the criteria, in the order specified by the reverse flag. + internal IEnumerable GetDisplayedColumns(bool reverse, Predicate filter) + { + return reverse ? GetDisplayedColumnsReverse(filter) : GetDisplayedColumns(filter); + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// The columns are returned in descending DisplayIndex order. + /// + /// Criteria for inclusion. + /// Columns that meet the criteria, in descending DisplayIndex order. + internal IEnumerable GetDisplayedColumnsReverse(Predicate filter) + { + for (int displayIndex = DisplayIndexMap.Count - 1; displayIndex >= 0; displayIndex--) + { + DataGridColumn column = ItemsInternal[DisplayIndexMap[displayIndex]]; + if (filter(column)) + { + yield return column; + } + } + } + + internal DataGridColumn GetFirstColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count); + int index = 0; + while (index < DisplayIndexMap.Count) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + index++; + } + return null; + } + + internal DataGridColumn GetLastColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count); + int index = DisplayIndexMap.Count - 1; + while (index >= 0) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + index--; + } + return null; + } + + internal DataGridColumn GetNextColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextColumn(DataGridColumn dataGridColumnStart, + bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + Debug.Assert(dataGridColumnStart != null); + Debug.Assert(ItemsInternal.Contains(dataGridColumnStart)); + Debug.Assert(ItemsInternal.Count == DisplayIndexMap.Count); + + int index = dataGridColumnStart.DisplayIndexWithFiller + 1; + while (index < DisplayIndexMap.Count) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + + if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + index++; + } + return null; + } + + internal DataGridColumn GetNextVisibleColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextVisibleFrozenColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, true /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextVisibleWritableColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + + internal DataGridColumn GetPreviousColumn(DataGridColumn dataGridColumnStart, + bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + int index = dataGridColumnStart.DisplayIndexWithFiller - 1; + while (index >= 0) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || (dataGridColumn.IsVisible) == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + index--; + } + return null; + } + + internal DataGridColumn GetPreviousVisibleNonFillerColumn(DataGridColumn dataGridColumnStart) + { + DataGridColumn column = GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + return (column is DataGridFillerColumn) ? null : column; + } + + internal DataGridColumn GetPreviousVisibleScrollingColumn(DataGridColumn dataGridColumnStart) + { + return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetPreviousVisibleWritableColumn(DataGridColumn dataGridColumnStart) + { + return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + + internal int GetVisibleColumnCount(int fromColumnIndex, int toColumnIndex) + { + int columnCount = 0; + DataGridColumn dataGridColumn = ItemsInternal[fromColumnIndex]; + + while (dataGridColumn != ItemsInternal[toColumnIndex]) + { + dataGridColumn = GetNextVisibleColumn(dataGridColumn); + columnCount++; + } + return columnCount; + } + + internal IEnumerable GetVisibleColumns() + { + Predicate filter = column => column.IsVisible; + return GetDisplayedColumns(filter); + } + + internal IEnumerable GetVisibleFrozenColumns() + { + Predicate filter = column => column.IsVisible && column.IsFrozen; + return GetDisplayedColumns(filter); + } + + internal double GetVisibleFrozenEdgedColumnsWidth() + { + double visibleFrozenColumnsWidth = 0; + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) + { + if (ItemsInternal[columnIndex].IsVisible && ItemsInternal[columnIndex].IsFrozen) + { + visibleFrozenColumnsWidth += ItemsInternal[columnIndex].ActualWidth; + } + } + return visibleFrozenColumnsWidth; + } + + internal IEnumerable GetVisibleScrollingColumns() + { + Predicate filter = column => column.IsVisible && !column.IsFrozen; + return GetDisplayedColumns(filter); + } + + private void RemoveItemPrivate(int columnIndex, bool isSpacer) + { + try + { + _owningGrid.NoCurrentCellChangeCount++; + + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + int columnIndexWithFiller = columnIndex; + if (!isSpacer && RowGroupSpacerColumn.IsRepresented) + { + columnIndexWithFiller++; + } + + DataGridColumn dataGridColumn = ItemsInternal[columnIndexWithFiller]; + DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnRemovingColumn(dataGridColumn); + ItemsInternal.RemoveAt(columnIndexWithFiller); + if (dataGridColumn.IsVisible) + { + VisibleEdgedColumnsWidth -= dataGridColumn.ActualWidth; + } + dataGridColumn.OwningGrid = null; + dataGridColumn.RemoveEditingElement(); + + // continue with the base remove + _owningGrid.OnRemovedColumn_PreNotification(dataGridColumn); + _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/); + if (!isSpacer) + { + base.RemoveItem(columnIndex); + } + _owningGrid.OnRemovedColumn_PostNotification(newCurrentCellCoordinates); + _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/); + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + + } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs new file mode 100644 index 0000000000..6cb0807e29 --- /dev/null +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -0,0 +1,806 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Collections; +using Avalonia.Media; +using System.ComponentModel; +using System.Diagnostics; +using Avalonia.Utilities; +using System; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls +{ + /// + /// Represents an individual
+ /// 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. + ///