|
@@ -6,7 +6,6 @@ using System.ComponentModel;
|
|
using System.Linq;
|
|
using System.Linq;
|
|
using Avalonia.Controls.Generators;
|
|
using Avalonia.Controls.Generators;
|
|
using Avalonia.Controls.Selection;
|
|
using Avalonia.Controls.Selection;
|
|
-using Avalonia.Controls.Utils;
|
|
|
|
using Avalonia.Data;
|
|
using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Platform;
|
|
using Avalonia.Input.Platform;
|
|
@@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Defines the <see cref="SelectedItems"/> property.
|
|
/// Defines the <see cref="SelectedItems"/> property.
|
|
/// </summary>
|
|
/// </summary>
|
|
- protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
|
|
|
|
- AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>(
|
|
|
|
|
|
+ protected static readonly DirectProperty<SelectingItemsControl, IList?> SelectedItemsProperty =
|
|
|
|
+ AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList?>(
|
|
nameof(SelectedItems),
|
|
nameof(SelectedItems),
|
|
o => o.SelectedItems,
|
|
o => o.SelectedItems,
|
|
(o, v) => o.SelectedItems = v);
|
|
(o, v) => o.SelectedItems = v);
|
|
@@ -111,12 +110,11 @@ namespace Avalonia.Controls.Primitives
|
|
RoutingStrategies.Bubble);
|
|
RoutingStrategies.Bubble);
|
|
|
|
|
|
private static readonly IList Empty = Array.Empty<object>();
|
|
private static readonly IList Empty = Array.Empty<object>();
|
|
- private SelectedItemsSync? _selectedItemsSync;
|
|
|
|
private ISelectionModel? _selection;
|
|
private ISelectionModel? _selection;
|
|
private int _oldSelectedIndex;
|
|
private int _oldSelectedIndex;
|
|
private object? _oldSelectedItem;
|
|
private object? _oldSelectedItem;
|
|
- private int _initializing;
|
|
|
|
private bool _ignoreContainerSelectionChanged;
|
|
private bool _ignoreContainerSelectionChanged;
|
|
|
|
+ private UpdateState? _updateState;
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
|
|
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
|
|
@@ -149,8 +147,23 @@ namespace Avalonia.Controls.Primitives
|
|
/// </summary>
|
|
/// </summary>
|
|
public int SelectedIndex
|
|
public int SelectedIndex
|
|
{
|
|
{
|
|
- get => Selection.SelectedIndex;
|
|
|
|
- set => Selection.SelectedIndex = value;
|
|
|
|
|
|
+ get
|
|
|
|
+ {
|
|
|
|
+ return _updateState?.SelectedIndex.HasValue == true ?
|
|
|
|
+ _updateState.SelectedIndex.Value :
|
|
|
|
+ Selection.SelectedIndex;
|
|
|
|
+ }
|
|
|
|
+ set
|
|
|
|
+ {
|
|
|
|
+ if (_updateState is object)
|
|
|
|
+ {
|
|
|
|
+ _updateState.SelectedIndex = value;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ Selection.SelectedIndex = value;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -158,17 +171,56 @@ namespace Avalonia.Controls.Primitives
|
|
/// </summary>
|
|
/// </summary>
|
|
public object? SelectedItem
|
|
public object? SelectedItem
|
|
{
|
|
{
|
|
- get => Selection.SelectedItem;
|
|
|
|
- set => Selection.SelectedItem = value;
|
|
|
|
|
|
+ get
|
|
|
|
+ {
|
|
|
|
+ return _updateState?.SelectedItem.HasValue == true ?
|
|
|
|
+ _updateState.SelectedItem.Value :
|
|
|
|
+ Selection.SelectedItem;
|
|
|
|
+ }
|
|
|
|
+ set
|
|
|
|
+ {
|
|
|
|
+ if (_updateState is object)
|
|
|
|
+ {
|
|
|
|
+ _updateState.SelectedItem = value;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ Selection.SelectedItem = value;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Gets or sets the selected items.
|
|
/// Gets or sets the selected items.
|
|
/// </summary>
|
|
/// </summary>
|
|
- protected IList SelectedItems
|
|
|
|
|
|
+ /// <remarks>
|
|
|
|
+ /// By default returns a collection that can be modified in order to manipulate the control
|
|
|
|
+ /// selection, however this property will return null if <see cref="Selection"/> is
|
|
|
|
+ /// re-assigned; you should only use _either_ Selection or SelectedItems.
|
|
|
|
+ /// </remarks>
|
|
|
|
+ protected IList? SelectedItems
|
|
{
|
|
{
|
|
- get => SelectedItemsSync.SelectedItems;
|
|
|
|
- set => SelectedItemsSync.SelectedItems = value;
|
|
|
|
|
|
+ get
|
|
|
|
+ {
|
|
|
|
+ return _updateState?.SelectedItems.HasValue == true ?
|
|
|
|
+ _updateState.SelectedItems.Value :
|
|
|
|
+ (Selection as InternalSelectionModel)?.SelectedItems;
|
|
|
|
+ }
|
|
|
|
+ set
|
|
|
|
+ {
|
|
|
|
+ if (_updateState is object)
|
|
|
|
+ {
|
|
|
|
+ _updateState.SelectedItems = new Optional<IList?>(value);
|
|
|
|
+ }
|
|
|
|
+ else if (Selection is InternalSelectionModel i)
|
|
|
|
+ {
|
|
|
|
+ i.SelectedItems = value;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ throw new InvalidOperationException("Cannot set both Selection and SelectedItems.");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -178,19 +230,30 @@ namespace Avalonia.Controls.Primitives
|
|
{
|
|
{
|
|
get
|
|
get
|
|
{
|
|
{
|
|
- if (_selection is null)
|
|
|
|
|
|
+ if (_updateState?.Selection.HasValue == true)
|
|
{
|
|
{
|
|
- _selection = CreateDefaultSelectionModel();
|
|
|
|
- InitializeSelectionModel(_selection);
|
|
|
|
|
|
+ return _updateState.Selection.Value;
|
|
}
|
|
}
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ if (_selection is null)
|
|
|
|
+ {
|
|
|
|
+ _selection = CreateDefaultSelectionModel();
|
|
|
|
+ InitializeSelectionModel(_selection);
|
|
|
|
+ }
|
|
|
|
|
|
- return _selection;
|
|
|
|
|
|
+ return _selection;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
set
|
|
set
|
|
{
|
|
{
|
|
value ??= CreateDefaultSelectionModel();
|
|
value ??= CreateDefaultSelectionModel();
|
|
|
|
|
|
- if (_selection != value)
|
|
|
|
|
|
+ if (_updateState is object)
|
|
|
|
+ {
|
|
|
|
+ _updateState.Selection = new Optional<ISelectionModel>(value);
|
|
|
|
+ }
|
|
|
|
+ else if (_selection != value)
|
|
{
|
|
{
|
|
if (value.Source != null && value.Source != Items)
|
|
if (value.Source != null && value.Source != Items)
|
|
{
|
|
{
|
|
@@ -234,20 +297,18 @@ namespace Avalonia.Controls.Primitives
|
|
/// </summary>
|
|
/// </summary>
|
|
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
|
|
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
|
|
|
|
|
|
- private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
|
|
|
|
-
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
public override void BeginInit()
|
|
public override void BeginInit()
|
|
{
|
|
{
|
|
base.BeginInit();
|
|
base.BeginInit();
|
|
- ++_initializing;
|
|
|
|
|
|
+ BeginUpdating();
|
|
}
|
|
}
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
public override void EndInit()
|
|
public override void EndInit()
|
|
{
|
|
{
|
|
base.EndInit();
|
|
base.EndInit();
|
|
- --_initializing;
|
|
|
|
|
|
+ EndUpdating();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -351,30 +412,14 @@ namespace Avalonia.Controls.Primitives
|
|
protected override void OnDataContextBeginUpdate()
|
|
protected override void OnDataContextBeginUpdate()
|
|
{
|
|
{
|
|
base.OnDataContextBeginUpdate();
|
|
base.OnDataContextBeginUpdate();
|
|
- ++_initializing;
|
|
|
|
-
|
|
|
|
- if (_selection is object)
|
|
|
|
- {
|
|
|
|
- _selection.Source = null;
|
|
|
|
- }
|
|
|
|
|
|
+ BeginUpdating();
|
|
}
|
|
}
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
protected override void OnDataContextEndUpdate()
|
|
protected override void OnDataContextEndUpdate()
|
|
{
|
|
{
|
|
base.OnDataContextEndUpdate();
|
|
base.OnDataContextEndUpdate();
|
|
- --_initializing;
|
|
|
|
-
|
|
|
|
- if (_selection is object && _initializing == 0)
|
|
|
|
- {
|
|
|
|
- _selection.Source = Items;
|
|
|
|
-
|
|
|
|
- if (Items is null)
|
|
|
|
- {
|
|
|
|
- _selection.Clear();
|
|
|
|
- _selectedItemsSync?.SelectedItems?.Clear();
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ EndUpdating();
|
|
}
|
|
}
|
|
|
|
|
|
protected override void OnInitialized()
|
|
protected override void OnInitialized()
|
|
@@ -411,9 +456,7 @@ namespace Avalonia.Controls.Primitives
|
|
{
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
base.OnPropertyChanged(change);
|
|
|
|
|
|
- if (change.Property == ItemsProperty &&
|
|
|
|
- _initializing == 0 &&
|
|
|
|
- _selection is object)
|
|
|
|
|
|
+ if (change.Property == ItemsProperty && _updateState is null && _selection is object)
|
|
{
|
|
{
|
|
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
|
|
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
|
|
_selection.Source = newValue;
|
|
_selection.Source = newValue;
|
|
@@ -789,7 +832,7 @@ namespace Avalonia.Controls.Primitives
|
|
|
|
|
|
private ISelectionModel CreateDefaultSelectionModel()
|
|
private ISelectionModel CreateDefaultSelectionModel()
|
|
{
|
|
{
|
|
- return new SelectionModel<object>
|
|
|
|
|
|
+ return new InternalSelectionModel
|
|
{
|
|
{
|
|
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
|
|
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
|
|
};
|
|
};
|
|
@@ -797,7 +840,7 @@ namespace Avalonia.Controls.Primitives
|
|
|
|
|
|
private void InitializeSelectionModel(ISelectionModel model)
|
|
private void InitializeSelectionModel(ISelectionModel model)
|
|
{
|
|
{
|
|
- if (_initializing == 0)
|
|
|
|
|
|
+ if (_updateState is null)
|
|
{
|
|
{
|
|
model.Source = Items;
|
|
model.Source = Items;
|
|
}
|
|
}
|
|
@@ -825,9 +868,6 @@ namespace Avalonia.Controls.Primitives
|
|
|
|
|
|
UpdateContainerSelection();
|
|
UpdateContainerSelection();
|
|
|
|
|
|
- _selectedItemsSync ??= new SelectedItemsSync(model);
|
|
|
|
- _selectedItemsSync.SelectionModel = model;
|
|
|
|
-
|
|
|
|
if (SelectedIndex != -1)
|
|
if (SelectedIndex != -1)
|
|
{
|
|
{
|
|
RaiseEvent(new SelectionChangedEventArgs(
|
|
RaiseEvent(new SelectionChangedEventArgs(
|
|
@@ -845,5 +885,96 @@ namespace Avalonia.Controls.Primitives
|
|
model.SelectionChanged -= OnSelectionModelSelectionChanged;
|
|
model.SelectionChanged -= OnSelectionModelSelectionChanged;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ private void BeginUpdating()
|
|
|
|
+ {
|
|
|
|
+ _updateState ??= new UpdateState();
|
|
|
|
+ _updateState.UpdateCount++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void EndUpdating()
|
|
|
|
+ {
|
|
|
|
+ if (_updateState is object && --_updateState.UpdateCount == 0)
|
|
|
|
+ {
|
|
|
|
+ var state = _updateState;
|
|
|
|
+ _updateState = null;
|
|
|
|
+
|
|
|
|
+ if (state.Selection.HasValue)
|
|
|
|
+ {
|
|
|
|
+ Selection = state.Selection.Value;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (state.SelectedItems.HasValue)
|
|
|
|
+ {
|
|
|
|
+ SelectedItems = state.SelectedItems.Value;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Selection.Source = Items;
|
|
|
|
+
|
|
|
|
+ if (Items is null)
|
|
|
|
+ {
|
|
|
|
+ Selection.Clear();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (state.SelectedIndex.HasValue)
|
|
|
|
+ {
|
|
|
|
+ SelectedIndex = state.SelectedIndex.Value;
|
|
|
|
+ }
|
|
|
|
+ else if (state.SelectedItem.HasValue)
|
|
|
|
+ {
|
|
|
|
+ SelectedItem = state.SelectedItem.Value;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
|
|
|
|
+ // defer changes to the selection model because we have no idea in which order properties
|
|
|
|
+ // will be set. Consider:
|
|
|
|
+ //
|
|
|
|
+ // - Both Items and SelectedItem are bound
|
|
|
|
+ // - The DataContext changes
|
|
|
|
+ // - The binding for SelectedItem updates first, producing an item
|
|
|
|
+ // - Items is searched to find the index of the new selected item
|
|
|
|
+ // - However Items isn't yet updated; the item is not found
|
|
|
|
+ // - SelectedIndex is incorrectly set to -1
|
|
|
|
+ //
|
|
|
|
+ // This logic cannot be encapsulated in SelectionModel because the selection model can also
|
|
|
|
+ // be bound, consider:
|
|
|
|
+ //
|
|
|
|
+ // - Both Items and Selection are bound
|
|
|
|
+ // - The DataContext changes
|
|
|
|
+ // - The binding for Items updates first
|
|
|
|
+ // - The new items are assigned to Selection.Source
|
|
|
|
+ // - The binding for Selection updates, producing a new SelectionModel
|
|
|
|
+ // - Both the old and new SelectionModels have the incorrect Source
|
|
|
|
+ private class UpdateState
|
|
|
|
+ {
|
|
|
|
+ private Optional<int> _selectedIndex;
|
|
|
|
+ private Optional<object?> _selectedItem;
|
|
|
|
+
|
|
|
|
+ public int UpdateCount { get; set; }
|
|
|
|
+ public Optional<ISelectionModel> Selection { get; set; }
|
|
|
|
+ public Optional<IList?> SelectedItems { get; set; }
|
|
|
|
+
|
|
|
|
+ public Optional<int> SelectedIndex
|
|
|
|
+ {
|
|
|
|
+ get => _selectedIndex;
|
|
|
|
+ set
|
|
|
|
+ {
|
|
|
|
+ _selectedIndex = value;
|
|
|
|
+ _selectedItem = default;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Optional<object?> SelectedItem
|
|
|
|
+ {
|
|
|
|
+ get => _selectedItem;
|
|
|
|
+ set
|
|
|
|
+ {
|
|
|
|
+ _selectedItem = value;
|
|
|
|
+ _selectedIndex = default;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|