RSS Feeds

18 Nisan 2010 Pazar

Control Template İpuçları

Makaleler, screencast’ler, webinerler, kitaplar, seminerler, eğitimler..

Belli bir uzmanlık alanında bu kadar çok kaynak bulunması, sadece ilk adımlarını atanlar için değil, o alanda çalışan herkes için gerçekten çok büyük bir nimet. Özellikle, üniversite döneminde sınavlara hazırlanırken, çalıştığım konularla ilgili (Endüstri Mühendisliği) internette kaynak araştırması yaptığımda, elle tutulur pek birşey bulamamış olmam beni çok ama çok şaşırtmıştı. Halbuki, konu yazılım olduğunda, en ufak bir alt alt alt başlık hakkında dahi, çok fazla yazı, tartışma, görsel materyal bulabiliyorsunuz. Bunun en büyük sebeplerinden biri belki de, bu dünyada zamanın biraz hızlı işlemesi. Geriye yaslanıp şöyle bir düşünsenize; uygulama geliştirirken kullandığımız kütüphaneler, biricik Framework’ümüz 4.0 sürümüne ulaşmış, veritabanı programcılığının belki de en güçlü ve en modern programlama dili olan C#’ın sağına 4.0 eklenmiş, ailemizin runtime’ı CLR da 4.0 mertebesine yükselenler arasında, hem de daha 10. yaşını bile doldurmadan!

Gelişim süreci bu kadar kısayken, gelinen noktada, biz yazılım geliştiriciler, Microsoft tarafından adeta bir bilgi bombardımanına tutuluyoruz. İş böyle olunca da “Tips&Tricks” tadındaki, konuyu anlatmaktan ziyade konunun ipuçlarına değinen kaynak bulmak biraz güçleşiyor. Ben de bu defa, bir konuyu anlatmak yerine, konuyla ilgili projelerde karşıma çıkmış birkaç önemli gördüğüm noktaya değineceğim.

İpucu 1 : Template içindeki nesnelere ait property’lerin, kontrolün property’lerine bağlanması

Senaryoyu görmeden önce, Window’un Resource’larına bir ControlTemplate nesnesi tanımlayalım. Örneğin benim tanımladığım template, uygulandığı butonun, köşeleri yuvarlatılmış kırmızı bir dörtgen olarak çizilmesini sağlamaktadır.

<Window.Resources>
        <ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Button}">
                    <Grid>
                        <Rectangle RadiusX="20" RadiusY="20" Fill="Red" Stroke="Black" Margin="0"/>
                    </Grid>
        </ControlTemplate>
</Window.Resources>

Daha sonra da oluşturduğumuz ControlTemplate’i, birkaç butona uygulayalım.

<Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Margin="5" Template="{StaticResource MyTemplate}" />
        <Button Grid.Row="1" Margin="5" Template="{StaticResource MyTemplate}" />
        <Button Grid.Row="2" Margin="5" Template="{StaticResource MyTemplate}" />
</Grid>

Window’un son hali aşağıdaki gibi olacaktır.

s12

Dikkat ederseniz, üç butonunda Template’ine, tanımladığımız ControlTemplate nesnesi verilmiş, bunun sonucu olarak da ekrandaki üç buton da aynı görüntüye sahip olmuştur. Peki ya, hem aynı template üç butona da uygulanarak hem her butonun farklı bir renkte görünmesi nasıl sağlanabilir ?

Örneğin butonların herbirine ayrı ayrı arkaplan renkleri atayalım.

<Button Grid.Row="0" Margin="5" Template="{StaticResource MyTemplate}" Background=”Blue” />
<Button Grid.Row="1" Margin="5" Template="{StaticResource MyTemplate}" Background=”White” />
<Button Grid.Row="2" Margin="5" Template="{StaticResource MyTemplate}" Background=”Black” />

Fakat XAML’de bu değişikliğin yapılmasına rağmen, üç butonun da hala kırmızı renkte olduğunu görünmektedir. Bunun sebebi gayet basittir : ControlTemplate’in içindeki Rectangle nesnesine Template içinde kırmızı rengi atandığından uygulandığı tüm butonlar da her zaman kırmızı olarak görünmektedir. Bu durumda rengin, template’in uygulanması anında, uygulanan buton tarafından belirlenmesi sağlanmalıdır. Çok açıkça görünmektedir ki bu, XAML tarafında ele alabileceğimiz basit bir Binding işlemidir. Bir ControlTemplate içindeki nesnenin property’sini (Rectangle ’ın Fill property’si), uygulandığı kontrolün bir property’sine (Button ’ın Background property’si) bağlamak için ise, XAML’de TemplateBinding Markup Extension ’ı kullanılır. Kodun son halini aşağıdaki gibi değiştirelim.

