16 Şubat 2022 Çarşamba

UWP Uygulamalarında Data Binding

 Binding (veriye bağlama) işlemi kullanarak gösterdiğimiz elemanların içeriklerini , stillerini vs değiştirerek bağlanan veriye göre görselimizi değiştirebiliriz. Şu isteklerimiz olabilir.

  • Bir nesnenin bulunulan andaki değerlerini göstermek isteyebiliriz. Bağlama anındaki değerler kullanılır, daha sonra o nesneye ne olduğu ile ilgilenilmez.
  • Nesnelerin en son değerlerini göstermek. Bu durumda bağlı nesne değiştikçe görsel de değişecektir.
  • Kullanıcıların kontrollerden yaptığı değişikliklerin hafızadaki bağlı nesneleri de değiştirmesini isteyebiliriz.
  • Görseldeki bazı kontrollerin görünümlerini görseldeki diğer kontroller ile değiştirmek isteyebiliriz.
  • Olay işleyicileri ve şablonları hafızadaki nesnelere göre değiştirmek isteyebiliriz.
  • Nesneleri sayfada gösterirken kullanıcının veride yaptığı değişikliklere göre görüntüsünün değişmesini isteyebiliriz.


Tüm bu ihtiyaçlarımızı yerine getirmek için Universal Windows Platformunun (UWP) Binding ve x:Bind markup extensionlarını kullanırız. Her iki yöntem de benzer özellikler ve benzer komut yapısı gösterir. İlki bağlanan nesnenin tüm özelliklerine odaklanabilirken, ikincisi nesnelerin tek bir özelliğine odaklanarak daha az kaynak harcamasında bulunur. Binding'e göre x:Bind daha iyi performans gösterir ama biz burada her ikisini de göreceğiz. Her yiğidin bir yoğurt yiyişi vardır, işinize geleni kullanarak hedefe ulaşırsınız.


Elemandan Elemana Bağlama


Basit olanla başlayalım. Bu bize bir kontrolün özelliklerini diğer kontrol değerlerine bağlı olarak değiştirme imkanı tanır. Yukarıda bahsedilen her iki markup geliştirmesini de bu işlemi yapmak için kullanabiliriz. Şu aşağıdaki görsele bir bakalım. (Yeni bir UWP uygulaması başlatıp, görsel XAML dosyasına bunları yazarak deneyebilirsiniz)

<Grid VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot">
      <Image Source="Assets/drone.jpg" Width="400">
         <Image.Projection>
            <PlaneProjection RotationY="{Binding Value, ElementName=slider}"/>
         </Image.Projection>
      </Image>
      <Slider Minimum="0" Maximum="360" Name="slider" Width="400" Margin="10"/>
   </StackPanel>
</Grid>

Resmi projeye eklemek için proje ağacında Assets klasörüne sağ tıklayıp Ekle -> Var olan öğe... seçerek resmi seçerseniz bir kopyasını proje klasörünüze alacaktır. 


Bu uygulamayı çalıştırınca sayfaya bir resim ve bir slider gelecektir. Slider hareket ettirildikçe resim Y ekseni etrafında dönecektir. Dikkat ederseniz C#'ta hiç bir kod yazmadık, sadece XAML kodunda bir özellik bağlaması yaptık. Burada kullandığımız Binding iki parametreye sahip:
  • Path: Bağlama amacıyla kaynağın hangi özelliğini kullanacağınızı belirtir. Path default parametre olduğu için Path=Value şeklinde yazmamıza gerek yok, sadece Value yazınca bu default Path parametresi değeri olarak alınır.
  • ElementName: Bağlamanın kaynağı olacak elemanın adını belirtir. 
Yani burada biz resimin RotationY özelliğini kaynak olarak gösterdiğimiz slider'ın Value değerine bağlıyoruz. Böylece slider hareket ettirildikçe resim de ona bağlı olarak dönüyor. 

Bence olayı daha iyi anlamak için ama şu bağlama işindeki kaynak ve hedef olayını biraz da kafa karıştırarak anlamak için koda biraz ilave yapacağız. Yukarıda slider kaynak eleman, image ise (daha doğrusu image içindeki PlaneProjection) hedef eleman oluyor. Yani hedefin kodunda Binding kelimesi kullanarak kaynağı gösterip bağlama yapıyoruz. Kodumuzu şu şekle getirelim.

