长话短说,我想创建一个树视图控件,但是当有很多项目时,我遇到了一个巨大的性能问题。这主要是因为两件事,但我现在只想问你一件事。
所以我有一个继承自 StackLayout 的 TreeViewNode 类型的 ObservableCollection(这是我的 treeView 项目源)
这是treeviewnode类:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Text;
using Xamarin.Forms;
namespace Involys.PraxisDemo.Controls.InCustomTreeView
{
public class TreeViewNode : StackLayout
{
#region Image source for icons
private DataTemplate _ExpandButtonTemplate = null;
#endregion
#region Fields
private TreeViewNode _ParentTreeViewItem;
private DateTime _ExpandButtonClickedTime;
private readonly BoxView _SpacerBoxView = new BoxView();
private readonly BoxView _EmptyBox = new BoxView { BackgroundColor = Color.Blue, Opacity = .5 };
private const int ExpandButtonWidth = 32;
private ContentView _ExpandButtonContent = new ContentView();
private readonly Grid _MainGrid = new Grid
{
VerticalOptions = LayoutOptions.StartAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
RowSpacing = 2
};
private readonly StackLayout _ContentStackLayout = new StackLayout { Orientation = StackOrientation.Horizontal };
private readonly ContentView _ContentView = new ContentView
{
HorizontalOptions = LayoutOptions.FillAndExpand
};
private readonly StackLayout _ChildrenStackLayout = new StackLayout
{
Orientation = StackOrientation.Vertical,
Spacing = 0,
IsVisible = false
};
private IList<TreeViewNode> _Children = new ObservableCollection<TreeViewNode>();
private readonly TapGestureRecognizer _TapGestureRecognizer = new TapGestureRecognizer();
private readonly TapGestureRecognizer _ExpandButtonGestureRecognizer = new TapGestureRecognizer();
private readonly TapGestureRecognizer _DoubleClickGestureRecognizer = new TapGestureRecognizer();
#endregion
#region Internal Fields
internal readonly BoxView SelectionBoxView = new BoxView { Color = Color.Red, Opacity = .5, IsVisible = false };
#endregion
#region Private Properties
private TreeView ParentTreeView => Parent?.Parent as TreeView;
private double IndentWidth => Depth * SpacerWidth;
private int SpacerWidth { get; } = 30;
private int Depth => ParentTreeViewItem?.Depth + 1 ?? 0;
private bool _ShowExpandButtonIfEmpty = false;
private Color _SelectedBackgroundColor = Color.Blue;
private double _SelectedBackgroundOpacity = .3;
#endregion
#region Events
public event EventHandler Expanded;
/// <summary>
/// Occurs when the user double clicks on the node
/// </summary>
public event EventHandler DoubleClicked;
#endregion
#region Protected Overrides
protected override void OnParentSet()
{
base.OnParentSet();
Render();
}
#endregion
#region Public Properties
public string NodeKey { get; set; }
public int NodeId { get; set; }
public bool IsSelected
{
get => SelectionBoxView.IsVisible;
set => SelectionBoxView.IsVisible = value;
}
public bool IsExpanded
{
get => _ChildrenStackLayout.IsVisible;
set
{
_ChildrenStackLayout.IsVisible = value;
Render();
if (value)
{
Expanded?.Invoke(this, new EventArgs());
}
}
}
/// <summary>
/// set to true to show the expand button in case we need to poulate the child nodes on demand
/// </summary>
public bool ShowExpandButtonIfEmpty
{
get { return _ShowExpandButtonIfEmpty; }
set { _ShowExpandButtonIfEmpty = value; }
}
/// <summary>
/// set BackgroundColor when node is tapped/selected
/// </summary>
public Color SelectedBackgroundColor
{
get { return _SelectedBackgroundColor; }
set { _SelectedBackgroundColor = value; }
}
/// <summary>
/// SelectedBackgroundOpacity when node is tapped/selected
/// </summary>
public Double SelectedBackgroundOpacity
{
get { return _SelectedBackgroundOpacity; }
set { _SelectedBackgroundOpacity = value; }
}
/// <summary>
/// customize expand icon based on isExpanded property and or data
/// </summary>
public DataTemplate ExpandButtonTemplate
{
get { return _ExpandButtonTemplate; }
set { _ExpandButtonTemplate = value; }
}
public View Content
{
get => _ContentView.Content;
set => _ContentView.Content = value;
}
public IList<TreeViewNode> Children
{
get => _Children;
set
{
if (_Children is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged -= ItemsSource_CollectionChanged;
}
_Children = value;
if (_Children is INotifyCollectionChanged notifyCollectionChanged2)
{
notifyCollectionChanged2.CollectionChanged += ItemsSource_CollectionChanged;
}
TreeView.RenderNodes(_Children, _ChildrenStackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), this);
Render();
}
}
/// <summary>
/// TODO: Remove this. We should be able to get the ParentTreeViewNode by traversing up through the Visual Tree by 'Parent', but this not working for some reason.
/// </summary>
public TreeViewNode ParentTreeViewItem
{
get => _ParentTreeViewItem;
set
{
_ParentTreeViewItem = value;
Render();
}
}
#endregion
#region Constructor
/// <summary>
/// Constructs a new TreeViewItem
/// </summary>
public TreeViewNode()
{
var itemsSource = (ObservableCollection<TreeViewNode>)_Children;
itemsSource.CollectionChanged += ItemsSource_CollectionChanged;
_TapGestureRecognizer.Tapped += TapGestureRecognizer_Tapped;
GestureRecognizers.Add(_TapGestureRecognizer);
_MainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
_MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
_MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
_MainGrid.Children.Add(SelectionBoxView);
_ContentView.GestureRecognizers.Add(_TapGestureRecognizer);
_ContentStackLayout.Children.Add(_SpacerBoxView);
_ContentStackLayout.Children.Add(_ExpandButtonContent);
_ContentStackLayout.Children.Add(_ContentView);
SetExpandButtonContent(_ExpandButtonTemplate);
_ExpandButtonGestureRecognizer.Tapped += ExpandButton_Tapped;
_ExpandButtonContent.GestureRecognizers.Add(_ExpandButtonGestureRecognizer);
_DoubleClickGestureRecognizer.NumberOfTapsRequired = 2;
_DoubleClickGestureRecognizer.Tapped += DoubleClick;
_ContentView.GestureRecognizers.Add(_DoubleClickGestureRecognizer);
_MainGrid.Children.Add(_ContentStackLayout);
_MainGrid.Children.Add(_ChildrenStackLayout, 0, 1);
base.Children.Add(_MainGrid);
HorizontalOptions = LayoutOptions.FillAndExpand;
VerticalOptions = LayoutOptions.Start;
Render();
}
void _DoubleClickGestureRecognizer_Tapped(object sender, EventArgs e)
{
}
#endregion
#region Private Methods
/// <summary>
/// TODO: This is a little stinky...
/// </summary>
private void ChildSelected(TreeViewNode child)
{
//Um? How does this work? The method here is a private method so how are we calling it?
ParentTreeViewItem?.ChildSelected(child);
ParentTreeView?.ChildSelected(child);
}
private void Render()
{
_SpacerBoxView.WidthRequest = IndentWidth;
if ((Children == null || Children.Count == 0) && !ShowExpandButtonIfEmpty)
{
SetExpandButtonContent(_ExpandButtonTemplate);
return;
}
SetExpandButtonContent(_ExpandButtonTemplate);
foreach (var item in Children)
{
item.Render();
}
}
/// <summary>
/// Use DataTemplae
/// </summary>
private void SetExpandButtonContent(DataTemplate expandButtonTemplate)
{
if (expandButtonTemplate != null)
{
_ExpandButtonContent.Content = (View)expandButtonTemplate.CreateContent();
}
else
{
_ExpandButtonContent.Content = (View)new ContentView { Content = _EmptyBox };
}
}
#endregion
#region Event Handlers
private void ExpandButton_Tapped(object sender, EventArgs e)
{
_ExpandButtonClickedTime = DateTime.Now;
IsExpanded = !IsExpanded;
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
//TODO: Hack. We don't want the node to become selected when we are clicking on the expanded button
// if (DateTime.Now - _ExpandButtonClickedTime > new TimeSpan(0, 0, 0, 0, 50))
// {
ChildSelected(this);
// }
}
private void DoubleClick(object sender, EventArgs e)
{
DoubleClicked?.Invoke(this, new EventArgs());
}
private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
TreeView.RenderNodes(_Children, _ChildrenStackLayout, e, this);
Render();
}
#endregion
}
}
和 TreeView 控件:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Xamarin.Forms;
namespace Involys.PraxisDemo.Controls.InCustomTreeView
{
public class TreeView : ScrollView
{
#region Fields
private static StackLayout _StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };
//TODO: This initialises the list, but there is nothing listening to INotifyCollectionChanged so no nodes will get rendered
private static ObservableCollection<TreeViewNode> _RootNodes = new ObservableCollection<TreeViewNode>();
private TreeViewNode _SelectedItem;
#endregion
#region Public Properties
/// <summary>
/// The item that is selected in the tree
/// TODO: Make this two way - and maybe eventually a bindable property
/// </summary>
public static readonly BindableProperty RootNodesProperty =
BindableProperty.Create(nameof(RootNodes), typeof(IEnumerable<TreeViewNode>), typeof(TreeView), null,
BindingMode.Default, null, OnRootNodesChanged);
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create(nameof(SelectedItem), typeof(TreeViewNode), typeof(TreeView), null,
BindingMode.TwoWay);
public IEnumerable<TreeViewNode> RootNodes
{
get
{
return (IEnumerable<TreeViewNode>)GetValue(RootNodesProperty);
}
set
{
SetValue(RootNodesProperty, value);
}
}
public TreeViewNode SelectedItem
{
get { return (TreeViewNode)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
#endregion
#region Events
/// <summary>
/// Occurs when the user selects a TreeViewItem
/// </summary>
public event EventHandler SelectedItemChanged;
#endregion
#region Constructor
public TreeView()
{
_StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };
Content = _StackLayout;
}
#endregion
//selection
#region Internal Methods
/// <summary>
/// TODO: A bit stinky but better than bubbling an event up...
/// </summary>
internal void ChildSelected(TreeViewNode child)
{
SelectedItem = child;
child.IsSelected = true;
child.SelectionBoxView.Color = child.SelectedBackgroundColor;
child.SelectionBoxView.Opacity = child.SelectedBackgroundOpacity;
RemoveSelectionRecursive(RootNodes);
}
#endregion
#region Private Methods
private void RemoveSelectionRecursive(IEnumerable<TreeViewNode> nodes)
{
foreach (var treeViewItem in nodes)
{
if (treeViewItem != SelectedItem)
{
treeViewItem.IsSelected = false;
}
RemoveSelectionRecursive(treeViewItem.Children);
}
}
#endregion
//end selection
#region Internal Static Methods
internal static void RenderNodes(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, NotifyCollectionChangedEventArgs e, TreeViewNode parentTreeViewItem)
{
if (e.Action != NotifyCollectionChangedAction.Add)
{
//TODO: Reintate this...
parent.Children.Clear();
AddItems(childTreeViewItems, parent, parentTreeViewItem);
}
else
{
AddItems(e.NewItems.Cast<TreeViewNode>(), parent, parentTreeViewItem);
}
}
static void OnRootNodesChanged(BindableObject bindable, object oldvalue, object newvalue)
{
System.Diagnostics.Debug.WriteLine("source changed");
if (oldvalue is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += (s, e) =>
{
RenderNodes((IEnumerable<TreeViewNode>)newvalue, _StackLayout, e, null);
};
}
RenderNodes((IEnumerable<TreeViewNode>)newvalue, _StackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), null);
}
#endregion
#region Private Static Methods
private static void AddItems(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, TreeViewNode parentTreeViewItem)
{
foreach (var childTreeNode in childTreeViewItems)
{
if (!parent.Children.Contains(childTreeNode))
{
parent.Children.Add(childTreeNode);
}
childTreeNode.ParentTreeViewItem = parentTreeViewItem;
}
}
#endregion
}
}
所以在我的视图模型中,我创建了我的TreeViewNode
并将其绑定到RootNodes
TreeView 的属性。所以我补充说:
private ObservableCollection<TreeViewNode> _treeviewSource;
public ObservableCollection<TreeViewNode> TreeviewSource
{
get => _treeviewSource;
set
{
_treeviewSource = value;
RaisePropertyChanged(() => TreeviewSource);
}
}
但是在拥有大量 TreeViewNode 列表的同时提升属性会花费太多时间。这就是我的问题......我想问题是它通知 UI foreach TreeViewNode(stacklayout) 在列表中以及它需要什么时间。
我希望你们能对此有所帮助。谢谢