<Window.Resources>
        <ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Button}">
                    <Grid>
                        <Rectangle RadiusX="20" RadiusY="20" Fill="{TemplateBinding Background}" Stroke="Black" Margin="0"/>
                    </Grid>
        </ControlTemplate>
</Window.Resources>

Window’un son hali aşağıdaki gibi görünmelidir.

s2

Bu işlem Blend’in arayüzü kullanılarak çok daha kolay bir şekilde yapılabilir. Bunun için ControlTemplate'in içindeki Rectangle seçilerek Properties penceresinden Fill property’sinin hemen yanında bulunan küçük kare simgesine tıklanır ve ardından açılan menüde, Template Binding’in altındaki Background property’si seçilir.

s3

Bu noktada basit bir ayrıntının üzerinden geçmekte fayda var. Dikkat ederseniz, menüde Template Binding seçeneği altında sadece Background,BorderBrush,Foreground ve OpacityMask property’leri bulunmaktadır. Bunun sebebi oldukça basittir. Listede çıkan property’lerin hepsi Brush tipinden property’lerdir. Peki neden? Çünkü bağlanılmaya çalışılan property, yani Rectangle’ın Fill property’si Brush tipli bir property idi. Demek ki bağlanılan property’lerin tipleri aynı olmalıdır.

İpucu 2 : Template içindeki nesnelerin yerleşimlerini sağlamak

Template oluştururken, göz önünde bulundurulması gereken noktalardan bir tanesi de kontrollerin template içindeki yerleşimleridir (layout). Özellikle, template’in uygulanacağı kontroller boyutlandırılacaksa, yani tamamı aynı boyutta olmayacaksa, template içinde yer alan tüm kontrollerin HorizontalAlignment, VerticalAlignment, Margin, Padding gibi yerleşimle ilgili property’leri gözden geçirilmelidir.

Örneğin aşağıdaki buton template’ini, Blend’in dizayn ekranında, yerleşimle ilgili property’lere hiç dikkat etmeden hızlı bir şekilde gerçekleştirdiğimde, aşağıdaki XAML çıktısını elde ettim.

<Window.Resources>
        <ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Black" BorderThickness="2">
                <Grid>
                    <Ellipse Stroke="{x:Null}" HorizontalAlignment="Left" Margin="8,8,0,8" Width="83">
                        <Ellipse.Fill>
                            <RadialGradientBrush>
                                <GradientStop Color="#FFE3FF69" Offset="0"/>
                                <GradientStop Color="#FF92A7B9" Offset="1"/>
                            </RadialGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <TextBlock Margin="111,30,26,23" FontSize="29.333" FontStyle="Italic" Text="My Button" TextWrapping="Wrap"/>
                </Grid>
            </Border>
        </ControlTemplate>
</Window.Resources>

Şimdi bu template’i ikinci bir butona uygulayıp, bu butonun boyutlarıyla oynayalım.

s4

Boyutlar değiştirildiğinde, template’de yer alan elips’in genişlediği ve yazının yukarıda kaldığı açıkça görünmektedir. Oluşan durum etüd edildiğinde, şu sonuçlar ortaya çıkar.

  • Elips, dikey eksende boyutunu arttırmaktadır.
  • Yazı, kontrolün sol ve üst kenarlarıyla olan mesafesini korumaktadır.

WPF ‘te kontrollerin yerleşimleriyle ilgili durumlar kolaylıkla ele alınabilmektedir. Bunun için Blend’in Property ekranındaki Layout sekmesindeki property’lerden faydalanılır. Öncelikle elipsle ilgili durumun ele alınmasına bakalım.

Template içinden elips nesnesini seçerek, Layout sekmesindeki property’lerinden VerticalAlignment’ı Center olarak değiştirelim.

s5

Fakat bu işlem yapıldığında, elipsin tamamen ekrandan kaybolduğu görünecektir. Bunun sebebi de elipse sabit bir yükseklik verilmemiş olmasıdır. İlk resimdeki Height değeri 139px olmasına rağmen yanında Auto yazmaktadır; diğer bir deyişle, elips dikey olarak kendisini Strech ettiğinden, 139px yüksekliğe sahip oluncaya kadar genişlemiştir. Fakat VerticalAlignment Center olarak değiştirildiğinde, artık Strech etmediğinden, sabit bir yükseklik değeri verilmelidir. Bunun için Height’a da 83px değerini atayalım. Daha sonra aynı işlemleri TextBlock için de gerçekleştirelim. XAML’in son hali aşağıdaki gibidir.