<Grid VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot">
      <Image Source="Assets/drone.jpg" Width="400">
         <Image.Projection>
            <PlaneProjection RotationY="{Binding Value, ElementName=slider}"
                                     x:Name="planeProjection"/>
         </Image.Projection>
      </Image>
      <Slider Minimum="0" Maximum="360" Name="slider" Width="400" Margin="10"/>
      <Slider Minimum="0" Maximum="360" Width="400" Margin="10"
              Value="{Binding RotationY, ElementName=planeProjection, Mode=TwoWay}"/>
   </StackPanel>
</Grid>


İkinci bir slider ekledik ama bu sefer Slider hedef oldu PlaneProjection kaynak oldu. Ama Binding parametrelerine bir de Mode ekleyip Mode=TwoWay değeri verdik. Bu durumda kaynak ve hedef birbirine 2 yönde de bağlanır. Yani kaynak ya da hedef fark etmez, hangisinde değer değişirse diğeri de bağlı olarak değişir. 

Uygulamayı çalıştırırsak eski Slider'ı kaydırdığımızda hem resimin hem de ona bağlı olan 2. Slider'ın değerlerinin değiştiğini görürüz. Ama yeni Slider'ı kaydırınca resim değerleri değişir ama eski Slider'ın değeri değişmez. Tekrar eski Slider'ı kaydırsak artık etkisi kalmadığını görürüz. Bunun sebebi eski Slider'da Mode parametresine değer vermediğimiz için default olan OneWay (yani tek yönlü) bağlama yapıldı. OneWay bağlama türünde kaynaktaki değer değişince hedefteki değer değişir. Ama bir şekilde ya C# kod içinden ya da başka bir bağlama ile hedefteki değer kaynaktan bağımsız değişince bağ kopar. Artık kaynak değerindeki değişim hedefe etki edemez. 

Mode parametresine verilecek 3 değişik değer vardır:
  • OneTime: Binding yapılan nesne üretildiği andaki değerler esas alınarak bir kez bağlama yapılır. Daha sonra oluşacak değer değişimlerinin etkisi olmaz. 
  • OneWay: Bu modda kaynak nesne değeri değiştikçe hedef nesne özelliği de ona bağlı değişir. 
  • TwoWay: Bu modda kaynak ya da hedef hangi değer değişirse diğeri ona bağlı değişir. Yani aslında artık kaynak ya da hedef yok gibidir. Ama kodun yazılışına göre hedef Binding kelimesini kullandığımız nesnedir, kaynak da ElementName parametresinde belirttiğimiz nesne.
İkinci Slider örneğinde hedef Slider kaynak ise PlaneProjection elemanıdır. Biz hedefteki özellik değişiminin kaynakta değişiklik yapabilmesi için TwoWay bağlama yaptık. Bu mod bağlamada kodun başka bir yerinden de özellik değişse bağlantı kopmaz bağlı nesnelerin bağlı özellikleri hemen güncellenir. Deneme için bir de TextBox elemanı ekleyelim koda. İlk Slider'ı da uçuralım. 

<Grid VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot">
      <Image Source="Assets/drone.jpg" Width="400">
         <Image.Projection>
            <PlaneProjection x:Name="planeProjection"/>
         </Image.Projection>
      </Image>
      <Slider Minimum="0" Maximum="360" Width="400" Margin="10"
           Value="{Binding RotationY, ElementName=planeProjection, Mode=TwoWay}"/>
      <TextBox Width="200" 
           Text="{Binding RotationY, ElementName=planeProjection, Mode=TwoWay}"/>
   </StackPanel>
</Grid>

TextBox elemanı da Slider ile aynı kaynağa TwoWay bağlı olduğu için bir kaynak, iki hedef eleman birbrine bağlı olarak özellikler değişir. Hangisini değiştirirsek diğerlerinin de değiştiğini görürüz.

Aynı şeyleri x:Bind kullanarak da yapabiliriz. Ama biraz kullanım farklı tabi. 

<Grid VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot">
      <Image Source="Assets/drone.jpg" Width="400">
         <Image.Projection>
            <PlaneProjection x:Name="planeProjection"/>
         </Image.Projection>
      </Image>
      <Slider Minimum="0" Maximum="360" Width="400" Margin="10"
              Value="{x:Bind planeProjection.RotationY, Mode=TwoWay}"/>
      <TextBox Width="200" 
               Text="{x:Bind planeProjection.RotationY, Mode=TwoWay}"/>
   </StackPanel>
