Drag’n’Drop is just another way the user can interact with the data in a mobile application (quite a powerful one I might add). It can be used to move items in a list from one category to another or simply to adjust the priority of list items. In this context, ListView ordering with drag and drop can be treated as a user input and in the world of MVVM and Xamarin.Forms these changes should be reflected on the ViewModel.
In this (hopefully) series of posts, we will be implementing drag and drop reordering functionality for iOS and Android platforms on Xamarin.Forms.
Implementing Native Drag’n’Drop ListView Re-Order (iOS)
On the iOS side of the story, developers are quite lucky since the native UITableView supports drag&drop re-ordering of items.
In order to demonstrate, we will be implementing a re-order functionality on the players list that we used in our previous example(s). The desired behavior is that when the user taps on the edit button, the list should become sortable. Each re-order action should be reflected in the source collection.
NOTE: If you are planning to use classic Xamarin.iOS there is quite a detailed example implemented at Tables and Cells – Editing |
However, this functionality is not exposed through Xamarin.Forms ListView control. In order to expose this behavior we will be implementing a XAML extension together with a native effect implementation.
Before starting the UI customizations, let us first create the interface that our data source is supposed to implement so that the user input (i.e. drag-and-drop) can be pushed to the view-model.
/// <summary>
/// Used by bound collections to expose ordering methods/events
/// </summary>
public interface IOrderable
{
/// <summary>
/// Event fired when the items in the collection are re-ordered.
/// </summary>
event EventHandler OrderChanged;
/// <summary>
/// Used to change the item orders in an enumerable
/// </summary>
///
/// The old index of the item.
///
///
/// The new index of the item.
///
void ChangeOrdinal(int oldIndex, int newIndex);
}
So once the view detects that the items are re-ordered, it will use the ChangeOrdinal method to re-order the source data, and any change in the data source order will be published to the view-model using the OrderChanged.
Now, we can start implementing the UI customizations. Let us start with the XAML attached property that will be used to extend the ListView implementation. We will start by creating an attached property for sorting so that we can inject the native effect in the runtime.
public static class Sorting { public static readonly BindableProperty IsSortableProperty = BindableProperty.CreateAttached( "IsSortabble", typeof(bool), typeof(ListViewSortableEffect), false, propertyChanged: OnIsSortabbleChanged); public static bool GetIsSortable(BindableObject view) { return (bool)view.GetValue(IsSortableProperty); } public static void SetIsSortable(BindableObject view, bool value) { view.SetValue(IsSortableProperty, value); } static void OnIsSortabbleChanged(BindableObject bindable, object oldValue, object newValue) { var view = bindable as ListView; if (view == null) { return; } if (!view.Effects.Any(item => item is ListViewSortableEffect)) { view.Effects.Add(new ListViewSortableEffect()); } } class ListViewSortableEffect : RoutingEffect { public ListViewSortableEffect() : base("CubiSoft.ListViewSortableEffect") { } } }
So, as one can see, now we should be able to use the Sorting.IsSortable extension in XAML to insert the ListViewSortableEffect to the ListView. Notice that we are inserting the effect when the IsSortable changed, but we are not actually evaluating whether it is enabled or disabled. The reason for this is to avoid further performance penalty by attaching and detaching from the native element on the platform specific implementation every time the IsSortable binding value changes. Instead, the change for the IsSortable is going to be handled on the native implementation.
We can now add the set the sorting on the view using the attached property we created
<ListView
x:Name="allContacts"
ItemsSource="{Binding AllContacts, Mode=TwoWay}"
IsGroupingEnabled="false"
GroupDisplayBinding="{Binding Key}"
IsPullToRefreshEnabled="false"
HasUnevenRows="true"
SeparatorVisibility="None"
effects:Sorting.IsSortable="{Binding AllowOrdering}">
Next step is the actual implementation on the target platform (i.e. iOS).
Another problem is that in order to modify the data source, we would like to inherit from the data source that is being used by Xamarin.Forms for the ListView element. Xamarin.Forms uses ListViewDataSource (or UnevenListViewDataSource) as the UITableView.Source. Both of these classes are internal, thus cannot be modified by us. In order to override the certain behavior, we will create a proxy class (better than copying the source from github), and modify the functions we need to.
public class ListSortableTableSource : UITableViewSource { //... removed public ListSortableTableSource(UITableViewSource source, ListView element) { _originalSource = source; _formsElement = element; } public UITableViewSource OriginalSource { get { return _originalSource; } } //...removed
The derived table source is initialized with the original table view source and the ListView element (it will be used to propagate the sort updates) and original source is used to implement pass-through methods:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) { return OriginalSource.GetCell(tableView, indexPath); } public override nfloat GetHeightForHeader(UITableView tableView, nint section) { return OriginalSource.GetHeightForHeader(tableView, section); } public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) { return OriginalSource.GetHeightForRow(tableView, indexPath); } public override UIView GetViewForHeader(UITableView tableView, nint section) { return OriginalSource.GetViewForHeader(tableView, section); } public override void HeaderViewDisplayingEnded(UITableView tableView, UIView headerView, nint section) { OriginalSource.HeaderViewDisplayingEnded(tableView, headerView, section); } public override nint NumberOfSections(UITableView tableView) { return OriginalSource.NumberOfSections(tableView); } public override void RowDeselected(UITableView tableView, NSIndexPath indexPath) { OriginalSource.RowDeselected(tableView, indexPath); } public override void RowSelected(UITableView tableView, NSIndexPath indexPath) { OriginalSource.RowSelected(tableView, indexPath); } public override nint RowsInSection(UITableView tableview, nint section) { return OriginalSource.RowsInSection(tableview, section); } public override void Scrolled(UIScrollView scrollView) { OriginalSource.Scrolled(scrollView); } public override string[] SectionIndexTitles(UITableView tableView) { return OriginalSource.SectionIndexTitles(tableView); }
Once our proxy class is ready, we can now modify the move/edit related methods:
public override UITableViewCellEditingStyle EditingStyleForRow(UITableView tableView, NSIndexPath indexPath) { // We do not want the "-" icon near each row (or the "+" icon) return UITableViewCellEditingStyle.None; } public override bool CanEditRow(UITableView tableView, NSIndexPath indexPath) { // We still want the row to be editable return true; } public override bool CanMoveRow(UITableView tableView, NSIndexPath indexPath) { // We do want each row to be movable return true; } public override bool ShouldIndentWhileEditing(UITableView tableView, NSIndexPath indexPath) { // We do not want the "weird" indent for the rows when they are in editable mode. return false; }
Finally, the MoveRow function implementation, which should propagate any change on the view side to the actually data source (i.e. in this case the collection that implements the IOrderable interface that we defined).
public override void MoveRow(UITableView tableView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath) { if (_formsElement.ItemsSource is IOrderable orderableList) { var sourceIndex = sourceIndexPath.Row; var targetIndex = destinationIndexPath.Row; orderableList.ChangeOrdinal(sourceIndex, targetIndex); } }
Now, that we have our proxy table source ready, we implement the native effect that will push our “middle-man” to the UITableView and toggle the editing if needed.
public class ListViewSortableEffect : PlatformEffect
{
internal UITableView TableView
{
get
{
return Control as UITableView;
}
}
protected override void OnAttached()
{
if (TableView == null)
{
return;
}
var isSortable = Sorting.GetIsSortable(Element);
TableView.Source = new ListSortableTableSource(TableView.Source, Element as ListView);
TableView.SetEditing(isSortable, true);
}
protected override void OnDetached()
{
TableView.Source = (TableView.Source as ListSortableTableSource).OriginalSource;
TableView.SetEditing(false, true);
}
protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs args)
{
if (args.PropertyName == Sorting.IsSortableProperty.PropertyName)
{
TableView.SetEditing(Sorting.GetIsSortable(Element), true);
}
}
}
When the effect is attached to the control, we are initializing a new table source, and setting the edit mode according to the IsSortable property. When the effect is detached, we are assigning the original source back to the table view, and setting the edit mode (back) to false. If/When the IsSortable property changes according to binding value, we are toggling the edit mode.
NOTE: In the implementation, instead of actual references to the forms element and/or the original table source, weak references should be considered. Additionally, UITableSource should be implementing IDisposable. |
We are now ready to use the sort functionality on the list view. One more thing we should be doing is to implement an event handler to deal with the IOrderable.OrderChanged event. In our case, whenever the list order changes, we should update the Jersey number for players.
_allContacts.OrderChanged += (sender, e) => { int jersey = 1; foreach(var item in _allContacts) { item.Jersey = jersey++; } };
Before, we forget, we should also extend the ObservableListCollection class from our previous post so that it implements the IOrderable interface.
public event EventHandler OrderChanged; public void ChangeOrdinal(int oldIndex, int newIndex) { var changedItem = Items[oldIndex]; if (newIndex < oldIndex) { // add one to where we delete, because we're increasing the index by inserting oldIndex += 1; } else { // add one to where we insert, because we haven't deleted the original yet newIndex += 1; } Items.Insert(newIndex, changedItem); Items.RemoveAt(oldIndex); OrderChanged?.Invoke(this, EventArgs.Empty); }
That’s about it, we now have the list sortable using the native row move functionality exposed through the implemented effect.
Reference: Bilgin, C. (2018) Re-Order ListView Items with Drag & Drop – I Available at: https://canbilgin.wordpress.com/2018/03/04/re-order-listview-items-with-drag-drop-i/. [Accessed 14th June 2018]