ItemsSource Destekli Özel Custom UserControl

MVVM desenine sadık kalmaya çalışıyorsunuz ama Xamarin ile gelen hazır kontroller sizi üzüyor mu? O zaman öncelikle ItemsSource sorununa el atalım.

Bu konudaki ilk sorunum elimdeki bir veri kümesinin her bir elamanı için arayüzde alt alta nesneler oluşturmak oldu. Ekrana bir StackLayout ekleyip tek tek Add dersem bu MVVM yapısından uzaklaşmış olmam demek. Yok bu iş için ListView kullanırsam, hem tasarıma uymayacak hem de karıncaya atom bombası atmış olacağım.

WPF kullanıyor olsam orada ItemsControl ile basit bir binding işlemi ile bilgileri StackPanel içine dizmem mümkün olacaktı. Ama o da elimde yok. Bu sorunla ilk ben karşılaşmadığım için Internet üzerinde bir çok çözüm geliştirilmiş. Ben daha basit çözüm aradığım için hem de Bindable Property’ler için daha güzel bir örnek yapabilmek için basitçe WPF ItemsControlü taklit etmeye çalışacağım.

Not: Bu yazı için MVVM ve Bindable Property’ler hakkında bilginiz olduğunu varsayıyorum. Xamarin TR içinde her iki konu içinde kaynak bulabilirsiniz.

Oluşturacağım ItemsControl nesnesinin özellik(property)leri:

  • ItemsPanel : Taşıyıcı elemanı belirtecek. StackLayout, Grid… kullanıcı ne arzu ederse. Varsayılan olarak StackLayout olacak.
  • ItemTemplate : Taşıyıcı eleman içerisine koyulacak her bir elemanın şablonu.
  • ItemsSource : Asıl niyetimiz, elemanların kaynağı.

Hemen yeni bir proje açıyoruz ve dosyaları aşağıdaki gibi hazırlıyoruz :

│   App.xaml
│   App.xaml.cs
├───Models
│       Ogrenci.cs
│       
├───UserControls
│       ItemsControl.cs
│       
├───ViewModels
│       MainPageViewModel.cs
│       
└───Views
        MainPage.xaml
        MainPage.xaml.cs

 

Önce basitlerle başlayalım,

Ogrenci.cs içeriğimiz bir öğrencinin adını ve soyadını tutacak:

namespace App1.Models
{
    public sealed class Ogrenci
    {
        public string Ad { get; set; }
        public string Soyad { get; set; }
    }
}

MainPageViewModel.cs de klasik PropertyChanged olaylarına girmiyorum. Basit bir listem olacak :

using System.Collections.ObjectModel;
using App1.Models;

namespace App1.ViewModels
{
    public sealed class MainPageViewModel
    {
        public MainPageViewModel()
        {
            Ogrenciler = new ObservableCollection<Ogrenci>
            {
                new Ogrenci {Ad = "Cihan", Soyad = "Yakar"},
                new Ogrenci {Ad = "Yiğit", Soyad = "Özaksüt"}
            };
        }
        public ObservableCollection<Ogrenci> Ogrenciler { get; set; }
    }
}

Hazırlık aşamasını bitirdik. Gelelim WPF den esinlendiğimiz ItemsControlümüze:

Sınıfımız ilk açtığımızda şu şekilde olacak :

using System;
using System.Collections;
using Xamarin.Forms;

namespace App1.UserControls
{
    public sealed class ItemsControl : ContentView
    {}
}

Sınfımız ContentView den türüyor. Yani ekrana koyulabilir ve sadece 1 içeriği olabilir.

Bu içerik ise ItemsPanel özelliğimiz olacak. Ben buna dışarıdan bir şey bind etmeyeceğim için bildiğimiz property mantığı ile lazy olarak tanımlıyorum :

private Layout<View> _itemsPanel;

public Layout<View> ItemsPanel
{
    get { return _itemsPanel ?? (_itemsPanel = new StackLayout()); }
    set
    {
        _itemsPanel = value;
        Redraw(this);
    }
}

Bu özelliğin türü Layout<View> yani kendisi bir çeşit Container, daha da açıklarsak StackLayout, RelativeLayout, Grid gibi birden fazla içeriği olabilir.

Get metodunda bu özellik atanmamış ise yeni bir StackLayout oluşturmasını istiyorum. Set metodunda ise bu atama neticesinde arayüzü tekrar oluşturmak için Redraw metodumu çağırıyorum. ItemsControle benzetmek için adlandırmaları İngilizce yaptığımdan dolayı geriye kalan adlandırmalarda türkülizceden kaçınmak için İngilizce devam ediyorum.

ItemsSource ve ItemTemplate bindable propertylerimi oluşturuyorum.

  public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
     "ItemsSource",
     typeof(IEnumerable),
     typeof(ItemsControl),
     null,
     BindingMode.OneWay,
     propertyChanged: (bindable, value, newValue) => Redraw(bindable));

 public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
     "ItemTemplate",
     typeof(DataTemplate),
     typeof(ItemsControl),
     null,
     propertyChanged: (bindable, value, newValue) => Redraw(bindable));


 public IEnumerable ItemsSource
 {
     get { return (IEnumerable) GetValue(ItemsSourceProperty); }
     set { SetValue(ItemsSourceProperty, value); }
 }

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

ItemsSource herhangi bir çoğul nesne olabileceği için en geniş ifade şekli olarak IEnumerable arayüzünü tip olarak gösteriyorum.

