21 Ocak 2011 Cuma

Derinlemesine Genişletilebilir Uygulama Yazma

Genişletilebilir uygulamaların asıl gücü tak-çalıştır desteğidir. Bir önce ki yazıda ele aldığımız katalog ve taşıyıcı kavramlarının çalışma anında tak-çalıştır modüllerini nasıl takip ettiğini ve kullandığını inceliyoruz.

Çalışma Zamanı Desteği

Çalışma anında uygulamazın tak-çalıştır modülleri nasıl yönettiğini görelim. Öncelikle katalog ve taşıyıcıyı oluşturalım.

[cs]

var  assemblyCatalog = new  AssemblyCatalog (Assembly .GetExecutingAssembly ());
 var  directoryCatalog = new  DirectoryCatalog (Path .Combine ("../.." , "Plugins" ));
 var  aggregateCatalog = new  AggregateCatalog (assemblyCatalog, directoryCatalog);
 _compositionContainer  = new  CompositionContainer (aggregateCatalog);

DirectoryCatalog örneğinin Plugins dizinini dinlediğini görüyorsunuz. DirectoryCatalog siz yenilemedikçe dizin içinde ki modülleri tekrar yüklemeyecektir. Plugins dizinini dinlemeli ve oluşan değişikliklerde DirectoryCatalog örneğinizi yenilemelisiniz.

[cs]

// Plugins dizinini takip et 
 var  watcher = new  FileSystemWatcher (directoryCatalog.FullPath )
 {
     Filter  = "*.dll" ,
     EnableRaisingEvents  = true 
 };
 // dizindeğişince katalogu yenile
 watcher.Changed  +=  (s, e) => directoryCatalog.Refresh ();

Yukarıda ki kodlama ile Plugins dizininde ki dosyalar değiştiğinde katalogumuzu yeniliyoruz. Katalog ilgili dizinde ki tüm assembly dosyalarına bakarak [Export] tanımlarını almaktadır. Basit bir uygulama ile çalışma anında katalogun nasıl değiştiğini görelim. Elimizde karakter dizisi dönen bir ISeriesFactory interface var.

[cs]

