Created
August 4, 2016 12:27
-
-
Save joacar/07d49a15cab3bbb7c70e894e25110a05 to your computer and use it in GitHub Desktop.
BindablePicker for Xamarin.Forms that handles Add, Remove, Reset,Clear and Move actions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Specialized; | |
using System.Reflection; | |
using Xamarin.Forms; | |
namespace Clistr.Controls | |
{ | |
public class BindablePicker : Picker | |
{ | |
public static readonly BindableProperty DisplayMemberPathProperty = | |
BindableProperty.Create( | |
nameof(DisplayMemberPath), | |
typeof(string), | |
typeof(BindablePicker), | |
default(string)); | |
public static readonly BindableProperty ItemsSourceProperty = | |
BindableProperty.Create( | |
nameof(ItemsSource), | |
typeof(IList), | |
typeof(BindablePicker), | |
default(IList), | |
propertyChanged: OnItemsSourceChanged); | |
public static readonly BindableProperty SelectedItemProperty = | |
BindableProperty.Create( | |
nameof(SelectedItem), | |
typeof(object), | |
typeof(BindablePicker), | |
defaultBindingMode: BindingMode.TwoWay, | |
propertyChanged: OnSelectedItemChanged); | |
public BindablePicker() | |
{ | |
SelectedIndexChanged += OnSelectedIndexChanged; | |
} | |
public string DisplayMemberPath | |
{ | |
get { return (string)GetValue(DisplayMemberPathProperty); } | |
set { SetValue(DisplayMemberPathProperty, value); } | |
} | |
public IList ItemsSource | |
{ | |
get { return (IList)GetValue(ItemsSourceProperty); } | |
set { SetValue(ItemsSourceProperty, value); } | |
} | |
public object SelectedItem | |
{ | |
get { return GetValue(SelectedItemProperty); } | |
set { SetValue(SelectedItemProperty, value); } | |
} | |
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue) | |
{ | |
var picker = (BindablePicker)bindable; | |
picker.SetSelectedItem(newValue); | |
} | |
private void SetSelectedItem(object selectedItem) | |
{ | |
var displayMember = GetDisplayMember(selectedItem); | |
var index = Items.IndexOf(displayMember); | |
// TODO Should we prevent call to FindObject since the object is already known | |
// by setting a flag, or otherwise indicate, that we, internally, forced a SelectedIndex changed | |
SelectedIndex = index; | |
} | |
private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue) | |
{ | |
var picker = (BindablePicker)bindable; | |
var observable = oldValue as INotifyCollectionChanged; | |
if (observable != null) | |
{ | |
observable.CollectionChanged -= picker.CollectionChanged; | |
} | |
observable = newValue as INotifyCollectionChanged; | |
if (observable != null) | |
{ | |
observable.CollectionChanged += picker.CollectionChanged; | |
} | |
picker.BindItems(); | |
} | |
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | |
{ | |
switch (e.Action) | |
{ | |
case NotifyCollectionChangedAction.Reset: | |
BindItems(); | |
return; | |
case NotifyCollectionChangedAction.Remove: | |
RemoveItems(e); | |
break; | |
case NotifyCollectionChangedAction.Add: | |
AddItems(e); | |
break; | |
case NotifyCollectionChangedAction.Move: | |
// TODO Make more intelligent decision | |
BindItems(); | |
break; | |
case NotifyCollectionChangedAction.Replace: | |
// TODO Make more intelligent decision | |
BindItems(); | |
break; | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private void AddItems(NotifyCollectionChangedEventArgs e) | |
{ | |
var index = e.NewStartingIndex < 0 ? Items.Count : e.NewStartingIndex; | |
foreach (var newItem in e.NewItems) | |
{ | |
Items.Insert(index++, GetDisplayMember(newItem)); | |
} | |
} | |
private void RemoveItems(NotifyCollectionChangedEventArgs e) | |
{ | |
var index = e.OldStartingIndex < Items.Count ? e.OldStartingIndex : Items.Count; | |
// TODO: How do we determine the order of which the items were removed | |
foreach (var _ in e.OldItems) | |
{ | |
Items.RemoveAt(index--); | |
} | |
} | |
private void OnSelectedIndexChanged(object sender, EventArgs e) | |
{ | |
if (SelectedIndex < 0 || SelectedIndex > Items.Count - 1) | |
{ | |
SelectedItem = null; | |
} | |
else | |
{ | |
SelectedItem = ItemsSource[SelectedIndex]; | |
} | |
} | |
private void BindItems() | |
{ | |
Items.Clear(); | |
foreach (var item in ItemsSource) | |
{ | |
Items.Add(GetDisplayMember(item)); | |
} | |
} | |
private static bool IsPrimitive(object item) | |
{ | |
// TODO What is missing | |
return item is string || item is int || item is double || item is decimal || item is Enum || | |
item is DateTime; | |
} | |
protected virtual string GetDisplayMember(object item) | |
{ | |
// TODO How to handle null properly. Here or earlier in the process | |
if (item == null) | |
{ | |
return null; | |
} | |
// TODO How to handle Nullable types | |
if (IsPrimitive(item) || string.IsNullOrEmpty(DisplayMemberPath)) | |
{ | |
return item.ToString(); | |
} | |
// Find the property name and returns its value | |
var type = item.GetType(); | |
var property = type.GetRuntimeProperty(DisplayMemberPath); | |
if (property == null) | |
{ | |
throw new ArgumentException($"No property with name '{DisplayMemberPath}' was found on '{type.FullName}'"); | |
} | |
var displayMember = property.GetValue(item, null).ToString(); | |
return displayMember; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you. You saved my life