Search code examples
c#.netxamlmaui

Is there a variable that refers to the current item in a MAUI collectionview?


I'm struggling with how to refer/bind to the current item in a CollectionView's DataTemplate.

I've looked at questions like this, and thought I could apply the solution(s) to MAUI due to it and WPF both using XAML, but Intellisense complains that {Binding /} is not a valid binding path. Doing more digging revealed that I should use {Binding .}. So I tried that, however, trying to run my application results in it not even loading at all.

My xaml currently looks like this:

TreeNode.xaml

<maui:ReactiveContentView
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:maui="clr-namespace:ReactiveUI.Maui;assembly=ReactiveUI.Maui"
    xmlns:vm="clr-namespace:turquoise_frontend.Controls"
    xmlns:math="clr-namespace:HexInnovation;assembly=MathConverter.Maui"
    x:TypeArguments="vm:TreeNodeModel"
    x:DataType="vm:TreeNodeModel"
    x:Class="turquoise_frontend.Controls.TreeNode" x:Name="this">
    <VerticalStackLayout x:Name="sl2">
        <!-- stuff omitted due to being irrelevant here... -->
        <CollectionView ItemsSource="Children" x:Name="_ChildrenStackLayout" CanMixGroups="True" CanReorderItems="True" ReorderCompleted="_ChildrenStackLayout_ReorderCompleted">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <vm:TreeNode/>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</maui:ReactiveContentView>

I'm guessing the slowdown and the error is caused by the runtime interpreting {Binding .} to mean the object that the current xaml file represents as opposed to whatever the ItemTemplate is for, thus causing an infinite recursion.

So what am I supposed to write if I want to create a recursive layout that can take advantage of a CollectionView and also doesn't loop infinitely? Is what I'm trying to do even possible right now?

TreeNode.xaml.cs


    public partial class TreeNode : ReactiveContentView<TreeNodeModel>
    {
        public double? IndentWidth => Depth * SpacerWidth;
        public double? PaddingWidth => IndentWidth + 30;
        public int SpacerWidth { get; } = 40;
        private int? Depth => ViewModel?.Value.Depth ?? 0;
        public string ExpandText { get => (ViewModel?.Children?.Count ?? 0) > 0 ? "▷" : ""; }

        public TreeNode(TreeNodeModel tnm)
        {
            this.WhenAnyValue(x => x.ViewModel.isExpanded).ObserveOn(RxApp.MainThreadScheduler).Subscribe(b =>
            {
                _ChildrenStackLayout.IsVisible = b;
            });

            this.WhenAnyValue(b => b.ViewModel.selected).ObserveOn(RxApp.MainThreadScheduler).Subscribe(a =>
            {
                SelectionBoxView.IsVisible = a;
            });
            this.WhenAnyValue(b => b.ViewModel).ObserveOn(RxApp.MainThreadScheduler).WhereNotNull().Subscribe(vm =>
            {
                vm.Children.CollectionChanged += (o, e) =>
                            {
                                _SpacerBoxView.WidthRequest = IndentWidth ?? 0;
                            };
            });

            
            BindingContext = ViewModel = tnm;

            InitializeComponent();
        }
}

