fix(Selection): refresh option state after source changes

This commit is contained in:
chinboy
2026-06-03 17:20:32 +08:00
parent fe3140db9c
commit 151ad2f6eb
10 changed files with 594 additions and 34 deletions
+151 -15
View File
@@ -244,6 +244,7 @@ public class Cascader : AbstractSelect
private CascaderView? _cascaderView;
private CompositeDisposable? _contentRightAddOnBindings;
private bool _needSkipSyncSelectedOptions;
private bool _isDefaultSelectOptionPathApplied;
static Cascader()
{
@@ -267,7 +268,42 @@ public class Cascader : AbstractSelect
private void HandleCascaderSourceChanged(AvaloniaPropertyChangedEventArgs args)
{
var selectedOptionPath = !IsMultiple ? BuildOptionIdentityPath(SelectedOption) : null;
var selectedOptionPaths = IsMultiple ? BuildSelectedOptionIdentityPaths(SelectedOptions) : null;
_options.SetItemsSource(args.GetNewValue<IEnumerable<ICascaderOption>?>());
if (!IsMultiple)
{
if (selectedOptionPath != null &&
TryParseSelectPath(selectedOptionPath, out var selectedOptions) &&
selectedOptions.Count > 0)
{
SelectedOption = selectedOptions[^1];
_isDefaultSelectOptionPathApplied = false;
}
else
{
ApplyDefaultSelectOptionPath(_isDefaultSelectOptionPathApplied);
}
}
else if (selectedOptionPaths != null)
{
var remappedOptions = new List<ICascaderOption>(selectedOptionPaths.Count);
foreach (var path in selectedOptionPaths)
{
if (TryParseSelectPath(path, out var selectedOptions) &&
selectedOptions.Count > 0)
{
remappedOptions.Add(selectedOptions[^1]);
}
}
if (remappedOptions.Count > 0)
{
SelectedOptions = remappedOptions;
}
}
}
private void HandleCascaderOptionsChanged(object? sender, NotifyCollectionChangedEventArgs args)
@@ -294,6 +330,7 @@ public class Cascader : AbstractSelect
public void Clear()
{
_isDefaultSelectOptionPathApplied = false;
SelectedOption = null;
SelectedOptionPath = null;
SelectedOptions = null;
@@ -422,8 +459,20 @@ public class Cascader : AbstractSelect
if (change.Property == SelectedOptionProperty)
{
if (SelectedOption != null)
{
_isDefaultSelectOptionPathApplied = false;
}
ConfigureSelectedOptionPath();
}
else if (change.Property == DefaultSelectOptionPathProperty)
{
_isDefaultSelectOptionPathApplied = false;
if (SelectedOption == null)
{
ApplyDefaultSelectOptionPath(true);
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -941,6 +990,82 @@ public class Cascader : AbstractSelect
return options;
}
private static List<TreeNodePath>? BuildSelectedOptionIdentityPaths(ICollection<ICascaderOption>? options)
{
if (options == null)
{
return null;
}
var paths = new List<TreeNodePath>(options.Count);
foreach (var option in options)
{
var path = BuildOptionIdentityPath(option);
if (path != null)
{
paths.Add(path);
}
}
return paths;
}
private static TreeNodePath? BuildOptionIdentityPath(ICascaderOption? option)
{
if (option == null)
{
return null;
}
var depth = CountOptionPathDepth(option);
var segments = new string[depth];
var current = option;
for (var i = segments.Length - 1; current != null; i--)
{
var segment = current.ItemKey?.ToString() ?? current.Value?.ToString();
if (string.IsNullOrEmpty(segment))
{
return null;
}
segments[i] = segment;
current = current.ParentNode as ICascaderOption;
}
return new TreeNodePath(segments);
}
private bool TryParseSelectPath(TreeNodePath path, out IList<ICascaderOption> pathNodes)
{
var segments = path.Segments;
var isPathValid = true;
var currentItems = BuildOptionsList(Options);
var options = new List<ICascaderOption>(segments.Count);
for (var i = 0; i < segments.Count; i++)
{
var segment = segments[i];
var found = false;
foreach (var currentItem in currentItems)
{
if (segment == currentItem.ItemKey || segment == currentItem.Value?.ToString())
{
options.Add(currentItem);
currentItems = BuildOptionsList(currentItem.Children);
found = true;
break;
}
}
if (!found)
{
isPathValid = false;
break;
}
}
pathNodes = options;
return isPathValid;
}
private static bool AreAllChildrenSelected(
IEnumerable<ICascaderOption> children,
ISet<ICascaderOption> selected)
@@ -1010,24 +1135,35 @@ public class Cascader : AbstractSelect
return count;
}
private void ApplyDefaultSelectOptionPath(bool forceRefresh = false)
{
if (IsMultiple || DefaultSelectOptionPath == null)
{
return;
}
if (!forceRefresh && (SelectedOption != null || SelectedOptionPath != null))
{
return;
}
if (TryParseSelectPath(DefaultSelectOptionPath, out var options) &&
options.Count > 0)
{
var parts = new string[options.Count];
for (var i = 0; i < options.Count; i++)
{
parts[i] = options[i].Header?.ToString() ?? string.Empty;
}
SetCurrentValue(SelectedOptionPathProperty, string.Join("/", parts));
_isDefaultSelectOptionPathApplied = true;
}
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (DefaultSelectOptionPath != null && SelectedOptionPath == null)
{
if (_cascaderView != null)
{
if (_cascaderView.TryParseSelectPath(DefaultSelectOptionPath, out var options))
{
var parts = new string[options.Count];
for (var i = 0; i < options.Count; i++)
{
parts[i] = options[i].Header?.ToString() ?? string.Empty;
}
SetCurrentValue(SelectedOptionPathProperty, string.Join("/", parts));
}
}
}
ApplyDefaultSelectOptionPath();
}
#region 实现 FormItem 接口
@@ -459,7 +459,49 @@ public partial class CascaderView : TemplatedControl,
private void HandleCascaderSourceChanged(AvaloniaPropertyChangedEventArgs args)
{
ResetLevelListsForOptionsSourceChanging();
_options.SetItemsSource(args.GetNewValue<IEnumerable<ICascaderOption>?>());
RestoreRootLevelListItemsSource();
if (DefaultExpandedPath != null && IsLoaded)
{
_defaultExpandPathApplied = false;
ApplyDefaultExpandPath();
_defaultExpandPathApplied = true;
}
}
private void ResetLevelListsForOptionsSourceChanging()
{
if (_itemsPanel == null)
{
return;
}
for (var i = _itemsPanel.Children.Count - 1; i >= 0; i--)
{
if (_itemsPanel.Children[i] is CascaderViewLevelList levelList)
{
levelList.ResetVirtualizingContext();
levelList.SelectedIndex = -1;
levelList.SelectedItem = null;
levelList.ItemsSource = null;
if (i > 0)
{
levelList.Items.Clear();
_itemsPanel.Children.RemoveAt(i);
}
}
}
}
private void RestoreRootLevelListItemsSource()
{
if (_rootLevelList == null)
{
return;
}
_rootLevelList.ItemsSource = Options;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -78,18 +78,16 @@ internal class CascaderViewLevelList : SelectingItemsControl, IListVirtualizingC
if (this is IListVirtualizingContextAware listVirtualizingContextAwareControl &&
cascaderViewItem is IListItemVirtualizingContextAware virtualListItem)
{
if (item is ICascaderOption option)
{
NotifyRestoreDefaultContext(cascaderViewItem, option);
}
if (_virtualRestoreContext.TryGetValue(index, out var context))
{
listVirtualizingContextAwareControl.RestoreVirtualizingContext(cascaderViewItem, context);
_virtualRestoreContext.Remove(index);
}
else
{
if (item is ICascaderOption option)
{
NotifyRestoreDefaultContext(cascaderViewItem, option);
}
}
virtualListItem.VirtualIndex = index;
}
else
@@ -245,6 +243,7 @@ internal class CascaderViewLevelList : SelectingItemsControl, IListVirtualizingC
{
item.SetCurrentValue(CascaderViewItem.HeaderProperty, option);
item.ItemKey = option.ItemKey;
item.SetCurrentValue(CascaderViewItem.ValueProperty, option.Value);
item.SetCurrentValue(CascaderViewItem.IconProperty, option.Icon);
item.SetCurrentValue(CascaderViewItem.IsCheckedProperty, option.IsChecked);
item.SetCurrentValue(CascaderViewItem.IsEnabledProperty, option.IsEnabled);
@@ -311,6 +310,10 @@ internal class CascaderViewLevelList : SelectingItemsControl, IListVirtualizingC
protected virtual void NotifyClearContainerForVirtualizingContext(CascaderViewItem item)
{
item.ClearValue(CascaderViewItem.HeaderProperty);
item.ClearValue(CascaderViewItem.ValueProperty);
item.ItemKey = null;
item.ClearValue(CascaderViewItem.IconProperty);
item.ClearValue(CascaderViewItem.IsEnabledProperty);
item.ClearValue(CascaderViewItem.IsCheckedProperty);
item.ClearValue(CascaderViewItem.IsExpandedProperty);
@@ -318,6 +321,11 @@ internal class CascaderViewLevelList : SelectingItemsControl, IListVirtualizingC
item.AsyncLoaded = false;
}
internal void ResetVirtualizingContext()
{
_virtualRestoreContext.Clear();
}
void IListVirtualizingContextAware.SaveVirtualizingContext(Control item, IDictionary<object, object?> context)
{
if (item is CascaderViewItem cascaderViewItem)
@@ -352,6 +352,10 @@ public class ListBox : AvaloniaListBox,
{
listBoxItem[!ListBoxItem.ContentTemplateProperty] = this[!ItemTemplateProperty];
}
else
{
listBoxItem.ClearValue(ListBoxItem.ContentTemplateProperty);
}
listBoxItem[!ListBoxItem.SizeTypeProperty] = this[!SizeTypeProperty];
listBoxItem[!ListBoxItem.IsMotionEnabledProperty] = this[!IsMotionEnabledProperty];
listBoxItem[!ListBoxItem.SelectedIndicatorProperty] = this[!SelectedIndicatorProperty];
@@ -626,14 +630,13 @@ public class ListBox : AvaloniaListBox,
protected virtual void NotifyRestoreDefaultContext(ListBoxItem item, IListItemData itemData)
{
if (!item.IsSet(ListBoxItem.ContentProperty))
{
item.SetCurrentValue(ListBoxItem.ContentProperty, itemData);
}
item.SetCurrentValue(ListBoxItem.ContentProperty, itemData);
}
protected virtual void NotifyClearContainerForVirtualizingContext(ListBoxItem item)
{
item.ClearValue(ListBoxItem.ContentProperty);
item.ClearValue(ListBoxItem.ContentTemplateProperty);
item.ClearValue(ListBoxItem.IsEnabledProperty);
}
@@ -43,10 +43,7 @@ public partial class ListView : IListVirtualizingContextAware
protected virtual void NotifyRestoreDefaultContext(ListViewItem item, IListItemData itemData)
{
if (!item.IsSet(ListViewItem.ContentProperty))
{
item.SetCurrentValue(ListViewItem.ContentProperty, itemData);
}
item.SetCurrentValue(ListViewItem.ContentProperty, itemData);
if (itemData is IGroupListItemData groupListItemData)
{
item.SetCurrentValue(ListViewItem.IsGroupItemProperty, groupListItemData.IsGroupItem);
@@ -56,6 +53,8 @@ public partial class ListView : IListVirtualizingContextAware
protected virtual void NotifyClearContainerForVirtualizingContext(ListViewItem item)
{
item.ClearValue(ListViewItem.ContentProperty);
item.ClearValue(ListViewItem.ContentTemplateProperty);
item.ClearValue(ListViewItem.IsEnabledProperty);
item.ClearValue(ListViewItem.IsGroupItemProperty);
}
@@ -649,6 +649,14 @@ public partial class ListView : ItemsControl, ISizeTypeAware, IMotionAwareContro
{
listItem[!ListViewItem.ContentTemplateProperty] = this[!GroupItemTemplateProperty];
}
else if (ItemTemplate != null)
{
listItem[!ListViewItem.ContentTemplateProperty] = this[!ItemTemplateProperty];
}
else
{
listItem.ClearValue(ListViewItem.ContentTemplateProperty);
}
}
else
{
@@ -656,6 +664,10 @@ public partial class ListView : ItemsControl, ISizeTypeAware, IMotionAwareContro
{
listItem[!ListViewItem.ContentTemplateProperty] = this[!ItemTemplateProperty];
}
else
{
listItem.ClearValue(ListViewItem.ContentTemplateProperty);
}
}
listItem[!ListViewItem.SizeTypeProperty] = this[!SizeTypeProperty];
+85 -1
View File
@@ -989,12 +989,96 @@ public partial class Select : AbstractSelect
private void HandleOptionsSourcePropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
ClearValue();
var selectedOptionIdentity = Mode == SelectMode.Single ? BuildOptionIdentity(SelectedOption) : null;
var selectedOptionIdentities = Mode != SelectMode.Single ? BuildSelectedOptionIdentities(SelectedOptions) : null;
if (!Options.IsReadOnly)
{
Options.Clear();
}
Options.SetItemsSource(change.GetNewValue<IEnumerable<ISelectOption>?>());
if (Mode == SelectMode.Single)
{
if (selectedOptionIdentity != null &&
TryFindOptionByIdentity(selectedOptionIdentity, out var remappedOption))
{
SelectedOption = remappedOption;
}
else
{
SelectedOption = null;
ConfigureDefaultValues();
}
}
else if (selectedOptionIdentities != null)
{
var remappedOptions = new List<ISelectOption>(selectedOptionIdentities.Count);
foreach (var identity in selectedOptionIdentities)
{
if (TryFindOptionByIdentity(identity, out var remappedOption))
{
remappedOptions.Add(remappedOption);
}
}
SelectedOptions = remappedOptions.Count > 0 ? remappedOptions : null;
ConfigureDefaultValues();
}
else
{
ConfigureDefaultValues();
}
}
private static List<string>? BuildSelectedOptionIdentities(ICollection<ISelectOption>? options)
{
if (options == null)
{
return null;
}
var identities = new List<string>(options.Count);
foreach (var option in options)
{
var identity = BuildOptionIdentity(option);
if (identity != null)
{
identities.Add(identity);
}
}
return identities;
}
private static string? BuildOptionIdentity(ISelectOption? option)
{
if (option == null)
{
return null;
}
var itemKey = option.ItemKey?.ToString();
if (!string.IsNullOrEmpty(itemKey))
{
return itemKey;
}
return option.Content?.ToString();
}
private bool TryFindOptionByIdentity(string identity, out ISelectOption option)
{
foreach (var item in Options)
{
if (item is ISelectOption currentOption &&
identity == BuildOptionIdentity(currentOption))
{
option = currentOption;
return true;
}
}
option = null!;
return false;
}
private void ConfigureDefaultValues()
@@ -356,7 +356,38 @@ public class TreeSelect : AbstractSelect
private void HandleItemsSourceChanged(AvaloniaPropertyChangedEventArgs args)
{
var selectedItemPath = !IsMultiple ? BuildNodeIdentityPath(SelectedItem) : null;
var selectedItemPaths = IsMultiple ? BuildSelectedNodeIdentityPaths(SelectedItems) : null;
_items.SetItemsSource(args.GetNewValue<IEnumerable<ITreeItemNode>?>());
if (!IsMultiple)
{
if (selectedItemPath != null &&
TryParseNodePath(selectedItemPath, out var selectedItems) &&
selectedItems.Count > 0)
{
SelectedItem = selectedItems[^1];
}
else
{
SelectedItem = null;
}
}
else if (selectedItemPaths != null)
{
var remappedItems = new List<ITreeItemNode>(selectedItemPaths.Count);
foreach (var path in selectedItemPaths)
{
if (TryParseNodePath(path, out var selectedItems) &&
selectedItems.Count > 0)
{
remappedItems.Add(selectedItems[^1]);
}
}
SelectedItems = remappedItems.Count > 0 ? remappedItems : null;
}
}
private void HandleItemsChanged(object? sender, NotifyCollectionChangedEventArgs args)
@@ -1153,6 +1184,95 @@ public class TreeSelect : AbstractSelect
return items;
}
private static List<TreeNodePath>? BuildSelectedNodeIdentityPaths(ICollection<ITreeItemNode>? nodes)
{
if (nodes == null)
{
return null;
}
var paths = new List<TreeNodePath>(nodes.Count);
foreach (var node in nodes)
{
var path = BuildNodeIdentityPath(node);
if (path != null)
{
paths.Add(path);
}
}
return paths;
}
private static TreeNodePath? BuildNodeIdentityPath(ITreeItemNode? node)
{
if (node == null)
{
return null;
}
var depth = CountNodePathDepth(node);
var segments = new string[depth];
var current = node;
for (var i = segments.Length - 1; current != null; i--)
{
var segment = current.ItemKey?.ToString() ?? current.Value?.ToString();
if (string.IsNullOrEmpty(segment))
{
return null;
}
segments[i] = segment;
current = current.ParentNode as ITreeItemNode;
}
return new TreeNodePath(segments);
}
private bool TryParseNodePath(TreeNodePath path, out IList<ITreeItemNode> pathNodes)
{
var segments = path.Segments;
var isPathValid = true;
var currentItems = BuildTreeNodeList(Items);
var nodes = new List<ITreeItemNode>(segments.Count);
for (var i = 0; i < segments.Count; i++)
{
var segment = segments[i];
var found = false;
foreach (var currentItem in currentItems)
{
if (segment == currentItem.ItemKey || segment == currentItem.Value?.ToString())
{
nodes.Add(currentItem);
currentItems = BuildTreeNodeList(currentItem.Children);
found = true;
break;
}
}
if (!found)
{
isPathValid = false;
break;
}
}
pathNodes = nodes;
return isPathValid;
}
private static int CountNodePathDepth(ITreeItemNode node)
{
var count = 0;
var current = node;
while (current != null)
{
count++;
current = current.ParentNode as ITreeItemNode;
}
return count;
}
private static bool AreAllChildrenSelected(
IEnumerable<ITreeItemNode> children,
ISet<ITreeItemNode> selected)
@@ -1090,7 +1090,7 @@ public partial class TreeView : AvaloniaTreeView,
{
return null;
}
if (treeViewItem.ItemKey != null && treeViewItem.ItemKey.Value == segment)
if (IsPathSegmentMatched(treeViewItem, segment))
{
treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, true);
if (treeViewItem.Presenter?.Panel == null)
@@ -1148,7 +1148,7 @@ public partial class TreeView : AvaloniaTreeView,
{
return null;
}
if (treeViewItem.ItemKey != null && treeViewItem.ItemKey.Value == segment)
if (IsPathSegmentMatched(treeViewItem, segment))
{
treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, true);
if (treeViewItem.Presenter?.Panel == null)
@@ -1217,7 +1217,7 @@ public partial class TreeView : AvaloniaTreeView,
return null;
}
if (treeViewItem.ItemKey != null && treeViewItem.ItemKey.Value == segment)
if (IsPathSegmentMatched(treeViewItem, segment))
{
pathNodeExpandStatus.Add(treeViewItem.IsExpanded);
treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, true);
@@ -1262,6 +1262,15 @@ public partial class TreeView : AvaloniaTreeView,
SetCurrentValue(IsMotionEnabledProperty, originIsMotionEnabled);
}
}
private static bool IsPathSegmentMatched(TreeViewItem treeViewItem, string segment)
{
if (treeViewItem.ItemKey != null && treeViewItem.ItemKey.Value == segment)
{
return true;
}
return treeViewItem.Value?.ToString() == segment;
}
private (ISet<object>, ISet<object>) SetupParentNodeCheckedStatus(TreeViewItem viewItem)
{
@@ -1314,6 +1323,147 @@ public partial class TreeView : AvaloniaTreeView,
return (checkedParents, unCheckedParents);
}
private void ConfigureStateAfterItemsSourceChanged()
{
if (!IsLoaded)
{
return;
}
var selectedItemPath = BuildNodeIdentityPath(SelectedItem as ITreeItemNode);
var selectedItemPaths = BuildNodeIdentityPaths(SelectedItems);
var checkedItemPaths = BuildNodeIdentityPaths(CheckedItems);
SetCurrentValue(SelectedItemProperty, null);
SelectedItems.Clear();
CheckedItems.Clear();
var selectionRestored = false;
if (selectedItemPath != null)
{
selectionRestored = TrySelectNodePath(selectedItemPath);
}
if (!selectionRestored && selectedItemPaths != null)
{
foreach (var path in selectedItemPaths)
{
selectionRestored |= TrySelectNodePath(path);
}
}
if (!selectionRestored)
{
ConfigureDefaultSelectedPaths();
}
var checkedRestored = false;
if (checkedItemPaths != null)
{
foreach (var path in checkedItemPaths)
{
checkedRestored |= TryCheckNodePath(path);
}
}
if (!checkedRestored)
{
ConfigureDefaultCheckedPaths();
}
if (IsDefaultExpandAll)
{
ExpandAll(false);
}
else
{
ConfigureDefaultExpandedPaths();
}
}
private static List<TreeNodePath>? BuildNodeIdentityPaths(IList? nodes)
{
if (nodes == null)
{
return null;
}
var paths = new List<TreeNodePath>(nodes.Count);
foreach (var node in nodes)
{
if (node is ITreeItemNode treeItemNode)
{
var path = BuildNodeIdentityPath(treeItemNode);
if (path != null)
{
paths.Add(path);
}
}
}
return paths;
}
private static TreeNodePath? BuildNodeIdentityPath(ITreeItemNode? node)
{
if (node == null)
{
return null;
}
var depth = CountTreeItemPathDepth(node);
var segments = new string[depth];
var current = node;
for (var i = segments.Length - 1; current != null; i--)
{
var segment = current.ItemKey?.ToString() ?? current.Value?.ToString();
if (string.IsNullOrEmpty(segment))
{
return null;
}
segments[i] = segment;
current = current.ParentNode as ITreeItemNode;
}
return new TreeNodePath(segments);
}
private bool TrySelectNodePath(TreeNodePath path)
{
var selected = false;
TraverTreeViewPath(path, (treeViewItem, i) =>
{
if (i == path.Length - 1)
{
var item = TreeItemFromContainer(treeViewItem);
if (item != null)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
if (SelectionMode == SelectionMode.Single)
{
SetCurrentValue(SelectedItemProperty, item);
}
selected = true;
}
}
});
return selected;
}
private bool TryCheckNodePath(TreeNodePath path)
{
var isChecked = false;
TraverTreeViewPath(path, (treeViewItem, i) =>
{
if (i == path.Length - 1)
{
treeViewItem.SetCurrentValue(TreeViewItem.IsCheckedProperty, true);
isChecked = true;
}
});
return isChecked;
}
private void GetChildCheckStatus(TreeViewItem parentTreeItem, out bool isAllChecked, out bool isAnyChecked)
{
isAllChecked = false;
@@ -1490,6 +1640,11 @@ public partial class TreeView : AvaloniaTreeView,
SelectedItems.Clear();
}
}
if (change.Property == ItemsSourceProperty)
{
Dispatcher.Post(ConfigureStateAfterItemsSourceChanged);
}
}
protected void HandleSwitcherRotationIconChanged()
@@ -847,6 +847,7 @@ public class TreeViewItem : AvaloniaTreeItem, IRadioButton, ITreeItemNode
treeViewItem.SetCurrentValue(IsEnabledProperty, treeItemData.IsEnabled);
treeViewItem.SetCurrentValue(IsExpandedProperty, treeItemData.IsExpanded);
treeViewItem.SetCurrentValue(IsIndicatorEnabledProperty, treeItemData.IsIndicatorEnabled);
treeViewItem.SetCurrentValue(ValueProperty, treeItemData.Value);
treeViewItem.ItemKey = treeItemData.ItemKey;
treeViewItem.IsLeaf = treeItemData.IsLeaf;