</Grid>

Biraz daha nesnel temelli yazım gibi oldu. Path ve ElementName parametreleri tek bir parametrede <element name>.<property> şeklide birleştirilmiş. Bu birleşik parametre x:Bind yapısında default parametre olduğu için direk yazılıyor. Bir de bu şekil kullanımda Visual Studio kod yardımcısı otomatik tamamlamaları da gösterebiliyor. Karmaşık projelerde nesne seçmek zor olabiliyor.

 x:Bind yapısının en önemli ve dikkat edilmesi gereken farkı Mode parametresi default değeri OneTime 'dır. Sakın ola ki OneWay diye parametreyi default değere bırakmayın. 


Kod İçinde Bağlama


Binding çalışma zamanında aktif olduğu için, görseldeki kontroller ve hafızadaki nesneler arasında ya da kontroller arasında uygulama çalışırken de dinamik bağlantılar yapabiliriz. 

Bunu gerçekleştirmek için Binding sınıfını kullanır ve aynı XAML kodda olduğu gibi özelliklere değer veririz. 

<Grid VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot">
      <Image Source="Assets/drone.jpg" Width="400">
         <Image.Projection>
            <PlaneProjection x:Name="planeProjection"/>
         </Image.Projection>
      </Image>
      <Slider Minimum="0" Maximum="360" 
           Width="400" Margin="10" Name="slider"/>
   </StackPanel>
</Grid>

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            Binding binding = new Binding();
            binding.ElementName = "slider";
            binding.Path = new PropertyPath("Value");
            binding.Mode = BindingMode.TwoWay;
            BindingOperations.SetBinding(planeProjection, 
                PlaneProjection.RotationYProperty, binding);

            base.OnNavigatedTo(e);
        }

Kodla yaparken biraz zahmetli oluyor ama bu kod içinde görsel elemanlara müdahale sadece burada değil her yerde lazım oluyor. Kodu biraz dikkatle incelerseniz yukarıda XAML kodunda yaptığımız Binding işlemini C# ile yapılışı. Sanki Almanca cümleyi İngilizce'ye çevirir gibi bir şey. 

Önce binding adında bir Binding nesnesi oluşturuluyor. Bu aslında ilk örnekte PlaneProjection elemanının RotationY özelliğine değer yazarken girdiğimiz o süslü parantezler içinde ifade edilen nesne ( {Binding Value, ElementName=slider} ). Gördüğümüz gibi binding nesnesinin Path özelliğine Value , ElementName özelliğine slider değeri veriliyor, Mode özelliği de TwoWay yapılıyor. Süslü parantez artık binding isimli nesnede hazır. Sonra da bunu planeProjection nesnesinin RotationY özelliğine bir Binding olarak ayarlıyoruz. 

Burada BindingOperations sınıfı ile Bindings ataması yapıldı. Bu işlem 3 parametre alır. Bir nesne adı, atama yapılacak özellik ve Binding nesnesi. Eğer kendi kontrolünüzü üretiyorsanız, hedef olarak sadece DependencyProperty kullanılması gerektiğini unutmayın.



Elamandan Nesneye Bağlamalar


En çok kullanılan veri bağlama işlemi görsel kontrollerin kod içinde üretilen nesnelere bağlanmasıdır. Örneğin bir veritabanından değerler okunup ekranda gösterimi yapılırken çok kullanılır ya da bizim otomasyon projelerimizde PLC lerden okunan değişken değerlerine göre görselde renkler yazılar vs değişmesini isteriz gibi. 

Yeni bir proje başlatalım. Örnek olarak bir çalışanın bilgilerini içeren sınıf tanımı yapalım:

    public class Employee
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string EMail { get; set; }

        public int Age { get; set; }
    }

Bu sınıf tanımı içinde 4 tane public özellik tanımlaması var. Binding işlemi private ya da protected özelliklere erişemez. 

Şimdi bu sınıfın gösterimi için bir Grid tablosu oluşturalım.

<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
   <Grid.RowDefinitions>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="Auto"/>
   </Grid.ColumnDefinitions>
        
   <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding FirstName, Mode=TwoWay}"
                 Grid.Row="0" Grid.Column="1"/>
   <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding LastName, Mode=TwoWay}"
                 Grid.Row="1" Grid.Column="1"/>
   <TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding EMail, Mode=TwoWay}"
                 Grid.Row="2" Grid.Column="1"/>
   <TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding Age, Mode=TwoWay}"
                 Grid.Row="3" Grid.Column="1"/>