TreeNodeModel.cs

    public class TreeNodeModel : ReactiveObject
    {
        private ITreeNode _value;

        private ObservableCollection<TreeNodeModel> _children;
        private TreeNodeModel? _parent;

        private bool _toggled;
        private bool _selected;
        private bool _expanded;

        private string contract;

        [Reactive]
        public ITreeNode Value { get => _value; set => this.RaiseAndSetIfChanged(ref _value, value); }

        [Reactive]
        public TreeNodeModel? Parent
        {
            get => _parent; set
            {
                _parent = value;
                Value.Parent = value?.Value;
            }
        }

        [Reactive]
        public ObservableCollection<TreeNodeModel> Children
        {
            get
            {
                var nc = new ObservableCollection<TreeNodeModel>();
                foreach (var i in Value.Children ?? new ObservableCollection<ITreeNode>())
                {
                    var b = new TreeNodeModel(i, this.contract)
                    {
                        Tree = this.Tree
                    };
                    b.Parent = this;
                    nc.Add(b);
                }
                return nc;
            }
        }

        public TreeNodeModel Root
        {
            get
            {
                TreeNodeModel top = this;
                do
                {
                    if (top.Parent != null)
                        top = top.Parent;
                    else break;
                } while (top != null);
                return top ?? this;
            }
        }

        [Reactive]
        public bool isExpanded { get => _expanded; set => this.RaiseAndSetIfChanged(ref _expanded, value); }
        [Reactive] public bool Toggled { get => _toggled; set => this.RaiseAndSetIfChanged(ref _toggled, value); }

        public TreeModel Tree { get; set; }

        [Reactive]
        public bool selected { get => _selected; set => this.RaiseAndSetIfChanged(ref _selected, value); }

        public TreeNodeModel(ITreeNode val, string contract)
        {
            this.contract = contract;
            Toggled = val.Toggled;
            selected = false;
            isExpanded = false;
            
            _value = val;
            if (val.Parent != null)
                _parent = new TreeNodeModel(val.Parent, contract) { 
                    Tree = this.Tree
                };
            this.WhenAnyValue(b => b.Toggled).Subscribe(a =>
            {
                _value.Toggled = a;
            });
            this.WhenAnyValue(b => b.isExpanded);
            this.WhenAnyValue(b => b.Toggled);
            this.WhenAnyValue(b => b.selected);
        }
    }

ITreeNode interface

public interface ITreeNode
    {
        public object Value { get; set; }
        public ObservableCollection<ITreeNode> Children { get; }
        public ITreeNode? Parent { get; set; }
        public int Depth { get; }
        public int ID();
        public bool IsBranch { get; }
        public string AsString { get; }
        public bool Toggled { get; set; }
        public void Add(ITreeNode tn, int index);
        public void Add(ITreeNode tn);
        public void Remove(ITreeNode tn);
        public void Remove(int index);
        public bool Equals(ITreeNode other);
        public event NotifyCollectionChangedEventHandler NotifyCollectionChanged;
    }

Solution

  • Lets take a look at this part here:

    var nc = new ObservableCollection<TreeNodeModel>();
                foreach (var i in Value.Children ?? new ObservableCollection<ITreeNode>())
                {
                    var b = new TreeNodeModel(i, this.contract)
                    {
                        Tree = this.Tree
                    };
                    b.Parent = this;
                    nc.Add(b);
                }
                return nc;
    

    And notice how "b" is calling the constructor:

    public TreeNodeModel(ITreeNode val, string contract)
            {
                this.contract = contract;
                Toggled = val.Toggled;
                selected = false;
                isExpanded = false;
                
                _value = val;
                if (val.Parent != null)
                    _parent = new TreeNodeModel(val.Parent, contract) { 
                        Tree = this.Tree
                    };
                this.WhenAnyValue(b => b.Toggled).Subscribe(a =>
                {
                    _value.Toggled = a;
                });
                this.WhenAnyValue(b => b.isExpanded);
                this.WhenAnyValue(b => b.Toggled);
                this.WhenAnyValue(b => b.selected);
            }
    

    So, what happens here:

    "i" (this is ITreeNode, reminder to name your variables right) gets put in the constructor.

    We are constructing "b" (this is TreeNodeModel, same reminder)

    Should it turn out that i has parent,

    The constructor is getting called again, and parent is set. at this line:

    if (val.Parent != null)
                    _parent = new TreeNodeModel(val.Parent, contract) { 
                        Tree = this.Tree
                    };
    

    However, after this recursion is done, we go from this line:

    var b = new TreeNodeModel(i, this.contract)
    

    to this line:

    b.Parent = this;
    

    And we realize that half of our code is doing nothing anyway. And what happens to the newly constructed children and their parents (And the subscription to that event) is not very clear.

    Also this:

     <VerticalStackLayout x:Name="sl2">
        <CollectionView...
    

    Is a no.