Xamarin.Forms Controls: RepeaterView

Manchmal kommt man in die Verlegenheit, dass man gerne zwei ListViews innerhalb einer ScrollView verwenden möchte, was jedoch gar keine gute Idee ist, denn so hat man mehrere verschachtelte ScrollViews auf einer Page, was zu unerwünschtem Verhalten führen kann. In diesem Fall kann es hilfreich sein ein einfaches Control zur Verfügung zu haben, welches eine Liste von Objekten entgegen nimmt und diese entsprechend eines Templates anzeigt. Bei Bedarf kann man dann dieses selbst in eine ScrollView packen und erreicht so das gewünschte Ergebnis.

In diesem Beitrag möchte ich nun zeigen, wir ihr euch mit wenig Aufwand eine eigenes Control (ohne CustomRenderer für die einzelnen Plattformen) schreiben könnt, nämlich die RepeaterView. Eigentlich handelt es sich dabei nur um eine Erweiterung eines StackLayouts, welches über drei zusätzliche Properties verfügt: ItemsSource, ItemTemplate und ItemTappedCommand. Schauen wir uns das ganze doch einmal Schritt für Schritt genauer an.

Wir starten mit der Klassendefinition. Der Name der Klasse lautet RepeaterView und leitet vom StackLayout ab. Damit ergibt sich die folgende Signatur:

public class RepeaterView : StackLayout
{
    
}

Anschließend fügen wir den Standard-Konstruktor hinzu und setzen die Property Spacing auf 0, aber später lässt sich dieser ja wieder frei konfigurieren.

public RepeaterView()
{
    Spacing = 0;
}

Wir fügen nun unsere drei zusätzlichen Properties hinzu.

public static readonly BindableProperty ItemsSourceProperty =
    BindableProperty.Create(nameof(ItemsSource), typeof(ICollection), 
      typeof(RepeaterView), propertyChanged: ItemsSourcePropertyOnChanged);

public ICollection ItemsSource
{
    get => (ICollection)GetValue(ItemsSourceProperty);
    set => SetValue(ItemsSourceProperty, value);
}

public static readonly BindableProperty ItemTemplateProperty =
    BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), 
      typeof(RepeaterView), default(DataTemplate));

public DataTemplate ItemTemplate
{
    get => (DataTemplate)GetValue(ItemTemplateProperty);
    set => SetValue(ItemTemplateProperty, value);
}

public static readonly BindableProperty ItemTappedCommandProperty =
    BindableProperty.Create(nameof(ItemTappedCommand), typeof(ICommand), 
      typeof(RepeaterView));

public ICommand ItemTappedCommand
{
    get => GetValue(ItemTappedCommandProperty) as ICommand;
    set => SetValue(ItemTappedCommandProperty, value);
}

Im nächsten Schritt fügen wir eine Methode AddTapGestureToChildView hinzu, welche die Aufgabe übernimmt den ItemTappedCommand entsprechend an unsere View zu hängen, so dass wir auf einen Tap auf den Eintrag entsprechend einen Command ausführen können, sofern dies gewünscht ist.

private void AddTapGestureToChildView(View view)
{
    if (view == null)
        throw new ArgumentNullException(nameof(view));

    view.GestureRecognizers.Add(new TapGestureRecognizer
    {
        Command = new Command(() =>
        {
            if (ItemTappedCommand.CanExecute(view.BindingContext))
                ItemTappedCommand.Execute(view.BindingContext);
        })
    });
}

Wir erstellen nun die Methode ViewFor, welche die Aufgabe hat mit der Hilfe unserer ItemTemplates die entsprechende View zu erzeugen. Ebenfalls wird hier bei Bedarf der ItemTappedCommand entsprechend registriert.

protected virtual View ViewFor(object item)
{
    if (ItemTemplate == null)
        return null;

    var content = ItemTemplate.CreateContent();

    var view = content is ViewCell viewCell 
	    ? viewCell.View 
		: content as View;

    view.BindingContext = item;

    if (ItemTappedCommand != null)
        AddTapGestureToChildView(view);

    return view;
}

Als nächstes erstellen wir eine Methode, welche wir benötigen sobald wir mit ObservableCollections arbeiten, damit sich die View entsprechend aktualisiert und die Einträge hinzugefügt bzw. entfernt werden.

private void AddCollectionNotificationsListener(
    INotifyCollectionChanged observableCollection)
{
    if (observableCollection == null)
        throw new ArgumentNullException(nameof(observableCollection));

    observableCollection.CollectionChanged -= ObsersavableCollectionOnCollectionChanged;
    observableCollection.CollectionChanged += ObsersavableCollectionOnCollectionChanged;
}

private void ObsersavableCollectionOnCollectionChanged(object s,
    NotifyCollectionChangedEventArgs e)
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (var newItem in e.NewItems)
                Children.Add(ViewFor(newItem));
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (var oldItem in e.OldItems)
            {
                var viewToRemove = Children.FirstOrDefault(x => x.BindingContext == oldItem);
                if (viewToRemove != null)
                    Children.Remove(viewToRemove);
            }
            break;
        default:
            Children.Clear();
            foreach (var item in (ICollection)s)
                Children.Add(ViewFor(item));
            break;
    }
}

Als letztes müssen wir nun noch das ProperyChanged-Event für unsere ItemsSource ausimplementieren. Hier wird entsprechend für jedes Element die View erzeugt und dem StackLayout hinzugefügt.

private static void ItemsSourcePropertyOnChanged(BindableObject bindable, 
    object oldValue, object newValue)
{
    if (!(bindable is RepeaterView control))
        return;

    control.Children.Clear();

    var collection = (ICollection)newValue;
    if (collection == null)
        return;

    if (collection is INotifyCollectionChanged observableCollection)
        control.AddCollectionNotificationsListener(observableCollection);

    foreach (var item in collection)
        control.Children.Add(control.ViewFor(item));
}

Die gesamte Datei liegt auch noch hier als Gist bereit. Zur Demonstration habe ich auch noch eine kleine App gebaut, welche die Verwendung der RepeaterView aufzeigt. Der Code hierfür steht auf GitHub bereits und die folgenden Screenshots zeigen das Ergebnis.

Viel Spaß bei der Implementierung eurer eigenen RepeaterView.

Swagger-Dokumentation für eigene API XF: Custom Control – LabeledSwitch Xamarin.iOS App mit Azure DevOps bauen