<Window.Resources>
        <ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Black" BorderThickness="2">
                <Grid>
                    <Ellipse Stroke="{x:Null}" HorizontalAlignment="Left" Margin="8,8,0,8" Width="83" VerticalAlignment="Center" Height="83">
                        <Ellipse.Fill>
                            <RadialGradientBrush>
                                <GradientStop Color="#FFE3FF69" Offset="0"/>
                                <GradientStop Color="#FF92A7B9" Offset="1"/>
                            </RadialGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <TextBlock Margin="111,30,26,23" FontSize="29.333" FontStyle="Italic" Text="My Button" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Left"/>
                </Grid>
            </Border>
        </ControlTemplate>
</Window.Resources>

Yerleşim, programlamadan ziyade, tasarımla ilgili olduğundan, Blend kullanan programcılar ,alışık olmadıklarından olsa gerek, genelde bu tarz noktalara özen göstermiyorlar. Bu yüzden de kontrollerin boyutlandırılması anında yerleşimin bozulmasına çokça rastlanabiliyor. Sonuç olarak, daha sonra boyutlandırılsın ya da boyutlandırılmasın, template’in oluşturulması anında template’te yer alan her kontrol için Layout property’lerine dikkat edilmelidir.

İpucu 3 : Resource yönetimi

Oluşturulan template içinde, Style,Storyboard,Brush gibi birçok farklı tipten nesneler kullanılıyor olabilir. Bu nesnelerin XAML içerisinde resourcelar olarak tanımlanması, büyük projelerde çok kolaylık sağlamaktadır. Hemen bu senaryoyu da basit bir örnek üzerinde birlikte inceleyelim. Örneğin aşağıdaki gibi, iki farklı kontrol için hazırlanmış iki ayrı template olsun:

<Window.Resources>

        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Grid>
                <Rectangle RadiusX="30" RadiusY="30">
                    <Rectangle.Fill>
                        <LinearGradientBrush EndPoint="0,1">
                            <GradientStop Color="Black" Offset="0"/>
                            <GradientStop Color="Brown" Offset="0.5"/>
                            <GradientStop Color="Black" Offset="1"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
                <TextBlock Text="Custom Button" Foreground="White" FontSize="15" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Grid>
        </ControlTemplate>

        <ControlTemplate x:Key="ButtonTemplate2" TargetType="{x:Type Button}">
            <Grid>
                <Rectangle RadiusX="30" RadiusY="30">
                    <Rectangle.Fill>
                        <LinearGradientBrush EndPoint="0,1">
                            <GradientStop Color="Black" Offset="0"/>
                            <GradientStop Color="Brown" Offset="0.5"/>
                            <GradientStop Color="Black" Offset="1"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
                <CheckBox VerticalAlignment="Center" HorizontalAlignment="Left" Margin="20,0,0,0" />
                <TextBlock Text="Custom Button" Margin="10,0,0,0" Foreground="White" FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Grid>
        </ControlTemplate>

</Window.Resources>

Bu template’lerin ekranda görüntüsü şu şekilde olacaktır :

s7

Bu iki template arasındaki ortak nokta, kullanılan LinearGradientBrush nesnelerinin aynı özelliklere sahip olmasıdır; ancak bahsi geçen brush’ın her iki template içinde de ayrı ayrı tanımlandığı açıkça görünmektedir. Bu durumda en doğru iş, ilgili brush’ın da resource olarak oluşturulup, her iki template’in ortak kullanmasını sağlamaktır. Yukarıdaki XAML’in düzenlenmiş hali şu şekilde olmalıdır:

<Window.Resources>

    <LinearGradientBrush x:Key="MyBrush" EndPoint="0,1">
                        <GradientStop Color="Black" Offset="0"/>
                        <GradientStop Color="Brown" Offset="0.5"/>
                        <GradientStop Color="Black" Offset="1"/>
    </LinearGradientBrush>

    <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
        <Grid>
            <Rectangle RadiusX="30" RadiusY="30" Fill="{StaticResource MyBrush}"/>
            <TextBlock Text="Custom Button" Foreground="White" FontSize="15" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        </Grid>
    </ControlTemplate>

    <ControlTemplate x:Key="ButtonTemplate2" TargetType="{x:Type Button}">
        <Grid>
            <Rectangle RadiusX="30" RadiusY="30" Fill="{StaticResource MyBrush}"/>
            <CheckBox VerticalAlignment="Center" HorizontalAlignment="Left" Margin="20,0,0,0" />
            <TextBlock Text="Custom Button" Margin="10,0,0,0" Foreground="White" FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        </Grid>
    </ControlTemplate>

</Window.Resources>