</Grid>

Son olarak Employee sınıfından bir nesne üreteceğiz ve onu aktif formumuza veri kaynağı olarak göstereceğiz. 

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            Employee emp = new Employee
            {
                FirstName = "Ümit",
                LastName = "Kayacık",
                EMail = "umit@email.com",
                Age = 56
            };
            this.DataContext = emp;

            base.OnNavigatedTo(e);
        }

Uygulamayı çalıştırırsak şu görüntüyü alırız.


Bir satır bişey yazdık elimizdeki veriyi sayfaya bağladık. this.DataContext = emp; 

DataContext özelliği FrameworkElement sınıfı içinde tanımlıdır ve herhangi bir nesneye referans bilgisi içerebilir. Tek başına bu özellik iş yapmıyor aslında. Binding tanımlarken bir ElementName belirtmedik, otomatik olarak bağlı değeri almak için o hedef elemanın DataContext özelliğine bakar. Orada bir değer olmayınca (Null görürse) elemanı kapsayan üst elemana bakar , acaba onun DataContext'i var mı diye. Örnek kodda Page elemanına tanımlanan DataContext'e kadar gidip onu bulacaktır. Bir elemanın DataContext özelliğini değiştirirseniz içindeki elemanlara da sirayet edecektir. C# kod yazmadan XAML içinde şu şekilde de data tanımlanabilir. Hani lazım olamz ya, onu da bilelim.

...
    <Page.Resources>
        <local:Employee x:Key="emp" FirstName="Ümit" LastName="Kayacık" 
                       EMail="umit@email.com" Age="56"></local:Employee>
    </Page.Resources>
    <Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource emp}">
...

Burada direk sayfanın DataContext özelliğine atanan bir nesne ürettik. İlk bakışta verinin XAML kod içinde tanımlı olması pek bir şey ifade etmez gibi geliyor. Ama veritabanı uygulamalarında View Model'lerle çalışırken çok yardımcı olurlar. 

Son olarak aynı formu gösteren şu koda da bakalım.

<Page.Resources>
   <local:Employee x:Key="emp" FirstName="Ümit" LastName="Kayacık" 
                EMail="umit@email.com" Age="56"></local:Employee>
</Page.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.RowDefinitions>...
<Grid.ColumnDefinitions>...
       
   <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding FirstName, Mode=TwoWay, Source={StaticResource emp}}"
                 Grid.Row="0" Grid.Column="1"/>
   <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding LastName, Mode=TwoWay, Source={StaticResource emp}}"
                 Grid.Row="1" Grid.Column="1"/>
   <TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding EMail, Mode=TwoWay, Source={StaticResource emp}}"
                 Grid.Row="2" Grid.Column="1"/>
   <TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding Age, Mode=TwoWay, Source={StaticResource emp}}"
                 Grid.Row="3" Grid.Column="1"/>
</Grid>

Bu sefer DataContext özelliğine atama yapmayıp, her Binding için parametre olarak ayrı ayrı Source belirttik. Olur da lazım olur birden fazla veri kaynağına bağlanmak gerekirse böyle de yapılabilir.

Biraz da x:Bind ile nasıl yapılıra bakalım. Binding ile benzer deyim yapısına sahip ama DataContext özelliğini desteklemez. DataContext özelliği çalışma zamanında değiştirilebilir bir özellik, ancak x:Bind yapısı derlemeyi yaparken net olarak bağlantıyı görmesi lazım. Source özelliği de aynı sebepten desteklenmez. Çalışmadığını görmek için şu kodu deneyelim:

<Page.Resources>
   <local:Employee x:Key="emp" FirstName="Ümit" LastName="Kayacık" 
                       EMail="umit@email.com" Age="56"></local:Employee>
</Page.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource emp}">
   <Grid.RowDefinitions>...
   <Grid.ColumnDefinitions>...
        
   <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
   <TextBox Text="{x:Bind FirstName, Mode=TwoWay}"
                 Grid.Row="0" Grid.Column="1"/>
   <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
   <TextBox Text="{x:Bind LastName, Mode=TwoWay}"
                 Grid.Row="1" Grid.Column="1"/>
   <TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
   <TextBox Text="{x:Bind EMail, Mode=TwoWay}"
                 Grid.Row="2" Grid.Column="1"/>
   <TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
   <TextBox Text="{x:Bind Age, Mode=TwoWay}"
                 Grid.Row="3" Grid.Column="1"/>
