-
-
Save NVentimiglia/2723411428cdbb72fac6 to your computer and use it in GitHub Desktop.
// MIT License | |
// Nicholas Ventimiglia | |
// 2016-9-19 | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Collections.Specialized; | |
using System.Linq; | |
using System.Windows.Input; | |
using Xamarin.Forms; | |
/// <summary> | |
/// For repeated content without a automatic scroll view. Supports DataTemplates, Horizontal and Vertical layouts ! | |
/// </summary> | |
/// <remarks> | |
/// Warning TODO NO Visualization or Paging! Handle this in your view model. | |
/// </remarks> | |
public class ItemsStack : StackLayout | |
{ | |
#region BindAble | |
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<ItemsStack, IEnumerable>(p => p.ItemsSource, default(IEnumerable<object>), BindingMode.TwoWay, null, ItemsSourceChanged); | |
public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create<ItemsStack, object>(p => p.SelectedItem, default(object), BindingMode.TwoWay, null, OnSelectedItemChanged); | |
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<ItemsStack, DataTemplate>(p => p.ItemTemplate, default(DataTemplate)); | |
public event EventHandler<SelectedItemChangedEventArgs> SelectedItemChanged; | |
public IEnumerable ItemsSource | |
{ | |
get { return (IEnumerable)GetValue(ItemsSourceProperty); } | |
set { SetValue(ItemsSourceProperty, value); } | |
} | |
public object SelectedItem | |
{ | |
get { return GetValue(SelectedItemProperty); } | |
set { SetValue(SelectedItemProperty, value); } | |
} | |
public DataTemplate ItemTemplate | |
{ | |
get { return (DataTemplate)GetValue(ItemTemplateProperty); } | |
set { SetValue(ItemTemplateProperty, value); } | |
} | |
private static void ItemsSourceChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue) | |
{ | |
var itemsLayout = (ItemsStack)bindable; | |
itemsLayout.SetItems(); | |
} | |
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue) | |
{ | |
var itemsView = (ItemsStack)bindable; | |
if (newValue == oldValue) | |
return; | |
itemsView.SetSelectedItem(newValue); | |
} | |
#endregion | |
#region item rendering | |
protected readonly ICommand ItemSelectedCommand; | |
protected virtual void SetItems() | |
{ | |
Children.Clear(); | |
if (ItemsSource == null) | |
{ | |
ObservableSource = null; | |
return; | |
} | |
foreach (var item in ItemsSource) | |
Children.Add(GetItemView(item)); | |
var isObs = ItemsSource.GetType().IsGenericType && ItemsSource.GetType().GetGenericTypeDefinition() == typeof(ObservableCollection<>); | |
if (isObs) | |
{ | |
ObservableSource = new ObservableCollection<object>(ItemsSource.Cast<object>()); | |
} | |
} | |
protected virtual View GetItemView(object item) | |
{ | |
var content = ItemTemplate.CreateContent(); | |
var view = content as View; | |
if (view == null) | |
return null; | |
view.BindingContext = item; | |
var gesture = new TapGestureRecognizer | |
{ | |
Command = ItemSelectedCommand, | |
CommandParameter = item | |
}; | |
AddGesture(view, gesture); | |
return view; | |
} | |
protected void AddGesture(View view, TapGestureRecognizer gesture) | |
{ | |
view.GestureRecognizers.Add(gesture); | |
var layout = view as Layout<View>; | |
if (layout == null) | |
return; | |
foreach (var child in layout.Children) | |
AddGesture(child, gesture); | |
} | |
protected virtual void SetSelectedItem(object selectedItem) | |
{ | |
var handler = SelectedItemChanged; | |
if (handler != null) | |
handler(this, new SelectedItemChangedEventArgs(selectedItem)); | |
} | |
ObservableCollection<object> _observableSource; | |
protected ObservableCollection<object> ObservableSource | |
{ | |
get { return _observableSource; } | |
set | |
{ | |
if (_observableSource != null) | |
{ | |
_observableSource.CollectionChanged -= CollectionChanged; | |
} | |
_observableSource = value; | |
if (_observableSource != null) | |
{ | |
_observableSource.CollectionChanged += CollectionChanged; | |
} | |
} | |
} | |
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | |
{ | |
switch (e.Action) | |
{ | |
case NotifyCollectionChangedAction.Add: | |
{ | |
int index = e.NewStartingIndex; | |
foreach (var item in e.NewItems) | |
Children.Insert(index++, GetItemView(item)); | |
} | |
break; | |
case NotifyCollectionChangedAction.Move: | |
{ | |
var item = ObservableSource[e.OldStartingIndex]; | |
Children.RemoveAt(e.OldStartingIndex); | |
Children.Insert(e.NewStartingIndex, GetItemView(item)); | |
} | |
break; | |
case NotifyCollectionChangedAction.Remove: | |
{ | |
Children.RemoveAt(e.OldStartingIndex); | |
} | |
break; | |
case NotifyCollectionChangedAction.Replace: | |
{ | |
Children.RemoveAt(e.OldStartingIndex); | |
Children.Insert(e.NewStartingIndex, GetItemView(ObservableSource[e.NewStartingIndex])); | |
} | |
break; | |
case NotifyCollectionChangedAction.Reset: | |
Children.Clear(); | |
foreach (var item in ItemsSource) | |
Children.Add(GetItemView(item)); | |
break; | |
} | |
} | |
#endregion | |
public ItemsStack() | |
{ | |
ItemSelectedCommand = new Command<object>(item => | |
{ | |
SelectedItem = item; | |
}); | |
} | |
} |
Thanks so much. Given the documentation at https://blog.stephencleary.com/2009/07/interpreting-notifycollectionchangedeve.html though it seems the CollectionChanged implementation needs work. I am trying something like:
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
//
// Good documentation on NotifyCollectionChangedEventArgs:
// https://blog.stephencleary.com/2009/07/interpreting-notifycollectionchangedeve.html
//
// We may need to do a full reset regardless of the change action because the
// change parameters may be invalid.
var doReset = false;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
if (e.NewStartingIndex == -1 || e.NewItems == null)
{
doReset = true;
}
else
{
int index = e.NewStartingIndex;
foreach (var item in e.NewItems)
Children.Insert(index++, GetItemView(item));
}
}
break;
case NotifyCollectionChangedAction.Move:
{
if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1 || e.OldItems == null ||
e.NewItems == null)
{
doReset = true;
}
else
{
List<View> movedViews = new List<View>();
foreach (var removedItem in e.OldItems)
{
movedViews.Add(Children[e.OldStartingIndex]);
Children.RemoveAt(e.OldStartingIndex);
}
var newStartingIndex = e.NewStartingIndex;
foreach (var movedView in movedViews)
{
Children.Insert(newStartingIndex++, movedView);
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
{
if (e.OldStartingIndex == -1 || e.OldItems == null)
{
doReset = true;
}
else
{
foreach (var removedItem in e.OldItems)
Children.RemoveAt(e.OldStartingIndex);
}
}
break;
case NotifyCollectionChangedAction.Replace:
{
if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1 || e.OldItems == null ||
e.NewItems == null || e.OldStartingIndex != e.NewStartingIndex)
{
doReset = true;
}
else
{
foreach (var removedItem in e.OldItems)
{
Children.RemoveAt(e.OldStartingIndex);
}
var newStartingIndex = e.NewStartingIndex;
foreach (var insertedItem in e.NewItems)
{
Children.Insert(newStartingIndex++, GetItemView(insertedItem));
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
doReset = true;
break;
}
if (doReset)
{
Children.Clear();
foreach (var item in ItemsSource)
Children.Add(GetItemView(item));
}
}
@eric-weiser to simplify your fix, ObservableSource can just be a INotifyCollectionChanged. As a bugfix, one should set ObservableSource to null when the ItemSource is changed from a ObservableCollection to a non-ObservableCollection, to do this SetItems is simplified to just be:
ObservableSource = newvalue as INotifyCollectionChanged;
Great job! Many thanks! I'm wondering, why so simple but powerful solution is still not part of the framework?
thanks.. helpfully..
Thanks sirs. it's possible to say that you have saved my life in a way...
@eric-wieser and @fschwiet, would one of you be willing to post a complete gist with your revisions? When I try to apply your fixes to the original gist, it's not compiling. Thanks in advance!
@eric-wieser error in ObservableSource = (IObservableReadOnlyCollection)o;
@fschwiet in setItem where do you get newValue?
@bmasis +1
@bmasis @barryculhane @luismts
Chuck this in SetItems (and you can remove ObservableSource). Should allow CollectionChanged to trigger (my issue at least was that it wasn't triggering for adding/removing elements from my ObservableCollection).
var itemsSourceINotifyCollectionChanged = ItemsSource as INotifyCollectionChanged;
if (itemsSourceINotifyCollectionChanged != null) {
itemsSourceINotifyCollectionChanged.CollectionChanged += CollectionChanged;
}
when i am using this itemStack , it does not have scrolling functionality and when i put that in scroll it does not scroll .
I encounter an problem on this line:
var isObs = ItemsSource.GetType().IsGenericType && ItemsSource.GetType().GetGenericTypeDefinition() == typeof(ObservableCollection<>);
I get the following error:
'Type' does not contain a definition for 'IsGenericType' and no extension method 'IsGenericType' accepting a first argument of type 'Type' could be found (are you missing a using directive or an assembly reference?)
Any idea?
None of this joins up, the original class and all the edits are incomplete unfortunately. Writing my own, thanks anyway.
I uses a modified version of it for some extra feature, it was working all good until I upgraded my Xamarin.Forms version.
I gets null pointer exception over var content = ItemTemplate.CreateContent();
Anyone got the same issue or someone got a fix for it?
previous XF V - 2.3
current XF V - 4.2 (Forcefully needed to upgrade it cuz of iOS13 iPad issue with masterPage)
Why would an ItemsControl or Repeater have a SelectedItem? I feel that should be abstracted up to a higher level if needed. Else looks good.