我正在为 AvaloniaUI 创建一个完全通用的文件浏览器。它有一个显示文件夹树的视图,您可以通过展开每个目录来导航。
我的目标是让这个视图足够灵活,以支持从多个节点选择多个项目。
问题?树是动态的。不仅子项会延迟加载,而且用户还可以创建新项目(例如文件夹)并选择它们。
IFileItem
接口来表示文件结构中的项目。我目前有 Folder
和 File
。interface IFileItem
{
string Path { get; }
}
class Folder : IFileItem
{
ReadOnlyObservableCollection<IFileEntry> { get; }
...
}
class File : IFileItem
{
...
}
为了支持选择功能,我的计划是添加一个属性
ReadOnlyObservableCollection<IFileItem> SelectedItems
:
class Folder : IFileEntry
{
ReadOnlyObservableCollection<IFileItem> Children { get; }
bool IsSelected { get; set; }
ReadOnlyObservableCollection<IFileItem> SelectedItems { get; }
...
}
SelectedItems
属性应该保存在给定分支中选择的任何内容的列表,即给定节点+子节点。
这种方法可能很有用,并且可能是我所需要的,但我不确定该解决方案对于支持单个文件夹选取等场景的效果如何。
现在的问题是:我们如何定义
SelectedItems
属性???
它应该递归地观察 this.IsSelected 和 Children.IsSelected 的变化。现在这很难了。 我不熟悉 DynamicData 中的任何此类用例,但我认为应该处理它。现在,我完全陷入困境。
我希望有人能以纯粹的DD方式提出一个很好的解决方案:)
这是部分实现的解决方案。这很痛苦,而且需要做相当多的工作才能完全发挥作用,但这可以演示如何在所有级别维护
SelectedItems
集合。
从基本接口和抽象类开始:
public interface IFileItem
{
string Path { get; }
bool IsSelected { get; set; }
event Action<bool> Selected;
}
public abstract class FileItemBase : IFileItem
{
public FileItemBase(string path) => this.Path = path;
public virtual string Path { get; }
private bool _isSelected = false;
public virtual bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
this?.Selected(value);
}
}
public event Action<bool> Selected;
}
然后我实现了
File
:
public class File : FileItemBase
{
public File(string path) : base(path) { }
}
然后
Folder
:
public class Folder : FileItemBase
{
public Folder(string path) : base(path)
{
_children = new ObservableCollection<IFileItem>();
this.Children = new ReadOnlyObservableCollection<IFileItem>(_children);
_selectedItems = new ObservableCollection<IFileItem>();
this.SelectedItems = new ReadOnlyObservableCollection<IFileItem>(_selectedItems);
}
private ObservableCollection<IFileItem> _children;
public ReadOnlyObservableCollection<IFileItem> Children { get; }
private ObservableCollection<IFileItem> _selectedItems = new ObservableCollection<IFileItem>();
public ReadOnlyObservableCollection<IFileItem> SelectedItems { get; }
private IEnumerable<IFileItem> Recurse()
{
foreach (var _child in _children)
{
yield return _child;
if (_child is Folder folder)
{
foreach (var x in folder.Recurse())
{
yield return x;
}
}
}
}
public void Add(IFileItem fileItem)
{
_children.Add(fileItem);
if (fileItem.IsSelected)
{
_selectedItems.Add(fileItem);
}
fileItem.Selected += isSelected =>
{
if (isSelected)
{
_selectedItems.Add(fileItem);
}
else
{
_selectedItems.Remove(fileItem);
}
};
if (fileItem is Folder folder)
{
(folder.SelectedItems as System.Collections.Specialized.INotifyCollectionChanged).CollectionChanged += (s, e) =>
{
switch (e.Action)
{
case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems.OfType<IFileItem>())
if (item.IsSelected)
_selectedItems.Add(item);
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Move:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems.OfType<IFileItem>())
_selectedItems.Remove(item);
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
var removes = this.SelectedItems.Except(this.Recurse().Where(x => x.IsSelected)).ToList();
foreach (var remove in removes)
_selectedItems.Remove(remove);
break;
}
};
}
}
}
现在我的测试是在 LINQPad 中完成的:
void Main()
{
var f1 = new Folder("X");
var f2 = new Folder("XY");
var f3 = new Folder("XZ");
var f4 = new File("Xa");
var f5 = new File("XYa");
var f6 = new File("XYb");
var f7 = new File("XZa");
f1.Add(f2);
f1.Add(f3);
f1.Add(f4);
f2.Add(f5);
f2.Add(f6);
f3.Add(f7);
f1.SelectedItems.Dump();
f7.IsSelected = true;
f1.SelectedItems.Dump();
f7.IsSelected = false;
f1.SelectedItems.Dump();
}
因此,即使
f7
嵌套在 f3
下面并且 f3
在 f1
下面,它仍然会在 f1
的集合中出现和消失。