</Grid>

Daha kodu yazarken Visual Studio alan adlarının altına hata çizgileri çekmeye başlar. Örneğin FirstName üzerine gittiğinizde Tooltip'te "MainPage türünde FirstName özelliği bulunamadı" yazar. x:Bind sayfada DataContext özelliğine bakacağına tüm özellikler içinde FirstName adında birşey arıyor. 

Önce verimizi MainPage sınıfı içinde tanımlayalım ki x:Bind bulabilsin.

    public sealed partial class MainPage : Page
    {
        Employee emp = new Employee
        {
            FirstName = "Ümit",
            LastName = "Kayacık",
            EMail = "umit@email.com",
            Age = 56
        };
        public MainPage()
        {
            this.InitializeComponent();
        }
    }

Şimdi emp nesnesine başvuru yapılabilir. 

<TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
<TextBox Text="{x:Bind emp.FirstName, Mode=TwoWay}"
                 Grid.Row="0" Grid.Column="1"/>
<TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
<TextBox Text="{x:Bind emp.LastName, Mode=TwoWay}"
                 Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
<TextBox Text="{x:Bind emp.EMail, Mode=TwoWay}"
                 Grid.Row="2" Grid.Column="1"/>
<TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
<TextBox Text="{x:Bind emp.Age, Mode=TwoWay}"
                 Grid.Row="3" Grid.Column="1"/>

Yaşasın, bunun da nasıl çalışacağını çözdük. Şimdi görselimize bir de Button elemanı ekleyelim.

<Page.Resources>
   <local:Employee x:Key="emp" x:Name="emp" FirstName="Ümit" 
                        LastName="Kayacık" EMail="umit@email.com" Age="56"/>
</Page.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource emp}">
   <Grid.RowDefinitions>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
      <RowDefinition></RowDefinition>
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="Auto"/>
   </Grid.ColumnDefinitions>
        
   <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding FirstName, Mode=TwoWay}"
                 Grid.Row="0" Grid.Column="1"/>
   <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding LastName, Mode=TwoWay}"
                 Grid.Row="1" Grid.Column="1"/>
   <TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding EMail, Mode=TwoWay}"
                 Grid.Row="2" Grid.Column="1"/>
   <TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding Age, Mode=TwoWay}"
                 Grid.Row="3" Grid.Column="1"/>
   <Button Content="Email Oluştur" Grid.Row="4" Grid.ColumnSpan="2"
                HorizontalAlignment="Center" Margin="5"
                Click="Button_Click"/>
</Grid>

Butonu kullanarak EMail adresini değiştirmek istiyorum. Button_Click kodunu da programa ekleyelim.

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            emp.EMail = "umit.kayacik@xmail.com";
        }
    }

Kodu çalıştırınca görüntü:


Butona tıklayınca TwoWay bağlantı yaptığımız halde TextBox içinde gösterilen değer değişmedi. 

Bu sorunu çözmek için izleyebileceğimiz 2 yöntem var
  • Employee sınıfımızı DependencyObject sınıfından kalıtım olarak üretmek ve tüm özelliklerini Dependency özellik olarak tanıtmak. Kullanıcı tanımlı kontrollerde bunu yapmak mantıklı çünkü kullanıcı kontrolleri DependencyObject sınıfından üretilir. Ama diğer sınıf tanımlarına başka bir yöntem daha kolay olacaktır. 
  • Binding nesnesini değişikliklerden haberdar etmek için Employee sınıfımızın tanımına INotifyPropertyChanged arabirimini uygularız. 
İkinci yaklaşımı uygulamak için Employee sınıf tanımını şöyle değiştirelim.

    public class Employee: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }
        private string firstName;
        private string lastName;
        private string eMail;
        private int age;

        public string FirstName
        {
            get { return firstName; }
            set
            {
                firstName = value;
                OnPropertyChanged(new PropertyChangedEventArgs("FirstName"));
            }
        }
        public string LastName
        {
            get { return lastName; }
            set
            {
                lastName = value;
                OnPropertyChanged(new PropertyChangedEventArgs("LastName"));
            }
        }
        public string EMail
        {
            get { return eMail; }
            set
            {
                eMail = value;
                OnPropertyChanged(new PropertyChangedEventArgs("EMail"));
            }
        }
        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                OnPropertyChanged(new PropertyChangedEventArgs("Age"));
            }
        }
    }

