bykovme/nswallet

View on GitHub
src/NSWallet/NSWallet/Controls/FlowListView/FlowListView.cs

Summary

Maintainability
D
1 day
Test Coverage
using System;
using Xamarin.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Windows.Input;
using System.Reflection;

namespace DLToolkit.Forms.Controls
{
    /// <summary>
    /// FlowListView.
    /// </summary>
    public class FlowListView : ListView, IDisposable
    {
        /// <summary>
        /// Used to avoid linking issues
        /// eg. when using only XAML
        /// </summary>
        public static void Init()
        {
            #pragma warning disable 0219
            var dummy = new FlowListView();
            #pragma warning restore 0219
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DLToolkit.Forms.Controls.FlowListView"/> class.
        /// </summary>
        public FlowListView() : base(ListViewCachingStrategy.RecycleElement)
        {
            InitialSetup();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:DLToolkit.Forms.Controls.FlowListView"/> class.
        /// </summary>
        /// <param name="cachingStrategy">Caching strategy.</param>
        public FlowListView(ListViewCachingStrategy cachingStrategy) : base(cachingStrategy)
        {
            InitialSetup();
        }

        private void InitialSetup()
        {
            RefreshDesiredColumnCount();
            SizeChanged += FlowListSizeChanged;
            PropertyChanged += FlowListViewPropertyChanged;
            PropertyChanging += FlowListViewPropertyChanging;

            FlowColumnExpand = FlowColumnExpand.None;
            FlowColumnCount = default(int?);
            FlowColumnMinWidth = 50d;
            FlowRowBackgroundColor = Color.Transparent;
            FlowTappedBackgroundColor = Color.Transparent;
            FlowTappedBackgroundDelay = 0;

            var flowListViewRef = new WeakReference<FlowListView>(this);
            ItemTemplate = new DataTemplate(() => new FlowListViewInternalCell(flowListViewRef));
            SeparatorVisibility = SeparatorVisibility.None;
            SeparatorColor = Color.Transparent;

            ItemSelected += FlowListViewItemSelected;
            ItemAppearing += FlowListViewItemAppearing;
            ItemDisappearing += FlowListViewItemDisappearing;
        }

        /// <summary>
        /// The flow group grouping key selector property.
        /// </summary>
        public static BindableProperty FlowColumnExpandProperty = BindableProperty.Create(nameof(FlowColumnExpand), typeof(FlowColumnExpand), typeof(FlowListView), FlowColumnExpand.None);

        /// <summary>
        /// Gets or sets FlowListView column expand mode.
        /// It defines how columns should expand when 
        /// row current column count is less than defined columns templates count
        /// </summary>
        /// <value>FlowListView column expand mode.</value>
        public FlowColumnExpand FlowColumnExpand
        {
            get { return (FlowColumnExpand)GetValue(FlowColumnExpandProperty); }
            set { SetValue(FlowColumnExpandProperty, value); }
        }

        BindingBase _flowGroupDisplayBinding;

        /// <summary>
        /// Gets or sets the flow group display binding.
        /// </summary>
        /// <value>The flow group display binding.</value>
        public BindingBase FlowGroupDisplayBinding
        {
            get { return _flowGroupDisplayBinding; }
            set
            {
                if (_flowGroupDisplayBinding == value)
                    return;

                OnPropertyChanging();
                _flowGroupDisplayBinding = value;
                OnPropertyChanged();

                if (value != null)
                    GroupDisplayBinding = new Binding("Key");
                else
                    GroupDisplayBinding = default(Binding);
            }
        }

        BindingBase _flowGroupShortNameBinding;

        /// <summary>
        /// Gets or sets the flow group short name binding.
        /// </summary>
        /// <value>The flow group short name binding.</value>
        public BindingBase FlowGroupShortNameBinding
        {
            get { return _flowGroupShortNameBinding; }
            set
            {
                if (_flowGroupShortNameBinding == value)
                    return;

                OnPropertyChanging();
                _flowGroupShortNameBinding = value;
                OnPropertyChanged();

                if (value != null)
                    GroupShortNameBinding = new Binding("Key");
                else
                    GroupShortNameBinding = default(Binding);
            }
        }

        /// <summary>
        /// The flow column count property.
        /// </summary>
        public static BindableProperty FlowColumnCountProperty = BindableProperty.Create(nameof(FlowColumnCount), typeof(int?), typeof(FlowListView), default(int?));

        /// <summary>
        /// Enables or disables FlowListView auto/manual column count.
        /// Auto Column count is calculated basing on View width 
        /// and <c>FlowColumnMinWidth</c> property
        /// </summary>
        /// <value>The flow column count.</value>
        public int? FlowColumnCount
        {
            get { return (int?)GetValue(FlowColumnCountProperty); }
            set { SetValue(FlowColumnCountProperty, value); }
        }

        /// <summary>
        /// The flow column default minimum width property.
        /// </summary>
        public static BindableProperty FlowColumnMinWidthProperty = BindableProperty.Create(nameof(FlowColumnMinWidth), typeof(double), typeof(FlowListView), 50d);

        /// <summary>
        /// Gets or sets the minimum column width of FlowListView.
        /// Currently used only with <c>FlowAutoColumnCount</c> option
        /// </summary>
        /// <value>The minimum column width.</value>
        public double FlowColumnMinWidth
        {
            get { return (double)GetValue(FlowColumnMinWidthProperty); }
            set { SetValue(FlowColumnMinWidthProperty, value); }
        }

        /// <summary>
        /// The flow row background color property.
        /// </summary>
        public static BindableProperty FlowRowBackgroundColorProperty = BindableProperty.Create(nameof(FlowRowBackgroundColor), typeof(Color), typeof(FlowListView), Color.Transparent);

        /// <summary>
        /// Gets or sets the color of the flow default row background.
        /// Default: Transparent
        /// </summary>
        /// <value>The color of the flow default row background.</value>
        public Color FlowRowBackgroundColor
        {
            get { return (Color)GetValue(FlowRowBackgroundColorProperty); }
            set { SetValue(FlowRowBackgroundColorProperty, value); }
        }

        /// <summary>
        /// Occurs when FlowListView item is tapped.
        /// </summary>
        public event EventHandler<ItemTappedEventArgs> FlowItemTapped;

        /// <summary>
        /// Occurs when flow item is appearing.
        /// </summary>
        public event EventHandler<ItemVisibilityEventArgs> FlowItemAppearing;

        /// <summary>
        /// Occurs when flow item is disappearing.
        /// </summary>
        public event EventHandler<ItemVisibilityEventArgs> FlowItemDisappearing;

        /// <summary>
        /// FlowTappedBackgroundColor property.
        /// </summary>
        public static BindableProperty FlowTappedBackgroundColorProperty = BindableProperty.Create(nameof(FlowTappedBackgroundColor), typeof(Color), typeof(FlowListView), Color.Transparent);

        /// <summary>
        /// Forces FlowListView to use AbsoluteLayout internally
        /// When Enabled, auto row height can't be measured automatically,
        /// but it can improve performance
        /// </summary>
        /// <value><c>true</c> if flow use absolute layout internally; otherwise, <c>false</c>.</value>
        public bool FlowUseAbsoluteLayoutInternally { get; set; } = false;

        /// <summary>
        /// Gets or sets the background color of the cell when tapped.
        /// </summary>
        /// <value>The color of the flow tapped background.</value>
        public Color FlowTappedBackgroundColor
        {
            get { return (Color)GetValue(FlowTappedBackgroundColorProperty); }
            set { SetValue(FlowTappedBackgroundColorProperty, value);  }
        }

        /// <summary>
        /// FlowTappedBackgroundDelay property.
        /// </summary>
        public static BindableProperty FlowTappedBackgroundDelayProperty = BindableProperty.Create(nameof(FlowTappedBackgroundDelay), typeof(int), typeof(FlowListView), 0);

        /// <summary>
        /// Gets or sets the background color delay of the cell when tapped (miliseconds).
        /// </summary>
        /// <value>The flow tapped background delay.</value>
        public int FlowTappedBackgroundDelay
        {
            get { return (int)GetValue(FlowTappedBackgroundDelayProperty); }
            set { SetValue(FlowTappedBackgroundDelayProperty, value);  }
        }

        /// <summary>
        /// FlowLastTappedItemProperty.
        /// </summary>
        public static BindableProperty FlowLastTappedItemProperty = BindableProperty.Create(nameof(FlowLastTappedItem), typeof(object), typeof(FlowListView), default(object), BindingMode.OneWayToSource);

        /// <summary>
        /// Gets FlowListView last tapped item.
        /// </summary>
        /// <value>FlowListView last tapped item.</value>
        public object FlowLastTappedItem
        {
            get { return GetValue(FlowLastTappedItemProperty); }
            private set { SetValue(FlowLastTappedItemProperty, value);  }
        }

        /// <summary>
        /// FlowItemTappedCommandProperty.
        /// </summary>
        public static BindableProperty FlowItemTappedCommandProperty = BindableProperty.Create(nameof(FlowItemTappedCommand), typeof(ICommand), typeof(FlowListView), null);

        /// <summary>
        /// Gets or sets FlowListView item tapped command.
        /// </summary>
        /// <value>FlowListView item tapped command.</value>
        public ICommand FlowItemTappedCommand
        {
            get { return (ICommand)GetValue(FlowItemTappedCommandProperty); }
            set { SetValue(FlowItemTappedCommandProperty, value); }
        }

        /// <summary>
        /// FlowItemAppearingCommandProperty.
        /// </summary>
        public static BindableProperty FlowItemAppearingCommandProperty = BindableProperty.Create(nameof(FlowItemAppearingCommand), typeof(ICommand), typeof(FlowListView), null);

        /// <summary>
        /// Gets or sets FlowListView item tapped command.
        /// </summary>
        /// <value>FlowListView item tapped command.</value>
        public ICommand FlowItemAppearingCommand
        {
            get { return (ICommand)GetValue(FlowItemAppearingCommandProperty); }
            set { SetValue(FlowItemAppearingCommandProperty, value); }
        }

        /// <summary>
        /// FlowItemDisappearingCommandProperty.
        /// </summary>
        public static BindableProperty FlowItemDisappearingCommandProperty = BindableProperty.Create(nameof(FlowItemDisappearingCommand), typeof(ICommand), typeof(FlowListView), null);

        /// <summary>
        /// Gets or sets FlowListView item tapped command.
        /// </summary>
        /// <value>FlowListView item tapped command.</value>
        public ICommand FlowItemDisappearingCommand
        {
            get { return (ICommand)GetValue(FlowItemDisappearingCommandProperty); }
            set { SetValue(FlowItemDisappearingCommandProperty, value); }
        }

        /// <summary>
        /// FlowItemsSourceProperty.
        /// </summary>
        public static BindableProperty FlowItemsSourceProperty = BindableProperty.Create(nameof(FlowItemsSource), typeof(IList), typeof(FlowListView), default(IList));

        /// <summary>
        /// Gets FlowListView items source.
        /// </summary>
        /// <value>FlowListView items source.</value>
        public IList FlowItemsSource
        {
            get { return (IList)GetValue(FlowItemsSourceProperty); }
            set { SetValue(FlowItemsSourceProperty, value); }
        }

        /// <summary>
        /// FlowColumnsTemplatesProperty.
        /// </summary>
        public static readonly BindableProperty FlowColumnTemplateProperty = BindableProperty.Create(nameof(FlowColumnTemplate), typeof(DataTemplate), typeof(FlowListView), default(DataTemplate));

        /// <summary>
        /// Gets or sets FlowListView columns templates.
        /// Use instance of <c>FlowColumnSimpleTemplateSelector</c> for simple single view scenarios
        /// or implement your own FlowColumnTemplateSelector which can return cell type 
        /// basing on current cell BindingContext
        /// </summary>
        /// <value>FlowListView columns templates.</value>
        public DataTemplate FlowColumnTemplate
        {
            get
            {
                return (DataTemplate)GetValue(FlowColumnTemplateProperty);
            }
            set
            {        
                SetValue(FlowColumnTemplateProperty, value);
            }
        }

        /// <summary>
        /// Forces FlowListView reload.
        /// </summary>
        public void ForceReload()
        {
            RefreshDesiredColumnCount();

            if (IsGroupingEnabled)
                ReloadGroupedContainerList();
            else
                ReloadContainerList();
        }

        internal void FlowPerformTap(object item)
        {
            FlowLastTappedItem = item;

            EventHandler<ItemTappedEventArgs> handler = FlowItemTapped;
            if (handler != null)
            {
                handler(this, new ItemTappedEventArgs(null, item));
            }

            var command = FlowItemTappedCommand;
            if (command != null && command.CanExecute(item)) 
            {
                command.Execute(item);
            }
        }

        int desiredColumnCount;
        internal int DesiredColumnCount
        {
            get
            {
                if (desiredColumnCount == 0)
                    return 1;
                
                return desiredColumnCount;
            }
            set
            {
                desiredColumnCount = value;
            }
        }

        private void RefreshDesiredColumnCount()
        {
            if (!FlowColumnCount.HasValue)
            {
                double listWidth = Math.Max(Math.Max(Width, WidthRequest), MinimumWidthRequest);

                if (listWidth > 0)
                {
                    DesiredColumnCount = (int)Math.Floor(listWidth / FlowColumnMinWidth);
                }
            }
            else
            {
                DesiredColumnCount = FlowColumnCount.Value;
            }
        }

        double? lastWidth = null;
        private void FlowListSizeChanged(object sender, EventArgs e)
        {
            if (!FlowColumnCount.HasValue)
            {
                double listWidth = Math.Max(Math.Max(Width, WidthRequest), MinimumWidthRequest);

                if (listWidth > 0)
                {
                    if ((lastWidth.HasValue && Math.Abs(lastWidth.Value - listWidth) > Epsilon.DoubleValue)
                        || !lastWidth.HasValue)
                    {
                        if (ItemsSource != null)
                            ForceReload();
                    }

                    lastWidth = listWidth;
                }    
            }
        }

        private void FlowListViewPropertyChanging(object sender, Xamarin.Forms.PropertyChangingEventArgs e)
        {
            if (e.PropertyName == FlowItemsSourceProperty.PropertyName)
            {
                var flowItemSource = FlowItemsSource as INotifyCollectionChanged;
                if (flowItemSource != null)
                    flowItemSource.CollectionChanged -= FlowItemsSourceCollectionChanged;
            }
        }

        private void FlowListViewPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == FlowItemsSourceProperty.PropertyName)
            {
                var flowItemSource = FlowItemsSource as INotifyCollectionChanged;
                if (flowItemSource != null)
                    flowItemSource.CollectionChanged += FlowItemsSourceCollectionChanged;

                if (FlowColumnTemplate == null || FlowItemsSource == null)
                {
                    ItemsSource = null;
                    return;
                }

                ForceReload();
            }
        }

