diff --git a/samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs b/samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs index 2be1344c..43a69e33 100644 --- a/samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs +++ b/samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs @@ -23,16 +23,20 @@ public CountriesPageViewModel() { new TextColumn("Country", x => x.Name, (r, v) => r.Name = v, new GridLength(6, GridUnitType.Star), new() { - IsTextSearchEnabled = true, + IsTextSearchEnabled = true }), - new TemplateColumn("Region", "RegionCell", "RegionEditCell"), - new TextColumn("Population", x => x.Population, new GridLength(3, GridUnitType.Star)), - new TextColumn("Area", x => x.Area, new GridLength(3, GridUnitType.Star)), - new TextColumn("GDP", x => x.GDP, new GridLength(3, GridUnitType.Star), new() + new TextColumn("Region", x => x.Region, new GridLength(4, GridUnitType.Star)), + new GrouppedColumnn("Stats") { - TextAlignment = Avalonia.Media.TextAlignment.Right, - MaxWidth = new GridLength(150) - }), + new TextColumn("Population", x => x.Population, new GridLength(3, GridUnitType.Star)), + new TextColumn("Area", x => x.Area, new GridLength(3, GridUnitType.Star)), + new TextColumn("GDP", x => x.GDP, new GridLength(3, GridUnitType.Star), new() + { + TextAlignment = Avalonia.Media.TextAlignment.Right, + MaxWidth = new GridLength(150), + }) + } + , } }; Source.RowSelection!.SingleSelect = false; diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/NotifyingListBase.cs b/src/Avalonia.Controls.TreeDataGrid/Models/NotifyingListBase.cs index 82bd3685..0a018707 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/NotifyingListBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/NotifyingListBase.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.Runtime.CompilerServices; using Avalonia.Controls.Models.TreeDataGrid; namespace Avalonia.Controls.Models @@ -249,5 +250,30 @@ private enum BatchUpdateType Remove, Reset, } + + protected bool RaiseAndSetIfChanged( + ref TField field, + TField value, + [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + RaisePropertyChanged(propertyName); + return true; + } + + return false; + } + + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected void RaisePropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs index 5b21ff2c..d30a0d41 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs @@ -30,7 +30,7 @@ public ColumnBase( object? header, GridLength? width, ColumnOptions options) - { + { _header = header; Options = options; SetWidth(width ?? GridLength.Auto); diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GrouppedColumn.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GrouppedColumn.cs new file mode 100644 index 00000000..e4c9e35d --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GrouppedColumn.cs @@ -0,0 +1,193 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Models.TreeDataGrid +{ + public class GrouppedColumnn : ColumnList, IGruppedColumn, IColumn, IUpdateColumnLayout + where TModel : class + { + private static readonly ColumnOptions _defaultColumnOptions = new(); + private double _actualWidth = double.NaN; + private GridLength _width; + private double _autoWidth = double.NaN; + private double _starWidth = double.NaN; + private bool _starWidthWasConstrained; + private object? _header; + private ListSortDirection? _sortDirection; + private readonly Comparison _comparison; + + public GrouppedColumnn( + object? header, + GridLength? width = default, + ColumnOptions? options = default) + { + _header = header; + Options = options ?? new(); + _comparison = GrouppedColumnnComparison; + SetWidth(width ?? GridLength.Auto); + } + + /// + /// Gets the actual width of the column after measurement. + /// + public double ActualWidth + { + get => _actualWidth; + private set => RaiseAndSetIfChanged(ref _actualWidth, value); + } + + /// + /// Gets the width of the column. + /// + /// + /// To set the column width use . + /// + public GridLength Width + { + get => _width; + private set => RaiseAndSetIfChanged(ref _width, value); + } + + /// + /// Gets or sets the column header. + /// + public object? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + /// + /// Gets the column options. + /// + public ColumnOptions Options { get; } + + /// + /// Gets or sets the sort direction indicator that will be displayed on the column. + /// + /// + /// Note that changing this property does not change the sorting of the data, it is only + /// used to display a sort direction indicator. To sort data according to a column use + /// . + /// + public ListSortDirection? SortDirection + { + get => _sortDirection; + set => RaiseAndSetIfChanged(ref _sortDirection, value); + } + + /// + /// Gets or sets a user-defined object attached to the column. + /// + public object? Tag { get; set; } + + bool? IColumn.CanUserResize => Options.CanUserResizeColumn; + double IUpdateColumnLayout.MinActualWidth => CoerceActualWidth(0); + double IUpdateColumnLayout.MaxActualWidth => CoerceActualWidth(double.PositiveInfinity); + bool IUpdateColumnLayout.StarWidthWasConstrained => _starWidthWasConstrained; + + /// + /// Creates a cell for this column on the specified row. + /// + /// The row. + /// The cell. + public ICell CreateCell(IRow row) => + new GruppedCell(row, this); + + public Comparison? GetComparison(ListSortDirection direction) => + _comparison; + + double IUpdateColumnLayout.CellMeasured(double width, int rowIndex) + { + double autoWidth = 0.0; + for (int i = 0; i < this.Count; i++) + { + if (this[i] is IUpdateColumnLayout columnLayout) + { + var w = columnLayout.CellMeasured(width, rowIndex); + autoWidth += w; + } + } + _autoWidth = Math.Max(NonNaN(_autoWidth), CoerceActualWidth(width)); + return Width.GridUnitType == GridUnitType.Auto || double.IsNaN(ActualWidth) ? + _autoWidth : ActualWidth; + } + + void IUpdateColumnLayout.CalculateStarWidth(double availableWidth, double totalStars) + { + if (!Width.IsStar) + throw new InvalidOperationException("Attempt to calculate star width on a non-star column."); + + var width = (availableWidth / totalStars) * Width.Value; + _starWidth = CoerceActualWidth(width); + _starWidthWasConstrained = !MathUtilities.AreClose(_starWidth, width); + } + + bool IUpdateColumnLayout.CommitActualWidth() + { + var width = Width.GridUnitType switch + { + GridUnitType.Auto => _autoWidth, + GridUnitType.Pixel => CoerceActualWidth(Width.Value), + GridUnitType.Star => _starWidth, + _ => throw new NotSupportedException(), + }; + + var oldWidth = ActualWidth; + ActualWidth = width; + _starWidthWasConstrained = false; + return !MathUtilities.AreClose(oldWidth, ActualWidth); + } + + void IUpdateColumnLayout.SetWidth(GridLength width) => SetWidth(width); + + private double CoerceActualWidth(double width) + { + width = Options.MinWidth.GridUnitType switch + { + GridUnitType.Auto => Math.Max(width, _autoWidth), + GridUnitType.Pixel => Math.Max(width, Options.MinWidth.Value), + GridUnitType.Star => throw new NotImplementedException(), + _ => width + }; + + return Options.MaxWidth?.GridUnitType switch + { + GridUnitType.Auto => Math.Min(width, _autoWidth), + GridUnitType.Pixel => Math.Min(width, Options.MaxWidth.Value.Value), + GridUnitType.Star => throw new NotImplementedException(), + _ => width + }; + } + + private void SetWidth(GridLength width) + { + _width = width; + + if (width.IsAbsolute) + ActualWidth = width.Value; + } + + private static double NonNaN(double v) => double.IsNaN(v) ? 0 : v; + + private int GrouppedColumnnComparison(TModel? x, TModel? y) + { + var result = 0; + var direction = SortDirection ?? ListSortDirection.Ascending; + foreach (var item in this.OfType>()) + { + if (item.GetComparison(direction) is { } comparer) + { + result = comparer(x, y); + if (result != 0) + { + break; + } + } + } + return result; + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GruppedCell.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GruppedCell.cs new file mode 100644 index 00000000..4e495e49 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/GruppedCell.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Avalonia.Controls.Models.TreeDataGrid +{ + public interface IGruppedCell : ICell + { + IColumns Columns { get; } + } + public class GruppedCell : IGruppedCell + { + private readonly IRow _row; + private readonly IColumns _columns; + public GruppedCell(IRow row, IColumns columns) + { + _row = row; + _columns = columns; + } + + public bool CanEdit => false; + + public object? Value => default; + + public BeginEditGestures EditGestures => BeginEditGestures.None; + + public IColumns Columns => _columns; + + internal IEnumerable CreateCells() + { + foreach (var column in _columns) + { + if (column is ColumnBase cb) + { + yield return cb.CreateCell(_row); + } + } + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IGruppedColumn.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IGruppedColumn.cs new file mode 100644 index 00000000..a7c4df49 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/IGruppedColumn.cs @@ -0,0 +1,5 @@ +namespace Avalonia.Controls.Models.TreeDataGrid; + +public interface IGruppedColumn : IColumn, IColumns +{ +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs index dccb2b66..d57431bc 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs @@ -18,10 +18,15 @@ public abstract class TreeDataGridCell : TemplatedControl, ITreeDataGridCell nameof(IsSelected), o => o.IsSelected); + private static readonly DirectProperty ModelProperty = + AvaloniaProperty.RegisterDirect(nameof(Model) + , o => o.Model); + private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN); private bool _isSelected; private TreeDataGrid? _treeDataGrid; private Point _pressedPoint = s_invalidPoint; + private ICell? _model; static TreeDataGridCell() { @@ -32,7 +37,7 @@ static TreeDataGridCell() public int ColumnIndex { get; private set; } = -1; public int RowIndex { get; private set; } = -1; public bool IsEditing { get; private set; } - public ICell? Model { get; private set; } + public ICell? Model { get => _model; private set => SetAndRaise(ModelProperty, ref _model , value); } public bool IsSelected { diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs index 7448138a..e0fcd898 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs @@ -1,10 +1,8 @@ using System; -using System.Xml.Linq; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridColumnHeader.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridColumnHeader.cs index 148c8f5e..5527aaa7 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridColumnHeader.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridColumnHeader.cs @@ -59,7 +59,7 @@ public void Realize(IColumns columns, int columnIndex) _columns = columns; _model = columns[columnIndex]; ColumnIndex = columnIndex; - UpdatePropertiesFromModel(); + UpdatePropertiesFromModel(_model); if (_model is INotifyPropertyChanged newInpc) newInpc.PropertyChanged += OnModelPropertyChanged; @@ -73,7 +73,7 @@ public void Unrealize() _columns = null; _model = null; ColumnIndex = -1; - UpdatePropertiesFromModel(); + UpdatePropertiesFromModel(_model); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -122,7 +122,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (newModel is INotifyPropertyChanged newInpc) newInpc.PropertyChanged += OnModelPropertyChanged; - UpdatePropertiesFromModel(); + UpdatePropertiesFromModel(newModel); } else if (change.Property == ParentProperty) { @@ -131,7 +131,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang _owner = change.GetNewValue()?.TemplatedParent as TreeDataGrid; if (_owner is not null) _owner.PropertyChanged += OnOwnerPropertyChanged; - UpdatePropertiesFromModel(); + UpdatePropertiesFromModel(_model); } base.OnPropertyChanged(change); @@ -142,7 +142,7 @@ private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) if (e.PropertyName == nameof(IColumn.CanUserResize) || e.PropertyName == nameof(IColumn.Header) || e.PropertyName == nameof(IColumn.SortDirection)) - UpdatePropertiesFromModel(); + UpdatePropertiesFromModel(_model); } private void OnOwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) @@ -167,7 +167,7 @@ private void ResizerDragDelta(object? sender, VectorEventArgs e) _columns.SetColumnWidth(ColumnIndex, width); } - private void UpdatePropertiesFromModel() + internal protected virtual void UpdatePropertiesFromModel(IColumn? model) { CanUserResize = _model?.CanUserResize ?? _owner?.CanUserResizeColumns ?? false; Header = _model?.Header; diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridElementFactory.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridElementFactory.cs index 2129e056..7b128730 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridElementFactory.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridElementFactory.cs @@ -66,6 +66,8 @@ protected virtual Control CreateElement(object? data) { CheckBoxCell => new TreeDataGridCheckBoxCell(), TemplateCell => new TreeDataGridTemplateCell(), + IGruppedColumn => new TreeDataGridGruppedColumnHeader(), + IGruppedCell => new TreeDataGridGruppedTemplateCell(), IExpanderCell => new TreeDataGridExpanderCell(), ICell => new TreeDataGridTextCell(), IColumn => new TreeDataGridColumnHeader(), @@ -81,6 +83,7 @@ protected virtual string GetDataRecycleKey(object? data) CheckBoxCell => typeof(TreeDataGridCheckBoxCell).FullName!, TemplateCell => typeof(TreeDataGridTemplateCell).FullName!, IExpanderCell => typeof(TreeDataGridExpanderCell).FullName!, + IGruppedCell => typeof(TreeDataGridGruppedTemplateCell).FullName!, ICell => typeof(TreeDataGridTextCell).FullName!, IColumn => typeof(TreeDataGridColumnHeader).FullName!, IRow => typeof(TreeDataGridRow).FullName!, diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs index bffafe63..726c2812 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs @@ -2,7 +2,6 @@ using System.ComponentModel; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; -using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedColumnHeader.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedColumnHeader.cs new file mode 100644 index 00000000..42197ff6 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedColumnHeader.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls.Models.TreeDataGrid; + +namespace Avalonia.Controls.Primitives +{ + public class TreeDataGridGruppedColumnHeader : TreeDataGridColumnHeader + { + private IColumns? _columns; + + public static readonly DirectProperty ColumnsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Columns), + o => o.Columns); + + public IColumns? Columns + { + get => _columns; + private set => SetAndRaise(ColumnsProperty, ref _columns, value); + } + + protected internal override void UpdatePropertiesFromModel(IColumn? model) + { + base.UpdatePropertiesFromModel(model); + if (model is IGruppedColumn columns) + { + _columns = columns; + } + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedTemplateCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedTemplateCell.cs new file mode 100644 index 00000000..a76fc395 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridGruppedTemplateCell.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls.Primitives +{ + public class TreeDataGridGruppedTemplateCell : TreeDataGridCell + { + private IColumns? _columns; + + public static readonly DirectProperty ColumnsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Columns), + o => o.Columns); + + public IColumns? Columns + { + get => _columns; + private set => SetAndRaise(ColumnsProperty, ref _columns, value); + } + + public override void Realize(TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) + { + this.InvalidateMeasure(); + base.Realize(factory, selection, model, columnIndex, rowIndex); + } + + public override void Unrealize() + { + DataContext = null; + base.Unrealize(); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property.Name == nameof(Model)) + { + if (change.NewValue is IGruppedCell grupped) + { + Columns = grupped.Columns; + } + } + } + + + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs index 90a74a61..ca40d8e4 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; -using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Media; namespace Avalonia.Controls.Primitives @@ -26,7 +24,7 @@ public class TreeDataGridTextCell : TreeDataGridCell (o, v) => o.Value = v); public static readonly DirectProperty TextAlignmentProperty = - AvaloniaProperty.RegisterDirect < TreeDataGridTextCell, TextAlignment>( + AvaloniaProperty.RegisterDirect ( nameof(TextAlignment), o => o.TextAlignment, (o,v)=> o.TextAlignment = v); @@ -64,6 +62,7 @@ public TextAlignment TextAlignment get => _textAlignment; set => SetAndRaise(TextAlignmentProperty, ref _textAlignment, value); } + public override void Realize( TreeDataGridElementFactory factory, ITreeDataGridSelectionInteraction? selection, diff --git a/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml b/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml index 7db92b09..bbbf4f61 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml +++ b/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml @@ -325,4 +325,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +