Skip to content

Instantly share code, notes, and snippets.

@TrueGeek
Created June 3, 2019 15:13
Show Gist options
  • Save TrueGeek/1e68aa0d17d2ca7ef5a938a2aaf4db59 to your computer and use it in GitHub Desktop.
Save TrueGeek/1e68aa0d17d2ca7ef5a938a2aaf4db59 to your computer and use it in GitHub Desktop.
Xamarin Forms Bindable Scrollable StackLayout
<?xml version="1.0" encoding="UTF-8"?>
<ScrollView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Project.Controls.BindableScrollableStackLayout">
<StackLayout x:Name="stackLayout" />
</ScrollView>
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
namespace Project.Controls
{
public partial class BindableScrollableStackLayout : ScrollView
{
public event DataUpdateEventHandler OnDataUpdate;
public BindableScrollableStackLayout()
{
InitializeComponent();
stackLayout.Spacing = 0;
}
public ICollection ItemsSource
{
get => (ICollection)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(ICollection), typeof(BindableStackLayout), new List<object>(), BindingMode.TwoWay, null, propertyChanged: (bindable, oldValue, newValue) => { ItemsChanged(bindable, (ICollection)oldValue, (ICollection)newValue); });
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(BindableStackLayout), default(DataTemplate));
public ICommand SelectedItemCommand
{
get => (ICommand)GetValue(SelectedItemCommandProperty);
set => SetValue(SelectedItemCommandProperty, value);
}
public static readonly BindableProperty SelectedItemCommandProperty = BindableProperty.Create(nameof(SelectedItemCommand), typeof(ICommand), typeof(BindableStackLayout), default(ICommand));
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == SelectedItemCommandProperty.PropertyName)
{
if (SelectedItemCommand == null)
return;
foreach (var view in stackLayout.Children)
{
if (view.GestureRecognizers.Any())
view.GestureRecognizers.Clear();
BindSelectedItemCommand(view);
}
}
else if (propertyName == "Height")
{
// height changed
// probably because keyboard was shown and then hidden
// (or because it was just created)
if (stackLayout.Children.Any())
{
Device.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(100);
await ScrollToBottom(false);
});
}
}
}
private static void ItemsChanged(BindableObject bindable, ICollection oldValue, ICollection newValue)
{
var repeater = (BindableScrollableStackLayout)bindable;
if (oldValue != null)
{
if (oldValue is INotifyCollectionChanged observable)
{
observable.CollectionChanged -= repeater.CollectionChanged;
}
}
if (newValue != null)
{
repeater.UpdateItems();
if (repeater.ItemsSource is INotifyCollectionChanged observable)
{
observable.CollectionChanged += repeater.CollectionChanged;
}
}
}
private void UpdateItems()
{
if (ItemsSource.Count > 0) BuildItems();
OnDataUpdate?.Invoke(this, new EventArgs());
}
public void BuildItems()
{
stackLayout.Children.Clear();
try
{
foreach (object item in ItemsSource)
{
stackLayout.Children.Add(GetItemView(item));
}
Device.BeginInvokeOnMainThread(async () =>
{
await ScrollToBottom(false);
});
}
catch
{
// sometimes, if the list is updated quickly - we get an
// error that the collection was modified during ennumeration
// not much to do but eat the exception and then the entire
// thing will be rebuilt on the next refresh
}
}
private View GetItemView(object item)
{
var content = CreateItemContent(ItemTemplate, item);
if (!(content is View) && !(content is ViewCell))
{
throw new Exception($"Templated control must be a View or a ViewCell ({nameof(ItemTemplate)})");
}
var view = content as View ?? ((ViewCell)content).View;
view.BindingContext = item;
if (view.GestureRecognizers.Any())
view.GestureRecognizers.Clear();
if (SelectedItemCommand != null && SelectedItemCommand.CanExecute(item))
{
BindSelectedItemCommand(view);
}
else
{
view.InputTransparent = true;
}
return view;
}
private void BindSelectedItemCommand(View view)
{
var tapGestureRecognizer = new TapGestureRecognizer { Command = SelectedItemCommand, CommandParameter = view.BindingContext };
view.GestureRecognizers.Add(tapGestureRecognizer);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var items = ItemsSource.Cast<object>().ToList();
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
var index = e.NewStartingIndex;
foreach (var newItem in e.NewItems)
{
Device.BeginInvokeOnMainThread(async () =>
{
stackLayout.Children.Insert(index++, GetItemView(newItem));
await Task.Delay(25); // wait a sec to give the UI time to add the element we just added
await ScrollToBottom();
});
}
break;
case NotifyCollectionChangedAction.Move:
var moveItem = items[e.OldStartingIndex];
stackLayout.Children.RemoveAt(e.OldStartingIndex);
stackLayout.Children.Insert(e.NewStartingIndex, GetItemView(moveItem));
break;
case NotifyCollectionChangedAction.Remove:
stackLayout.Children.RemoveAt(e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
stackLayout.Children.RemoveAt(e.OldStartingIndex);
stackLayout.Children.Insert(e.NewStartingIndex, GetItemView(items[e.NewStartingIndex]));
break;
case NotifyCollectionChangedAction.Reset:
UpdateItems();
break;
}
OnDataUpdate?.Invoke(this, new EventArgs());
}
private static object CreateItemContent(DataTemplate dataTemplate, object item)
{
if (dataTemplate is DataTemplateSelector dts)
{
var template = dts.SelectTemplate(item, null);
template.SetValue(BindingContextProperty, item);
return template.CreateContent();
}
dataTemplate.SetValue(BindingContextProperty, item);
return dataTemplate.CreateContent();
}
private async Task ScrollToBottom(bool animate = true)
{
await this.ScrollToAsync(stackLayout.Children.Last(), ScrollToPosition.End, animate);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment