I was just hanging around in the IDE this week and I had a chat with ListView
. He was a very nice guy, but he was complaining about the fact that for some reason he could only ever select one item at a time…
I think of myself as a somewhat decent coder, and I decided to solve this problem – at least some part of it.
The easiest, and quickest, way to add multi-select capabilities to a ListView
requires two steps: the “selectable” item and the “select” action.
The Selectable Item
I started with the model:
public class SelectableItem { public object Data { get; set; } public bool IsSelected { get; set; } }
But, since we are doing data binding, I created a bindable object that has several bindable properties:
public class SelectableItem : BindableObject { public static readonly BindableProperty DataProperty = BindableProperty.Create( nameof(Data), typeof(object), typeof(SelectableItem), (object)null); public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create( nameof(IsSelected), typeof(bool), typeof(SelectableItem), false); public SelectableItem(object data) { Data = data; IsSelected = false; } public SelectableItem(object data, bool isSelected) { Data = data; IsSelected = isSelected; } public object Data { get { return (object)GetValue(DataProperty); } set { SetValue(DataProperty, value); } } public bool IsSelected { get { return (bool)GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } }
You may have noticed that I didn’t create a generic type. This is because later on we are going to want to get the value of the IsSelected
property from the ItemSelected
event handler, and we won’t know what the type is. However, we can create a generic type that inherits from this base type:
public class SelectableItem<T> : SelectableItem { public SelectableItem(T data) : base(data) { } public SelectableItem(T data, bool isSelected) : base(data, isSelected) { } // this is safe as we are just returning the base value public new T Data { get { return (T)base.Data; } set { base.Data = value; } } }
We now have a nice generic type for the developer (us) and a non-generic type for the event (the machine).
The Selectable ListView
The next step is to hook up the ListView
with the selectable items. So, we will need a data field on the view model or the Page
– depending on how you work (note the generic SelectableItem
):
public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); Items = new ObservableCollection<SelectableItem<string>>(); BindingContext = this; } public ObservableCollection<SelectableItem<string>> Items { get; } }
And, we need a list item that has a “checked” state (here it is just a red block):
<ListView ItemsSource="{Binding Items}"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <StackLayout Orientation="Horizontal"> <!-- the "check" mark --> <BoxView IsVisible="{Binding IsSelected}" WidthRequest="12" HeightRequest="12" Color="Red" /> <!-- the text/data --> <Label Text="{Binding Data}" /> </StackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
We should now have a list of items that can show either a selected or unselected state. To handle when an item is selected by the user, we just need an event on the list:
<ListView ... ItemSelected="OnItemSelected"> ... </ListView>
And then we write the code to toggle the selection (note the non-generic SelectableItem
):
private void OnItemSelected(object sender, SelectedItemChangedEventArgs e) { var item = e.SelectedItem as SelectableItem; if (item != null) { // toggle the selection property item.IsSelected = !item.IsSelected; } // deselect the item ((ListView)sender).SelectedItem = null; }
This is all we need, and we will have a ListView
that that can support multiple items being selected.
Improvements
We could call it a day, but we are not going to. The day has just started, and we are thinking that we might want to use this in multiple places. We can just copy the logic – mainly the event and cell view, but there are better ways (and we don’t like duplicate code).
Attached Property
The first thing that we will look at is if we can magically do the event hookup with a single property instead of the event:
<ListView ... local:MultiSelectListView.IsMultiSelect="True"> ... </ListView>
That looks nicer than the event, and we have a couple of benefits: no code in the page, the event code is written once and the property is bindable.
What we are doing here is using an attached property to “inject” a bindable property into an existing view type:
public static class MultiSelectListView { public static readonly BindableProperty IsMultiSelectProperty = BindableProperty.CreateAttached( "IsMultiSelect", typeof(bool), typeof(ListView), false, propertyChanged: OnIsMultiSelectChanged); public static bool GetIsMultiSelect(BindableObject view) => (bool)view.GetValue(IsMultiSelectProperty); public static void SetIsMultiSelect(BindableObject view, bool value) => view.SetValue(IsMultiSelectProperty, value); private static void OnIsMultiSelectChanged( BindableObject bindable, object oldValue, object newValue) { var listView = bindable as ListView; if (listView != null) { // always remove event listView.ItemSelected -= OnItemSelected; // add the event if true if (true.Equals(newValue)) { listView.ItemSelected += OnItemSelected; } } } private static void OnItemSelected( object sender, SelectedItemChangedEventArgs e) { var item = e.SelectedItem as SelectableItem; if (item != null) { // toggle the selection property item.IsSelected = !item.IsSelected; } // deselect the item ((ListView)sender).SelectedItem = null; } }
We now have a super easy way to turn any list into a multi-select list with a single property.
The ItemsSource
Another area we can improve is the ItemsSource
collection. Right now we have quite a long type with two generic types:
public ObservableCollection<SelectableItem<string>> Items { get; }
We can not only make this shorter, but more convenient by creating a new type that derives from ObservableCollection
. And, now that we have a new type, we can add additional methods so that we don’t even have to deal with the SelectableItem
type at all:
public class SelectableObservableCollection<T> : ObservableCollection<SelectableItem<T>> { ... // a constructor overload public SelectableObservableCollection(IEnumerable<T> collection) : base(collection.Select(c => new SelectableItem<T>(c))) { } ... // a convenience property public IEnumerable<T> SelectedItems => this.Where(i => i.IsSelected).Select(i => i.Data); ... // a method overload public void Add(T item) { Add(new SelectableItem<T>(item)); } }
When we use this new type, we can do some cool things:
// a nice type public SelectableObservableCollection<string> Items { get; } // cool methods and properties private void OnDoSomething() { Debug.WriteLine("You have selected:"); // use a property foreach (var selected in Items.SelectedItems) { Debug.WriteLine($" - {selected.Data}"); } // use a method Items.Add("A New String"); }
The Cell
Finally, as you may have a really cool check mark for selected items (unlike our red block now), we will want to be able to create a custom cell type that will allow us to just specify the item contents, and automatically handle the check mark:
<ListView ItemsSource="{Binding Items}" local:MultiSelectListView.IsMultiSelect="True"> <ListView.ItemTemplate> <DataTemplate> <local:SelectableViewCell> <Label Text="{Binding}" /> </local:SelectableViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
This requires a new Cell
type:
[ContentProperty(nameof(DataView))] public class SelectableViewCell : ViewCell { public SelectableViewCell(); public View CheckView { get; set; } public View DataView { get; set; } }
I have left the implementation of this cell to your imagination… just kidding, I have it all in my my repository. You can find all this code, a sample app and more there too:
I hope you enjoyed this short(ish) post and are able to make your list views handle the selection of multiple items.
[…] Multi-Select ListView (Xamarin) (Matthew Leibowitz) […]
LikeLike
Nice Article.
How to get and bind selected data to label from listview
LikeLike
Hi, I have implemented, But I found that If I remove selected Items items are removing properly from list , But when trying to select remaining items, some items are not getting selected. could you please let me know have you ever faced an issue and fix for this. Thank you
LikeLike