public  interface  ISeriesFactory 
 {
     ///  
     ///   seri oluştur 
     ///  
     /// dizide olması gereken eleman sayısı       /// oluşturdupu seriyi dönderir       List  Create (int  numberOfElement);  }

Bu interface ait uygulama yazmak istiyoruz. Fakat interface uygulama sınıfları çalışma anında eklenecekler. Aşağıdakine benzer bir önyüz hazırlıyoruz. Sol tarafta katalogda yer alan ihraç [Export] tanımları yer alacak. Sağ tarafta ise katalogda yer alan ISeries interface’sini uygulayan sınıflar listelenmiş. Seçilen sınıfı çağıracak olan birde buton eklenmiş durumdadır.

Uygulamayı çalıştırıp uygulamanın dinlediği Plugins dizinine ISeries interface’sini uygulayan assembly dosyalarını kopyalıyoruz. Katalog listesinin doğrudan güncellendiğini görürüz. Bir tane ISeries uygulaması seçip serinin eleman sayısını girelim ve çalıştır diyelim.

ContractName ve MetaData

Uygulama katalogu içinde birden çok ISeriesFactory uygulaması var. Bu durumda elimizde ki parçalar arasında sorgular çalıştırarak istediğimiz parçayı bulmamız gerekmektedir. [ExportMetadata] niteliği ile katalog üzerinde arama yapabilmek için[ Export] tanımlarımıza etiket değerleri verebilmekteyiz. Parçalara ContractName değeri ekleyerek doğrudan istediğimiz parçayı içe aktarabilir veya kendimiz özel sorgular yazarak aradığınız parçayı taşıyıcı üzerinde bulabiliriz.

[cs]

[Export ("Fibanacci" ,typeof  (ISeriesFactory ))]
 public  class  Fibanacci  : ISeriesFactory
 private LazyGetFibanacciSeries(CompositionContainer compositionContainer)
 {
     Lazy  fibanacci =
         compositionContainer.GetExport ("Fibanacci" );
     return  fibanacci;
 }

Veya taşıyıcının sizin yerinize ContractName metadata değeri ile [Import] işlemi yapmasını sağlayabilirsiniz.

[cs]

[Import ("Fibanacci" )]private  ISeriesFactory  _fibanacci ;

Yukarıda ki tanımlama katalog üzerinde aşağıdaki gibi saklanmaktadır.

Görüldüğü üzere yukarıda ki [Export] tanımına ait bir Meradata dizisi bulunmaktadır. [Export] tanımın ilk parametresinde verdiğimiz isim ContractName olarak saklanmaktadır. Metadata dizisinde ExportTypeIdentity etiketinin değeri ise [Export] yapılan tipin tam adıdır. Eğer bir [Export] kontratı için bir isim belirtmez iseniz ExportTypeIdentity değeri ContractName alanına atanacaktır. ContractName ve ExportTypeIdentity metadata değeri ile katalog üzerinde sorgulama yapabilirsiniz.

Buraya kadar her şey güzel ama matematik ve karakter serilerinin aynı listede görülmesini istemiyorsunuz. O zaman kendinize ait [ExportMetadata] etiketleri tanımlama zamanı gelmiştir.

[cs]

[Export ("Fibanacci" ,typeof  (ISeriesFactory ))]
 [ExportMetadata ("Series" ,"Math" )]
 public  class  Fibanacci  : ISeriesFactory
 [Export (typeof  (ISeriesFactory ))]
 [ExportMetadata ("Series" , "String" )]
 public  class  UpperLetters  : ISeriesFactory

Yukarda Prime serisinin bir matematik seri olduğunu benzer şekilde UpperLatter serinin bir karakter serisi olduğunu belirtiyoruz. Şimdi formumuza yeni bir combobox ekleyerek kullanıcıya seri türlerini gösterelim. Her seçilen seri türünde de seri uygulamaları listemizi güncelleyelim.

[cs]

private  void  cbTypes_SelectedIndexChanged (object  sender, EventArgs  e)
 {
     var  selectedType = cbTypes .SelectedItem .ToString ();
     var  seriesList = new  StringList ();
     _compositionContainer 
         .Catalog .Parts 
         .SelectMany (part => part.ExportDefinitions )
         .Where (c=>c.Metadata .ContainsKey ("Series" ) 
                 && c.Metadata ["Series" ].ToString () == selectedType)
         .ToList ()
         .ForEach (c=>seriesList.Add (c.ContractName ));
     sourceSeries .DataSource  = seriesList;
 }

Yukarıda görüldüğü üzere seçilen seri türüne göre seri uygulama sınıflarını listeletebiliyoruz. Fakat bu yöntemde benim eklenti sınıflarında verilen etiket değerlerini bilmem gerekiyor. Metadata nın adı “Series” olmalı değeri “Math” veya “String” olmalı şeklinde kurallarım var. Eklenti kodlanırken bu kurallarımı zorlayan bir şey yok. Benim ihtiyacım ISeriesFactory interface ile birlikte sınıflara Metadata bilgisini de dağıtmalı ve böylece beklenmedik metadata değerleri ile karşılaşmamalıyım. O halde yeni bir Metadata sınıfı yazalım ve bu Metadata ya sahip ISeriesFactory sınıfları ile birlikte çalışalım.

[cs]

public  enum  SeriesType  {Math, String}
 public  interface  ISeriesFactoryMetadata 
 {
     SeriesType  SeriesType  { get ;  }
     string  Name  { get ;  }
 }
 [MetadataAttribute ]
 [AttributeUsage (AttributeTargets.Class,AllowMultiple= false )]
 public  class  SeriesFactoryMetadataAttribute  
     : ExportAttribute , ISeriesFactoryMetadata 
 {
     public  SeriesFactoryMetadataAttribute () 
         : base (typeof (ISeriesFactory )) { }
     public  SeriesType  SeriesType  { get ; set ; }
     public  string  Name  { get ; set ; }
 }

Yukarıda [Export] kontratını türettiğimizi ve sınıfmıza [Metadata] özelliği eklediğimizi görüyorsunuz. Böylelikle [Export] kontratı tanımlarken [Export] ve metadatayı ayrı ayrı tanımlamak zorunda değiliz. Ayrıca Metadata string olarak girilen Metadata etiketlerimizin yerini ISeriesFactoryMetadata interface property'leri aldı. Şimdide uygulamamızda ISeriesFactoryMetadata'yı kullanalım.

[cs]

//[Export("Fibanacci",typeof (ISeriesFactory))] 
 //[ExportMetadata("Series", "Math")] 
 [SeriesFactoryMetadata (Name ="Fibannacci Series" , SeriesType =SeriesType.Math)]
 public  class  Fibanacci  : ISeriesFactory

Görüldüğü üzere kod çok daha anlaşılır oldu. Uygulamayıda ISeriesFactoryMetadata'yı kullanacak şekilde değiştirelim.

[cs]

private  void  btnCreate_Click (object  sender, EventArgs  e)
 {
     var  selectedType = (SeriesType ) cbTypes .SelectedItem ;
     var  selectedSeries = cbSeries .SelectedItem .ToString (); 
     var  series =  _series .First (c => 
         c.Metadata .SeriesType  == selectedType 
         && c.Metadata .Name  == selectedSeries).Value ;
     var  list = new  StringList ();
     list.AddRange (series.Create (int .Parse (txtNumber .Text )));
     sourceResult .DataSource  = list;
 }
 [ImportMany ] private  Lazy [] _series ;
 private  void  cbTypes_SelectedIndexChanged (object  sender, EventArgs  e)
 {
     var  selectedType = (SeriesType ) cbTypes .SelectedItem ;
     var  seriesList = new  StringList ();
     _series 
         .Where (c=>c.Metadata .SeriesType  == selectedType)
         .ToList ()
         .ForEach (c => seriesList.Add (c.Metadata .Name ));
     sourceSeries .DataSource  = seriesList;
 }

Uygulama geliştirici olarak eklentilernden geleçek olan Metadata bilgilerimi güçlü bir tipe çekmiş oldum. Eklenti geliştiricilerede uygulama içinde gerekli olan Metadata bilgilerine doğrudan ulaşmış oldular.

Neden SeriesFactoryMetadataAttribute sınıfnıda bir interface uygulamak zorundadır ? Compositon geri tarafta metadata sınıfı için hala bir anahtar-değer sözlüğü üretmektedir. Doğrudan SeriesFactoryMetadataAttribute sınıfnı [Import] ile almak için SeriesFactoryMetadataAttribute sınıfının üretilen sözlüğü constractor parametresi olarak alması gerekmektedir. Daha kolay yöntem ise SeriesFactoryMetadataAttribute sınıfın bir interface uygulaması ve interface üzerinde ki property değerlerinin composition tarafından okunmasıdır.

Parçaların Hayat Döngüsünü Belirlemek

Ben asal sayıları hesaplayan dizinin her çağrılmada tekrar hesaplama yapmasını istemiyorum. Çünkü bu hesapla uzun süremektedir. Bu sınıfın uygulamada sadece bir defa oluşturulmasını istiyorum. Prime sınıfı ürettiği asay sayıları saklasın ve mümkün oldupunca az hesapla yapsın istiyorum. Taşıyıcıya bu sınıfı her çağrıda oluşturma sadece bir defa oluştur ve kullan demem gerekiyor.

[cs]

[SeriesFactoryMetadata (Name  = "Prime Series" , SeriesType  = SeriesType.Math)]
 [PartCreationPolicy (CreationPolicy.Shared)]
 public  class  Prime  : ISeriesFactory

Böylece Prime sınıfını paylaşımlı/Shared işaretleyerek kullandığım taşıyıcı üzerinde aynı anda sadece bir tane örneği bulunacağını belirtiyoruz. Eğere bir nesnenin her erişimde yeniden oluşturulmasını istiyor iseniz NonShared olarak işaretlemeniz gerekmektedir. Fibanacci serisinde nasıl kullanılması gerektiğini [Import] anında karar verilmesini istiyorum. Yani Fibanacci serisinin [Import] eden parça isterse taşıyıcı üzerinde mevcut olan örneği isterse yeni bir örneği kullanabilir.

[cs]

[SeriesFactoryMetadata (Name  = "Fibannacci Series" , SeriesType=SeriesType.Math)]
 [PartCreationPolicy (CreationPolicy.Any)]
 public  class  Fibanacci  : ISeriesFactory
 [Import("Fibanacci", RequiredCreationPolicy = CreationPolicy.NonShared )]
 private  ISeriesFactory  _fibanacci ;

Composition aksi belirtilmedikce [Export] kontratı olarak Any, [Import] kontratı olrakta Shared kullanmaktadır. Yani aksi belirtilmedikce taşıyıcı üzerinde aynı [Export] kontratı için bir tane parça var olacaktır.

Katalogu Çalışma Zamanında Değiştirme

Fibanacci sınıfın yeni versiyonunu yazdık. Şimdi uygulamayı dinamik olarak güncellememiz gerekiyor. Taşıyıcıya [Import] ettiğiniz parçalarda bir değişiklik olursa tekrar [Import] işlemini yapmasını söyleyebiliyoruz.

[cs]

[ImportMany (AllowRecomposition  = true )] 
private  Lazy [] _series ;

Ayrıca her hangi bir [Import] parçası taşıyan bir sınıfta IPartImportsSatisifiedNotification interface'i uyguladı ise tüm [Import] işlemleri bittikten sonra ImportCompleted olayı taşıyıcı tarafından çağırılmaktadır.

Sonuç

Böylelikle [Export] kontratlarımıza Metadata özelliklerini eklemeyi ve eklediğimiz Metadata özelliklerini bir sınıf yapısına getirmeyi inceledik. Taşıyıcının Metadata ve ContractName ile nasıl çalıştığını ve taşıyıcı üzerinde sorgulama yapmayı gördük. Daha sonra taşıyıcı üzerinde ki parçaların hayat döngüsünü yönetmeyi inceledik. Son olarakta çalışma anında değişen katalogdan haberdar olmayı gördük. Bu iki yazı ile her hangi bir .NET platformunda genişletilebilir uygulama yazacak duruma geldik. Bir sonra ki makalede Genişletilebilir Uygulamalarda Hata Ayıklama ve Takip etme özelliklerini inceliyor olacağız.