Burada INotifyPropertyChanged sadece PropertyChanged olayını tanımlıyor. Ve Binding nesneleri kendilerini bu olaya bağlıyor. Olay tetiklendiği anda bağlı Binding nesnesi de değerini yenileme yapacaktır. Ama sınıf tanımında her alakalı yere PropertyChanged olayını tetikleyen kodları kendimiz eklemek zorundayız. Bu yüzden sınıf tanımındaki standart { get; set; } yapısını ayrıntılı yapıya çevirdik ve tüm set işlemlerinde olay tetiklememizi yaptık. Şu yukarıdaki ayrıntıyı çözebilmek için zamanında internette 2 gün arama yaptığımı hatırlıyorum. 



DÖNÜŞTÜRMELER


Geldik ana konulardan birine , bazen veriyi aynen olduğu gibi değil de üzerinde bir değişim yaparak görüntülememiz gerekebilir. Mesela Employee sınıfımızda Salary adında bir özellik daha ekleyelim , çalışanın maaşını da bilelim. 

   private double salary;
   public double Salary
   {
      get { return salary; }
      set
      {
         salary = value;
         OnPropertyChanged(new PropertyChangedEventArgs("Salary"));
      }
   }

Bu özelliği göstermek için Grid elemanına bir satır daha eklemeliyiz. Bu değeri Binding ile bir TextBox elemanına bağladığımızda standart işlemle double değeri string'e çevirecek ve gösterecektir. Ama bir para değeri gösterimi için standardın dışında bir format uygulayarak göstermek daha doğru olacaktır. Mesela para birimini de yanında ilave etmek iyi olur. Ayrıca çift yönlü bağlama var yani TextBox içinde yapılan değişikliğin de double bir değere dönüştürülmesi lazım. Tüm bunları yerine getirmek için yeni bir özel sınıfa, bir dönüştürücüye ihtiyacımız var. Bu sınıfı IValueConverter sınıfından türetmemiz gerekiyor. 

    public class MoneyConverter:IValueConverter
    {
        public object Convert(object value, Type targetType, 
               object parameter, string language)
        {
            return ((double)value).ToString("C", new CultureInfo("tr-TR"));
        }

        public object ConvertBack(object value, Type targetType, 
               object parameter, string language)
        {
            double result;
            try
            {
                result = double.Parse((string)value, NumberStyles.AllowThousands |
                    NumberStyles.AllowDecimalPoint | NumberStyles.AllowCurrencySymbol);
            }
            catch
            {
                return DependencyProperty.UnsetValue;
            }
            return result;
        }
    }

IValueConverter sadece 2 metoda ihtiyaç duyuyor. İlk metod değeri alır ve Binding hedefinde yayınlanacak şekle çevirir. Bizim durumda bu bir string ama buradan herşey çıkartılabilir. mesela LinearGradientBrush çıkartıp renklere bağlama yapılabilir ya da bir kolleksiyona bağlama yapılabilir gibi. TextBox içinde yapılan değişiklik de ConvertBack metodu ile işlenip double değere dönüştürülüyor. Eğer dönüştürme başarılı olmazsa DependencyProperty.UnsetValue ile değişimden önceki değerin kalması sağlanıyor. 

Bu yaptıklarımızı görselde uygulayalım.

<Page.Resources>
   <local:Employee x:Key="emp" x:Name="emp" FirstName="Ümit" 
          LastName="Kayacık" EMail="umit@email.com" 
          Age="56" Salary="350.5"/>
   <local:MoneyConverter x:Key="money"></local:MoneyConverter>
</Page.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource emp}">
   <Grid.RowDefinitions>...
   <Grid.ColumnDefinitions>...
   <TextBlock Text="First Name:" Grid.Row="0" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding FirstName, Mode=TwoWay}"
                 Grid.Row="0" Grid.Column="1"/>
   <TextBlock Text="Last Name:" Grid.Row="1" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding LastName, Mode=TwoWay}"
                 Grid.Row="1" Grid.Column="1"/>
   <TextBlock Text="E-Mail:" Grid.Row="2" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding EMail, Mode=TwoWay}"
                 Grid.Row="2" Grid.Column="1"/>
   <TextBlock Text="Age:" Grid.Row="3" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding Age, Mode=TwoWay}"
                 Grid.Row="3" Grid.Column="1"/>
   <TextBlock Text="Salary:" Grid.Row="4" Grid.Column="0" Margin="5"/>
   <TextBox Text="{Binding Salary, Mode=TwoWay,
          Converter={StaticResource money}}"
          Grid.Row="4" Grid.Column="1"/>
   <Button Content="Email Oluştur" Grid.Row="5" Grid.ColumnSpan="2"
                HorizontalAlignment="Center" Margin="5"
                Click="Button_Click"/>
</Grid>



Dönüştürücüler hakkında konuştuğumuz için Binding nesnesinin 2 özelliğinden daha bahsetmek gerekiyor: TargetNullValue ve FallbackValue. Birincisi eğer kaynak özellik değeri null ise ne değer alınacağını belirtir. İkincisi bağlama gerçekleşirken bir şeyler ters giderse (örneğin dönüştürme yaparken) oluşacak exception'da ne değer gösterileceği. 

Salary için koyduğumuz TextBox elemanına bu parametreleri de ekleyelim.

    <Page.Resources>
        ...
        <x:Double x:Key="exValue">-1</x:Double>
        <x:Double x:Key="nullValue">23.3</x:Double>
    </Page.Resources>
    <Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource emp}">
        ...
        <TextBlock Text="Salary:" Grid.Row="4" Grid.Column="0" Margin="5"/>
        <TextBox Text="{Binding Salary, Mode=TwoWay,
            Converter={StaticResource money}, 
            FallbackValue={StaticResource exValue},
            TargetNullValue={StaticResource nullValue}}"
                 Grid.Row="4" Grid.Column="1"/>
        <Button Content="Email Oluştur" Grid.Row="5" Grid.ColumnSpan="2"
                HorizontalAlignment="Center" Margin="5"
                Click="Button_Click"/>
    </Grid>

Çalışmasını görmek için dönüştürücü Convert metodundan bir Exception atalım.

        public object Convert(object value, Type targetType, object parameter, string language)
        {
            throw new Exception();
            return ((double)value).ToString("C", new CultureInfo("tr-TR"));
        }

Uygulamayı çalıştırınca hata verecek , devam ettirince Salary değerine -1 olan FallbackValue gelecektir. null değerini test edebilmek için şunu yapabilirsiniz: önce Employee sınıf tanımında özelliğe null değeri alabilen bir tip girin (mesela string yapın), Binding ifadesinden Converter'i çıkarın ki oraya gitmesin ve Salary özelliğine değer vermeden çalıştırın. Bu durumda değer null olacağı için TargetNullValue için verdiğiniz değer gelir. 






Kolleksiyonlara Bağlamak


Veri deyince aklımıza şimdiye kadar örnekte olduğu gibi bir tek nesne gelmiyor aslında. Bir sürü çalışanlar olur mesela, bu durumda Binding işlemini kolleksiyonlar üzerinde yapmamız gerekecek. ListView, GridView, Pivot, ve FlipView gibi kontroller aynı anda birçok elemanı barındırabilirler. Eğer kolleksiyonlarla çalışacağımız kontroller düşünüyorsak bunlar ItemsControl sınıfından türetilmiştir ve Binding işleminde kullanılacak önemli özelliklere sahiptirler. 
  • ItemsSource: Bir nesneler kolleksiyonuna referans vermenize imkan tanır. ItemsSource özelliğine bağlama yaparken Binding ve x:Bind yapılarının her ikisini de kullanabiliriz. Bu özelliğe kod içinde de değer verebiliriz.
  • ItemTemplate: Kolleksiyon içinden sıradaki nesne özelliklerini gösterirken bu şablon kullanılır. 
Bu özellikleri test etmek için uygulamamıza bir sınıf tanımı ekliyoruz.

    class EmployeeViewModel
    {
        public List<Employee> Items { get; set; }

        public EmployeeViewModel()
        {
            Items = new List<Employee>();
            Items.Add(new Employee
            {
                FirstName = "Ümit",
                LastName = "Kayacık",
                EMail = "umit@email.com",
                Age = 56,
                Salary = 350.5
            });
            Items.Add(new Employee
            {
                FirstName = "Alp",
                LastName = "Kayacık",
                EMail = "alpka@email.com",
                Age = 20,
                Salary = 1350.5
            });
            Items.Add(new Employee
            {
                FirstName = "Turan",
                LastName = "Yılmaz",
                EMail = "turan@email.com",
                Age = 28,
                Salary = 750.5
            });
        }
    }