        private void FlowItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            ForceReload();
        }

        private void FlowListViewItemSelected (object sender, SelectedItemChangedEventArgs e)
        {
            SelectedItem = null;
        }

        private void FlowListViewItemAppearing (object sender, ItemVisibilityEventArgs e)
        {
            var container = e.Item as IEnumerable;
            if (container != null)
            {
                EventHandler<ItemVisibilityEventArgs> handler = FlowItemAppearing;
                var command = FlowItemAppearingCommand;

                if (handler == null && command == null)
                    return;

                foreach (var item in container)
                {
                    handler?.Invoke(this, new ItemVisibilityEventArgs(item));

                    if (command != null && command.CanExecute(item))
                        command.Execute(item);
                }
            }
        }

        private void FlowListViewItemDisappearing(object sender, ItemVisibilityEventArgs e)
        {
            var container = e.Item as IEnumerable;
            if (container != null)
            {
                EventHandler<ItemVisibilityEventArgs> handler = FlowItemDisappearing;
                var command = FlowItemDisappearingCommand;

                if (handler == null && command == null)
                    return;

                foreach (var item in container)
                {
                    handler?.Invoke(this, new ItemVisibilityEventArgs(item));

                    if (command != null && command.CanExecute(item))
                        command.Execute(item);
                }
            }
        }