Yapılan düzenleme çok basit görünse de, büyük çapta template’lerin yazılması sırasında genelde gözden kaçabilmektedir. Ayrıca söz konusu olan brush her iki template içinde kullanılmasa, yani ortak olmasa dahi, kendi başına resource olarak tanımlanması XAML’in yönetimini kolaylaştırır. Bu sebeple, template içinde yer alan Brush,Storyboard,Style gibi nesnelerin, kendi başlarına resource olarak tanımlanması çoğu zaman doğru bir harekettir.

İpucu 4 : Farklı tipten property’ler arasında template binding kurulması

İpucu 1’de de bahsedildiği üzere, TemplateBinding kullanımıyla bir template içindeki kontrole ait property, template’in uygulandığı kontrolün property’sine bağlanabilmektedir. Fakat TemplateBinding markup extension’ı kullanımında, bağlanan property’lerin tipleri aynı olmalıdır. Diğer bir deyişle, eğer bağlanan property’lerin tipleri farklıysa, bağlama işlemi gerçekleştirilememektedir.

Farklı tipten property’ler arasında Binding yapmaya neden ihtiyaç duyulacağını görmek için aşağıdaki basit senaryoyu birlikte inceleyelim.

<Window.Resources>
    <ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Button}">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Image Margin="5"/>
            <TextBlock HorizontalAlignment="Left" Margin="8,0,0,0" VerticalAlignment="Center" Grid.Column="1" Text="My Button" TextWrapping="Wrap" FontSize="32" Height="Auto"/>
        </Grid>
    </ControlTemplate>
</Window.Resources>

Yukarıdaki XAML kodu ile butonun, Image ve TextBlock kontrollerinden oluşan bir görünüme sahip olması amaçlanmaktadır. Dikkat ederseniz, Image kontrolünün Source property’sine Template içinde değer atanmamıştır. Çünkü bu noktada bir değer ataması yapıldığında, template’in kullanıldığı her butonda aynı resim görünecektir. Eğer her butonda farklı resimler isteniyorsa, Source değerinin, kullanıldığı anda, template’in uygulandığı kontrole ait bir property’den alınabilmesi sağlanmalıdır. Kısacası, template binding yapılmalıdır. Fakat Source property’si ImageSource tipinden olup, ne yazık ki, Button  sınıfında bu tipten bir property bulunmadığından, TemplateBinding markup extension’ını kullanmak mümkün değildir.

Peki bu durumda ne yapılabilir? Hemen Windows programcılığı bilgilerimizi şöyle bir gözden geçirelim. Windows Forms uygulamalarında kullanılan tüm kontrollerde Tag isminde bir property bulunmaktadır ve bu property Object tipinden olup, kontrolün üzerinde herhangi bir nesnenin referansını saklama imkanı vermektedir. Acaba aynı property WPF içinde de var mıdır? Tabi ki evet! Ancak unutmayalım ki, Tag property’si Object, bağlamak istenilen Source ise ImageSource tipindendir; dolayısıyla TemplateBinding kullanılamamaktadır. Hatta, Blend’in arayüzünde TemplateBinding seçeneğı tıklanamaz durumdadır.

s6

Buna rağmen XAML’e müdahale edilip, aşağıdaki düzenleme yapılsa dahi sonuç alınmayacaktır.

<Image Source=”{TemplateBinding Tag}” Margin="5"/>

Peki, farklı tipten property’ler arasında template binding kurmanın başka bir yöntemi yok mu? Tip farklılığı olduğunda bunu çözümleyebilecek bir bağlama tekniği yapılamaz mı?

Gelelim projelerde karşınıza çıkma ihtimali yüksek olan bu durumun  çözümüne. Tek yapılması gereken, TemplateBinding yerine klasik Binding’i kullanmaktır. Çünkü TemplateBinding daha optimize edilmiş ve performans kazandıran bir bağlama tekniği olmasına rağmen tiplerin aynı olması konusunda bir kısıtlaması söz konusudur. Dolayısıyla bu örnekteki gibi tipler farklı ise, klasik Binding kullanılmalıdır. Kod üzerinde gerekli düzenlemeyi yapalım.

<Image Source=”{Binding Path=Tag,RelativeSource={RelativeSource TemplatedParent}}” Margin="5"/>

Relative Source ’un yukarıdaki kullanımında, template ’in uygulanacağı nesneye Binding yapıldığı belirtilmekte olup, sonuçta her iki yöntem de  template ’in uygulandığı nesneye Binding yapılmasını sağmaktadır. Fakat gerek duyulmadıkça Relative Source kullanımının tercih edilmemesi gerektiği unutulmamalıdır. Bu tarz veri bağlamalarında, daha performanslı çalıştığından ötürü, öncelikli tercih her zaman TemplateBinding’dir.