Standart List elemanı olarak kolleksiyonumuzu tanımladık. Şimdi ListView kullanarak kolleksiyonumuzu gösterelim.

    <Page.Resources>
        <local:EmployeeViewModel x:Key="viewModel"/>
    </Page.Resources>
    <Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource viewModel}">
        <ListView ItemsSource="{Binding Items}"></ListView>
    </Grid>

Çalıştırınca şunu alırız.


ItemsSource özelliğini Binding ile bulunulan DataContext'in Items alanına bağladık. 

Herhangi bir şablon belirtmediğimiz için direk olarak 3 adet nesnenin gösterimi ToString metodu sonucuna göre geldi karşımıza. Önce Employee sınıf tanımında ToString() metodu üzerine yazarak değiştirelim. 

        public override string ToString()
        {
            return $"{FirstName} {LastName}";
        }

Şimdi çalıştırırsak liste artık isim soyisim şeklinde gelecektir. ToString() metodu sadece yazı dönüyor , pek faydalı değil. Halbuki biz listemizi daha güzel görüntülerde göstermek için görsel kontrollerle yapmak isteriz. ItemTemplate kullanarak kendi görselimizi oluşturabiliriz.

<ListView ItemsSource="{Binding Items}">
   <ListView.ItemTemplate>
      <DataTemplate>
         <Grid>
            <Grid.ColumnDefinitions>
               <ColumnDefinition Width="Auto"/>
               <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding FirstName}" Margin="8"/>
            <TextBlock Text="{Binding LastName}" Margin="8" Grid.Column="1"/>
         </Grid>
      </DataTemplate>
   </ListView.ItemTemplate>
</ListView>

Sonuç


Pek bir şey değişmedi gibi gelebilir ama artık o görüntüdeki herşeyle, renkler, yazı büyüklüğü vs. oynama yapabiliriz. 

Aynı şeyi x:Bind ile şöyle yaparız.

<Page.Resources>
   <local:EmployeeViewModel x:Key="viewModel" x:Name="viewModel"/>
</Page.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center"
          DataContext="{StaticResource viewModel}">
   <ListView ItemsSource="{x:Bind viewModel.Items}">
      <ListView.ItemTemplate>
         <DataTemplate x:DataType="local:Employee">
            <Grid>
               <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="Auto"/>
                  <ColumnDefinition Width="Auto"/>
               </Grid.ColumnDefinitions>
               <TextBlock Text="{x:Bind FirstName}" Margin="8"/>
               <TextBlock Text="{x:Bind LastName}" Margin="8" Grid.Column="1"/>
            </Grid>
         </DataTemplate>
      </ListView.ItemTemplate>
   </ListView>
</Grid>

Hemen hemen aynı ama x:Bind kolleksiyondaki elemanların veri tipini bilmek ister. DataTemplate elemanının x:DataType özelliğinde bunu belirttik. 

Bir List kolleksiyonu kullanıyorsak, aynı problemlerle kodda özellikler değiştirmeye çalışırken de karşılaşabiliriz. 

Binding böyle List kolleksiyonlarda veri değişimini takip etmez. Kendi tanımladığımız nesneler konusunda bu sorunu çözmek için INotifyPropertyChanged olayını yapılandırmıştık. List kolleksiyonlar ile çalışırken de aynı şeyi INotifyCollectionChanged olayını yapılandırarak elde ederiz. Ama tüm bunlar özellikle listelerde çok sıkıntılı işler bunun için UWP uygulamalarında ObservableCollection tipinde kolleksiyonlar kullanmak tavsiye edilir. Sadece Items özelliğinin tanımında List yerine ObservableCollection  kullansak yeterli. Binding artık tüm değişiklikleri takip edecektir. 

        public ObservableCollection<Employee> Items { get; set; }

Şimdilik bu kadar. Bunları aslında kendime de not olsun diye yazıyorum. En çok hit alan yazılarım zaten benim de dönüp dönüp nasıl yapmıştım diye baktığım yazılarım. Umarım yeni makalelerde görüşürüz. Kalın sağlıcakla..





Hiç yorum yok:

Yorum Gönder