Skip to content

Instantly share code, notes, and snippets.

@NVentimiglia
Last active October 31, 2021 21:06
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save NVentimiglia/2723411428cdbb72fac6 to your computer and use it in GitHub Desktop.
Save NVentimiglia/2723411428cdbb72fac6 to your computer and use it in GitHub Desktop.
Need a Items control or a repeater for Xamarin ? Here you go. Bind to observable collections, define your data template and enjoy.
// 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;
});
}
}
@eric-wieser
Copy link

eric-wieser commented Jun 11, 2016

This doesn't handle ObservableCollection<T> correctly. The following should be on these lines;

            var t = ItemsSource.GetType();
            var isObs = t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(ObservableCollection<>);
            if (isObs)
            {
                object o = Activator.CreateInstance(typeof(ObservableReadOnlyCollection<>).MakeGenericType(t.GenericTypeArguments), ItemsSource);
                ObservableSource = (IObservableReadOnlyCollection<object>)o;
            }

With this class definition:

    public interface IObservableReadOnlyCollection<out T> : IEnumerable<T>, INotifyCollectionChanged
    {
        T this[int i] { get; }
        int Count { get; }
    }

    public class ObservableReadOnlyCollection<T> : IObservableReadOnlyCollection<T>
    {
        public ObservableCollection<T> _inner;
        public ObservableReadOnlyCollection(ObservableCollection<T> inner) { _inner = inner; }

        public T this[int i] => _inner[i];
        public int Count => _inner.Count;

        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add { _inner.CollectionChanged += value; }
            remove { _inner.CollectionChanged -= value; }
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _inner.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _inner.GetEnumerator();
        }
    }

This then works perfectly

@francedot
Copy link

Awesome! I recommed using it insted of a Listview inside a scrollview

@paulocinf
Copy link

Wonderfull help! This peace of code is just what i was looking for. The purpose is the same as @francibon93 recommendation. It sure will help my day.

@Solethia
Copy link

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.

@fschwiet
Copy link

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));
        }
    }

@fschwiet
Copy link

@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;

@thefallman
Copy link

Great job! Many thanks! I'm wondering, why so simple but powerful solution is still not part of the framework?

@Lexor7
Copy link

Lexor7 commented Aug 15, 2017

thanks.. helpfully..

@Jsrc1990
Copy link

Thanks sirs. it's possible to say that you have saved my life in a way...

@barryculhane
Copy link

barryculhane commented Aug 22, 2017

@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!

@bmasis
Copy link

bmasis commented Aug 25, 2017

@eric-wieser error in ObservableSource = (IObservableReadOnlyCollection)o;
@fschwiet in setItem where do you get newValue?

@luismts
Copy link

luismts commented Sep 2, 2017

@bmasis +1

@CalvinFong
Copy link

@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;
}

@veamitkumar
Copy link

veamitkumar commented Nov 24, 2017

when i am using this itemStack , it does not have scrolling functionality and when i put that in scroll it does not scroll .

@pcdus
Copy link

pcdus commented Jan 25, 2018

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?

@jamsoft
Copy link

jamsoft commented Sep 26, 2018

None of this joins up, the original class and all the edits are incomplete unfortunately. Writing my own, thanks anyway.

@ItsBluee
Copy link

ItsBluee commented Feb 5, 2020

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment