using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using Xamarin.Forms.Internals; using ElmSharp; namespace Xamarin.Forms.Platform.Tizen.Native { /// /// Type alias which identifies list of cells whose data model was transformed by Xamarin. /// using GroupList = TemplatedItemsList, Cell>; /// /// Native ListView implementation for Xamarin renderer /// /// /// This internally uses GenList class. /// One should note that it is optimized for displaying many elements which may be /// unavailable at first. This means that only currently visible elements will be constructed. /// Whenever element disappears from visible space its content is destroyed for time being. /// This is carried out by so called Cell Handlers. /// public class ListView : GenList { /// /// ItemContext helper class. This represents the association between Xamarin.Forms.Cell and /// native elements. It also stores useful context for them. /// public class ItemContext { public ItemContext() { Item = null; Cell = null; Renderer = null; ListOfSubItems = null; } public GenItem Item; public Cell Cell; public bool IsGroupItem; public CellRenderer Renderer; internal TemplatedItemsList, Cell> ListOfSubItems; } /// /// The item context list for each added element. /// readonly List _itemContextList = new List(); /// /// Registered cell handlers. /// protected readonly IDictionary _cellRendererCache = new Dictionary(); /// /// Registered group handlers. /// protected readonly IDictionary _groupCellRendererCache = new Dictionary(); /// /// The header context. /// ItemContext _headerContext; /// /// The header element. /// VisualElement _headerElement; /// /// The footer context. /// ItemContext _footerContext; /// /// The footer element. /// VisualElement _footerElement; /// /// The item class for header and footer. /// GenItemClass _headerFooterItemClass = null; /// /// Gets or sets a value indicating whether this instance has grouping enabled. /// /// true if this instance has grouping enabled. public bool IsGroupingEnabled { get; set; } /// /// Constructor of ListView native control. /// /// ElmSharp object which is parent of particular list view public ListView(EvasObject parent) : base(parent) { ItemRealized += OnItemAppear; ItemUnrealized += OnItemDisappear; } /// /// Gets the item context based on Cell item. /// /// The item context. /// Cell for which context should be found. internal ItemContext GetItemContext(Cell cell) { if (cell == null) { return null; } else { return _itemContextList.Find(X => X.Cell == cell); } } /// /// Sets the HasUnevenRows property. /// /// If true, the list will allow uneven sizes for its rows. public void SetHasUnevenRows(bool hasUnevenRows) { Homogeneous = !hasUnevenRows; UpdateRealizedItems(); } /// /// Adds elements to the list and defines its presentation based on Cell type. /// /// IEnumerable on Cell collection. /// Cell before which new items will be placed. /// Null value may also be passed as this parameter, which results in appending new items to the end. /// public void AddSource(IEnumerable _source, Cell beforeCell = null) { foreach (var data in _source) { GroupList groupList = data as GroupList; if (groupList != null) { AddGroupItem(groupList, beforeCell); foreach (var item in groupList) { AddItem(item as Cell, groupList.HeaderContent); } } else { AddItem(data as Cell, null, beforeCell); } } } /// /// Deletes all items from a given group. /// /// Group of items to be deleted. internal void ResetGroup(GroupList group) { var items = _itemContextList.FindAll(x => x.ListOfSubItems == group && x.Cell != group.HeaderContent); foreach (var item in items) { item.Item?.Delete(); } } /// /// Adds items to the group. /// /// Group to which elements will be added. /// New list items to be added. /// A reference to the Cell already existing in a ListView. /// Newly added cells will be put just before this cell. public void AddItemsToGroup(IEnumerable itemGroup, IEnumerable newItems, Cell cellBefore = null) { ItemContext groupCtx = GetItemContext((itemGroup as GroupList)?.HeaderContent); if (groupCtx != null) { foreach (var item in newItems) { AddItem(item as Cell, groupCtx.Cell, cellBefore); } } } /// /// Removes the specified cells. /// /// Cells to be removed. public void Remove(IEnumerable cells) { foreach (var data in cells) { var group = data as GroupList; if (group != null) { ItemContext groupCtx = GetItemContext(group.HeaderContent); Remove(groupCtx.ListOfSubItems); groupCtx.Item.Delete(); } else { ItemContext itemCtx = GetItemContext(data as Cell); itemCtx?.Item?.Delete(); } } } /// /// Scrolls the list to a specified cell. /// /// /// Different scrolling behaviors are also possible. The element may be positioned in the center, /// top or bottom of the visible part of the list depending on the value of the position parameter. /// /// Cell which will be displayed after scrolling . /// This will defines scroll to behavior based on ScrollToPosition values. /// If true, scrolling will be animated. Otherwise the cell will be moved instantaneously. public void ApplyScrollTo(Cell cell, ScrollToPosition position, bool animated) { GenListItem item = GetItemContext(cell)?.Item as GenListItem; if (item != null) this.ScrollTo(item, position.ToNative(), animated); } /// /// Selects the specified cell. /// /// Cell to be selected. public void ApplySelectedItem(Cell cell) { GenListItem item = GetItemContext(cell)?.Item as GenListItem; if (item != null) item.IsSelected = true; } /// /// Sets the header. /// /// Header of the list. public void SetHeader(VisualElement header) { if (header == null) { if (HasHeader()) { RemoveHeader(); } return; } GenItemClass headerTemplate = GetHeaderFooterItemClass(); _headerElement = header; if (HasHeader()) { FirstItem.UpdateItemClass(headerTemplate, header); } else { _headerContext = new ItemContext(); _headerContext.Item = _itemContextList.Count > 0 ? InsertBefore(headerTemplate, header, FirstItem) : Append(headerTemplate, header); _headerContext.Item.SelectionMode = GenItemSelectionMode.None; _headerContext.Item.Deleted += HeaderDeletedHandler; _itemContextList.Insert(0, _headerContext); } } /// /// Sets the footer. /// /// Footer of the list. public void SetFooter(VisualElement footer) { if (footer == null) { if (HasFooter()) { RemoveFooter(); } return; } GenItemClass footerTemplate = GetHeaderFooterItemClass(); _footerElement = footer; if (HasFooter()) { (_footerContext.Item as GenListItem).UpdateItemClass(footerTemplate, footer); } else { _footerContext = new ItemContext(); _footerContext.Item = Append(footerTemplate, footer); _footerContext.Item.SelectionMode = GenItemSelectionMode.None; _footerContext.Item.Deleted += FooterDeletedHandler; _itemContextList.Add(_footerContext); } } /// /// Removes the header. /// public void RemoveHeader() { _itemContextList.Remove(_headerContext); _headerContext?.Item?.Delete(); _headerContext = null; _headerElement = null; } /// /// Removes the footer. /// public void RemoveFooter() { _itemContextList.Remove(_footerContext); _footerContext?.Item?.Delete(); _footerContext = null; _footerElement = null; } /// /// Determines whether this instance has a header. /// /// true if the header is present. public bool HasHeader() { return _headerContext != null; } /// /// Determines whether this instance has a footer. /// /// true if the footer is present. public bool HasFooter() { return _footerContext != null; } /// /// Gets the header. /// /// The header. public VisualElement GetHeader() { return _headerElement; } /// /// Gets the footer. /// /// The footer. public VisualElement GetFooter() { return _footerElement; } /// /// Handles the header deleted event. /// /// Sender of the event. /// Empty argument. void HeaderDeletedHandler(object sender, EventArgs e) { _itemContextList.Remove(_headerContext); _headerContext = null; } /// /// Handles the footer deleted event. /// /// Sender of the event. /// Empty argument. void FooterDeletedHandler(object sender, EventArgs e) { _itemContextList.Remove(_footerContext); _footerContext = null; } /// /// Called every time an object gets realized. /// /// Sender of the event. /// GenListItemEventArgs. void OnItemAppear(object sender, GenListItemEventArgs evt) { ItemContext itemContext = (evt.Item.Data as ItemContext); if (itemContext != null && itemContext.Cell != null) { (itemContext.Cell as ICellController).SendAppearing(); } } /// /// Called every time an object gets unrealized. /// /// Sender of the event. /// GenListItemEventArgs. void OnItemDisappear(object sender, GenListItemEventArgs evt) { ItemContext itemContext = (evt.Item.Data as ItemContext); if (itemContext != null && itemContext.Cell != null) { (itemContext.Cell as ICellController).SendDisappearing(); itemContext.Renderer?.SendUnrealizedCell(itemContext.Cell); } } /// /// A convenience shorthand method for derivate classes. /// /// Cell to be added. protected void AddCell(Cell cell) { AddItem(cell); } /// /// Gets the cell renderer for given cell type. /// /// The cell handler. /// Cell to be added. /// If true, then group handlers will be included in the lookup as well. protected virtual CellRenderer GetCellRenderer(Cell cell, bool isGroup = false) { Type type = cell.GetType(); var cache = isGroup ? _groupCellRendererCache : _cellRendererCache; if (cache.ContainsKey(type)) return cache[type]; CellRenderer renderer = null; if (isGroup && type == typeof(TextCell)) { renderer = new GroupCellTextRenderer(); } renderer = renderer ?? Registrar.Registered.GetHandler(type); if (renderer == null) { Log.Error("Cell type is not handled: {0}", cell.GetType()); throw new ArgumentNullException("Unsupported cell type"); } return cache[type] = renderer; } /// /// Adds the group item. Group item is actually of class GroupList because /// group item has sub items (can be zero) which needs to be added. /// If beforeCell is not null, new group will be added just before it. /// /// Group to be added. /// Before cell. void AddGroupItem(GroupList groupList, Cell beforeCell = null) { Cell groupCell = groupList.HeaderContent; CellRenderer groupRenderer = GetCellRenderer(groupCell, true); ItemContext groupItemContext = new ItemContext(); groupItemContext.Cell = groupCell; groupItemContext.Renderer = groupRenderer; groupItemContext.IsGroupItem = true; groupItemContext.ListOfSubItems = groupList; _itemContextList.Add(groupItemContext); if (beforeCell != null) { GenListItem beforeItem = GetItemContext(beforeCell)?.Item as GenListItem; groupItemContext.Item = InsertBefore(groupRenderer.Class, groupItemContext, beforeItem, GenListItemType.Group); } else { groupItemContext.Item = Append(groupRenderer.Class, groupItemContext, GenListItemType.Group); } groupItemContext.Item.SelectionMode = GenItemSelectionMode.None; groupItemContext.Item.IsEnabled = groupCell.IsEnabled; groupItemContext.Item.Deleted += ItemDeletedHandler; } /// /// Adds the item. /// /// Cell to be added. /// Group to which the new item should belong. /// If the value of groupCell is not null, the new item will be put into the requested group. /// The cell before which the new item should be placed. /// If the value of beforeCell is not null, the new item will be placed just before the requested cell. void AddItem(Cell cell, Cell groupCell = null, Cell beforeCell = null) { CellRenderer renderer = GetCellRenderer(cell); GenListItem parentItem = null; ItemContext itemContext = new ItemContext(); itemContext.Cell = cell; itemContext.Renderer = renderer; _itemContextList.Add(itemContext); if (IsGroupingEnabled && groupCell != null) { var groupContext = GetItemContext(groupCell); itemContext.ListOfSubItems = groupContext.ListOfSubItems; parentItem = groupContext.Item as GenListItem; } if (beforeCell != null) { GenListItem beforeItem = GetItemContext(beforeCell)?.Item as GenListItem; itemContext.Item = InsertBefore(renderer.Class, itemContext, beforeItem, GenListItemType.Normal, parentItem); } else { itemContext.Item = Append(renderer.Class, itemContext, GenListItemType.Normal, parentItem); } itemContext.Item.SelectionMode = GenItemSelectionMode.Always; itemContext.Item.IsEnabled = cell.IsEnabled; itemContext.Item.Deleted += ItemDeletedHandler; cell.PropertyChanged += OnCellPropertyChanged; (cell as ICellController).ForceUpdateSizeRequested += OnForceUpdateSizeRequested; } /// /// Handles item deleted event. /// /// Sender of the event. /// Empty argument. void ItemDeletedHandler(object sender, EventArgs e) { ItemContext itemContext = (sender as GenListItem).Data as ItemContext; if (itemContext.Cell != null) { itemContext.Cell.PropertyChanged -= OnCellPropertyChanged; (itemContext.Cell as ICellController).ForceUpdateSizeRequested -= OnForceUpdateSizeRequested; } _itemContextList.Remove(itemContext); } /// /// Invoked whenever the properties of data model change. /// /// Sender of the event. /// PropertyChangedEventArgs. /// /// The purpose of this method is to propagate these changes to the presentation layer. /// void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) { var cell = sender as Cell; var context = GetItemContext(cell); context.Renderer.SendCellPropertyChanged(cell, context.Item, e.PropertyName); } void OnForceUpdateSizeRequested(object sender, EventArgs e) { var cell = sender as Cell; var itemContext = GetItemContext(cell); if (itemContext.Item != null) itemContext.Item.Update(); } /// /// Gets the item class used for header and footer cells. /// /// The header and footer item class. GenItemClass GetHeaderFooterItemClass() { if (_headerFooterItemClass == null) { _headerFooterItemClass = new GenItemClass("full") { GetContentHandler = (data, part) => { VisualElement element = data as VisualElement; var renderer = Platform.GetOrCreateRenderer(element); if (element.MinimumHeightRequest == -1) { SizeRequest request = element.Measure(double.PositiveInfinity, double.PositiveInfinity); renderer.NativeView.MinimumHeight = Forms.ConvertToScaledPixel(request.Request.Height); } else { renderer.NativeView.MinimumHeight = Forms.ConvertToScaledPixel(element.MinimumHeightRequest); } (renderer as LayoutRenderer)?.RegisterOnLayoutUpdated(); return renderer.NativeView; } }; } return _headerFooterItemClass; } } }