4 Temmuz 2013 Perşembe

C# ile Zamanlayıcı Kullanmak (System.Timers)

Genel olarak tasarlanmış bir yazılımın, farklı kullanıcıların tüm ihtiyaçlarını karşılaması çok zordur. Daha geniş bir kullanıcı grubuna hitab eden yazılım geliştirmek için izlenebilecek ilk yol tasarım sırasında olabildiğince farklı durumu incelemek ve tasarımı bu durumlara göre yapmak, diğer bir yol ise yazılıma esneklik katmaktır. Esnek bir yazılım, üretim ortamına taşındıktan sonra da kullanıcılar tarafından geliştirilmeye açık olan yazılımdır. Bir anlamda, programcıların bıraktıkları noktadan, yazılımı geliştirme işini kullanıcılar devralmalıdır. Kullanıcılara bu imkanı sağlamak için ise, tasarım araçları (form, iş akışı) ve programlama araçlarından (api’ler, scripting dilleri, script motorları) faydalanılır.
Bu yazıda C# ile programlarımıza nasıl esneklik katabileceğimizi, programımızı nasıl bir kod derleyiciye dönüştürebileceğimizi göreceğiz.
CSharpCodeProvider Sınıfı
Programımıza bir kod derleyicinin sahip olduğu yetenekleri vermek aslında düşünüldüğü kadar zor değil. Framework zaten bu görevi yerine getiren bir sınıf barındırıyor. Bizim yapacağımız şey ise bu sınıfı kullanmak ve birkaç ekstra özellik ekleyerek tek bir metod çağrısı ile istenen kodun derlenip çalıştırılmasını sağlamak. Sözünü ettiğimiz sınıf olan CSharpCodeProvider sınıfı, C# kodlarını derleyen ve CodeDOM kod modellerini kaynak koduna dönüştüren nesnelere ulaşmamızı sağlar.
Yukarıda saydığımız büyük işleri küçük bir metodla gerçekleştiren sınıfımızı yazmaya başlayalım. Sınıfımıza CodeRunner adını verdim.
Öncelikle bize gerekli olan isim uzaylarını belirtiyoruz.

using System;
using System.IO;
using System.Text;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Specialized;


Bir sonraki adım sınıf tanımını oluşturmak;

namespace CodeRunnerLib
{
public class CodeRunner
{
public static StringCollection ReferencedAssemblies = new StringCollection();
public static StringCollection NameSpaces = new StringCollection();
static CodeRunner()
{
ReferencedAssemblies.Add("System.dll");
NameSpaces.Add("System");
}

Yukarıdaki kod parçası içerisinde iki adet değişken tanımladık. ReferencedAssemblies adındaki StringCollection nesnesi, derlenecek olan kod için gerekli harici assembly’leri referanslara eklemek için kullandığımız değişken.
NameSpaces adındaki diğer StringCollection nesnesi ise derlenecek olan kodun başına “using ...” ifadesi ile eklenecek isim uzaylarının listesini saklayan değişken.
CodeRunner sınıfının yapılandırıcısını (constructor) statik olarak tanımladım çünkü CodeRunner sınıfı tek bir statik metoda sahip. Böylece CodeRunner sınıfını kullanabilmek için bir CodeRunner nesnesi oluşturmamıza da gerek kalmıyor. Statik yapılandırıcı içerisinde “System.dll” dosyasını referanslara ekliyoruz. Ayrıca “using” ifadesi ile derlenecek kodun başında yer alması için NameSpaces koleksiyonuna “System” isim uzayını ekliyoruz.


public static CompilerErrorCollection RunCode(string Code) {

CodeRunner sınıfının tek metodu olan RunCode metodunun tanımını yukarıda görüyorsunuz. Metod, parametre olarak bir string alıyor ve derleme işleminin sonuçlarını içerisinde bulunduran bir CompilerErrorCollection nesnesi döndürüyor.

CSharpCodeProvider cp = new CSharpCodeProvider();

Kodları derleme ve hataları döndürme işlemlerinde kullanacağımız ICodeCompiler arayüzüne ulaşmamızı sağlayan CSharpCodeProvider nesnesini tanımladık. Şimdi bahsettiğimiz işlemleri yapabilmek için CodeProvider nesnesinin bir kod derleyici üretmesini ve bize döndürmesini sağlayalım;

ICodeCompiler derleyici = cp.CreateCompiler();

CSharpCodeProvider sınıfının CreateCompiler metodunu kullanarak ICodeCompiler arayüzünü uygulayan bir derleyici sınıf elde ettik. RunCode metoduna parametre olarak geçilen kodları derlemek için artık bu arayüzü kullanabiliriz.

CompilerParameters derleyiciParams = new CompilerParameters();

Derleme işleminin konfigurasyonunu yapabilmek için bir CompilerParameters nesnesi tanımladık. Bu nesnenin birkaç özelliğini değiştirip ’’derleyici nesnesine, kodları nasıl derleyeceğini bildireceğiz.

foreach (string refAs in ReferencedAssemblies)
{
derleyiciParams.ReferencedAssemblies.Add(refAs);
}

Daha önce CodeRunner sınıfına eklediğimiz ReferencedAssemblies koleksiyonu içerisinde bulunan tüm assembly isimlerini ’’derleyiciParams adındaki CompilerParameters nesnesi aktardık. Böylece ’derleyici arayüzü, kodları derlerken hangi assembly’leri kullanması gerektiğini bilecek.

derleyiciParams.GenerateInMemory = true;

Yukarıdaki ifade ile derleme işlemi bittikten sonra, çıktının (assembly) hafızada tutulacağını belirtmiş oluyoruz. Eğer derleme işlemi sonucunda elde edilen assembly’nin hafızada değil de diskte saklanmasını istiyorsanız CompilerParameters sınıfının ’’GenerateInMemory özelliğini ’’false yapıp, ’’OutputAssembly özelliğinde bir dosya adı belirtmelisiniz.

StringBuilder sbKod = new StringBuilder();
IndentedTextWriter kodYazici = new IndentedTextWriter(new StringWriter(sbKod));
foreach (string nSpace in NameSpaces)
{
kodYazici.WriteLine("using {0};", nSpace);
}
kodYazici.WriteLine("public class DinamikSinif {");
kodYazici.Indent += 3;
kodYazici.WriteLine("public void DinamikMetod() {");
kodYazici.Indent += 3;
kodYazici.Write(Code);
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Close();

Yukarıdaki satırlarda tam bir kod dosyası hazırlayıp bir StringBuilder nesnesi içerisinde saklıyoruz. Kodları yazdırmak için kullandığım nesne bir IndentedTextWriter nesnesi. Bu nesneyi CodeDOM ile kod oluştururken ya da bizim yaptığımız gibi dinamik kod geliştirirken kullanabilirsiniz. IndentedTextWriter nesnesinin ilginç olan tek özelliği ’’Indent adındaki özellik. Bu özellik ’’WriteLine ya da ’’Write ile yazdığımız kod satırlarının ’’Indent kadar girintili olarak yazılmasını sağlıyor.
Yukarıdaki kodda önemli olan tek nokta şu:
CodeRunner.RunCode metoduna parametre olarak geçilen tüm kodları tam bir sınıf tanımı içerisinde saklıyoruz. Önce NameSpaces koleksiyonuna eklenen tüm isim uzaylarını ’’using ifadesi ile üretilen kodun başına ekliyoruz. Daha sonra dinamik bir sınıf tanımlıyor, sınıf içerisinde geçici bir metod oluşturuyor ve parametreden gelen kodları bu dinamik metod içerisine yerleştiriyoruz.
Örneklemek gerekirse;
RunCode metoduna parametre olarak “’MessageBox.Show(“Test”);” geçilmiş olsun. Bu durumda oluşturulan dinamik sınıf tanımı şu şekilde:

using System;
public class DinamikSinif
{
public void DinamikMetod()
{
MessageBox.Show("Test");
}
}

Tabi yukarıdaki kodun çalışabilmesi için CodeRunner.References koleksiyonuna “System.Windows.Forms.dll” ve NameSpaces koleksiyonuna “System.Windows.Forms” eklenmeli...

CompilerResults derlemeSonuclari = derleyici.CompileAssemblyFromSource(derleyiciParams, sbKod.ToString());

Bir sonraki adımda ’’derleyici nesnesini kullanarak oluşturduğumuz dinamik sınıf kodunu, belirlediğimiz ayarlarla (’’derleyiciParams) derliyoruz. Derleme işleminin sonucu ’’derlemeSonuclari adında bir CompilerResults nesnesi içerisinde saklanıyor. Eğer derleme sırasında bir hata varsa, hata ile ilgili bilgiler –hata olan satırın numarası ve hata mesajı- bu nesnenin Errors koleksiyonunda barındırılıyor. Ayrıca derleme işlemi başarılı ise, derlenen assembly’ye CompilerResults.CompiledAssembly özelliğinden ulaşıyoruz.

if (derlemeSonuclari.Errors.HasErrors)
{
return derlemeSonuclari.Errors;
}

Eğer derleme işlemi sırasında bir hata ile karşılaşılırsa hatalar RunCode metodunun dönüş değeri olarak belirleyip metoddan çıkıyoruz. Böylece RunCode metodunu çağıran tüketiciler işler yolunda gitmezse hatalarla ilgili bilgilere ulaşabiliyorlar.

Assembly derlenmis = derlemeSonuclari.CompiledAssembly;

Eğer hatasız bir derleme işlemi gerçekleşmişse derlenen assembly’yi ’’derlenmis adındaki değişkende saklıyoruz. Şimdi biraz Reflection kullanarak dinamik olarak derlediğimiz assembly içerisindeki dinamik sınıfımızın bir örneğini oluşturabiliriz;

object dinamikSinif = derlenmis.CreateInstance("DinamikSinif");

’’derlenmis ile sakladığımız assembly içerisindeki DinamikSinif nesnesinin bir örneğini assembly’nin ’’CreateInstance metodunu kullanarak oluşturduk. Bir sonraki adım yine dinamik olarak oluşturduğumuz metodu çağırmak;

Type dinamikSinifTipi = dinamikSinif.GetType();
dinamikSinifTipi.InvokeMember("DinamikMetod", BindingFlags.InvokeMethod, null, dinamikSinif, null);
return null;

Metodu çağırabilmek için önce Reflection kullanarak ’’dinamikSinif nesnesinin tip tanımına ulaşıyoruz. Daha sonra tip tanımı üzerinden ’’InvokeMember metodu ile ’’DinamikMetod adındaki metodu çağırıyoruz. Böylece RunCode metoduna parametre olarak geçilen C# kodu sanal bir sınıf içerisindeki sanal bir metod içine gömülerek çalıştırılmış oluyor.
Aşağıda CodeRunner sınıfının tam kodunu bulabilirsiniz. Ayrıca yazının sonunda CodeRunner sınıfı ve test için kullanabileceğiniz bir winforms uygulamasının kaynak kodları da mevcut...

using System;
using System.IO;
using System.Text;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Specialized;
namespace CodeRunnerLib {
public class CodeRunner {
public static StringCollection ReferencedAssemblies = new StringCollection();
public static StringCollection NameSpaces = new StringCollection();
static CodeRunner() {
// Statik yapılandırıcı fonksiyon içerisinde
// System isim uzayına referans gösteriyoruz
ReferencedAssemblies.Add("System.dll");
NameSpaces.Add("System");
}
public static CompilerErrorCollection RunCode(string Code) {
/* Metin kutusuna yazılan kodu derlemek için
* bir CSharpCodeProvider nesnesi oluşturuyoruz.
*/
CSharpCodeProvider cp = new CSharpCodeProvider();
ICodeCompiler derleyici = cp.CreateCompiler();
/* Kod derleyiciyinin birkaç özelliğini ayarlayabilmek
* için bir CompilerParameters nesnesi oluşturuyoruz.
*/
CompilerParameters derleyiciParams = new CompilerParameters();
// Derleyiciye gerekli olan assembly’leri referansla gösteriyoruz
foreach (string refAs in ReferencedAssemblies) {
derleyiciParams.ReferencedAssemblies.Add(refAs);
}
// Derlenen assembly’nin hafızada tutulmasını istiyoruz
derleyiciParams.GenerateInMemory = true;
/* Derlenecek olan kodu oluşturuyoruz.
* Metin kutusuna yazılan kodu, dinamik olarak oluşturduğumuz
* bir sınıf içerisine gömeceğiz
*/
StringBuilder sbKod = new StringBuilder();
IndentedTextWriter kodYazici = new IndentedTextWriter(new StringWriter(sbKod));
// Derlenecek kod için gerekli olan isimuzaylarını aktarıyoruz
foreach (string nSpace in NameSpaces) {
kodYazici.WriteLine("using {0};", nSpace);
}
// Sınıf tanımını oluşturalım
kodYazici.WriteLine("public class DinamikSinif {");
kodYazici.Indent += 3;
kodYazici.WriteLine("public void DinamikMetod() {");
kodYazici.Indent += 3;
// Parametrede gelen kodu, geçici sınıf metodu içerisine yerleştiriyoruz
kodYazici.Write(Code);
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Close();
// Kodu derliyoruz, derleme işleminin sonuçlarını saklamak için bir CompilerResult nesnesi oluşturuyoruz
CompilerResults derlemeSonuclari = derleyici.CompileAssemblyFromSource(derleyiciParams, sbKod.ToString());
// Derleme işlemi sonucunda bir problem varsa bunları gösteriyoruz
if (derlemeSonuclari.Errors.HasErrors) {
return derlemeSonuclari.Errors;
}
// Derlenmiş olan assembly’yi alıyoruz
Assembly derlenmis = derlemeSonuclari.CompiledAssembly;
// Assembly içerisinden kendi eklediğimiz sınıfın bir örneğini oluşturuyoruz
object dinamikSinif = derlenmis.CreateInstance("DinamikSinif");
// DinamikSinif’a ait DinamikMetod adındaki metodunu çağırıyoruz
Type dinamikSinifTipi = dinamikSinif.GetType();
dinamikSinifTipi.InvokeMember("DinamikMetod", BindingFlags.InvokeMethod, null, dinamikSinif, null);
return null;
}
}
}





Hiç yorum yok:

Yorum Gönder