ItemTemplate ise tanıdık olan DataTemplate türünden bir değer alacak.

Yine her iki property de değiştirildiğinde arayüzü tekrar şekillendirmek için Redraw methoduma gidiyor olacaklar.

Redraw metodumun içeriği ise şu şekilde :

 private static void Redraw(BindableObject bindable)
 {
     var itemsControl =  bindable as ItemsControl;
     if (itemsControl == null)
 	 {
     throw new Exception($"Invalid container {nameof(bindable)}");
     }
     
     itemsControl.Content = itemsControl.ItemsPanel;

     if (itemsControl.ItemsSource == null || itemsControl.ItemTemplate == null)
     {
         return;
     }

     foreach (var viewModel in itemsControl.ItemsSource)
     {
         var childContainer = itemsControl.ItemTemplate.CreateContent();

         if (!(childContainer is View || childContainer is ViewCell))
         {
             throw new Exception($"Invalid object {nameof(childContainer)}");
         }

         var view = childContainer is View 
                     ? childContainer as View 
                     : ((ViewCell) childContainer).View;
         view.BindingContext = viewModel;
         itemsControl.ItemsPanel.Children.Add(view);
     }
 }

BindableObject WPF den gelenler için DependencyObjectin Xamarincesi. Uzak olanlar için, binding yeteneğine sahip olan obje gibi düşünebilirsiniz. Burada kendisi ItemsPanel nesnemin kendisi oluyor.

Bu nesnemin özelliklerine erişebilmek için öncelikle kendi neseme dönüştürüyorum, eğer dönüştüremediysem hata fırlatıyorum.

ItemsControl bir ContentView idi bu durumda onun Content özelliğini atıyorum. Eğer bu ana kadar ItemsPanel özelliğine bir şey atanmadıysa yukarıdaki kodlardan hatırlayacağınız üzere kendisi yeni bir StackLayout olacak.

Ardından ItemsSource özelliğine ve ItemsPanel özelliğine null kontrolü yapıyorum. Eğer ki siz WPF davranışı gibi bu kontrol ItemsSource olmadığında kendi Childrenlarını göstersen isterseniz, ItemsControlü ContentView yerine Layout<View>den türetip bu kısımda kodu dallandırmanız gerekecek. Ben kodu kalabılıklaştırmamak adına oraya girmiyorum.

Son döngü de ise ItemsSource’un her bir elemanı için ItemTemplate den bir nesne oluşturup o nesnenin BindingContextine her bir elemanı bind ediyorum.

MainPage.xaml da kullanıma geçebiliriz artık :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewModels="clr-namespace:App1.ViewModels;assembly=App1"
             xmlns:userControls="clr-namespace:App1.UserControls;assembly=App1"
             x:Class="App1.MainPage">
    <ContentPage.BindingContext>
        <viewModels:MainPageViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <Frame>
            <userControls:ItemsControl ItemsSource="{Binding Ogrenciler}">
                <userControls:ItemsControl.ItemsPanel>
                    <StackLayout />
                </userControls:ItemsControl.ItemsPanel>
                <userControls:ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <StackLayout Orientation="Horizontal">
                            <Label Text="{Binding Ad}" />
                            <Label Text="{Binding Soyad}" />
                        </StackLayout>
                    </DataTemplate>
                </userControls:ItemsControl.ItemTemplate>
            </userControls:ItemsControl>
        </Frame>
    </ContentPage.Content>
</ContentPage>

Bu örnekte dilerseniz ItemsPanel düğümünü silebilirsiniz, bu durumda zaten varsayılan olarak StackLayout oluşturulacak.

Örneğimiz ekran çıktısı ise :

Örneği biraz değiştirelim :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewModels="clr-namespace:App1.ViewModels;assembly=App1"
             xmlns:userControls="clr-namespace:App1.UserControls;assembly=App1"
             x:Class="App1.MainPage">
    <ContentPage.BindingContext>
        <viewModels:MainPageViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <userControls:ItemsControl ItemsSource="{Binding Ogrenciler}" VerticalOptions="Start" HeightRequest="70">
            <userControls:ItemsControl.ItemsPanel>
                <StackLayout Orientation="Horizontal" />
            </userControls:ItemsControl.ItemsPanel>
            <userControls:ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Frame OutlineColor="#3d3d6d" BackgroundColor="#DDDDDD" Padding="12" Margin="4">
                        <StackLayout Orientation="Vertical">
                            <Label Text="{Binding Ad}" />
                            <Label Text="{Binding Soyad}" />
                        </StackLayout>
                    </Frame>
                </DataTemplate>
            </userControls:ItemsControl.ItemTemplate>
        </userControls:ItemsControl>
    </ContentPage.Content>
</ContentPage>

Buraya kadar olan örneklerde hep eleman pozisyon bilgisi olmayan (otomatik ayarlanan) StackLayout , WrapLayout gibi layoutlara göre işlem yaptık. Peki ya AbsoluteLayout veya RelativeLayout gibi kapsayıcılar kullanılırsa bunlara pozisyon bilgisini nasıl aktaracağız? Sorusu kafanızda canlanmıştır. Bunun çözümünü şimdilik size bırakıyorum (Redraw metoduna parametre taşıyor olacaksınız.). Yine de şöyle bir spoiler vereyim, bununla ilgili olarak ben attached property kullanıyor olacağım bir tablo örneği ile geleceğim.

Görüşmek üzere.

Cihan Yakar

Yorum Gönder