        private void ReloadContainerList()
        {
            var colCount = DesiredColumnCount;

            int capacity = (FlowItemsSource.Count / colCount) + 
                (FlowItemsSource.Count % colCount) > 0 ? 1 : 0;
            
            var tempList = new List<ObservableCollection<object>>(capacity);
            int position = -1;

            for (int i = 0; i < FlowItemsSource.Count; i++)
            {
                if (i % colCount == 0)
                {
                    position++;

                    tempList.Add(new ObservableCollection<object>() {
                        FlowItemsSource[i]
                    });
                }
                else
                {
                    var exContItm = tempList[position];
                    exContItm.Add(FlowItemsSource[i]);
                }
            }

            ItemsSource = new ObservableCollection<ObservableCollection<object>>(tempList);
        }

        private void ReloadGroupedContainerList()
        {
            var colCount = DesiredColumnCount;
            var flowGroupsList = new List<FlowGroup>(FlowItemsSource.Count);
            var groupDisplayPropertyName = (FlowGroupDisplayBinding as Binding)?.Path;

            foreach (var groupContainer in FlowItemsSource)
            {
                var isAlreadyFlowGroup = groupContainer as FlowGroup;

                if (isAlreadyFlowGroup != null)
                {
                    flowGroupsList.Add(isAlreadyFlowGroup);
                }
                else
                {
                    var gr = groupContainer as IList;
                    if (gr != null)
                    {
                        var type = gr?.GetType();

                        object groupKeyValue = null;

                        if (type != null && groupDisplayPropertyName != null)
                        {
                            PropertyInfo groupDisplayProperty = type?.GetRuntimeProperty(groupDisplayPropertyName);
                            groupKeyValue = groupDisplayProperty?.GetValue(gr);
                        }

                        var flowGroup = new FlowGroup(groupKeyValue);

                        int position = -1;

                        for (int i = 0; i < gr.Count; i++)
                        {
                            if (i % colCount == 0)
                            {
                                position++;

                                flowGroup.Add(new ObservableCollection<object>() { gr[i] });
                            }
                            else
                            {
                                var exContItm = flowGroup[position];
                                exContItm.Add(gr[i]);
                            }
                        }

                        flowGroupsList.Add(flowGroup);
                    }
                }
            }



            ItemsSource = new ObservableCollection<FlowGroup>(flowGroupsList);
        }

        /// <summary>
        /// Releases all resource used by the <see cref="DLToolkit.Forms.Controls.FlowListView"/> object.
        /// </summary>
        /// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="DLToolkit.Forms.Controls.FlowListView"/>.
        /// The <see cref="Dispose"/> method leaves the <see cref="DLToolkit.Forms.Controls.FlowListView"/> in an unusable
        /// state. After calling <see cref="Dispose"/>, you must release all references to the
        /// <see cref="DLToolkit.Forms.Controls.FlowListView"/> so the garbage collector can reclaim the memory that the
        /// <see cref="DLToolkit.Forms.Controls.FlowListView"/> was occupying.</remarks>
        public void Dispose()
        {
            ItemSelected -= FlowListViewItemSelected;
            ItemAppearing -= FlowListViewItemAppearing;
            ItemDisappearing -= FlowListViewItemDisappearing;
            PropertyChanged -= FlowListViewPropertyChanged;
            PropertyChanging -= FlowListViewPropertyChanging;
            SizeChanged -= FlowListSizeChanged;

            var flowItemSource = FlowItemsSource as INotifyCollectionChanged;
            if (flowItemSource != null)
            {
                flowItemSource.CollectionChanged -= FlowItemsSourceCollectionChanged;
            }
        }
    }
}