Forked from TrueGeek/BindableScrollableStackLayout.xaml
Created
July 15, 2021 22:44
-
-
Save luismts/560bcf736552443f1001df549be6e8ce to your computer and use it in GitHub Desktop.
Xamarin Forms Bindable Scrollable StackLayout
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
<?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> |
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.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