From 65866fc5261cefa2d9380c01b970c638f635fa27 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Jul 2022 10:40:54 +0200 Subject: [PATCH 01/82] Improve csv export - Wrap cell content in " [...] " in order to keep line breaks and other special chars - Replace any " with "" as " needs to be escaped in csv --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index d42468f47e..42350de4c2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5995,8 +5995,9 @@ namespace Avalonia.Controls var numberOfItem = clipboardRowContent.Count; for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) { - var cellContent = clipboardRowContent[cellIndex]; - text.Append(cellContent.Content); + var cellContent = clipboardRowContent[cellIndex].Content?.ToString(); + cellContent = cellContent?.Replace("\"", "\"\""); + text.Append($"\"{cellContent}\""); if (cellIndex < numberOfItem - 1) { text.Append('\t'); From 117631d0ef0554c752fa0837639debef21353254 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 13:41:29 +0100 Subject: [PATCH 02/82] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bbf358b8f4..ee778ed4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ csx AppPackages/ # NCrunch +.NCrunch_*/ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* From 16c1dc7a506629c487f953eb372176202d7e7d03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 15:13:52 +0100 Subject: [PATCH 03/82] Ported SelectionNodeBase from TreeDataGrid. `TreeDataGrid` has an updated version of `SelectionNodeBase` that fixes a few problems, and works as a base also for hierarchical selection models. Ported that here and tidied up a few other selection-related classes (change `is object` to `is not null`, fix nullability annotations). --- src/Avalonia.Controls/ItemsSourceView.cs | 16 + .../Selection/InternalSelectionModel.cs | 2 +- .../Selection/SelectedItems.cs | 17 +- .../Selection/SelectionModel.cs | 91 +++--- ...SelectionModelSelectionChangedEventArgs.cs | 8 +- .../Selection/SelectionNodeBase.cs | 291 ++++++++++++------ 6 files changed, 267 insertions(+), 158 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index c8fc76255c..1fa8f6a5cf 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -241,6 +241,22 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } + internal void AddListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.AddListener(incc, listener); + } + } + + internal void RemoveListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.RemoveListener(incc, listener); + } + } + /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d0715e402d..d0e6144f59 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -203,7 +203,7 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Reset) { diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index ef642b7bdc..74007805cd 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Controls.Selection { - internal class SelectedItems : IReadOnlyList + internal class SelectedItems : IReadOnlyList { private readonly SelectionModel? _owner; private readonly ItemsSourceView? _items; @@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection _items = items; } - [MaybeNull] - public T this[int index] + public T? this[int index] { -#pragma warning disable CS8766 get -#pragma warning restore CS8766 { if (index >= Count) { @@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection private ItemsSourceView? Items => _items ?? _owner?.ItemsView; private IReadOnlyList? Ranges => _ranges ?? _owner!.Ranges; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { if (_owner?.SingleSelect == true) { if (_owner.SelectedIndex >= 0) { -#pragma warning disable CS8603 yield return _owner.SelectedItem; -#pragma warning restore CS8603 } } else @@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection { for (var i = range.Begin; i <= range.End; ++i) { -#pragma warning disable CS8603 yield return items is object ? items[i] : default; -#pragma warning restore CS8603 } } } @@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection public class Untyped : IReadOnlyList { - private readonly IReadOnlyList _source; - public Untyped(IReadOnlyList source) => _source = source; + private readonly IReadOnlyList _source; + public Untyped(IReadOnlyList source) => _source = source; public object? this[int index] => _source[index]; public int Count => _source.Count; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index affe762ea7..d4c2b32974 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _untypedSelectionChanged; private IList? _initSelectedItems; + private bool _isSourceCollectionChanging; public SelectionModel() { @@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection if (RangesEnabled && _selectedIndex >= 0) { - CommitSelect(new IndexRange(_selectedIndex)); + CommitSelect(_selectedIndex, _selectedIndex); } RaisePropertyChanged(nameof(SingleSelect)); @@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection { get { - if (ItemsView is object) + if (ItemsView is not null) { return GetItemAt(_selectedIndex); } @@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection } set { - if (ItemsView is object) + if (ItemsView is not null) { SelectedIndex = ItemsView.IndexOf(value!); } else { Clear(); -#pragma warning disable CS8601 - SetInitSelectedItems(new T[] { value }); -#pragma warning restore CS8601 + SetInitSelectedItems(new T[] { value! }); } } } - public IReadOnlyList SelectedItems + public IReadOnlyList SelectedItems { get { @@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection { // If the collection is currently changing, commit the update when the // collection change finishes. - if (!IsSourceCollectionChanging) + if (!_isSourceCollectionChanging) { CommitOperation(_operation); } @@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection { if (base.Source != value) { - if (_operation is object) + if (_operation is not null) { throw new InvalidOperationException("Cannot change source while update is in progress."); } @@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection { update.Operation.IsSourceUpdate = true; - if (_initSelectedItems is object && ItemsView is object) + if (_initSelectedItems is object && ItemsView is not null) { foreach (T i in _initSelectedItems) { @@ -315,17 +314,23 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) + protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) { IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta)); } - private protected override void OnSourceReset() + protected override void OnSourceCollectionChangeStarted() + { + base.OnSourceCollectionChangeStarted(); + _isSourceCollectionChanging = true; + } + + protected override void OnSourceReset() { _selectedIndex = _anchorIndex = -1; - CommitDeselect(new IndexRange(0, int.MaxValue)); + CommitDeselect(0, int.MaxValue); - if (SourceReset is object) + if (SourceReset is not null) { SourceReset.Invoke(this, EventArgs.Empty); } @@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnSelectionChanged(IReadOnlyList deselectedItems) + protected override void OnSelectionRemoved(int index, int count, IReadOnlyList deselectedItems) { // Note: We're *not* putting this in a using scope. A collection update is still in progress // so the operation won't get committed by normal means: we have to commit it manually. @@ -347,7 +352,7 @@ namespace Avalonia.Controls.Selection update.Operation.DeselectedItems = deselectedItems; - if (_selectedIndex == -1 && LostSelection is object) + if (_selectedIndex == -1 && LostSelection is not null) { LostSelection(this, EventArgs.Empty); } @@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection CommitOperation(update.Operation, raisePropertyChanged: false); } - private protected override CollectionChangeState OnItemsAdded(int index, IList items) + protected override CollectionChangeState OnItemsAdded(int index, IList items) { var count = items.Count; var shifted = SelectedIndex >= index; @@ -420,7 +425,7 @@ namespace Avalonia.Controls.Selection }; } - private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) { if (_operation?.UpdateCount > 0) { @@ -451,6 +456,16 @@ namespace Avalonia.Controls.Selection } } + private protected void SetInitSelectedItems(IList items) + { + if (Source is object) + { + throw new InvalidOperationException("Cannot set init selected items when Source is set."); + } + + _initSelectedItems = items; + } + private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) { if (!base.IsValidCollectionChange(e)) @@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection return true; } - private protected void SetInitSelectedItems(IList items) - { - if (Source is object) - { - throw new InvalidOperationException("Cannot set init selected items when Source is set."); - } - - _initSelectedItems = items; - } - protected override void OnSourceCollectionChangeFinished() { - if (_operation is object) + _isSourceCollectionChanging = false; + + if (_operation is not null) { CommitOperation(_operation); } @@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection { index = Math.Max(index, -1); - if (ItemsView is object && index >= ItemsView.Count) + if (ItemsView is not null && index >= ItemsView.Count) { index = -1; } @@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection private IndexRange CoerceRange(int start, int end) { - var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue; + var max = ItemsView is not null ? ItemsView.Count - 1 : int.MaxValue; if (start > max || (start < 0 && end < 0)) { @@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection var oldSelectedIndex = _selectedIndex; var indexesChanged = false; - if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection) + if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection) { operation.UpdateCount++; LostSelection?.Invoke(this, EventArgs.Empty); @@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection _selectedIndex = operation.SelectedIndex; _anchorIndex = operation.AnchorIndex; - if (operation.SelectedRanges is object) + if (operation.SelectedRanges is not null) { - indexesChanged |= CommitSelect(operation.SelectedRanges) > 0; + foreach (var range in operation.SelectedRanges) + { + indexesChanged |= CommitSelect(range.Begin, range.End) > 0; + } } - if (operation.DeselectedRanges is object) + if (operation.DeselectedRanges is not null) { - indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0; + foreach (var range in operation.DeselectedRanges) + { + indexesChanged |= CommitDeselect(range.Begin, range.End) > 0; + } } - if (SelectionChanged is object || _untypedSelectionChanged is object) + if (SelectionChanged is not null || _untypedSelectionChanged is not null) { IReadOnlyList? deselected = operation.DeselectedRanges; IReadOnlyList? selected = operation.SelectedRanges; @@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection // CollectionChanged event. LostFocus may have caused another item to have been // selected, but it can't have caused a deselection (as it was called due to // selection being lost) so we're ok to discard `deselected` here. - var deselectedItems = operation.DeselectedItems ?? + var deselectedItems = (IReadOnlyList?)operation.DeselectedItems ?? SelectedItems.Create(deselected, deselectedSource); var e = new SelectionModelSelectionChangedEventArgs( SelectedIndexes.Create(deselected), SelectedIndexes.Create(selected), deselectedItems, - SelectedItems.Create(selected, ItemsView)); + SelectedItems.Create(selected, Source is not null ? ItemsView : null)); SelectionChanged?.Invoke(this, e); _untypedSelectionChanged?.Invoke(this, e); } diff --git a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs index 64c1b14253..8f6d256847 100644 --- a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs +++ b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs @@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection public SelectionModelSelectionChangedEventArgs( IReadOnlyList? deselectedIndices = null, IReadOnlyList? selectedIndices = null, - IReadOnlyList? deselectedItems = null, - IReadOnlyList? selectedItems = null) + IReadOnlyList? deselectedItems = null, + IReadOnlyList? selectedItems = null) { DeselectedIndexes = deselectedIndices ?? Array.Empty(); SelectedIndexes = selectedIndices ?? Array.Empty(); @@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection /// /// Gets the items that were removed from the selection. /// - public new IReadOnlyList DeselectedItems { get; } + public new IReadOnlyList DeselectedItems { get; } /// /// Gets the items that were added to the selection. /// - public new IReadOnlyList SelectedItems { get; } + public new IReadOnlyList SelectedItems { get; } protected override IReadOnlyList GetUntypedDeselectedItems() { diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 69a651aca6..22db0cbb6c 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,18 +2,23 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { + /// + /// Base class for selection models. + /// + /// The type of the element being selected. public abstract class SelectionNodeBase : ICollectionChangedListener { private IEnumerable? _source; private bool _rangesEnabled; private List? _ranges; - private int _collectionChanging; + /// + /// Gets or sets the source collection. + /// protected IEnumerable? Source { get => _source; @@ -21,18 +26,23 @@ namespace Avalonia.Controls.Selection { if (_source != value) { - if (ItemsView?.Inner is INotifyCollectionChanged inccOld) - CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); + ItemsView?.RemoveListener(this); _source = value; - ItemsView = value is object ? ItemsSourceView.GetOrCreate(value) : null; - if (ItemsView?.Inner is INotifyCollectionChanged inccNew) - CollectionChangedEventManager.Instance.AddListener(inccNew, this); + ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; + ItemsView?.AddListener(this); } } } - protected bool IsSourceCollectionChanging => _collectionChanging > 0; + /// + /// Gets an of the . + /// + protected internal ItemsSourceView? ItemsView { get; set; } + /// + /// Gets or sets a value indicating whether range selection is currently enabled for + /// the selection node. + /// protected bool RangesEnabled { get => _rangesEnabled; @@ -50,8 +60,6 @@ namespace Avalonia.Controls.Selection } } - internal ItemsSourceView? ItemsView { get; set; } - internal IReadOnlyList Ranges { get @@ -67,7 +75,7 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - ++_collectionChanging; + OnSourceCollectionChangeStarted(); } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -77,69 +85,173 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - if (--_collectionChanging == 0) - { - OnSourceCollectionChangeFinished(); - } + OnSourceCollectionChangeFinished(); } - protected abstract void OnSourceCollectionChangeFinished(); - - private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta); + /// + /// Called when the source collection starts changing. + /// + protected virtual void OnSourceCollectionChangeStarted() + { + } - private protected abstract void OnSourceReset(); + /// + /// Called when the collection changes. + /// + /// The details of the collection change. + /// + /// The implementation in calls + /// and + /// in order to calculate how the collection change affects the currently selected items. + /// It then calls and + /// if necessary, according + /// to the returned by those methods. + /// + /// Override this method and to provide + /// custom handling of source collection changes. + /// + protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + { + var shiftDelta = 0; + var shiftIndex = -1; + List? removed = null; - private protected abstract void OnSelectionChanged(IReadOnlyList deselectedItems); + if (!IsValidCollectionChange(e)) + { + return; + } - private protected int CommitSelect(IndexRange range) - { - if (RangesEnabled) + switch (e.Action) { - _ranges ??= new List(); - return IndexRange.Add(_ranges, range); + case NotifyCollectionChangedAction.Add: + { + var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!); + shiftIndex = change.ShiftIndex; + shiftDelta = change.ShiftDelta; + break; + } + case NotifyCollectionChangedAction.Remove: + { + var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); + shiftIndex = change.ShiftIndex; + shiftDelta = change.ShiftDelta; + removed = change.RemovedItems; + break; + } + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + { + var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); + var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!); + shiftIndex = removeChange.ShiftIndex; + shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; + removed = removeChange.RemovedItems; + } + break; + case NotifyCollectionChangedAction.Reset: + OnSourceReset(); + break; } - return 0; + if (shiftDelta != 0) + OnIndexesChanged(shiftIndex, shiftDelta); + if (removed is not null) + OnSelectionRemoved(shiftIndex, -shiftDelta, removed); } - private protected int CommitSelect(IReadOnlyList ranges) + /// + /// Called when the source collection has finished changing, and all CollectionChanged + /// handlers have run. + /// + /// + /// Override this method to respond to the end of a collection change instead of acting at + /// the end of + /// in order to ensure that all UI subscribers to the source collection change event have + /// had chance to run. + /// + protected virtual void OnSourceCollectionChangeFinished() { - if (RangesEnabled) - { - _ranges ??= new List(); - return IndexRange.Add(_ranges, ranges); - } + } - return 0; + /// + /// Called by , + /// detailing the indexes changed by the collection changing. + /// + /// The first index that was shifted. + /// + /// If positive, the number of items inserted, or if negative the number of items removed. + /// + protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta) + { } - private protected int CommitDeselect(IndexRange range) + /// + /// Called by , + /// on collection reset. + /// + protected abstract void OnSourceReset(); + + /// + /// Called by , + /// detailing the items removed by a collection change. + /// + protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList deselectedItems) + { + } + + /// + /// If , adds the specified range to the selection. + /// + /// The inclusive index of the start of the range to select. + /// The inclusive index of the end of the range to select. + /// The number of items selected. + protected int CommitSelect(int begin, int end) { if (RangesEnabled) { _ranges ??= new List(); - return IndexRange.Remove(_ranges, range); + return IndexRange.Add(_ranges, new IndexRange(begin, end)); } return 0; } - private protected int CommitDeselect(IReadOnlyList ranges) + /// + /// If , removes the specified range from the selection. + /// + /// The inclusive index of the start of the range to deselect. + /// The inclusive index of the end of the range to deselect. + /// The number of items selected. + protected int CommitDeselect(int begin, int end) { - if (RangesEnabled && _ranges is object) + if (RangesEnabled) { - return IndexRange.Remove(_ranges, ranges); + _ranges ??= new List(); + return IndexRange.Remove(_ranges, new IndexRange(begin, end)); } return 0; } - private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) + /// + /// Called by + /// when items are added to the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are added. + /// + protected virtual CollectionChangeState OnItemsAdded(int index, IList items) { var count = items.Count; var shifted = false; - if (_ranges is object) + if (_ranges is not null) { List? toAdd = null; @@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection // The range is after the inserted items, need to shift the range right if (range.End >= index) { - int begin = range.Begin; + var begin = range.Begin; // If the index left of newIndex is inside the range, // Split the range and remember the left piece to add later @@ -167,7 +279,7 @@ namespace Avalonia.Controls.Selection } } - if (toAdd is object) + if (toAdd is not null) { foreach (var range in toAdd) { @@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection }; } + /// + /// Called by + /// when items are removed from the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are removed. + /// private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items) { var count = items.Count; var removedRange = new IndexRange(index, index + count - 1); - bool shifted = false; + var shifted = false; List? removed = null; - if (_ranges is object) + if (_ranges is not null) { var deselected = new List(); @@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection }; } - private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) - { - var shiftDelta = 0; - var shiftIndex = -1; - List? removed = null; - - if (!IsValidCollectionChange(e)) - { - return; - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!); - shiftIndex = change.ShiftIndex; - shiftDelta = change.ShiftDelta; - break; - } - case NotifyCollectionChangedAction.Remove: - { - var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); - shiftIndex = change.ShiftIndex; - shiftDelta = change.ShiftDelta; - removed = change.RemovedItems; - break; - } - case NotifyCollectionChangedAction.Replace: - case NotifyCollectionChangedAction.Move: - { - var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); - var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!); - shiftIndex = removeChange.ShiftIndex; - shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; - removed = removeChange.RemovedItems; - } - break; - case NotifyCollectionChangedAction.Reset: - OnSourceReset(); - break; - } - - if (shiftDelta != 0) - { - OnIndexesChanged(shiftIndex, shiftDelta); - } - - if (removed is object) - { - OnSelectionChanged(removed); - } - } - private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) { // If the selection is modified in a CollectionChanged handler before the selection @@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection return true; } - private protected struct CollectionChangeState + /// + /// Details the results of a collection change on the current selection; + /// + protected class CollectionChangeState { - public int ShiftIndex; - public int ShiftDelta; - public List? RemovedItems; + /// + /// Gets or sets the first index that was shifted as a result of the collection + /// changing. + /// + public int ShiftIndex { get; set; } + + /// + /// Gets or sets a value indicating how the indexes after + /// were shifted. + /// + public int ShiftDelta { get; set; } + + /// + /// Gets or sets the items removed by the collection change, if any. + /// + public List? RemovedItems { get; set; } } } } From ddb9669c11e293e236fdb438ea9a3c4aacdc5c94 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Fri, 3 Mar 2023 14:02:15 +0200 Subject: [PATCH 04/82] Add ScrollBarAutomationPeer --- samples/IntegrationTestApp/MainWindow.axaml | 3 ++ .../Peers/ScrollBarAutomationPeer.cs | 29 ++++++++++++++++ src/Avalonia.Controls/Primitives/ScrollBar.cs | 3 ++ .../ScrollBarTests.cs | 33 +++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs create mode 100644 tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 090cf23b33..d4a96c5f17 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -167,6 +167,9 @@ + + + diff --git a/src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs new file mode 100644 index 0000000000..b8406bf20c --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class ScrollBarAutomationPeer : RangeBaseAutomationPeer + { + public ScrollBarAutomationPeer(ScrollBar owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "ScrollBar"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ScrollBar; + } + + // AutomationControlType.ScrollBar must return IsContentElement false. + // See http://msdn.microsoft.com/en-us/library/ms743712.aspx + override protected bool IsContentElementCore() + { + return false; + } + + } +} diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index e524db5444..8b2321e40b 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; using Avalonia.Controls.Metadata; +using Avalonia.Automation.Peers; namespace Avalonia.Controls.Primitives { @@ -286,6 +287,8 @@ namespace Avalonia.Controls.Primitives } } + protected override AutomationPeer OnCreateAutomationPeer() => new ScrollBarAutomationPeer(this); + private void InvokeAfterDelay(Action handler, TimeSpan delay) { if (_timer != null) diff --git a/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs new file mode 100644 index 0000000000..e9d0a5d3a4 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs @@ -0,0 +1,33 @@ +using OpenQA.Selenium.Appium; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class ScrollBarTests + { + private readonly AppiumDriver _session; + + public ScrollBarTests(DefaultAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("ScrollBarTab"); + tab.Click(); + } + + [Fact] + public void ScrollBar_Increases_Value_By_LargeChange_When_IncreaseButton_Is_Clicked() + { + var button = _session.FindElementByAccessibilityId("MyScrollBar"); + Assert.True(double.Parse(button.Text) == 20); + + button.Click(); + + // Default LargeChange value is 10 so when clicking the IncreaseButton + // ScrollBar value should be increased by 10. + Assert.Equal(30, double.Parse(button.Text)); + } + } +} From 21574f5607d687c24d1c831bd808733d71650439 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Mar 2023 13:59:00 +0100 Subject: [PATCH 05/82] Expose pre/post collection changed events. Instead of implementing `ICollectionChangedListener` on `SelectionNodeBase`. We may want to expose this publicly at some point. --- src/Avalonia.Controls/ItemsSourceView.cs | 39 +++++++++++-------- .../Selection/SelectionNodeBase.cs | 38 +++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 1fa8f6a5cf..416b909219 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -27,6 +27,7 @@ namespace Avalonia.Controls private readonly IList _inner; private NotifyCollectionChangedEventHandler? _collectionChanged; + private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged; private bool _listening; @@ -70,7 +71,7 @@ namespace Avalonia.Controls /// Gets a value that indicates whether the items source can provide a unique key for each item. /// /// - /// TODO: Not yet implemented in Avalonia. + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// internal bool HasKeyIndexMapping => false; @@ -92,6 +93,25 @@ namespace Avalonia.Controls } } + /// + /// Occurs when a collection has finished changing and all + /// event handlers have been notified. + /// + internal event NotifyCollectionChangedEventHandler? PreCollectionChanged + { + add + { + AddListenerIfNecessary(); + _preCollectionChanged += value; + } + + remove + { + _preCollectionChanged -= value; + RemoveListenerIfNecessary(); + } + } + /// /// Occurs when a collection has finished changing and all /// event handlers have been notified. @@ -229,6 +249,7 @@ namespace Avalonia.Controls void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { + _preCollectionChanged?.Invoke(this, e); } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -241,22 +262,6 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } - internal void AddListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.AddListener(incc, listener); - } - } - - internal void RemoveListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.RemoveListener(incc, listener); - } - } - /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 22db0cbb6c..caeff61f07 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { @@ -10,7 +9,7 @@ namespace Avalonia.Controls.Selection /// Base class for selection models. /// /// The type of the element being selected. - public abstract class SelectionNodeBase : ICollectionChangedListener + public abstract class SelectionNodeBase { private IEnumerable? _source; private bool _rangesEnabled; @@ -24,12 +23,28 @@ namespace Avalonia.Controls.Selection get => _source; set { + void OnPreChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeStarted(); + void OnChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChanged(e); + void OnPostChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeFinished(); + if (_source != value) { - ItemsView?.RemoveListener(this); + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged -= OnPreChanged; + ItemsView.CollectionChanged -= OnChanged; + ItemsView.PostCollectionChanged -= OnPostChanged; + } + _source = value; ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; - ItemsView?.AddListener(this); + + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged += OnPreChanged; + ItemsView.CollectionChanged += OnChanged; + ItemsView.PostCollectionChanged += OnPostChanged; + } } } } @@ -73,21 +88,6 @@ namespace Avalonia.Controls.Selection } } - void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeStarted(); - } - - void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChanged(e); - } - - void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeFinished(); - } - /// /// Called when the source collection starts changing. /// From 3b57b3496a12c619958f6afc7dbb66a42d353be7 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Mon, 6 Mar 2023 21:02:27 +0100 Subject: [PATCH 06/82] Added GenerateTypeSafeMetadata to property metadata This method is used to strip coercion methods out of StyledPropertyMetadata when applying it to a new owner. Also fixed AvaloniaProperty.(IPropertyInfo.CanSet) returning true for read-only properties --- Avalonia.Desktop.slnf | 5 +- src/Avalonia.Base/AvaloniaProperty.cs | 65 ++++++++----------- src/Avalonia.Base/AvaloniaPropertyMetadata.cs | 10 ++- src/Avalonia.Base/DirectPropertyMetadata`1.cs | 2 + src/Avalonia.Base/StyledProperty.cs | 7 +- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 2 + .../AvaloniaPropertyTests.cs | 46 +++++++++---- 7 files changed, 80 insertions(+), 57 deletions(-) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 477aaec6a8..d4cde99240 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -8,9 +8,9 @@ "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", - "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", @@ -41,6 +41,7 @@ "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", + "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", @@ -63,4 +64,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 24244c5068..9efbf678fe 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.PropertyStore; -using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -20,12 +19,20 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; + + /// + /// Provides a metadata object for types which have no metadata of their own. + /// private readonly AvaloniaPropertyMetadata _defaultMetadata; + + /// + /// Provides a fast path when the property has no metadata overrides. + /// + private KeyValuePair? _singleMetadata; + private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); - private bool _hasMetadataOverrides; - /// /// Initializes a new instance of the class. /// @@ -57,7 +64,8 @@ namespace Avalonia Id = s_nextId++; _metadata.Add(ownerType, metadata ?? throw new ArgumentNullException(nameof(metadata))); - _defaultMetadata = metadata; + _defaultMetadata = metadata.GenerateTypeSafeMetadata(); + _singleMetadata = new(ownerType, metadata); } /// @@ -80,9 +88,6 @@ namespace Avalonia Id = source.Id; _defaultMetadata = source._defaultMetadata; - // Properties that have different owner can't use fast path for metadata. - _hasMetadataOverrides = true; - if (metadata != null) { _metadata.Add(ownerType, metadata); @@ -442,33 +447,14 @@ namespace Avalonia } /// - /// Gets the property metadata for the specified type. + /// Gets the which applies to this property when it is used with the specified type. /// - /// The type. - /// - /// The property metadata. - /// - public AvaloniaPropertyMetadata GetMetadata() where T : AvaloniaObject - { - return GetMetadata(typeof(T)); - } + /// The type for which to retrieve metadata. + public AvaloniaPropertyMetadata GetMetadata() where T : AvaloniaObject => GetMetadata(typeof(T)); - /// - /// Gets the property metadata for the specified type. - /// - /// The type. - /// - /// The property metadata. - /// - public AvaloniaPropertyMetadata GetMetadata(Type type) - { - if (!_hasMetadataOverrides) - { - return _defaultMetadata; - } - - return GetMetadataWithOverrides(type); - } + /// + /// The type for which to retrieve metadata. + public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type); /// /// Checks whether the is valid for the property. @@ -567,7 +553,7 @@ namespace Avalonia _metadata.Add(type, metadata); _metadataCache.Clear(); - _hasMetadataOverrides = true; + _singleMetadata = null; } protected abstract IObservable GetChanged(); @@ -584,7 +570,12 @@ namespace Avalonia return result; } - Type? currentType = type; + if (_singleMetadata is { } singleMetadata) + { + return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata; + } + + var currentType = type; while (currentType != null) { @@ -598,13 +589,11 @@ namespace Avalonia currentType = currentType.BaseType; } - _metadataCache[type] = _defaultMetadata; - - return _defaultMetadata; + return _metadataCache[type] = _defaultMetadata; } bool IPropertyInfo.CanGet => true; - bool IPropertyInfo.CanSet => true; + bool IPropertyInfo.CanSet => !IsReadOnly; object? IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); void IPropertyInfo.Set(object target, object? value) => ((AvaloniaObject)target).SetValue(this, value); } diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 62bb65351f..ec29d14693 100644 --- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -5,7 +5,7 @@ namespace Avalonia /// /// Base class for avalonia property metadata. /// - public class AvaloniaPropertyMetadata + public abstract class AvaloniaPropertyMetadata { private BindingMode _defaultBindingMode; @@ -61,5 +61,13 @@ namespace Avalonia EnableDataValidation ??= baseMetadata.EnableDataValidation; } + + /// + /// Gets a copy of this object configured for use with any owner type. + /// + /// + /// For example, delegates which receive the owner object should be removed. + /// + public abstract AvaloniaPropertyMetadata GenerateTypeSafeMetadata(); } } diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 451ff6ce00..5471826f9f 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -45,5 +45,7 @@ namespace Avalonia UnsetValue ??= src.UnsetValue; } } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new DirectPropertyMetadata(UnsetValue, DefaultBindingMode, EnableDataValidation); } } diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 5052840013..dfe5a44f1b 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -18,7 +18,10 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. - /// A value validation callback. + /// + /// A method which returns "false" for values that are never valid for this property. + /// This method is not part of the property's metadata and so cannot be changed after registration. + /// /// A callback. public StyledProperty( string name, @@ -41,7 +44,7 @@ namespace Avalonia } /// - /// Gets the value validation callback for the property. + /// A method which returns "false" for values that are never valid for this property. /// public Func? ValidateValue { get; } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 6f10de3651..9db460dba3 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -58,5 +58,7 @@ namespace Avalonia } } } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new StyledPropertyMetadata(DefaultValue, DefaultBindingMode, enableDataValidation: EnableDataValidation ?? false); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 181596a681..bde750efdc 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.PropertyStore; -using Avalonia.Styling; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests @@ -29,7 +27,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); @@ -38,26 +36,30 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value_For_Derived_Class() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); } [Fact] - public void GetMetadata_Returns_Supplied_Value_For_Unrelated_Class() + public void GetMetadata_Returns_TypeSafe_Metadata_For_Unrelated_Class() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(BindingMode.OneWayToSource, true, x => { _ = (StyledElement)x; }); var target = new TestProperty("test", typeof(Class3), metadata); - Assert.Same(metadata, target.GetMetadata()); + var targetMetadata = (TestMetadata)target.GetMetadata(); + + Assert.Equal(metadata.DefaultBindingMode, targetMetadata.DefaultBindingMode); + Assert.Equal(metadata.EnableDataValidation, targetMetadata.EnableDataValidation); + Assert.Equal(null, targetMetadata.OwnerSpecificAction); } [Fact] public void GetMetadata_Returns_Overridden_Value() { - var metadata = new AvaloniaPropertyMetadata(); - var overridden = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); + var overridden = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -68,9 +70,9 @@ namespace Avalonia.Base.UnitTests [Fact] public void OverrideMetadata_Should_Merge_Values() { - var metadata = new AvaloniaPropertyMetadata(BindingMode.TwoWay); + var metadata = new TestMetadata(BindingMode.TwoWay); var notify = (Action)((a, b) => { }); - var overridden = new AvaloniaPropertyMetadata(); + var overridden = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -131,15 +133,31 @@ namespace Avalonia.Base.UnitTests [Fact] public void PropertyMetadata_BindingMode_Default_Returns_OneWay() { - var data = new AvaloniaPropertyMetadata(defaultBindingMode: BindingMode.Default); + var data = new TestMetadata(defaultBindingMode: BindingMode.Default); Assert.Equal(BindingMode.OneWay, data.DefaultBindingMode); } + private class TestMetadata : AvaloniaPropertyMetadata + { + public Action OwnerSpecificAction { get; } + + public TestMetadata(BindingMode defaultBindingMode = BindingMode.Default, + bool? enableDataValidation = null, + Action ownerSpecificAction = null) + : base(defaultBindingMode, enableDataValidation) + { + OwnerSpecificAction = ownerSpecificAction; + } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => + new TestMetadata(DefaultBindingMode, EnableDataValidation, null); + } + private class TestProperty : AvaloniaProperty { - public TestProperty(string name, Type ownerType, AvaloniaPropertyMetadata metadata = null) - : base(name, ownerType, metadata ?? new AvaloniaPropertyMetadata()) + public TestProperty(string name, Type ownerType, TestMetadata metadata = null) + : base(name, ownerType, metadata ?? new TestMetadata()) { } From 037ff6d265884f0e9499724b1a6139de07faf183 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Feb 2023 17:02:23 +0100 Subject: [PATCH 07/82] Don't allow window zoom when CanResize=false. Previously, even though the zoom button was disabled the user could still double-click on the title bar to zoom the window. Prevent that by returning the appropriate value from `NSWindow windowShouldZoom` and move the `CanZoom` logic into a central place for use by this method and `UpdateStyle`. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 ++ native/Avalonia.Native/src/OSX/WindowImpl.h | 2 ++ native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index b1fb915e04..16e1486acc 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -394,7 +394,7 @@ - (BOOL)windowShouldZoom:(NSWindow *_Nonnull)window toFrame:(NSRect)newFrame { - return true; + return _parent->CanZoom(); } -(void)windowDidResignKey:(NSNotification *)notification diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 93decef136..d00dffa65a 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -104,6 +104,8 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); + virtual bool CanZoom() { return false; } + protected: virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 29bb659039..5140124a17 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -97,6 +97,8 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); + bool CanZoom() override { return _isEnabled && _canResize; } + protected: virtual NSWindowStyleMask CalculateStyleMask() override; void UpdateStyle () override; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 840f2c9e88..104611eabc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -622,5 +622,5 @@ void WindowImpl::UpdateStyle() { [miniaturizeButton setHidden:!hasTrafficLights]; [miniaturizeButton setEnabled:_isEnabled]; [zoomButton setHidden:!hasTrafficLights]; - [zoomButton setEnabled:_isEnabled && _canResize]; + [zoomButton setEnabled:CanZoom()]; } From ef2a47bc9573f3f94710b22477b93145e7b77407 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 7 Mar 2023 13:16:51 +0100 Subject: [PATCH 08/82] Only try to create embedded font collection for valid assets location --- src/Avalonia.Base/Media/FontManager.cs | 2 +- src/Avalonia.Themes.Simple/Accents/Base.xaml | 2 +- .../Media/FontManagerImplTests.cs | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 595a2f3474..5890b90954 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -107,7 +107,7 @@ namespace Avalonia.Media source = new Uri(key.BaseUri, source); } - if (!_fontCollections.TryGetValue(source, out var fontCollection)) + if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) { var embeddedFonts = new EmbeddedFontCollection(source, source); diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 38b122d8b2..0640fe9a4a 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -76,7 +76,7 @@ - fonts://Inter#Inter, $Default + fonts:Inter#Inter, $Default #CC119EDA #99119EDA #66119EDA diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 21c46b836d..859726e871 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -76,5 +76,16 @@ namespace Avalonia.Skia.UnitTests.Media Assert.Throws(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface); } } + + [Fact] + public void Should_Return_False_For_Unregistered_FontCollection_Uri() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var result = FontManager.Current.TryGetGlyphTypeface(new Typeface("fonts:invalid#Something"), out _); + + Assert.False(result); + } + } } } From 02983e3c1e096a0f8b87d5afbd06a9860df2799d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 16:12:50 +0100 Subject: [PATCH 09/82] fix: Misc XML Document issue --- src/Avalonia.Base/AvaloniaProperty.cs | 13 ++++++++++++- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- src/Avalonia.Base/Platform/IDrawingContextImpl.cs | 1 + src/Avalonia.Base/Rendering/DisplayDirtyRect.cs | 2 +- .../Rendering/SceneGraph/GeometryNode.cs | 1 - src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 1 - .../Rendering/SceneGraph/OpacityMaskNode.cs | 1 - .../Rendering/SceneGraph/RectangleNode.cs | 1 - src/Avalonia.Controls/Platform/IInsetsManager.cs | 2 +- src/Avalonia.Controls/SplitButton/SplitButton.cs | 2 +- src/Browser/Avalonia.Browser/BrowserAppBuilder.cs | 2 +- .../Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 1 - 13 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 24244c5068..bda30c08fb 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,7 +257,18 @@ namespace Avalonia return result; } - /// + /// + /// Registers an attached . + /// + /// The type of the class that is registering the property. + /// The type of the property's value. + /// The name of the property. + /// The default value of the property. + /// Whether the property inherits its value. + /// The default binding mode for the property. + /// A value validation callback. + /// A value coercion callback. + /// if is set to true enable data validation. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index fc0ca2323e..8e6f7b0983 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -364,7 +364,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void Register(Type type, AvaloniaProperty property) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 12efb3c383..a40cbf95ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting /// Performs text wrapping returns a list of text lines. /// /// - /// Whether can be reused to store the split runs. + /// Whether can be reused to store the split runs. /// The first text source index. /// The paragraph width. /// The text paragraph properties. diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8962bc1586..ffdfa9aac1 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,6 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. + /// where to apply the opacity. void PushOpacity(double opacity, Rect bounds); /// diff --git a/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs b/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs index 7e6c3062cd..7a89e5b3cc 100644 --- a/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs +++ b/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs @@ -3,7 +3,7 @@ namespace Avalonia.Rendering { /// - /// Holds the state for a dirty rect rendered when is set. + /// Holds the state for a dirty rect rendered when is set. /// internal class DisplayDirtyRect { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 3ab535897a..f64a3e845d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IImmutableBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index f21791d038..61bffc3260 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index e10d712c2d..b0584038a8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Auxiliary data required to draw the brush. public OpacityMaskNode(IImmutableBrush mask, Rect bounds) : base(default, Matrix.Identity, mask) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index cee9ce9df7..94f61df47d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,6 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IImmutableBrush? brush, diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 6288142805..072bace154 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Platform SafeAreaPadding = safeArePadding; } - /// + /// public Thickness SafeAreaPadding { get; } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 19d2b1c5da..e790578675 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -432,7 +432,7 @@ namespace Avalonia.Controls } /// - /// Called when the property changes. + /// Called when the property changes. /// private void Flyout_PlacementPropertyChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 32637b6d1b..9bb471005b 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -16,7 +16,7 @@ public class BrowserPlatformOptions public static class BrowserAppBuilder { /// - /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. + /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. /// /// Application builder. /// ID of the html element where avalonia content should be rendered. diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 87fa963871..318b0fe9ae 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -29,7 +29,6 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// - /// The visual brush renderer. /// The render target to draw to. /// /// An object to use to create layers. May be null, in which case a From 3f306b48abd1dd35ee892d34d9bb375290c6486a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 17:06:40 +0100 Subject: [PATCH 10/82] fix(DrawingContext): CS0618 PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' ```bash Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Media\ConicGradientBrushTests.cs 203 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Media\LinearGradientBrushTests.cs 98 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Media\RadialGradientBrushTests.cs 188 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Media\TextFormatting\TextLayoutTests.cs 315 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Skia.UnitTests .\tests\Avalonia.Skia.UnitTests\Media\TextFormatting\TextFormatterTests.cs 795 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\CustomDrawingExampleControl.cs 136 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\CustomDrawingExampleControl.cs 140 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\CustomDrawingExampleControl.cs 144 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\CustomDrawingExampleControl.cs 146 Active Warning CS0618 'DrawingContext.PushPostTransform(Matrix)' is obsolete: 'Use PushTransform' RenderDemo .\samples\RenderDemo\Pages\RenderTargetBitmapPage.cs 32 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Base (net6.0), Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 76 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Controls\CustomRenderTests.cs 91 Active Warning CS0618 'DrawingContext.PushPreTransform(Matrix)' is obsolete: 'Use PushTransform' Avalonia.Direct2D1.RenderTests, Avalonia.Skia.RenderTests .\tests\Avalonia.RenderTests\Controls\CustomRenderTests.cs 115 Active ``` --- .../ControlCatalog/Pages/CustomDrawingExampleControl.cs | 8 ++++---- samples/RenderDemo/Pages/RenderTargetBitmapPage.cs | 2 +- src/Avalonia.Base/Media/DrawingGroup.cs | 2 +- src/Avalonia.Base/Rendering/ImmediateRenderer.cs | 4 ++-- src/Avalonia.Controls/SelectableTextBlock.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 1 - tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs | 4 ++-- .../Avalonia.RenderTests/Media/ConicGradientBrushTests.cs | 2 +- .../Media/LinearGradientBrushTests.cs | 2 +- .../Media/RadialGradientBrushTests.cs | 2 +- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- .../Media/TextFormatting/TextFormatterTests.cs | 2 +- 12 files changed, 16 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index db74be7c08..549cf3d740 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -133,17 +133,17 @@ namespace ControlCatalog.Pages // 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world - var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight))); + var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight))); // now 0,0 refers to the ViewportCenter(X,Y). var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation); - var rotationModifier = context.PushPreTransform(rotationMatrix); + var rotationModifier = context.PushTransform(rotationMatrix); // everything is rotated but not scaled - var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale)); + var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale)); - var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY))); + var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY))); // now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index b88dded39b..8d6fb15a32 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -29,7 +29,7 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { using (var ctx = _bitmap.CreateDrawingContext()) - using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) + using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateTranslation(100, 100))) { diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index a41054202e..a0ed29250b 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -73,7 +73,7 @@ namespace Avalonia.Media { var bounds = GetBounds(); - using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) + using (context.PushTransform(Transform?.Value ?? Matrix.Identity)) using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 4a12e78817..9b7d358b1d 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -83,7 +83,7 @@ namespace Avalonia.Rendering } } - using (context.PushPostTransform(m)) + using (context.PushTransform(m)) using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete @@ -95,7 +95,7 @@ namespace Avalonia.Rendering using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default) - using (context.PushTransformContainer()) + using (context.PushTransform(Matrix.Identity)) { visual.Render(context); diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 6603e20a2a..a737c94719 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -204,7 +204,7 @@ namespace Avalonia.Controls var rects = TextLayout.HitTestTextRange(start, length); - using (context.PushPostTransform(Matrix.CreateTranslation(origin))) + using (context.PushTransform(Matrix.CreateTranslation(origin))) { foreach (var rect in rects) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5c2a8c8a13..dcf387afab 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,7 +15,6 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Platform.Storage; -using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs index 1199184d14..7a2c60baf4 100644 --- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs @@ -88,7 +88,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Height = 200, Child = new CustomRenderer((control, context) => { - using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (var transform = context.PushTransform(Matrix.CreateTranslation(100, 100))) using (var clip = context.PushClip(new Rect(0, 0, 100, 100))) { context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200)); @@ -112,7 +112,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Height = 200, Child = new CustomRenderer((control, context) => { - using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (var transform = context.PushTransform(Matrix.CreateTranslation(100, 100))) using (var clip = context.PushClip(new Rect(0, 0, 100, 100))) { context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200)); diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs index ef400410a4..a6170f9e94 100644 --- a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs @@ -200,7 +200,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new DrawnControl(c => { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (c.PushTransform(Matrix.CreateTranslation(100, 100))) c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index dd9d2f9b39..ac49b6d078 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (c.PushTransform(Matrix.CreateTranslation(100, 100))) c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index e52f844359..78f1b5c23f 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -185,7 +185,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new DrawnControl(c => { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (c.PushTransform(Matrix.CreateTranslation(100, 100))) c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index 7b128076cd..65fd670415 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -312,7 +312,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media var rotate = Matrix.CreateTranslation(-100, -100) * Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) * Matrix.CreateTranslation(100, 100); - using var transform = c.PushPreTransform(rotate); + using var transform = c.PushTransform(rotate); c.DrawRectangle(Brushes.Yellow, null, rect); t.Draw(c, rect.Position); }), diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index dc8744b292..540c05f0da 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -792,7 +792,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public override double Baseline => 0; public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPreTransform(Matrix.CreateTranslation(new Vector(origin.X, 0)))) + using (drawingContext.PushTransform(Matrix.CreateTranslation(new Vector(origin.X, 0)))) { drawingContext.FillRectangle(_fill, _rect); } From 8c01795ef8e9f288d8e0a2688cb8f956b24e18ed Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 17:15:47 +0100 Subject: [PATCH 11/82] fix: Warning CS0628 new protected member declared in sealed type ```bash Warning CS0628 'DrawingGroup.DrawingGroupDrawingContext._rootDrawing': new protected member declared in sealed type Avalonia.Base (net6.0), Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 120 Active Warning CS0628 'DrawingGroup.DrawingGroupDrawingContext._currentDrawingGroup': new protected member declared in sealed type Avalonia.Base (net6.0), Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 123 Active ``` --- src/Avalonia.Base/Media/DrawingGroup.cs | 4 ++-- src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index a41054202e..5f87da9dcd 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -117,10 +117,10 @@ namespace Avalonia.Media // root DrawingGroup, and be the same value as the root _currentDrawingGroup. // // Either way, _rootDrawing always references the root drawing. - protected Drawing? _rootDrawing; + private Drawing? _rootDrawing; // Current DrawingGroup that new children are added to - protected DrawingGroup? _currentDrawingGroup; + private DrawingGroup? _currentDrawingGroup; // Previous values of _currentDrawingGroup private Stack? _previousDrawingGroupStack; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 4e0d37479b..a10b3eb3ea 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -52,7 +52,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector? previous) + TypeNameAndClassSelector(Selector? previous) { _previous = previous; } From ba7e8a20b5cd7e67f536dfe782e10972eda761ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Mar 2023 17:10:05 +0100 Subject: [PATCH 12/82] Added ItemsControl.ItemsSource. `ItemsControl` now works more like WPF, in that there are separate `Items` and `ItemsSource` properties. For backwards compatibility `Items` can still be set, though the setter is deprecated. `Items` needed to be changed from `IEnumerable` to `IList` though. --- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- .../Controls/ItemsRepeater.cs | 5 +- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 4 +- src/Avalonia.Controls/ItemCollection.cs | 114 ++++++++++++ src/Avalonia.Controls/ItemsControl.cs | 175 ++++++++++-------- src/Avalonia.Controls/ItemsSourceView.cs | 155 +++++++++------- .../Presenters/PanelContainerGenerator.cs | 15 -- .../Primitives/SelectingItemsControl.cs | 44 ++--- .../Selection/SelectionModel.cs | 4 +- .../SelectingItemsControlSelectionAdapter.cs | 4 +- src/Avalonia.Controls/VirtualizingPanel.cs | 20 +- .../ComboBoxTests.cs | 2 +- .../ItemsControlTests.cs | 60 +++--- .../ItemsSourceViewTests.cs | 39 ++++ .../Presenters/ItemsPresenterTests.cs | 2 +- .../TabControlTests.cs | 72 ++++--- .../TreeViewTests.cs | 2 +- .../VirtualizingCarouselPanelTests.cs | 2 +- .../VirtualizingStackPanelTests.cs | 13 +- .../CompiledBindingExtensionTests.cs | 5 +- 20 files changed, 452 insertions(+), 287 deletions(-) create mode 100644 src/Avalonia.Controls/ItemCollection.cs diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..54251417b3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 3d3d01e06e..499904deac 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -39,7 +39,10 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..8211d1baf2 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(x => x.Items, + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, (x, v) => x.Items = v); /// diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs new file mode 100644 index 0000000000..120aef41dc --- /dev/null +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + public class ItemCollection : ItemsSourceView, IList + { +// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique. +#pragma warning disable CA1825 + private static readonly object?[] s_uninitialized = new object?[0]; +#pragma warning restore CA1825 + + private Mode _mode; + + internal ItemCollection() + : base(s_uninitialized) + { + } + + public new object? this[int index] + { + get => base[index]; + set => WritableSource[index] = value; + } + + public bool IsReadOnly => _mode == Mode.ItemsSource; + + internal event EventHandler? SourceChanged; + + public int Add(object? value) => WritableSource.Add(value); + public void Clear() => WritableSource.Clear(); + public void Insert(int index, object? value) => WritableSource.Insert(index, value); + public void RemoveAt(int index) => WritableSource.RemoveAt(index); + + public bool Remove(object? value) + { + var c = Count; + WritableSource.Remove(value); + return Count < c; + } + + int IList.Add(object? value) => Add(value); + void IList.Clear() => Clear(); + void IList.Insert(int index, object? value) => Insert(index, value); + void IList.RemoveAt(int index) => RemoveAt(index); + + private IList WritableSource + { + get + { + if (IsReadOnly) + ThrowIsItemsSource(); + if (Source == s_uninitialized) + SetSource(CreateDefaultCollection()); + return Source; + } + } + + internal IList? GetItemsPropertyValue() + { + if (_mode == Mode.ObsoleteItemsSetter) + return Source == s_uninitialized ? null : Source; + return this; + } + + internal void SetItems(IList? items) + { + _mode = Mode.ObsoleteItemsSetter; + SetSource(items ?? s_uninitialized); + } + + internal void SetItemsSource(IEnumerable? value) + { + _mode = value is not null ? Mode.ItemsSource : Mode.Items; + SetSource(value ?? CreateDefaultCollection()); + } + + private new void SetSource(IEnumerable source) + { + var oldSource = Source; + + base.SetSource(source); + + if (oldSource.Count > 0) + RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0)); + if (Source.Count > 0) + RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0)); + SourceChanged?.Invoke(this, EventArgs.Empty); + } + + private static AvaloniaList CreateDefaultCollection() + { + return new() { ResetBehavior = ResetBehavior.Remove }; + } + + [DoesNotReturn] + private static void ThrowIsItemsSource() + { + throw new InvalidOperationException( + "Operation is not valid while ItemsSource is in use." + + "Access and modify elements with ItemsControl.ItemsSource instead."); + } + + private enum Mode + { + Items, + ItemsSource, + ObsoleteItemsSetter, + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..56ba9c7183 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -34,8 +34,13 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, +#pragma warning disable CS0618 // Type or member is obsolete + (o, v) => o.Items = v); +#pragma warning restore CS0618 // Type or member is obsolete /// /// Defines the property. @@ -56,23 +61,23 @@ namespace Avalonia.Controls AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly DirectProperty ItemsViewProperty = - AvaloniaProperty.RegisterDirect(nameof(ItemsView), o => o.ItemsView); + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); /// /// Defines the property /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); - + /// /// Defines the property. /// @@ -89,15 +94,14 @@ namespace Avalonia.Controls /// Gets or sets the to use for binding to the display member of each item. /// [AssignBinding] - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); set => SetValue(DisplayMemberBindingProperty, value); } - - private IEnumerable? _items = new AvaloniaList(); - private ItemsSourceView _itemsView; + + private readonly ItemCollection _items = new(); private int _itemCount; private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; @@ -110,9 +114,8 @@ namespace Avalonia.Controls /// public ItemsControl() { - _itemsView = ItemsSourceView.GetOrCreate(_items); - _itemsView.PostCollectionChanged += ItemsCollectionChanged; - UpdatePseudoClasses(0); + UpdatePseudoClasses(); + _items.CollectionChanged += OnItemsViewCollectionChanged; } /// @@ -129,10 +132,21 @@ namespace Avalonia.Controls /// Gets or sets the items to display. /// [Content] - public IEnumerable? Items + public IList? Items { - get => _items; - set => SetAndRaise(ItemsProperty, ref _items, value); + get => _items.GetItemsPropertyValue(); + + [Obsolete("Use ItemsSource to set or bind items.")] + set + { + var oldItems = _items.GetItemsPropertyValue(); + + if (value != oldItems) + { + _items.SetItems(value); + RaisePropertyChanged(ItemsProperty, oldItems, value); + } + } } /// @@ -140,17 +154,24 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get => GetValue(ItemContainerThemeProperty); + get => GetValue(ItemContainerThemeProperty); set => SetValue(ItemContainerThemeProperty, value); } /// - /// Gets the number of items in . + /// Gets the number of items being displayed by the . /// public int ItemCount { get => _itemCount; - private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); + private set + { + if (SetAndRaise(ItemCountProperty, ref _itemCount, value)) + { + UpdatePseudoClasses(); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + } + } } /// @@ -162,13 +183,44 @@ namespace Avalonia.Controls set => SetValue(ItemsPanelProperty, value); } + /// + /// Gets or sets a collection used to generate the content of the . + /// + /// + /// Since Avalonia 11, has both an property + /// and an property. The properties have the following differences: + /// + /// + /// is initialized with an empty collection and is a direct property, + /// meaning that it cannot be styled + /// is by default null, and is a styled property. This property + /// is marked as the content property and will be used for items added via inline XAML. + /// + /// + /// In Avalonia 11 the two properties can be used almost interchangeably but this will change + /// in a later version. In order to be ready for this change, follow the following guidance: + /// + /// + /// You should use the property when you're assigning a collection of + /// item containers directly, for example adding a collection of s + /// directly to a . + /// You should use the property when you're assigning or + /// binding a collection of models which will be transformed by a data template. + /// + /// + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + /// /// Gets or sets the data template used to display the items in the control. /// - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IDataTemplate? ItemTemplate { - get => GetValue(ItemTemplateProperty); + get => GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } @@ -182,32 +234,7 @@ namespace Avalonia.Controls /// public Panel? ItemsPanelRoot => Presenter?.Panel; - /// - /// Gets a standardized view over . - /// - /// - /// The property may be an enumerable which does not implement - /// or may be null. This view can be used to provide a standardized - /// view of the current items regardless of the type of the concrete collection, and - /// without having to deal with null values. - /// - public ItemsSourceView ItemsView - { - get => _itemsView; - private set - { - if (ReferenceEquals(_itemsView, value)) - return; - - var oldValue = _itemsView; - RemoveControlItemsFromLogicalChildren(_itemsView); - _itemsView.PostCollectionChanged -= ItemsCollectionChanged; - _itemsView = value; - _itemsView.PostCollectionChanged += ItemsCollectionChanged; - AddControlItemsToLogicalChildren(_itemsView); - RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView); - } - } + public ItemCollection ItemsView => _items; private protected bool WrapFocus { get; set; } @@ -262,7 +289,7 @@ namespace Avalonia.Controls /// public bool AreHorizontalSnapPointsRegular { - get => GetValue(AreHorizontalSnapPointsRegularProperty); + get => GetValue(AreHorizontalSnapPointsRegularProperty); set => SetValue(AreHorizontalSnapPointsRegularProperty, value); } @@ -271,7 +298,7 @@ namespace Avalonia.Controls /// public bool AreVerticalSnapPointsRegular { - get => GetValue(AreVerticalSnapPointsRegularProperty); + get => GetValue(AreVerticalSnapPointsRegularProperty); set => SetValue(AreVerticalSnapPointsRegularProperty, value); } @@ -295,7 +322,7 @@ namespace Avalonia.Controls /// public Control? ContainerFromItem(object item) { - var index = ItemsView.IndexOf(item); + var index = _items.IndexOf(item); return index >= 0 ? ContainerFromIndex(index) : null; } @@ -319,7 +346,7 @@ namespace Avalonia.Controls public object? ItemFromContainer(Control container) { var index = IndexFromContainer(container); - return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null; + return index >= 0 && index < _items.Count ? _items[index] : null; } /// @@ -389,7 +416,7 @@ namespace Avalonia.Controls if (itemTemplate is ITreeDataTemplate treeTemplate) { if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding) - BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null); + BindingOperations.Apply(hic, ItemsSourceProperty, itemsBinding, null); } } } @@ -485,19 +512,13 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if (change.Property == ItemsProperty) - { - ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue()); - ItemCount = ItemsView.Count; - } - else if (change.Property == ItemCountProperty) + if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { - UpdatePseudoClasses(change.GetNewValue()); - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + RefreshContainers(); } - else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) + else if (change.Property == ItemsSourceProperty) { - RefreshContainers(); + _items.SetItemsSource(change.GetNewValue()); } else if (change.Property == ItemTemplateProperty) { @@ -524,14 +545,12 @@ namespace Avalonia.Controls /// /// Called when the event is - /// raised on . + /// raised on . /// /// The event sender. /// The event args. - protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - ItemCount = _itemsView.Count; - switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -542,6 +561,8 @@ namespace Avalonia.Controls RemoveControlItemsFromLogicalChildren(e.OldItems); break; } + + ItemCount = ItemsView.Count; } /// @@ -585,7 +606,7 @@ namespace Avalonia.Controls { var itemContainerTheme = ItemContainerTheme; - if (itemContainerTheme is not null && + if (itemContainerTheme is not null && !container.IsSet(ThemeProperty) && ((IStyleable)container).StyleKey == itemContainerTheme.TargetType) { @@ -616,10 +637,6 @@ namespace Avalonia.Controls ClearContainerForItemOverride(container); } - /// - /// Given a collection of items, adds those that are controls to the logical children. - /// - /// The items. private void AddControlItemsToLogicalChildren(IEnumerable? items) { if (items is null) @@ -640,10 +657,6 @@ namespace Avalonia.Controls LogicalChildren.AddRange(toAdd); } - /// - /// Given a collection of items, removes those that are controls to from logical children. - /// - /// The items. private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) { if (items is null) @@ -681,10 +694,10 @@ namespace Avalonia.Controls return _displayMemberItemTemplate; } - private void UpdatePseudoClasses(int itemCount) + private void UpdatePseudoClasses() { - PseudoClasses.Set(":empty", itemCount == 0); - PseudoClasses.Set(":singleitem", itemCount == 1); + PseudoClasses.Set(":empty", ItemCount == 0); + PseudoClasses.Set(":singleitem", ItemCount == 1); } protected static IInputElement? GetNextControl( diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 416b909219..614b70d0ba 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Controls.Utils; @@ -17,15 +18,16 @@ namespace Avalonia.Controls /// and an items control. /// public class ItemsSourceView : IReadOnlyList, + IList, INotifyCollectionChanged, ICollectionChangedListener { /// - /// Gets an empty + /// Gets an empty /// - public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private readonly IList _inner; + private IList _source; private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged; @@ -35,30 +37,17 @@ namespace Avalonia.Controls /// Initializes a new instance of the ItemsSourceView class for the specified data source. /// /// The data source. - private protected ItemsSourceView(IEnumerable source) - { - _inner = source switch - { - ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), - IList list => list, - INotifyCollectionChanged => throw new ArgumentException( - "Collection implements INotifyCollectionChanged but not IList.", - nameof(source)), - IEnumerable iObj => new List(iObj), - null => throw new ArgumentNullException(nameof(source)), - _ => new List(source.Cast()) - }; - } + private protected ItemsSourceView(IEnumerable source) => SetSource(source); /// /// Gets the number of items in the collection. /// - public int Count => Inner.Count; + public int Count => Source.Count; /// - /// Gets the inner collection. + /// Gets the source collection. /// - public IList Inner => _inner; + public IList Source => _source; /// /// Retrieves the item at the specified index. @@ -67,12 +56,20 @@ namespace Avalonia.Controls /// The item. public object? this[int index] => GetAt(index); + bool IList.IsFixedSize => false; + bool IList.IsReadOnly => true; + bool ICollection.IsSynchronized => false; + object ICollection.SyncRoot => this; + + object? IList.this[int index] + { + get => GetAt(index); + set => ThrowReadOnly(); + } + /// - /// Gets a value that indicates whether the items source can provide a unique key for each item. - /// - /// /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. - /// + /// internal bool HasKeyIndexMapping => false; /// @@ -131,39 +128,14 @@ namespace Avalonia.Controls } } - private void AddListenerIfNecessary() - { - if (!_listening) - { - if (_inner is INotifyCollectionChanged incc) - CollectionChangedEventManager.Instance.AddListener(incc, this); - _listening = true; - } - } - - private void RemoveListenerIfNecessary() - { - if (_listening && _collectionChanged is null && _postCollectionChanged is null) - { - if (_inner is INotifyCollectionChanged incc) - CollectionChangedEventManager.Instance.RemoveListener(incc, this); - _listening = false; - } - } - /// /// Retrieves the item at the specified index. /// /// The index. /// The item. - public object? GetAt(int index) => Inner[index]; - - /// - /// Determines the index of a specific item in the collection. - /// - /// The object to locate in the collection. - /// The index of value if found in the list; otherwise, -1. - public int IndexOf(object? item) => Inner.IndexOf(item); + public object? GetAt(int index) => Source[index]; + public bool Contains(object? item) => Source.Contains(item); + public int IndexOf(object? item) => Source.IndexOf(item); /// /// Gets or creates an for the specified enumerable. @@ -201,7 +173,8 @@ namespace Avalonia.Controls { return items switch { - ItemsSourceView isv => isv, + ItemsSourceView isvt => isvt, + ItemsSourceView isv => new ItemsSourceView(isv.Source), null => ItemsSourceView.Empty, _ => new ItemsSourceView(items) }; @@ -236,7 +209,7 @@ namespace Avalonia.Controls yield return o; } - var inner = Inner; + var inner = Source; return inner switch { @@ -245,7 +218,7 @@ namespace Avalonia.Controls }; } - IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { @@ -262,15 +235,69 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } + int IList.Add(object? value) => ThrowReadOnly(); + void IList.Clear() => ThrowReadOnly(); + void IList.Insert(int index, object? value) => ThrowReadOnly(); + void IList.Remove(object? value) => ThrowReadOnly(); + void IList.RemoveAt(int index) => ThrowReadOnly(); + void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index); + /// - /// Retrieves the index of the item that has the specified unique identifier (key). + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// - /// The index. - /// The key - /// - /// TODO: Not yet implemented in Avalonia. - /// internal string KeyFromIndex(int index) => throw new NotImplementedException(); + + private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e) + { + _preCollectionChanged?.Invoke(this, e); + _collectionChanged?.Invoke(this, e); + _postCollectionChanged?.Invoke(this, e); + } + + [MemberNotNull(nameof(_source))] + private protected void SetSource(IEnumerable source) + { + if (_listening && _source is INotifyCollectionChanged inccOld) + CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); + + _source = source switch + { + ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged => throw new ArgumentException( + "Collection implements INotifyCollectionChanged but not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + null => throw new ArgumentNullException(nameof(source)), + _ => new List(source.Cast()) + }; + + if (_listening && _source is INotifyCollectionChanged inccNew) + CollectionChangedEventManager.Instance.AddListener(inccNew, this); + } + + private void AddListenerIfNecessary() + { + if (!_listening) + { + if (_source is INotifyCollectionChanged incc) + CollectionChangedEventManager.Instance.AddListener(incc, this); + _listening = true; + } + } + + private void RemoveListenerIfNecessary() + { + if (_listening && _collectionChanged is null && _postCollectionChanged is null) + { + if (_source is INotifyCollectionChanged incc) + CollectionChangedEventManager.Instance.RemoveListener(incc, this); + _listening = false; + } + } + + [DoesNotReturn] + private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only."); } public sealed class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -306,7 +333,7 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public new T GetAt(int index) => (T)Inner[index]!; + public new T GetAt(int index) => (T)Source[index]!; public new IEnumerator GetEnumerator() { @@ -316,7 +343,7 @@ namespace Avalonia.Controls yield return (T)o; } - var inner = Inner; + var inner = Source; return inner switch { @@ -325,6 +352,6 @@ namespace Avalonia.Controls }; } - IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); } } diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index a2df8c3e5f..1cf0202772 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters Debug.Assert(presenter.Panel is not null or VirtualizingPanel); _presenter = presenter; - _presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged; _presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged; OnItemsChanged(null, CollectionUtils.ResetEventArgs); @@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters { if (_presenter.ItemsControl is { } itemsControl) { - itemsControl.PropertyChanged -= OnItemsControlPropertyChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged; - ClearItemsControlLogicalChildren(); } @@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs); - private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == ItemsControl.ItemsProperty) - { - if (e.OldValue is INotifyCollectionChanged inccOld) - inccOld.CollectionChanged -= OnItemsChanged; - OnItemsChanged(null, CollectionUtils.ResetEventArgs); - if (e.NewValue is INotifyCollectionChanged inccNew) - inccNew.CollectionChanged += OnItemsChanged; - } - } - private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (_presenter.Panel is null || _presenter.ItemsControl is null) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2ee32b0dda..7716e32e26 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -145,6 +145,11 @@ namespace Avalonia.Controls.Primitives private BindingHelper? _bindingHelper; private bool _isSelectionChangeActive; + public SelectingItemsControl() + { + ItemsView.SourceChanged += OnItemsViewSourceChanged; + } + /// /// Initializes static members of the class. /// @@ -229,7 +234,7 @@ namespace Avalonia.Controls.Primitives /// property /// [AssignBinding] - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? SelectedValueBinding { get => GetValue(SelectedValueBindingProperty); @@ -322,7 +327,7 @@ namespace Avalonia.Controls.Primitives } else if (_selection != value) { - if (value.Source != null && value.Source != Items) + if (value.Source != null && value.Source != ItemsView.Source) { throw new ArgumentException( "The supplied ISelectionModel already has an assigned Source but this " + @@ -434,10 +439,9 @@ namespace Avalonia.Controls.Primitives return null; } - /// - protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender!, e); + base.OnItemsViewCollectionChanged(sender!, e); if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { @@ -547,7 +551,7 @@ namespace Avalonia.Controls.Primitives if (_selection is object) { - _selection.Source = Items; + _selection.Source = ItemsView.Source; } } @@ -635,16 +639,6 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } - if (change.Property == ItemsProperty && _updateState is null && _selection is object) - { - var newValue = change.GetNewValue(); - _selection.Source = newValue; - - if (newValue is null) - { - _selection.Clear(); - } - } else if (change.Property == SelectionModeProperty && _selection is object) { var newValue = change.GetNewValue(); @@ -880,6 +874,12 @@ namespace Avalonia.Controls.Primitives return false; } + private void OnItemsViewSourceChanged(object? sender, EventArgs e) + { + if (_selection is not null && _updateState is null) + _selection.Source = ItemsView.Source; + } + /// /// Called when is raised on /// . @@ -968,7 +968,7 @@ namespace Avalonia.Controls.Primitives /// The event args. private void OnSelectionModelLostSelection(object? sender, EventArgs e) { - if (AlwaysSelected && Items is object) + if (AlwaysSelected && ItemsView.Count > 0) { SelectedIndex = 0; } @@ -998,14 +998,14 @@ namespace Avalonia.Controls.Primitives } } - private object FindItemWithValue(object? value) + private object? FindItemWithValue(object? value) { if (ItemCount == 0 || value is null) { return AvaloniaProperty.UnsetValue; } - var items = Items; + var items = ItemsView; var binding = SelectedValueBinding; if (binding is null) @@ -1169,7 +1169,7 @@ namespace Avalonia.Controls.Primitives { if (_updateState is null) { - model.Source = Items; + model.Source = ItemsView.Source; } model.PropertyChanged += OnSelectionModelPropertyChanged; @@ -1236,9 +1236,9 @@ namespace Avalonia.Controls.Primitives SelectedItems = state.SelectedItems.Value; } - Selection.Source = Items; + Selection.Source = ItemsView.Source; - if (Items is null) + if (ItemsView.Count == 0) { Selection.Clear(); } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index d4c2b32974..68bad598d0 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection Source = source; } - public new IEnumerable? Source + public new IEnumerable? Source { - get => base.Source as IEnumerable; + get => base.Source; set => SetSource(value); } diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 3c1b1262ae..5f528e2c72 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -144,13 +144,13 @@ namespace Avalonia.Controls.Utils { get { - return SelectorControl?.Items; + return SelectorControl?.ItemsSource; } set { if (SelectorControl != null) { - SelectorControl.Items = value; + SelectorControl.ItemsSource = value; } } } diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index 7780843eb5..a95d4f1ffa 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -34,7 +34,8 @@ namespace Avalonia.Controls /// /// Gets the items to display. /// - protected IReadOnlyList Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty; + protected IReadOnlyList Items => (IReadOnlyList?)ItemsControl?.ItemsView ?? + Array.Empty(); /// /// Gets the that the panel is displaying items for. @@ -192,17 +193,13 @@ namespace Avalonia.Controls throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl"); ItemsControl = itemsControl; - ItemsControl.PropertyChanged += OnItemsControlPropertyChanged; ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged; } internal void Detach() { var itemsControl = EnsureItemsControl(); - - itemsControl.PropertyChanged -= OnItemsControlPropertyChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged; - ItemsControl = null; Children.Clear(); } @@ -216,20 +213,9 @@ namespace Avalonia.Controls return ItemsControl; } - private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == ItemsControl.ItemsViewProperty) - { - var (oldValue, newValue) = e.GetOldAndNewValue(); - oldValue.PostCollectionChanged -= OnItemsControlItemsChanged; - Refresh(); - newValue.PostCollectionChanged += OnItemsControlItemsChanged; - } - } - private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e); + OnItemsChanged(Items, e); } [DoesNotReturn] diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index e206d809d3..9e18285315 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -257,7 +257,7 @@ namespace Avalonia.Controls.UnitTests var target = new ComboBox { Template = GetTemplate(), - Items = items.Select(x => new ComboBoxItem { Content = x }) + Items = items.Select(x => new ComboBoxItem { Content = x }).ToList(), }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3aaf62f0bf..e7db6e3e67 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -16,6 +16,19 @@ namespace Avalonia.Controls.UnitTests { public class ItemsControlTests { + [Fact] + public void Setting_ItemsSource_Should_Populate_Items() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + ItemsSource = new[] { "foo", "bar" }, + }; + + Assert.Equal(target.ItemsSource, target.Items); + } + [Fact] public void Should_Use_ItemTemplate_To_Create_Control() { @@ -153,7 +166,7 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); Assert.Equal(child.Parent, target); Assert.Equal(child.GetLogicalParent(), target); @@ -206,11 +219,13 @@ namespace Avalonia.Controls.UnitTests { var target = new ItemsControl(); var child = new Control(); - var items = new AvaloniaList(child); target.Template = GetTemplate(); - target.Items = items; - items.RemoveAt(0); + target.Items.Add(child); + + Assert.Single(target.GetLogicalChildren()); + + target.Items.RemoveAt(0); Assert.Null(child.Parent); Assert.Null(child.GetLogicalParent()); @@ -224,8 +239,11 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; - target.Items = null; + target.Items.Add(child); + + Assert.Single(target.GetLogicalChildren()); + + target.Items.Clear(); Assert.Null(child.Parent); Assert.Null(((ILogical)child).LogicalParent); @@ -253,7 +271,7 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); // Should appear both before and after applying template. Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); @@ -299,7 +317,7 @@ namespace Avalonia.Controls.UnitTests [Fact] - public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged() + public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() { var target = new ItemsControl(); var child = new Control(); @@ -311,7 +329,7 @@ namespace Avalonia.Controls.UnitTests ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Add; - target.Items = new[] { child }; + target.Items.Add(child); Assert.True(called); } @@ -324,7 +342,7 @@ namespace Avalonia.Controls.UnitTests var called = false; target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); target.ApplyTemplate(); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => @@ -343,7 +361,7 @@ namespace Avalonia.Controls.UnitTests var called = false; target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); target.ApplyTemplate(); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; @@ -353,26 +371,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } - [Fact] - public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ItemsControl(); - var items = new AvaloniaList { "Foo" }; - var called = false; - - target.Template = GetTemplate(); - target.Items = items; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => - called = e.Action == NotifyCollectionChangedAction.Add; - - items.Add("Bar"); - - Assert.True(called); - } - [Fact] public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() { diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs index df842b21a7..faa143bb8e 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -38,6 +38,35 @@ namespace Avalonia.Controls.UnitTests Assert.Throws(() => ItemsSourceView.GetOrCreate(source)); } + [Fact] + public void Reassigning_Source_Unsubscribes_From_Previous_Source() + { + var source = new AvaloniaList(); + var target = new ReassignableItemsSourceView(source); + var debug = (INotifyCollectionChangedDebug)source; + + target.CollectionChanged += (s, e) => { }; + + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + + target.SetSource(new string[0]); + + Assert.Null(debug.GetCollectionChangedSubscribers()); + } + + [Fact] + public void Reassigning_Source_Subscribes_To_New_Source() + { + var source = new AvaloniaList(); + var target = new ReassignableItemsSourceView(new string[0]); + var debug = (INotifyCollectionChangedDebug)source; + + target.CollectionChanged += (s, e) => { }; + target.SetSource(source); + + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + } + private class InvalidCollection : INotifyCollectionChanged, IEnumerable { public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } } @@ -52,5 +81,15 @@ namespace Avalonia.Controls.UnitTests yield break; } } + + private class ReassignableItemsSourceView : ItemsSourceView + { + public ReassignableItemsSourceView(IEnumerable source) + : base(source) + { + } + + public new void SetSource(IEnumerable source) => base.SetSource(source); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..c1ec66b2e9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var itemsControl = new ItemsControl { - Items = items, + ItemsSource = items, Template = new FuncControlTemplate((_, _) => result) }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 0f72b2101a..e40ca44ba6 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -71,27 +71,25 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Logical_Children_Should_Be_TabItems() { - var items = new[] - { - new TabItem - { - Content = "foo" - }, - new TabItem - { - Content = "bar" - }, - }; - var target = new TabControl { Template = TabControlTemplate(), - Items = items, + Items = + { + new TabItem + { + Content = "foo" + }, + new TabItem + { + Content = "bar" + }, + } }; - Assert.Equal(items, target.GetLogicalChildren()); + Assert.Equal(target.Items.Cast(), target.GetLogicalChildren()); target.ApplyTemplate(); - Assert.Equal(items, target.GetLogicalChildren()); + Assert.Equal(target.Items.Cast(), target.GetLogicalChildren()); } [Fact] @@ -207,26 +205,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() { - var collection = new[] - { - new TabItem - { - Name = "first", - Content = "foo", - }, - new TabItem - { - Name = "second", - Content = "bar", - }, - new TabItem - { - Name = "3rd", - Content = "barf", - }, - }; - var template = new FuncControlTemplate((x, __) => new Decorator()); + TabControl target; var root = new TestRoot { Styles = @@ -239,13 +219,31 @@ namespace Avalonia.Controls.UnitTests } } }, - Child = new TabControl + Child = (target = new TabControl { Template = TabControlTemplate(), - Items = collection, - } + Items = + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }, + }) }; + var collection = target.Items.Cast().ToList(); Assert.Same(collection[0].Template, template); Assert.Same(collection[1].Template, template); Assert.Same(collection[2].Template, template); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2ca716fa8f..c397b0efb8 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1807,7 +1807,7 @@ namespace Avalonia.Controls.UnitTests return (TreeViewItem)c; } - private IList CreateTestTreeData() + private AvaloniaList CreateTestTreeData() { return new AvaloniaList { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs index ea6b9367cf..721e8bde68 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs @@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests { var carousel = new Carousel { - Items = items, + ItemsSource = items, Template = CarouselTemplate(), PageTransition = transition, }; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index ba8e7242a1..c3dafb02db 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -9,15 +9,12 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; -#nullable enable - namespace Avalonia.Controls.UnitTests { public class VirtualizingStackPanelTests @@ -99,7 +96,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (IList)itemsControl.Items!; + var items = (IList)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -131,7 +128,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (IList)itemsControl.Items!; + var items = (IList)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -161,7 +158,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (ObservableCollection)itemsControl.Items!; + var items = (ObservableCollection)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -190,7 +187,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (ObservableCollection)itemsControl.Items!; + var items = (ObservableCollection)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -473,7 +470,7 @@ namespace Avalonia.Controls.UnitTests var itemsControl = new ItemsControl { - Items = items, + ItemsSource = items, Template = new FuncControlTemplate((_, _) => scroll), ItemsPanel = new FuncTemplate(() => target), }; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 27634b457b..c37f779df9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1991,7 +1991,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public class DataGridLikeControl : Control { public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, + (x, v) => x.Items = v); private IEnumerable _items; public IEnumerable Items From 57c997bed79a73ffa4bdb6d10a04c24077aff744 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Mar 2023 19:36:38 +0100 Subject: [PATCH 13/82] Support multiple `InheritDataTypeFromItems`. To allow binding both `ItemsControl.Items` and `ItemsSource` we need to support multiple `InheritDataTypeFromItems` attributes. --- .../InheritDataTypeFromItemsAttribute.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 2 + .../Primitives/SelectingItemsControl.cs | 1 + ...valoniaXamlIlDataContextTypeTransformer.cs | 43 +++++++++++-------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index fac8cd8737..e9bd6ab89f 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// A typical usage example is a ListBox control, where is defined on the ItemTemplate property, /// allowing the template to inherit the data type from the Items collection binding. /// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public sealed class InheritDataTypeFromItemsAttribute : Attribute { /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 56ba9c7183..4914a833de 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -95,6 +95,7 @@ namespace Avalonia.Controls /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); @@ -218,6 +219,7 @@ namespace Avalonia.Controls /// Gets or sets the data template used to display the items in the control. /// [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7716e32e26..a31472fab4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -235,6 +235,7 @@ namespace Avalonia.Controls.Primitives /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IBinding? SelectedValueBinding { get => GetValue(SelectedValueBindingProperty); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index a24d4eb6e9..681d2a38d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -73,27 +73,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // Infer data type from collection binding on a control that displays items. var property = context.ParentNodes().OfType().FirstOrDefault(); var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute; - var attribute = property?.Property?.GetClrProperty().CustomAttributes - .FirstOrDefault(a => a.Type == attributeType); - - if (attribute is not null) + var attributes = property?.Property?.GetClrProperty().CustomAttributes + .Where(a => a.Type == attributeType).ToList(); + + if (attributes?.Count > 0) { - var propertyName = (string)attribute.Parameters.First(); - XamlAstConstructableObjectNode parentObject; - if (attribute.Properties.TryGetValue("AncestorType", out var type) - && type is IXamlType xamlType) - { - parentObject = context.ParentNodes().OfType() - .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); - } - else + foreach (var attribute in attributes) { - parentObject = context.ParentNodes().OfType().FirstOrDefault(); - } - - if (parentObject != null) - { - inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); + var propertyName = (string)attribute.Parameters.First(); + XamlAstConstructableObjectNode parentObject; + if (attribute.Properties.TryGetValue("AncestorType", out var type) + && type is IXamlType xamlType) + { + parentObject = context.ParentNodes().OfType() + .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); + } + else + { + parentObject = context.ParentNodes().OfType().FirstOrDefault(); + } + + if (parentObject != null) + { + inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); + if (inferredDataContextTypeNode != null) + break; + } } } From 0bbb66eeebe26975233f8ec74b8b615d2e0b5979 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 11:01:09 +0100 Subject: [PATCH 14/82] Fix failing selection tests. --- .../Primitives/SelectingItemsControl.cs | 16 ++++++----- .../Selection/InternalSelectionModel.cs | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a31472fab4..cb4efb344f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1232,16 +1232,18 @@ namespace Avalonia.Controls.Primitives Selection = state.Selection.Value; } - if (state.SelectedItems.HasValue) + if (_selection is InternalSelectionModel s) { - SelectedItems = state.SelectedItems.Value; + s.Update(ItemsView.Source, state.SelectedItems); } - - Selection.Source = ItemsView.Source; - - if (ItemsView.Count == 0) + else { - Selection.Clear(); + if (state.SelectedItems.HasValue) + { + SelectedItems = state.SelectedItems.Value; + } + + Selection.Source = ItemsView.Source; } if (state.SelectedValue.HasValue) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d0e6144f59..c8ad9bd88b 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; +using Avalonia.Data; namespace Avalonia.Controls.Selection { @@ -13,6 +14,7 @@ namespace Avalonia.Controls.Selection private IList? _writableSelectedItems; private int _ignoreModelChanges; private bool _ignoreSelectedItemsChanges; + private bool _skipSyncFromSelectedItems; private bool _isResetting; public InternalSelectionModel() @@ -60,6 +62,29 @@ namespace Avalonia.Controls.Selection } } + internal void Update(IEnumerable? source, Optional selectedItems) + { + var previousSource = Source; + var previousWritableSelectedItems = _writableSelectedItems; + + try + { + _skipSyncFromSelectedItems = true; + SetSource(source); + if (selectedItems.HasValue) + WritableSelectedItems = selectedItems.Value; + } + finally + { + _skipSyncFromSelectedItems = false; + } + + // We skipped the sync from WritableSelectedItems before; do it now that both + // the source and WritableSelectedItems are updated. + if (previousSource != Source || previousWritableSelectedItems != _writableSelectedItems) + SyncFromSelectedItems(); + } + private protected override void SetSource(IEnumerable? value) { if (Source == value) @@ -121,7 +146,7 @@ namespace Avalonia.Controls.Selection private void SyncFromSelectedItems() { - if (Source is null || _writableSelectedItems is null) + if (_skipSyncFromSelectedItems || Source is null || _writableSelectedItems is null) { return; } From 1143b335d6a87fe30cef49b74c88311c56657085 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 13:15:22 +0100 Subject: [PATCH 15/82] Add nullable reference checking to X11Window. --- src/Avalonia.Controls/Platform/IPopupImpl.cs | 2 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 2 +- src/Avalonia.X11/X11Window.cs | 76 +++++++++++-------- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index cd86045dee..320130bc91 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Platform [Unstable] public interface IPopupImpl : IWindowBaseImpl { - IPopupPositioner PopupPositioner { get; } + IPopupPositioner? PopupPositioner { get; } void SetWindowManagerAddShadowHint(bool enabled); } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index b3436d4176..952ba92e9b 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives private void UpdatePosition() { - PlatformImpl?.PopupPositioner.Update(_positionerParameters); + PlatformImpl?.PopupPositioner?.Update(_positionerParameters); } public void ConfigurePosition(Visual target, PlacementMode placement, Point offset, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6634ab4d7b..22748f6aa3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -25,6 +25,9 @@ using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo + +#nullable enable + namespace Avalonia.X11 { internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client @@ -35,11 +38,11 @@ namespace Avalonia.X11 private XConfigureEvent? _configure; private PixelPoint? _configurePoint; private bool _triggeredExpose; - private IInputRoot _inputRoot; + private IInputRoot? _inputRoot; private readonly MouseDevice _mouse; private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; - private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; + private readonly ITopLevelNativeMenuExporter? _nativeMenuExporter; private readonly IStorageProvider _storageProvider; private readonly X11NativeControlHost _nativeControlHost; private PixelPoint? _position; @@ -54,8 +57,8 @@ namespace Avalonia.X11 private bool _wasMappedAtLeastOnce = false; private double? _scalingOverride; private bool _disabled; - private TransparencyHelper _transparencyHelper; - private RawEventGrouper _rawEventGrouper; + private TransparencyHelper? _transparencyHelper; + private RawEventGrouper? _rawEventGrouper; private bool _useRenderWindow = false; private bool _usePositioningFlags = false; @@ -66,7 +69,7 @@ namespace Avalonia.X11 WaitPaint } - public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent) { _platform = platform; _popup = popupParent != null; @@ -196,7 +199,7 @@ namespace Avalonia.X11 XFlush(_x11.Display); if(_popup) - PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent!, MoveResize)); if (platform.Options.UseDBusMenu) _nativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); _nativeControlHost = new X11NativeControlHost(_platform, this); @@ -214,7 +217,7 @@ namespace Avalonia.X11 _storageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), () => GtkSystemDialog.TryCreate(this) }); } @@ -351,17 +354,17 @@ namespace Avalonia.X11 public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } //TODO - public Action ScalingChanged { get; set; } - public Action Deactivated { get; set; } - public Action Activated { get; set; } - public Func Closing { get; set; } - public Action WindowStateChanged { get; set; } + public Action? ScalingChanged { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } + public Func? Closing { get; set; } + public Action? WindowStateChanged { get; set; } - public Action TransparencyLevelChanged + public Action? TransparencyLevelChanged { get => _transparencyHelper?.TransparencyLevelChanged; set @@ -371,7 +374,7 @@ namespace Avalonia.X11 } } - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins { get; } = new Thickness(); @@ -379,15 +382,18 @@ namespace Avalonia.X11 public bool IsClientAreaExtendedToDecorations { get; } - public Action Closed { get; set; } - public Action PositionChanged { get; set; } - public Action LostFocus { get; set; } + public Action? Closed { get; set; } + public Action? PositionChanged { get; set; } + public Action? LostFocus { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, _platform.Compositor, () => Surfaces); private void OnEvent(ref XEvent ev) { + if (_inputRoot is null) + return; + if (ev.type == XEventName.MapNotify) { _mapped = true; @@ -434,7 +440,8 @@ namespace Avalonia.X11 2 => RawPointerEventType.MiddleButtonDown, 3 => RawPointerEventType.RightButtonDown, 8 => RawPointerEventType.XButton1Down, - 9 => RawPointerEventType.XButton2Down + 9 => RawPointerEventType.XButton2Down, + _ => throw new NotSupportedException("Unexepected RawPointerEventType.") }, ref ev, ev.ButtonEvent.state); else @@ -462,7 +469,8 @@ namespace Avalonia.X11 2 => RawPointerEventType.MiddleButtonUp, 3 => RawPointerEventType.RightButtonUp, 8 => RawPointerEventType.XButton1Up, - 9 => RawPointerEventType.XButton2Up + 9 => RawPointerEventType.XButton2Up, + _ => throw new NotSupportedException("Unexepected RawPointerEventType.") }, ref ev, ev.ButtonEvent.state); } @@ -618,7 +626,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -712,6 +720,8 @@ namespace Avalonia.X11 private void DispatchInput(RawInputEventArgs args) { + if (_inputRoot is null) + return; Input?.Invoke(args); if (!args.Handled && args is RawKeyEventArgsWithText text && !string.IsNullOrEmpty(text.Text)) Input?.Invoke(new RawTextInputEventArgs(_keyboard, args.Timestamp, _inputRoot, text.Text)); @@ -744,11 +754,13 @@ namespace Avalonia.X11 if (args is RawDragEvent drag) drag.Location = drag.Location / RenderScaling; - _rawEventGrouper.HandleEvent(args); + _rawEventGrouper?.HandleEvent(args); } private void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods) { + if (_inputRoot is null) + return; var mev = new RawPointerEventArgs( _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot, type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); @@ -783,7 +795,7 @@ namespace Avalonia.X11 } - public IInputRoot InputRoot => _inputRoot; + public IInputRoot? InputRoot => _inputRoot; public void SetInputRoot(IInputRoot inputRoot) { @@ -795,7 +807,7 @@ namespace Avalonia.X11 Cleanup(); } - public virtual object TryGetFeature(Type featureType) + public virtual object? TryGetFeature(Type featureType) { if (featureType == typeof(ITopLevelNativeMenuExporter)) { @@ -953,7 +965,7 @@ namespace Avalonia.X11 UpdateSizeHints(null); } - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { if (cursor == null) XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor); @@ -996,7 +1008,7 @@ namespace Avalonia.X11 public IMouseDevice MouseDevice => _mouse; public TouchDevice TouchDevice => _touch; - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); public void Activate() @@ -1082,7 +1094,7 @@ namespace Avalonia.X11 BeginMoveResize(side, e); } - public void SetTitle(string title) + public void SetTitle(string? title) { if (string.IsNullOrEmpty(title)) { @@ -1161,9 +1173,9 @@ namespace Avalonia.X11 { } - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { if (icon != null) { @@ -1218,7 +1230,7 @@ namespace Avalonia.X11 ); } - public IPopupPositioner PopupPositioner { get; } + public IPopupPositioner? PopupPositioner { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper?.SetTransparencyRequest(transparencyLevel); From a76eb7f58755f872d68693b24b58cb53e2905454 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 8 Mar 2023 14:11:15 +0100 Subject: [PATCH 16/82] Add additional null check as Tip may be null in some situations see https://github.com/AvaloniaUI/Avalonia/issues/10598 --- src/Avalonia.Controls/ToolTipService.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 9ec9013679..d983309a72 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -42,11 +42,12 @@ namespace Avalonia.Controls { Close(control); } - else + else { - var tip = control.GetValue(ToolTip.ToolTipProperty); - - tip!.Content = e.NewValue; + if (control.GetValue(ToolTip.ToolTipProperty) is { } tip) + { + tip.Content = e.NewValue; + } } } } From 59b1ba6d46680dd41a579394fac3ddddcc0e77fb Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 8 Mar 2023 14:18:21 +0100 Subject: [PATCH 17/82] Invariant family names --- src/Avalonia.Base/Media/FontManager.cs | 8 +++++--- .../Media/Fonts/EmbeddedFontCollection.cs | 15 ++++++++++----- .../Media/Fonts/SystemFontCollection.cs | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 595a2f3474..1c051e19e7 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -119,7 +119,9 @@ namespace Avalonia.Media } } - if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, + var familyName = fontFamily.FamilyNames.PrimaryFamilyName.ToUpperInvariant(); + + if (fontCollection != null && fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; @@ -133,13 +135,13 @@ namespace Avalonia.Media foreach (var familyName in fontFamily.FamilyNames) { - if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + if (SystemFonts.TryGetGlyphTypeface(familyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; } } - return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); + return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); } /// diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index f2fb490592..3350358d68 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -43,11 +42,13 @@ namespace Avalonia.Media.Fonts if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) { - if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + var familyName = glyphTypeface.FamilyName.ToUpperInvariant(); + + if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { glyphTypefaces = new ConcurrentDictionary(); - if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) + if (_glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) { _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); } @@ -86,6 +87,8 @@ namespace Avalonia.Media.Fonts public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { + familyName = familyName.ToUpperInvariant(); + var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) @@ -101,9 +104,11 @@ namespace Avalonia.Media.Fonts { var fontFamily = _fontFamilies[i]; - if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) + if (fontFamily.Name.ToUpperInvariant().StartsWith(familyName.ToUpperInvariant())) { - if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && + familyName = fontFamily.Name.ToUpperInvariant(); + + if (_glyphTypefaceCache.TryGetValue(familyName, out glyphTypefaces) && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) { return true; diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index fd332c6ebe..1687deb37b 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -42,6 +42,8 @@ namespace Avalonia.Media.Fonts familyName = _fontManager.DefaultFontFamilyName; } + familyName = familyName.ToUpperInvariant(); + var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) From d2bc8a09c94450a2c1d9414d7bc285891a99bf41 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 14:35:21 +0100 Subject: [PATCH 18/82] Update ncrunch config. --- .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject | 5 +++++ .ncrunch/Generators.Sandbox.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject create mode 100644 .ncrunch/Generators.Sandbox.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject b/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Generators.Sandbox.v3.ncrunchproject b/.ncrunch/Generators.Sandbox.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Generators.Sandbox.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From b917cc5a2e6c4c76d2855d4f85894b6b4e1e3dd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:06:23 +0100 Subject: [PATCH 19/82] Added some XML docs. --- src/Avalonia.Controls/ItemCollection.cs | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs index 120aef41dc..4daea44d5e 100644 --- a/src/Avalonia.Controls/ItemCollection.cs +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -6,6 +6,9 @@ using Avalonia.Collections; namespace Avalonia.Controls { + /// + /// Holds the list of items that constitute the content of an . + /// public class ItemCollection : ItemsSourceView, IList { // Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique. @@ -30,11 +33,55 @@ namespace Avalonia.Controls internal event EventHandler? SourceChanged; + /// + /// Adds an item to the . + /// + /// The item to add to the collection. + /// + /// The position into which the new element was inserted, or -1 to indicate that + /// the item was not inserted into the collection. + /// + /// + /// The collection is in ItemsSource mode. + /// public int Add(object? value) => WritableSource.Add(value); + + /// + /// Clears the collection and releases the references on all items currently in the + /// collection. + /// + /// + /// The collection is in ItemsSource mode. + /// public void Clear() => WritableSource.Clear(); + + /// + /// Inserts an element into the collection at the specified index. + /// + /// The zero-based index at which to insert the item. + /// The item to insert. + /// + /// The collection is in ItemsSource mode. + /// public void Insert(int index, object? value) => WritableSource.Insert(index, value); + + /// + /// Removes the item at the specified index of the collection or view. + /// + /// The zero-based index of the item to remove. + /// + /// The collection is in ItemsSource mode. + /// public void RemoveAt(int index) => WritableSource.RemoveAt(index); + /// + /// Removes the specified item reference from the collection or view. + /// + /// The object to remove. + /// True if the item was removed; otherwise false. + /// + /// The collection is in ItemsSource mode. + /// public bool Remove(object? value) { var c = Count; From 254d58da6ad2e053e7f62a2b1fdb6d1d32e36475 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:06:46 +0100 Subject: [PATCH 20/82] Change type of ItemsView property. It's a view so makes sense to expose the read-only view interface. --- src/Avalonia.Controls/ItemsControl.cs | 5 ++++- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bba551b477..c0b9ded2ee 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -236,7 +236,10 @@ namespace Avalonia.Controls /// public Panel? ItemsPanelRoot => Presenter?.Panel; - public ItemCollection ItemsView => _items; + /// + /// Gets a read-only view of the items in the . + /// + public ItemsSourceView ItemsView => _items; private protected bool WrapFocus { get; set; } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index cb4efb344f..b89a75787f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls.Primitives public SelectingItemsControl() { - ItemsView.SourceChanged += OnItemsViewSourceChanged; + ((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged; } /// From 2c56019d77688aa05ec69f0518fbfeec519e8b7b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:13:50 +0100 Subject: [PATCH 21/82] Update ItemsControl XML docs. --- src/Avalonia.Controls/ItemsControl.cs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c0b9ded2ee..b476df5fda 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -132,6 +132,29 @@ namespace Avalonia.Controls /// /// Gets or sets the items to display. /// + /// + /// Since Avalonia 11, has both an property + /// and an property. The properties have the following differences: + /// + /// + /// is initialized with an empty collection and is a direct property, + /// meaning that it cannot be styled + /// is by default null, and is a styled property. This property + /// is marked as the content property and will be used for items added via inline XAML. + /// + /// + /// In Avalonia 11 the two properties can be used almost interchangeably but this will change + /// in a later version. In order to be ready for this change, follow the following guidance: + /// + /// + /// You should use the property when you're assigning a collection of + /// item containers directly, for example adding a collection of s + /// directly to a . Add the containers to the pre-existing list, do not + /// reassign the property via the setter or with a binding. + /// You should use the property when you're assigning or + /// binding a collection of models which will be transformed by a data template. + /// + /// [Content] public IList? Items { @@ -204,7 +227,8 @@ namespace Avalonia.Controls /// /// You should use the property when you're assigning a collection of /// item containers directly, for example adding a collection of s - /// directly to a . + /// directly to a . Add the containers to the pre-existing list, do not + /// reassign the property via the setter or with a binding. /// You should use the property when you're assigning or /// binding a collection of models which will be transformed by a data template. /// From 9856711494f14cbf7d59cbd3f9ddc1dda7112cfc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:27:04 +0100 Subject: [PATCH 22/82] Don't modify logical tree in ItemsSource mode. --- src/Avalonia.Controls/ItemsControl.cs | 17 +++++----- .../ItemsControlTests.cs | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b476df5fda..ee2899e50c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -573,15 +573,18 @@ namespace Avalonia.Controls /// The event args. private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - switch (e.Action) + if (!_items.IsReadOnly) { - case NotifyCollectionChangedAction.Add: - AddControlItemsToLogicalChildren(e.NewItems); - break; + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddControlItemsToLogicalChildren(e.NewItems); + break; - case NotifyCollectionChangedAction.Remove: - RemoveControlItemsFromLogicalChildren(e.OldItems); - break; + case NotifyCollectionChangedAction.Remove: + RemoveControlItemsFromLogicalChildren(e.OldItems); + break; + } } ItemCount = ItemsView.Count; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 9b28ca11f0..12fc0a82ed 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -249,6 +249,37 @@ namespace Avalonia.Controls.UnitTests Assert.Null(((ILogical)child).LogicalParent); } + [Fact] + public void Assigning_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; + + var list = new AvaloniaList(new[] { child }); + target.ItemsSource = list; + + Assert.False(called); + } + + [Fact] + public void Changing_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; + + var list = new AvaloniaList(); + target.ItemsSource = list; + list.Add(child); + + Assert.False(called); + } + [Fact] public void Clearing_Items_Should_Clear_Child_Controls_Parent() { From 183fed8985287500d3687f5980a5c01f4a2822bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:34:50 +0100 Subject: [PATCH 23/82] Add more tests. One failing. --- .../ItemsControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 12fc0a82ed..83b82eb1d4 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -26,9 +26,36 @@ namespace Avalonia.Controls.UnitTests ItemsSource = new[] { "foo", "bar" }, }; + Assert.NotSame(target.ItemsSource, target.Items); Assert.Equal(target.ItemsSource, target.Items); } + [Fact] + public void Cannot_Set_ItemsSource_With_Items_Present() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + Items = { "foo", "bar" }, + }; + + Assert.Throws(() => target.ItemsSource = new[] { "baz" }); + } + + [Fact] + public void Cannot_Modify_Items_When_ItemsSource_Set() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + ItemsSource = Array.Empty(), + }; + + Assert.Throws(() => target.Items.Add("foo")); + } + [Fact] public void Should_Use_ItemTemplate_To_Create_Control() { From ddacc112acfc482bb35498bb63a0ce38db797ca0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:35:22 +0100 Subject: [PATCH 24/82] Throw if setting ItemsSource when Items present. --- src/Avalonia.Controls/ItemCollection.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs index 4daea44d5e..c9265558f0 100644 --- a/src/Avalonia.Controls/ItemCollection.cs +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -121,6 +121,10 @@ namespace Avalonia.Controls internal void SetItemsSource(IEnumerable? value) { + if (_mode != Mode.ItemsSource && Count > 0) + throw new InvalidOperationException( + "Items collection must be empty before using ItemsSource."); + _mode = value is not null ? Mode.ItemsSource : Mode.Items; SetSource(value ?? CreateDefaultCollection()); } From 59688302967ebbf541053b8d0392db84e8bec82c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:19 +0900 Subject: [PATCH 25/82] Update API, samples and BCL impl --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 4 ++-- samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs | 8 ++++++-- .../Platform/Storage/FileIO/BclStorageFolder.cs | 12 +++++++----- src/Avalonia.Base/Platform/Storage/IStorageFolder.cs | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e5f29abb68..02e3027aa6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -324,9 +324,9 @@ namespace ControlCatalog.Pages mappedResults.Add("+> " + FullPathOrName(selectedItem)); if (selectedItem is IStorageFolder folder) { - foreach (var innerItems in await folder.GetItemsAsync()) + await foreach (var innerItem in folder.GetItemsAsync()) { - mappedResults.Add("++> " + FullPathOrName(innerItems)); + mappedResults.Add("++> " + FullPathOrName(innerItem)); } } } diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 26430b4b61..7fb5bec589 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -104,8 +104,12 @@ namespace ControlCatalog.Pages } else if (item is IStorageFolder folder) { - var items = await folder.GetItemsAsync(); - contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + var childrenCount = 0; + await foreach (var _ in folder.GetItemsAsync()) + { + childrenCount++; + } + contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}"; } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index d8e3d91f75..e6551390d6 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -57,14 +57,16 @@ internal class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } - public Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - var items = DirectoryInfo.GetDirectories() + var items = DirectoryInfo.EnumerateDirectories() .Select(d => (IStorageItem)new BclStorageFolder(d)) - .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f))) - .ToArray(); + .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f))); - return Task.FromResult>(items); + foreach (var item in items) + { + yield return item; + } } public virtual Task SaveBookmarkAsync() diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 0ffb9f41c6..52b3256387 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -16,5 +16,5 @@ public interface IStorageFolder : IStorageItem /// /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. /// - Task> GetItemsAsync(); + IAsyncEnumerable GetItemsAsync(); } From 6412da10cf5f04bdf75f0eec57dbe7570fb99791 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:27 +0900 Subject: [PATCH 26/82] Update mobile impl --- .../Platform/Storage/AndroidStorageItem.cs | 16 ++++++---------- src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs | 9 +++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 0a34e6077c..8052b3911a 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -131,19 +131,17 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return Task.FromResult(new StorageItemProperties()); } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { if (!await EnsureExternalFilesPermission(false)) { - return Array.Empty(); + yield break; } - - List files = new List(); - + var contentResolver = Activity.ContentResolver; if (contentResolver == null) { - return files; + yield break; } var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri)); @@ -168,12 +166,10 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder continue; } - files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : - new AndroidStorageFile(Activity, uri)); + yield return mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : + new AndroidStorageFile(Activity, uri); } } - - return files; } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 27bd8faf64..baf31ebc73 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -114,8 +114,9 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { + // TODO: find out if it can be lazily enumerated. var tcs = new TaskCompletionSource>(); new NSFileCoordinator().CoordinateRead(Url, @@ -142,6 +143,10 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder throw new NSErrorException(error); } - return await tcs.Task; + var items = await tcs.Task; + foreach (var item in items) + { + yield return item; + } } } From 2fd5e6b9d9248195b1248036d9d2fc60b04cc65b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:41 +0900 Subject: [PATCH 27/82] Make browser GetItemsAsync trully async and lazy --- .../Interop/GeneralHelpers.cs | 16 +++++-- .../Avalonia.Browser/Interop/StorageHelper.cs | 6 +-- .../Storage/BrowserStorageProvider.cs | 47 ++++++++++++++----- .../webapp/modules/avalonia/generalHelpers.ts | 7 ++- .../webapp/modules/storage/storageItem.ts | 10 ++-- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs index 6e3b41c05b..67d1cfb776 100644 --- a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; namespace Avalonia.Browser.Interop; @@ -8,15 +9,22 @@ internal static partial class GeneralHelpers public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + [JSImport("GeneralHelpers.itemAt", AvaloniaModule.MainModuleName)] + public static partial JSObject ItemAtInt(JSObject jsObject, int key); + public static JSObject GetArrayItem(this JSObject jsObject, int key) => ItemAtInt(jsObject, key); + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodString(JSObject jsObject, string name); + public static partial string IntCallMethodStr(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStrStr(JSObject jsObject, string name, string arg1); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + public static partial Task IntCallMethodPromiseObj(JSObject jsObject, string name); - public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); - public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodStr(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStrStr(jsObject, name, arg1); + public static Task CallMethodObjectAsync(this JSObject jsObject, string name) => IntCallMethodPromiseObj(jsObject, name); } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 2d96ee8d1f..dc3372d2d0 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -40,9 +40,9 @@ internal static partial class StorageHelper [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)] public static partial Task OpenRead(JSObject item); - [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] - [return: JSMarshalAs>] - public static partial Task GetItems(JSObject item); + [JSImport("StorageItem.getItemsIterator", AvaloniaModule.StorageModuleName)] + [return: JSMarshalAs] + public static partial JSObject? GetItemsIterator(JSObject item); [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index fc32b3b4f7..fcb956f294 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -258,24 +258,45 @@ internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - using var items = await StorageHelper.GetItems(FileHandle); - if (items is null) + using var itemsIterator = StorageHelper.GetItemsIterator(FileHandle); + if (itemsIterator is null) { - return Array.Empty(); + yield break; } - var itemsArray = StorageHelper.ItemsArray(items); + while (true) + { + var nextResult = await itemsIterator.CallMethodObjectAsync("next"); + if (nextResult is null) + { + yield break; + } + + var isDone = nextResult.GetPropertyAsBoolean("done"); + if (isDone) + { + yield break; + } - return itemsArray - .Select(reference => reference.GetPropertyAsString("kind") switch + var valArray = nextResult.GetPropertyAsJSObject("value"); + var storageItem = valArray?.GetArrayItem(1); // 0 - item name, 1 - item instance + if (storageItem is null) { - "directory" => (IStorageItem)new JSStorageFolder(reference), - "file" => new JSStorageFile(reference), - _ => null - }) - .Where(i => i is not null) - .ToArray()!; + yield break; + } + + var kind = storageItem.GetPropertyAsString("kind"); + switch (kind) + { + case "directory": + yield return new JSStorageFolder(storageItem); + break; + case "file": + yield return new JSStorageFile(storageItem); + break; + } + } } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts index fa001006ab..31d167e38d 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -1,5 +1,5 @@ export class GeneralHelpers { - public static itemsArrayAt(instance: any, key: string): any[] { + public static itemsArrayAt(instance: any, key: any): any[] { const items = instance[key]; if (!items) { return []; @@ -12,6 +12,11 @@ export class GeneralHelpers { return retItems; } + public static itemAt(instance: any, key: any): any { + const item = instance[key]; + return item; + } + public static callMethod(instance: any, name: string /*, args */): any { const args = Array.prototype.slice.call(arguments, 2); return instance[name].apply(instance, args); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index f444717094..399e268915 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -89,16 +89,12 @@ export class StorageItem { } } - public static async getItems(item: StorageItem): Promise { + public static getItemsIterator(item: StorageItem): any | null { if (item.kind !== "directory" || !item.handle) { - return new StorageItems([]); + return null; } - const items: StorageItem[] = []; - for await (const [, value] of (item.handle as any).entries()) { - items.push(new StorageItem(value)); - } - return new StorageItems(items); + return (item.handle as any).entries(); } private async verityPermissions(mode: "read" | "readwrite"): Promise { From ad220a6af67f5498ec312bcf9ce152889fe3c7f1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 17:14:38 +0100 Subject: [PATCH 28/82] fix: CS0618 'DataFormats.FileNames' is obsolete: 'Use DataFormats.Files Warning CS0618 'DataFormats.FileNames' is obsolete: 'Use DataFormats.Files, this format is supported only on desktop platforms.' ControlCatalog (net6.0), ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\ClipboardPage.xaml.cs 69 Active Warning CS0618 'DataFormats.FileNames' is obsolete: 'Use DataFormats.Files, this format is supported only on desktop platforms.' ControlCatalog (net6.0), ControlCatalog (netstandard2.0) .\samples\ControlCatalog\Pages\ClipboardPage.xaml.cs 78 Active --- .../Pages/ClipboardPage.xaml.cs | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs index ef3d2bbafa..f3890aa49a 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs @@ -1,16 +1,22 @@ using System; using System.Collections.Generic; - +using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.Platform; +using Avalonia.Platform.Storage; namespace ControlCatalog.Pages { public partial class ClipboardPage : UserControl { + private INotificationManager? _notificationManager; + private INotificationManager NotificationManager => _notificationManager + ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!); public ClipboardPage() { InitializeComponent(); @@ -31,7 +37,7 @@ namespace ControlCatalog.Pages private async void PasteText(object? sender, RoutedEventArgs args) { - if(Application.Current!.Clipboard is { } clipboard) + if (Application.Current!.Clipboard is { } clipboard) { ClipboardContent.Text = await clipboard.GetTextAsync(); } @@ -59,15 +65,45 @@ namespace ControlCatalog.Pages { if (Application.Current!.Clipboard is { } clipboard) { - var files = (ClipboardContent.Text ?? String.Empty) - .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - if (files.Length == 0) + var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider; + var filesPath = (ClipboardContent.Text ?? string.Empty) + .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + if (filesPath.Length == 0) { return; } - var dataObject = new DataObject(); - dataObject.Set(DataFormats.FileNames, files); - await clipboard.SetDataObjectAsync(dataObject); + List invalidFile = new(filesPath.Length); + List files = new(filesPath.Length); + + for (int i = 0; i < filesPath.Length; i++) + { + var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]); + if (file is null) + { + invalidFile.Add(filesPath[i]); + } + else + { + files.Add(file); + } + } + + if (invalidFile.Count > 0) + { + NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning)); + } + + if (files.Count > 0) + { + var dataObject = new DataObject(); + dataObject.Set(DataFormats.Files, files); + await clipboard.SetDataObjectAsync(dataObject); + NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success)); + } + else + { + NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning)); + } } } @@ -75,8 +111,9 @@ namespace ControlCatalog.Pages { if (Application.Current!.Clipboard is { } clipboard) { - var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable; - ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty; + var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable; + + ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.Path)) : string.Empty; } } @@ -95,7 +132,7 @@ namespace ControlCatalog.Pages { await clipboard.ClearAsync(); } - + } } } From b9171f32f2dcde2eff9a8d966aa85c388326b437 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 16:06:03 +0100 Subject: [PATCH 29/82] Fix compile error. --- samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d759597b5..47f97e63a3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.SystemFonts; + fontComboBox.ItemsSource = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } From f8eceb4af9f78d9a9da582885593fc957ef8fffe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 16:18:36 +0100 Subject: [PATCH 30/82] Update usages of ItemsControl Items/ItemsSource. - Use `ItemsSource` when appropriate - When `Items` is appropriate, don't use the setter --- samples/ControlCatalog/MainView.xaml | 2 +- .../Pages/CompositionPage.axaml.cs | 2 +- .../Pages/ContextFlyoutPage.xaml | 2 +- .../ControlCatalog/Pages/ContextMenuPage.xaml | 4 +- samples/ControlCatalog/Pages/CursorPage.xaml | 2 +- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 12 +- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- samples/ControlCatalog/Pages/MenuPage.xaml | 6 +- .../Pages/NativeEmbedPage.xaml.cs | 8 +- .../Pages/NumericUpDownPage.xaml | 6 +- .../Pages/RefreshContainerPage.axaml | 2 +- .../ControlCatalog/Pages/ScrollSnapPage.xaml | 4 +- .../Pages/ScrollViewerPage.xaml | 4 +- .../ControlCatalog/Pages/TabStripPage.xaml | 2 +- .../ControlCatalog/Pages/ThemePage.axaml.cs | 2 +- .../TransitioningContentControlPage.axaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml | 2 +- samples/IntegrationTestApp/MainWindow.axaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 2 +- .../Themes/Simple/ColorPicker.xaml | 2 +- .../Themes/Simple/ColorView.xaml | 2 +- src/Avalonia.Controls/ItemsControl.cs | 1 + .../Diagnostics/Views/ConsoleView.xaml | 2 +- .../Diagnostics/Views/ControlDetailsView.xaml | 6 +- .../Diagnostics/Views/EventsPageView.xaml | 6 +- .../Views/PropertyValueEditorView.cs | 2 +- .../Diagnostics/Views/TreePageView.xaml | 2 +- .../Controls/DataValidationErrors.xaml | 4 +- .../Controls/ManagedFileChooser.xaml | 6 +- .../Controls/NativeMenuBar.xaml | 4 +- .../Controls/DataValidationErrors.xaml | 2 +- .../Controls/ManagedFileChooser.xaml | 6 +- .../Controls/NativeMenuBar.xaml | 4 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 2 +- .../Win32NativeToManagedMenuExporter.cs | 2 +- .../CarouselTests.cs | 20 +- .../ComboBoxTests.cs | 88 ++++--- .../ItemsControlTests.cs | 113 +++------ .../ListBoxTests.cs | 43 ++-- .../ListBoxTests_Multiple.cs | 6 +- .../ListBoxTests_Single.cs | 24 +- .../MenuItemTests.cs | 10 +- .../Presenters/ItemsPresenterTests.cs | 2 +- .../Primitives/PopupTests.cs | 4 +- .../Primitives/SelectingItemsControlTests.cs | 166 ++++++------- .../SelectingItemsControlTests_AutoSelect.cs | 12 +- .../SelectingItemsControlTests_Multiple.cs | 231 +++++++++--------- ...electingItemsControlTests_SelectedValue.cs | 26 +- .../Primitives/TabStripTests.cs | 94 ++++--- .../TabControlTests.cs | 154 ++++++------ .../TreeViewTests.cs | 116 +++++---- .../Utils/HotKeyManagerTests.cs | 4 +- .../VirtualizingStackPanelTests.cs | 2 +- tests/Avalonia.LeakTests/ControlTests.cs | 14 +- .../Data/MultiBindingTests.cs | 8 +- .../CompiledBindingExtensionTests.cs | 8 +- .../Xaml/BasicTests.cs | 8 +- .../Xaml/DataTemplateTests.cs | 2 +- .../Xaml/StyleTests.cs | 2 +- .../Xaml/XamlIlTests.cs | 2 +- .../AutoDataTemplateBindingHookTest.cs | 2 +- 62 files changed, 607 insertions(+), 677 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 3681298a72..9f06525821 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -241,7 +241,7 @@ diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index 8b12a2d663..0d3061f361 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - this.Get("Items").Items = CreateColorItems(); + this.Get("Items").ItemsSource = CreateColorItems(); } diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml index 6ef6a202b6..cb294442d2 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml @@ -61,7 +61,7 @@ diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 962f0308f7..06eba52605 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -51,13 +51,13 @@ - + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml index 30bad06d72..66f2b8b2e3 100644 --- a/samples/ControlCatalog/Pages/CursorPage.xaml +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -8,7 +8,7 @@ Defines a cursor (mouse pointer) - + @@ -68,7 +68,7 @@ - + - + - + @@ -173,7 +173,7 @@ @@ -220,7 +220,7 @@ + ItemsSource="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">