Watching changes in children recursively with DynamicData

68 views Asked by At

I'm creating a fully generic file explorer for AvaloniaUI. It has a view that shows a folder tree that you can navigate by expanding each directory.

My goal is to make this view flexible enough to support selecting multiple items from multiple nodes.

The problem? The tree is dynamic. Not only are the children lazily loaded, but also the user can potentially create new items (like folders), and select them.

  • I currently have a IFileItem interface to represent the items in the file structure. I have Folder and File currently.
  • Folders can have other entries (folders + files), of course.
  • Both File and Folder are ViewModels (even though I'm not adding the ViewModel suffix )
interface IFileItem
{
    string Path { get; }
}

class Folder : IFileItem
{
     ReadOnlyObservableCollection<IFileEntry> { get; }
     ...
}

class File : IFileItem
{
     ...
}

To support the selection feature, my plan is to add a property ReadOnlyObservableCollection<IFileItem> SelectedItems:

class Folder : IFileEntry
{
     ReadOnlyObservableCollection<IFileItem> Children { get; }
     bool IsSelected { get; set; } 
     ReadOnlyObservableCollection<IFileItem> SelectedItems { get; }
     
     ...
}

The SelectedItems property should hold a list of whatever it's selected in a given branch, this is, the given node + the children. This approach could be useful, and might be what I need, but I'm not sure how well is this solution for supporting scenarios like single folder picking.

Now the question: How do we define the SelectedItems property???

It should watch change of this.IsSelected AND Children.IsSelected RECURSIVELY. Now that's hard. I'm not familiarized with any use case like this in DynamicData, but I think it should be handled. For now, I'm completely stuck.

I hope someone could come with a nice solution in a purely DD way :)

1

There are 1 answers

2
Enigmativity On

Here's partially implemented solution. It's a pain and it's got a fair bit of work to be fully functional, but this works for demonstrating how to maintain the SelectedItems collection at all of the levels.

Start with the basic interface and abstract class:

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;
}

I then implemented File:

public class File : FileItemBase
{
    public File(string path) : base(path) { }
}

And then 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;
                }
            };
        }
    }
}

Now my test was done in 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();
}

So even though f7 is nested below f3 and f3 is below f1 it still appears and disappears in f1's collection.

results