Burak Selim Senyurt(MVP)
Matematik Mühendisi bir .NET Severin Yazıları...

Nasıl Yapılır? Adım adım özel Http Handler Geliştirmek

Pazartesi, 10 Aralık 2007 02:05 by bsenyurt

Uzun zaman önce Asp.Net 2.0 ile ilişkili makalelerimizden birisinde HttpHandler ve HttpModule kavramlarından bahsetmeye çalışmıştık. Bu makalemizde kendi Handler sınıfımızı geliştirmek isteyebileceğimiz örnek bir senaryo üzerinde daha durmaya çalışacağız. Bu sayede HttpHandler sınıfları yazarak neler yapılabileceğinide daha net bir şekilde görmüş olacağız. Konuyu daha net kavrayabilmek adına örnek senaryomuz üzerinden adım adım ilerleyeceğiz.

Bilindiği üzere web sunucusuna istemci tarafından gelen talepler(Requests) bazı program ara yüzleri(API) tarafından karşılanır ve uygun ortamlara işletilmek üzere iletilirler. Özellikle Asp.Net ile geliştirilen web uygulamalarında, talep edilen dosya tipine göre devreye giren HttpHandler sınıfları bulunmaktadır. Söz gelimi aspx uzantılı dosyalar PageHandlerFactory isimli sınıf tarafından ele alınırlar. PageHandlerFactory ve benzer işlevselliklere sahip handler tipleri IHttpHandler arayüzünü(interface) uygularlar. Dolayısıyla geliştiriciler kendi Handler tiplerini IHttpHandler arayüzünü kullanarak yazabilirler.

Gelelim örnek senaryomuza. Sunucu üzerinde barındırılan XML(eXtensible Markup Language) tabanlı basit bir metin dosyasını ele alacak özel bir Handler tipi geliştiriyor olacağız. XML tabanlı dosya içerisinde yer alan bilgilerden yararlanılarak ekrana herhangibir sorguya ait raporlama sonuçları aktarılacak. Dosyanın uzantısının örnek olarak rapx olduğunu düşünebiliriz. Peki bu raporlama işleminde önemli bir rol oynacak olan XML içeriğinde neler olması gerekmektedir? Bunların tespiti XML dosyasının mantıksal ağaç yapısının oluşturulmasınıda kolaylaştıracaktır. Söz konusu ihtiyaçları aşağıdaki maddeler halinde sıralanabilirler.

  • Hazırlanan rapor için bir başlık(Title) bilgisi tutulabilir. Hatta başlığın arka plan rengi(Background Color) verilerek zengin görünmesi sağlanabilir.
  • Rapor dosyasının kaç adet sorgu cümlesi(Query) için destek vereceğine karar vermek gerekmektedir. Örneğin basit olması açısından başlangıç itibariyle sadece tek bir sorgu sonucunun ele alınmasında fayda vardır.
  • Sorgu cümlesi, saklı yordam(stored procedure) veya text tabanlı ifadeleri işaret edebilir. Hatta bir veritabanı görünümünün(View) desteklenmesi bile sağlanabilir.
  • Sorgu cümlesinde parametre kullanımına destek verilebilir. Bu parametrelerinin QueryString yardımıyla Url üzerinden veya XML içeriğinden alınacağına dair tanımlamalar yapılabilir.
  • Sorgu cümlesinin çalışacağı sunucu(Server) ve veritabanı(Database) adı belirtilmelidir. Hatta SSPI ile bağlantıya destek verilmeside göz önüne alınmalıdır.
  • Hazırlanan raporların XML içeriğinde belirtilen mail adreslerine gönderilmesi de sağlanabilir.
  • Raporun kimler tarafında görülebileceğine dair tanımlamalar yapılabilir. Bu tanımlamalar kullanıcı(User) ve hatta rol(Role) bazında gerçekleştirilebilir.

Bu ihtiyaçlar dahada arttırılabilir. Bir anlamda geliştiricinin hayal gücü burada önem kazanmaktadır. Yukarıdaki maddelerde sözü geçen ihtiyaçların tamamı XML' in temel kavramları ile karşılanabilir. Bir başka deyişle eleman(element) ve nitelikler(attributes) sayesinde yukarıdaki istekler standartlara uygun olacak şekilde tasarlanabilir.

Not : Söz konusu XML içeriğinin çalışma zamanında(run time) uygun bir standartta olduğunun garanti altına alınması için şema (Xml Schema) kullanılması yararlı olacaktır. Bu şema ile Xml içeriğinin çarpıştırılması Handler tipi içerisinde yapılabilir. Şemaya uymayan XML içerikleri için Handler, istekleri özel olarak tasarlanmış bir hata sayfasına doğru yönlendirebilir.

Yukarıdaki maddeler ışığında aşağıdaki gibi örnek bir XML içeriği göz önüne alınabilir.

<?xml version="1.0" encoding="utf-8" ?>
<Raporlar>
    <Rapor Id="">
        <Baslik ArkaPlan=""></Baslik>
        <Baglanti SSPI="">
            <Sunucu></Sunucu>
            <Veritabani></Veritabani>
            <KullaniciAdi></KullaniciAdi>
            <Sifre></Sifre>
        </Baglanti>
        <Sorgu Sp="">
            <Cumle></Cumle>
            <Parametreler Nereden="">
                <Parametre Ad="" Deger=""/>
            </Parametreler>
        </Sorgu>
        <MailListesi MailGonder="">
            <Mail></Mail>
            <Mail></Mail>
        </MailListesi>
    </Rapor>
</Raporlar>

Raporlar ana boğumu(Root Node) birden fazla Rapor elementi içerebilir. Biz örneğin daha kolay ele alınabilmesi amacıyla tek bir Rapor elementi kullanıyor olacağız. Raporun alınacağı sunucu(Server), veritabanı adı(Database Name), kullanıcı adı(User name) ve şifre(Password) bilgileri ise Baglanti elementi altında tutulmaktadır. Hatta SSPI kullanımınada destek verilmesi amacıyla Baglanti elementi içerisinde bir nitelik(attribute) tanımlanmaktadır. Sorguların saklı yordam(Stored Procedure) olup olmadığı Sp niteliği ile belirtilebilir. Bunun dışında sorgu içerisinde kullanılan parametreler var ise bunların nereden alınacağı Nereden isimli nitelik(attribute) ile belirlenmektedir. Öyleki raporun parametreleri, rapx dosyasına tarayıcı üzerinden yapılan çağrılarda QueryString yardımıyla gelebilir. Yada Parametre elementleri içerisindeki Deger niteliklerinde doğrudan tanımlanabilir. Üretilen raporların mail yolu ile kimlere bildirileceğine dair MailListesi elementi ve Mail alt elementleri kullanılabilir.

Burada ortaya çıkan önemli bir ihtiyaç vardır. XML bilgilerinin tutulduğu rapx uzantılı dosyaların işlenmesi, bir HTML çıktısının üretilmesi ve talepte bulunan istemcilere gönderilmesi sağlanmalıdır. Bunu sadece özel bir Handler tipi karşılayabilir.

Dilerseniz vakit kaybetmeden Handler tipini yazarak işe başlayalım. Geliştirilecek olan Handler sınıfı ilk etapta tek bir web uygulamasında kullanılacaktır. Sonrasında ise bu handler tipinin sunucu üzerindeki her Asp.Net web uygulaması için geçerli olması sağlanacaktır. Bu nedenle Handler tipinin bir sınıf kütüphanesi(Class Library) içerisinde olmasında yarar vardır. Handler sınıfının IHttpHandler arayüzünden(Interface) türetilmesi içinde sınıf kütüphanesine System.Web.dll assembly' ının referans edilmesi şarttır. Söz konusu Handler sınıfının diagram görüntüsü ve içeriği aşağıdaki gibidir.

using System;
using System.Web;
using System.Data;
using System.Web.Hosting;
using System.Data.SqlClient;
using System.Xml;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.IO;

namespace ReportHandlerLibrary
{
    public class RaporHandler:IHttpHandler
    {
        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return false; }
        }

        // Gelen talep sonrası üretilecek HTML çıktısını bu metod içerisinden veriyor olacağız
        public void ProcessRequest(HttpContext context)
        {
            // Talep edilen sayfa Request.Path ile yakalanır.
            // VirtualPathProvider metodu ile sanal adresin karşılığı olan fiziki yoldaki dosya açılarak Stream halinde elde edilir.
            // Stream'den yararlanılarak XML içeriği XmlDocument nesnesi içerisine alınır.
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(VirtualPathProvider.OpenFile(context.Request.Path));
            if (context.Request.QueryString["MailGonder"] == null)
            {
                // Baslik elementinden raporun başlığı bilgisi alınır
                string baslik = xDoc.SelectSingleNode("Raporlar/Rapor/Baslik").InnerText;
                string baslikArkaPlanRengi = xDoc.SelectSingleNode("Raporlar/Rapor/Baslik").Attributes["ArkaPlan"].InnerText;
       
                // Ekran tasarımı oluşturulmaya başlanır
                // Üretilen çıktı bir HTML sayfası olacağından HTML elementlerinin kullanılması gerekmektedir.
                context.Response.Write("<HTML><HEAD><TITLE>" + baslik + "</TITLE></HEAD>");
                context.Response.Write("<BODY>");
                // Table elementi oluşturulur.
                context.Response.Write("<TABLE border='1' width='100%' cellspacing='0' cellpadding='5'>");
                // Tablo 3 satırdan oluşmaktadır. İlk satırın arka plan rengi ve içerisinde yer alacak metin bilgisi Baslik elementi ve ArkaPlan niteliklerinden alınır
                context.Response.Write("<TD style='background-color:" + baslikArkaPlanRengi + "'>");
                context.Response.Write("<H3>" + baslik + "</H3>");
                context.Response.Write("</TD></TR>");
                // İkinci satır içerisinde rapor sonucu üretilen grid içeriği olmalıdır.
                context.Response.Write("<TR><TD>");

                // Bu hücreye veri ile doldurulan Grid içeriğinin HTML çıktısı yazdırılır
                context.Response.Write(GridHTMLUret(xDoc,context).ToString());
   
                context.Response.Write("</TD></TR>");
                context.Response.Write("<TR><TD>");
   
                // Mail gönderme aksiyonu için basit bir hyperLink elementi eklenir
                context.Response.Write("<a href='"+context.Request.Path + "?MailGonder=1'>" + "Raporu Mail Olarak Gönder" + "</a>");
           
                context.Response.Write("</TD></TR>");
                context.Response.Write("</BODY>");
                context.Response.Write("</HTML>");
            }
            else
            {
                bool mailGondersinmi = false;
                Boolean.TryParse(xDoc.SelectSingleNode("Raporlar/Rapor/MailListesi").Attributes["MailGonder"].Value, out mailGondersinmi);
                if (mailGondersinmi)
                    MailGonder(xDoc,context);
            }
        }

        #endregion

        // GridView kontrolünün içeriği doldurulduktan sonra HTML içeriği elde edilir ve bu içeriği taşıyan StringWriter geriye döndürülür.
        private StringWriter GridHTMLUret(XmlDocument xDoc,HttpContext ctx)
        {
            // SqlDataAdapter nesne oluşturulur
            SqlDataAdapter adapter = new SqlDataAdapter(KomutHazirla(xDoc,ctx));
            DataTable table = new DataTable();
            // DataTable doldurulur
            adapter.Fill(table);
   
            // GridView kontrolü üretilir ve veriye bağlanır
            GridView grd = new GridView();
            grd.DataSource = table;
            grd.DataBind();
   
            //GridView kontrolünün HTML çıktısı elde edilir
            StringWriter strWriter = new StringWriter();
            HtmlTextWriter writer = new HtmlTextWriter(strWriter);
            grd.RenderControl(writer);
            return strWriter;
        }

        // Raporun üretilmesi için gerekli SqlCommand hazırlanıyor.
        private SqlCommand KomutHazirla(XmlDocument xDoc,HttpContext ctx)
        {
            bool sp = false;
            // Sorgu cümlesinin Stored Procedure olup olmadığı belirlenir.
            Boolean.TryParse(xDoc.SelectSingleNode("Raporlar/Rapor/Sorgu").Attributes["Sp"].Value, out sp);
           
            // SqlCommand tipi hazırlanır
            SqlCommand cmd = new SqlCommand();
            // Sorgu cümlesi alınır
            cmd.CommandText = xDoc.SelectSingleNode("Raporlar/Rapor/Sorgu/Cumle").InnerText.Trim();
            // Bağlantı cümlesi BaglantiCumlesiOlustur metodundan elde edilir ve Command için gerekli SqlConnection hazırlanır.
            cmd.Connection = new SqlConnection(BaglantiCumlesiOlustur(xDoc));
            // Eğer cümle Stored Procedure adını işaret ediyorsa CommandType için StoredProcedure enum sabiti değeri verilir
            if (sp)
                cmd.CommandType = CommandType.StoredProcedure;
   
            // Eğer girilmiş parametreler varsa bu parametreler Command nesnesine AddWithValue metodu ile teker teker eklenir.
            if (xDoc.SelectSingleNode("Raporlar/Rapor/Sorgu/Parametreler").ChildNodes.Count > 0)
            {
                string parametreNereden = xDoc.SelectSingleNode("Raporlar/Rapor/Sorgu/Parametreler").Attributes["Nereden"].Value;
                XmlNodeList parametreler = xDoc.SelectSingleNode("Raporlar/Rapor/Sorgu/Parametreler").ChildNodes;
                foreach (XmlNode parametre in parametreler)
                {
                    if(parametreNereden=="Xml")
                        cmd.Parameters.AddWithValue(parametre.Attributes["Ad"].Value, parametre.Attributes["Deger"].Value);
                    else if(parametreNereden=="QueryString")
                        cmd.Parameters.AddWithValue(parametre.Attributes["Ad"].Value, ctx.Request.QueryString[parametre.Attributes["Ad"].Value.Substring(1, parametre.Attributes["Ad"].Value.Length - 1)]);
                }
            }
            // Oluşturulan Command nesnesi geri döndürülür.
            return cmd;
        }

        // Sorguların çalıştırılması için gerekli Bağlantı cümlesini oluşturan metod
        private string BaglantiCumlesiOlustur(XmlDocument xDoc)
        {
            bool sspi = false;
   
            // Sql bağlantısı için gerekli bağlantı cümlesi(Connection String) SqlConnectionStringBuilder sınıfı yardımıyla oluşturulur.
            SqlConnectionStringBuilder conStrBuilder = new SqlConnectionStringBuilder();
       
            // Sunucu ve veritabanı bilgileri XPath ifadeleri ile alınır.
            conStrBuilder.DataSource = xDoc.SelectSingleNode("Raporlar/Rapor/Baglanti/Sunucu").InnerText;
            conStrBuilder.InitialCatalog = xDoc.SelectSingleNode("Raporlar/Rapor/Baglanti/Veritabani").InnerText;
            Boolean.TryParse(xDoc.SelectSingleNode("Raporlar/Rapor/Baglanti").Attributes["Sspi"].Value, out sspi);
            if (!sspi) // Eğer integrated security ile bağlanılmıyorsa kullanıcı adı(UserID) ve şifre(Password) bilgileri alınır.
            {
                conStrBuilder.UserID = xDoc.SelectSingleNode("Raporlar/Rapor/Baglanti/KullaniciAdi").InnerText;
                conStrBuilder.Password = xDoc.SelectSingleNode("Raporlar/Rapor/Baglanti/Sifre").InnerText;
            }
            else
                conStrBuilder.IntegratedSecurity = true;

            // Oluşturulan bağlantı cümlesi(Connection String) geri döndürülür.
            return conStrBuilder.ConnectionString;
        }

        // Mail gönderme seçeneği aktif ise postaların gönderilme işlemini gerçekleştirecek olan metod.
        private void MailGonder(XmlDocument doc,HttpContext ctx)
        {
            // Mail listesi MailListesi elementinin alt elementlerinden çekilir.
            XmlNodeList mailler = doc.SelectSingleNode("Raporlar/Rapor/MailListesi").ChildNodes;
            foreach (XmlNode mail in mailler)
            {
                //TODO: Bu noktada raporun mail olarak gönderilmesine ait işlemler yapılacaktır.
                ctx.Response.Write(mail.InnerText + " adresine rapor mail olarak gönderildi<br/>");
            }
        }
    }
}

Geliştirilen sınıf içerisinde parçaları daha kolay ele alabilmek adına yardımcı metodlar yer almaktadır. Dikkat edileceği üzere rapx içeriğinin XML formatında tasarlanması, işlemleri son derece kolaylaştırmaktadır. Nitekim içeriğin XmlDocument sınıfına ait bir nesne örneği ile belleğe alınması, içerisindeki elementlerin veya niteliklerin XPath ifadeleri ile yakalanması son derece kolaydır. Üstelik XML, platform bağımsızlık sunduğundan bu dökümanın başka bir ortama gönderilerek ele alınmasının sağlanması daha kolaydır. Bu sebepten dolayı günümüzün popüler kavramlarından olan Reporting Services veya LINQ To SQL içerisinde yer alan Database Markup Language(dbml) gibi yapılar XML üzerine oturmaktadır. Sınıf içerisinde dikkat edilmesi gereken noktalardan bir diğeri ise, HttpContext tipinin ektin şekilde kullanımıdır. HttpContext üzerinden ele alınan üyeler ile, üretilecek HTML içeriğinin tasarlanması veya talep ile gelen QueryString' lerin yakalanması söz konusudur. Hatta talep edilen sayfanın sanal yolu(Virtual Path) elde edilip XML içeriğinin VirtualPathProvider sınıfının OpenFile metodu ile kolay bir şekilde Stream' e dönüştürülmeside sağlanmaktadır. İşlemleri kolaylaştıran noktalardan biriside web kontrollerinin RenderControl metodudur. Bu metod ile kompleks bir GridView kontrolünün veri dolu içeriğinin HTML çıktısını almak son derece kolaylaşmaktadır.

Şimdi geliştirilen Handler tipini örnek bir web uygulamasında test edebiliriz. İlk olarak tek bir web uygulamasına özel olacak şekilde Handler tipinin kullanılmasını ele alacağız. Böyle bir durumda öncelikli olarak web uygulamasının Handler tipini içeren sınıf kütüphanesini(Class Library) referans etmesi gerekmektedir. Örnek olarak OzelHandlerKullanimi isimli web uygulaması aşağıdaki ekran görüntüsünden de anlaşılacağı üzere ReportHandlerLibrary isimli assembly' ı referans etmektedir.

Bunun dışında web uygulamasına ait web.config dosyası içerisinde aşağıda görüldüğü gibi gerekli bildirimler yapılmalıdır.

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <httpHandlers>
            <add path="*.rapx" type="ReportHandlerLibrary.RaporHandler,ReportHandlerLibrary" verb="*" validate="true"/>
        </httpHandlers>
        <compilation debug="true"/>
        <authentication mode="Windows"/>
    </system.web>
</configuration>

httpHandlers elementi system.web elementi içerisinde tanımlanmalıdır. Örnekte rapx uzantılı dosyalara gelecek herhangibir talebin(Get, Post vb...) type niteliğinde(attribute) belirtilen RaporHandler sınıfına ait nesne örneği tarafından karşılanacağı belirtilmektedir. Bu adım tek başına yeterli değildir. Ayrıca IIS(Internet Information Services) üzerinden, rapx uzantılı dosyalara gelecek olan talepler için AspNet_Isapi.dll' inin devreye gireceğinin belirtilmesi gerekmektedir. Örnek senaryo IIS 7.0 üzerinde geliştirilmektedir. IIS 7.0 üzerinden OzelHandlerKullanimi isimli web uygulaması adına rapx-AspNet_Isapi.dll eşleştirmesi için öncelikli olaran InetMgr aracı üzerinden ilgili web uygulamasına geçilmeli ve Handler Mappings kısmı aşağıdaki ekran görüntüsünde olduğu gibi seçilmelidir.

Handler Mappings kısmında varsayılan ve izin verilen uzantı eşleştirmeleri yer almaktadır. Bu kısımda sağ tıklanarak açılan menüden Add Script Map seçilmeli ve gerekli eşleştirme IIS tarafına bildirilmelidir.

Add Script Map ile açılan iletişim kutusunda ise aşağıdaki ayarların yapılması gerekmektedir.

Buna göre rapx uzantılı taleplerin .Net 2.0 çalışma zamanında yer alan AspNet_Isapi.dll tarafından ele alınacağı belirtilmektedir. Uygulamaya ait web.config dosyası içerisinde gerekli Handler tanımlamaları yapıldığı için AspNet_Isapi.dll program arayüzü, gelen talep(Request) için RaporHandler sınıfının devreye girmesini sağlayacaktır. Bu işlemin ardından Handler Mappings kısmına aşağıdaki ekran görüntüsünde de görüldüğü gibi rapx uzantısı için gerekli eşleştirme eklenecektir.

Yapılan bu değişikliker sonrasında web uygulmasında ait web.config dosyası içerisindede aşağıdaki gibi yeni bir tanımlama oluşacaktır. Bir başka deyişle IIS(Internet Information Services) üzerinde yapılan eşleştirmeye ait bilgiler system.webServer elementi içerisindeki handlers kısmına eklenmektedir.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appSettings />
    <connectionStrings />
    <system.web>
        <httpHandlers>
            <add path="*.rapx" type="ReportHandlerLibrary.RaporHandler,ReportHandlerLibrary" verb="*" validate="true" />
        </httpHandlers>
        <compilation debug="true" />
        <authentication mode="Windows" />
    </system.web>
    <system.webServer>
        <handlers>
            <add name="Rapor X" path="*.rapx" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\ Framework\v2.0.50727\aspnet_isapi.dll" resourceType="File" />
        </handlers>
    </system.webServer>
</configuration>

Artık test amacıyla web uygulamasına rapx uzantılı örnek bir içerik eklenebilir. Örnek olarak AdventureWorks veritabanında yer alan Products tablosu ile ilişkili bir rapor düşünülebilir. Bu raporun stok seviyesi belirli bir değerin altında olan ürünlerin kategori bazlı sayılarını verdiğini düşünelim. Buna göre web uygulaması altında tutulacak olan rapx uzantılı metin dosyasının içeriği aşağıdaki gibi tasarlanabilir.(Bu dosyayı eklerken Add New Item-Text File seçerek KategoriBazliUrunler.rapx gibi bir seçim yapılması gerektiğine dikkat edilmelidir.)

KategoriBazliUrunler.rapx;

<?xml version="1.0" encoding="utf-8" ?>
<Raporlar>
    <Rapor Id="1">
        <Baslik ArkaPlan="#FFCC11">Kategori Bazlı Ürün Sayıları</Baslik>
        <Baglanti Sspi="true">
            <Sunucu>localhost</Sunucu>
            <Veritabani>AdventureWorks</Veritabani>
            <KullaniciAdi></KullaniciAdi>
            <Sifre></Sifre>
        </Baglanti>
        <Sorgu Sp="false">
            <Cumle> Select Count(P.ProductID) [Toplam Ürün Sayısı],PSC.Name [Kategori Adı] From Production.Product P Join Production.ProductSubCategory PSC On P.ProductSubCategoryID=PSC.ProductSubCategoryID Group By PSC.Name,SafetyStockLevel Having P.SafetyStockLevel&lt;@StockLevel
            </Cumle>
            <Parametreler Nereden="Xml">
                <Parametre Ad="@StockLevel" Deger="10"/>
            </Parametreler>
        </Sorgu>
        <MailListesi MailGonder="true">
            <Mail>selim@bsenyurt.com</Mail>
            <Mail>bsenyurt@csharpnedir.com</Mail>
        </MailListesi>
    </Rapor>
</Raporlar>

Burada basit olarak Product ve ProductSubCategory tablolarının Join ile birleştirilmiş hali üzerinden bir gruplama sorgusu gerçekleştirilmektedir. Sorgular StockLevel parametresinin değeri 10'dan küçük olanlar için yapılmaktadır. Parametrenin değerinin XML içerisinden geleceğini belirtmek için Nereden niteliğine XML değeri atanmıştır. Söz konusu rapor için MailListesinde belirtilen kişilere mail gönderme opsiyonu açık bırakılmıştır. Rapor, localhost isimli sunucudaki AdventureWorks veritabanı üzerinden SSPI ile gerçekleştirilen bir bağlantı üzerinden alınmaktadır. Artık OzelHandlerKullanimi adresi üzerinden KategoriBazliUrunler.rapx dosyasına bir talepte bulunulursa aşağıdaki gibi bir sonuç ortaya çıkacaktır.

Görüldüğü gibi sorgu sonucu elde edilen rapor ekrana basit bir tablo olarak basılmıştır. Mail gönderme seçeneği aktif olduğu içinde Raporu Mail Olarak Gönder başlıklı linkte sayfa çıktısında yer almaktadır. Bu linke basıldığı takdirde sunucuya yeni bir talep daha gönderilecektir. Şimdilik sadece ilgili MailListesi elementinin içeriği değerlendirilmiştir. Aşağıdaki ekran görüntüsünde bu durum gösterilmektedir.

Yeni bir test ile devam edelim. Bu kez raporun parametrelerine ait değerlerin QueryString yardımıyla geldiğini göz önüne alalım. Ayrıca raporun sonuçlarının Northwind veritabanı altında yer alan SalesByCategory isimli saklı yordam(stored procedure) üzerinden elde edildiğini düşünelim. MailGonderme seçeneği yine aktif olsun. Bu durumda rapx dosyasının içeriğinin aşağıdaki gibi tasarlanması gerekecektir.

KategoriBazliSatislar.rapx;

<?xml version="1.0" encoding="utf-8" ?>
<Raporlar>
    <Rapor Id="1">
        <Baslik ArkaPlan="#CCBB77">Kategori Bazlı Satışlar</Baslik>
        <Baglanti Sspi="true">
            <Sunucu>localhost</Sunucu>
            <Veritabani>Northwind</Veritabani>
            <KullaniciAdi></KullaniciAdi>
            <Sifre></Sifre>
        </Baglanti>
        <Sorgu Sp="true">
            <Cumle>SalesByCategory</Cumle>
            <Parametreler Nereden="QueryString">
                <Parametre Ad="@CategoryName"/>
                <Parametre Ad="@OrdYear"/>
            </Parametreler>
        </Sorgu>
        <MailListesi MailGonder="false">
            <Mail>selim@bsenyurt.com</Mail>
            <Mail>bsenyurt@csharpnedir.com</Mail>
        </MailListesi>
    </Rapor>
</Raporlar>

Buna göre tarayıcı penceresinden http://localhost/OzelHandlerKullanimi/KategoriBazliSatislar.rapx?CategoryName=Beverages&OrdYear=1999 adresi talep edilirse aşağıdakine benzer bir ekran görüntüsü ile karşılaşılacaktır.

Elbette uygulamada pek çok hata göz ardı edilmektedir. Söz gelimi KategoriBazliSatislar.rapx için QueryString parametreleri kullanılmassa çalışma zamanı hataları(Run time exceptions) alınması çok doğaldır. Bu gibi hataların ele alınması bir başka deyişle kodun tekrardan revize edilerek düzenlenmesi gerekmektedir.

Gelelim Handler tipinin sunucu üzerinde nasıl ele alınabileceğine. Bunun için geliştirilen Handler tipini içeren assembly' ın Global Assembly Cache(GAC) içerisine atılması ve root web.config dosyası içerisindeki httpHandlers elementi altında bildirilmesi yeterlidir. Örneğimizde geliştirdiğimiz ReportHandlerLibraray.dll isimli assembly' ı GAC'a atmadan önce Visual Studio 2005 ortamında aşağıdaki gibi Strong Name ile imzalamamız gerekmektedir. (İstenirse bu imzalama işlemi komut satırından sn.exe aracı ilede gerçekleştirilebilir.)

Bu işlemin ardından ReportHandlerLibrary' in gacutil.exe aracı yardımıyla yada sürükle bırak tekniği ile Windows\Assembly klasörüne atılarak GAC' a eklenmesi yeterlidir.

Not : Burada ReportHandlerLibrary projesinin Relase modda üretiminin yapılarak, Output Path olarak çıktının C:\Windows\Microsoft.NET\Framework\v2.0.50727\ klasörünü işaret edecek şekilde düzenlenmesi ve daha sonra buradan GAC' a atılmasıda tercih edilebilir.

Sonuç olarak Windows\Assembly klasörü altına ReportHandlerLibrary' si aşağıdaki ekran görüntüsünde olduğu gibi eklenmiş olacaktır.

Şimdi tek yapılması gereken bu assembly' ın root web.config içerisinde bildirilmesidir. Bunun için C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG klasöründe yer alan web.config dosyasına aşağıdaki gibi handler bildiriminin yapılması gerekmektedir.

Bu işlemlerin ardından IIS üzerinden rapx uzantılı dosyalara gelecek olan taleplerin AspNet_Isapi.dll tarafından ele alınacağınında belirtilmesi gerekir. Makalemizin başında OzelHandlerKullanimi isimli web sitesi için yaptığımız bu işlemin aynısı bu kez root web sitesi için benzer şekilde yapılarak gerçekleştirilmelidir. Sonuç itibariyle sunucu üzerinde Asp.Net 2.0 ile geliştirilen herhangibir web sitesi altında tasarlanacak olan tüm rapx uzantılı dosyalar RaporHandler sınıfı tarafından ele alınabilecektir.

Not : Kendi handler tipimizi root web.config dosyasında tanımladığımızda, web uygulaması içerisinde yer alan web.config dosyası içerisinde bırakılan handler tanımlamaları nedeni ile çalışma zamanı hataları(Run time exceptions) alınır. Bu nedenle makaledeki örnekte yer alan OzelHandlerKullanimi sitesindeki rapx dosyaları, root web.config' de yapılan handler bildirimleri sonrası çalışmayacaktır.

Sebep handler tanımlamasının hem root web.config hemde site içerisindeki web.config'de iki kez yapılmış olmasıdır. Bunu düzeltmek için OzelHandlerKullanimi sitesine ait web.config içerisinde yapılan handler bildirimlerini kaldırmak gerekmektedir.

Kendi HttpHandler tiplerimizi tasarlamak için ele aldığımız örnek senaryo çok daha fazla geliştirilebilir. Hatta bir web sunucusu üzerinde konuşlandırılacak olan rapx dosyalarının daha kolay hazırlanabilmesini sağlamak için görsel bir arabirimde geliştirilebilir. Sadece Rapx uzantılı dosyaları barındıracak olan bir Asp.Net uygulaması geliştirilerek tüm raporların tek bir merkezde toplanması sağlanabilir. Hatta bu işi dahada ileriye götürecek olursak, raporların yetki tabanlı olacak şekilde ele alınabilmesi için gerekli hazırlıklarda yapılabilir. Bu noktada, tasarlanan bu sistemin Reporting Service alt yapısına ne kadar benzediğini tartışmakta yarar vardır. Sistemin herhangibir veritabanına destek verecek şekilde ele alınması ise çok daha etkili bir raporlama arayüzü oluşturulmasına ön ayak olacaktır.

Bu makalemizde daha önceden ele aldığımız ancak geçerli bir ihtiyaç veya senaryo üzerine oturtamadığımız HttpHandler kavramını bir örnek üzerinde adım adım incelemeye çalıştık. Sizlerde farklı senaryoları göz önüne alarak ve makalede yer alan örneği dahada geliştirirek son derece etkili sonuçlara varabilirsiniz. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Tags:  
Categories:   Asp.Net 2.0
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Daha Etkili Profil Yönetimi

Çarşamba, 17 Ekim 2007 05:12 by bsenyurt

Uzun süre önce Asp.Net 2.0 ile geliştirilen web uygulamalarında Profile API' sinin nasıl kullanıldığını kısa bir makale üzerinden incelemeye çalışmıştık. Geçtiğimiz günlerde Asp.Net 2.0 ile ilgili bilgilerimi tazelerken profil yönetiminin daha etkin bir şekilde nasıl kullanılabileceğine dair pek çok örnek ile karşılaştım. İşte bu makalemizde temel olarak profil yönetiminin daha etkin hale getirilmeye çalışması için uğraşıyor olacağız. İnceleyeceğimiz temel konu başlıklarını aşağıdaki gibi sıralayabiliriz.

  • ProfileBase tipinden türetmek(Inherit).
  • Profil bilgilerini kod üzerinden yönetebilmek(ProfileManager).
  • İsimsiz(Anonymous) kullanıcılar için profil bilgilerini kullanabilmek.

Başlamadan önce profil kavramını kısaca tanımlamakta yarar olduğu kanısındayım. Bir web uygulamasına bağlanan kullanıcıların her biri için ortak tanımlanıp değerleri farklı olabilecek özellikler topluluğu profil bilgisini oluşturmaktadır. Bu anlamda özellikle, bir doğrulama(authentication) ve yetkilendirme(authorization) sistemine sahip olan web uygulamalarında her kullanıcı için değerleri farklı olabilecek özelliklerin tutulması ve kullanılması mümkün olabilmektedir. Bu tip bir sistemin özellikle Asp.Net 1.1 ile geliştirilmesi ekstra kodlamayı gerektirirken Asp.Net 2.0 üzerinde yer alan Profile API sayesinde son derece kolaylaşmıştır. Gelelim Profile API yeteneklerini daha etkili bir şekilde nasıl ele alabileceğimize.

ProfileBase Tipinden Türetmek(Inherit);

Normal şartlarda bir web uygulaması içerisinde profil bilgilerini kullanabilmek için web.config dosyası içerisinde profile elementinin ele alınması gerekmektedir. Nitekim bir web uygulamasında kullanılan profil bilgilerinin, başka web uygulamasında(web uygulamalarında) ele alınmasının istendiği vakkalarda mevcuttur. Bu tip bir durumda çözüm olarak, ProfileBase tipinden türetme yapılaraktan birden fazla web uygulamasında ele alınabilecek bir profil sınıfı geliştirmek mümkündür. ProfileBase sınıfının temel üyeleri aşağıdaki sınıf diagramında(Class Diagram) görüldüğü gibidir.

PorfileBase tipi sınıf diagramından(class diagram) da görüldüğü gibi SettingBase isimli abstract sınıftan(class) türemeketedir. ProfileBase tipine ait üyelerden bazılarının görevleri aşağıdaki tabloda belirtildiği gibidir.

Metodlar(Methods) Açıklama
Create Bu metod ile bir kullanıcı için profil nesne örneği oluşturulur. Özel profil tiplerinin yazılmasında veya Asp.Net ortamı dışındaki çevrelerde profil yönetimi söz konusu olduğunda ele alınmaktadır. Metod geriye ProfileBase tipinin taşıyabileceği referansları döndürür. İki farklı versiyonu vardır. Her iki versiyonda ilk parametre olarak kullanıcı adını alır. İkinci parametre bool bir değerdir ve kullanıcının isimsiz(anonymous) yada doğrulanmış(authenticated) olup olmadığını belirtir. Metodun dönüş değerinin true olması kullanıcının doğrulandığı anlamına gelmektedir.
Save Profil bilgilerini kaydetmek amacıyla kullanılır. Herhangibir parametre almaz ve geriye değer döndürmez(void). Var olan bir ProfileBase nesne örneği üzerinden çağrılabildiği için ilgili tipe ait özelliklerde yapılan değişikliklerin kaydedilmesini sağlar. Kaydetme işlemi veri kaynağına(data source) doğru yapılmaktadır. Bu işlem sırasında IsDirty özelliği true değerini alır. İşlem tamamlandıktan sonra ise false değerini alır.
GetPropertyValue Parametre olarak verilen özelliğin değerini object tipinden döndürür.
SetPropertyValue İki parametre alan bu metodun ilk parametresi değeri verilecek özellik adını, ikinci parametresi ise object tipinden ilgili değeri almaktadır. Bu metod yardımıyla profil içerisindeki bir özelliğe değer atanabilmesi sağlanabilmektedir.
GetProfileGroup Profil içerisinde yer alan özellikler istenirse grup halinde ayrılabilirler. Bunun için profile elementi içerisinde yer alan properties elementlerinde, group alt elementi kullanılmaktadır. Böyle bir durumda gruplanan özelliklerin listesini elde etmek için GetProfileGroup metodu kullanılabilir. Bu metod geriye ProfileGroupBase tipinden bir referans döndürmektedir. Bu referansın üzerinden hareket ederek grup içerisindeki özelliklere ve değerlerine erişmek mümkün olmaktadır. Aşağıdaki sınıf diagramı görüntüsünde ProfileGruopBase sınıfının üyeleri görülmektedir.

Özellikler(Properties) Açıklama
Item Profil içerisinde tanımlanmış olan özellik adlarını parametre olarak alabilen indeksleyici sayesinde ilgili özelliğin değerinin verilmesi(set) veya elde edilmesi(get) mümkündür. Özellik adı string tipinden verilmekte olup indeksleyicinin dönüş değeri object tipindendir.
Properties Static olarak tanımlanmış olan bu özellik sayesinde profil özelliklerinin bir listesinin SettingsPropertyCollection koleksiyon tipinden elde edilmesi mümkündür. Bu koleksiyonun her bir elemanı SettingsProperty sınıfı tipindendir. Bu tipin üyeleri ise aşağıdaki sınıf diagramında görüldüğü gibidir. Dikkat edilecek olursa bu üyelerden yola çıkarak profilin özelliği hakkında detaylı bilgilere ulaşmak veya yönetmek mümkündür.

UserName Profilin sahibi olan kullanıcı adını verir. Eğer isimsiz bir kullanıcı girişi söz konusu ise identifier değerini döndürecektir.
IsDirty Profil özelliklerinden herhangibiri değiştiriliyorken true değerini döndürür. Aksi durumda false değerini döndürmektedir.
IsAnonymous Eğer kullanıcı isimsiz(anonymous) ise true değerini döndürür. Aksi durumda false' dur.

Şimdi örnek bir senaryo üzerinden hareket ederek konuyu biraz daha iyi kavramaya çalışalım. Öncelikli olarak hedefimiz birden fazla web uygulamasının kullanabileceği bir Profile tipi geliştirmek olduğundan bir sınıf kütüphanesi(class library) geliştirerek işe başlanabilir. Çok doğal olarak bu sınıf kütüphanesi içerisinde ProfileBase tipi kullanılacağından ve yeri geldiğinde güncel HTTP içeriğine(HttpContext) erişilmesi gerektiğinden System.Web.dll assembly' ının ilgili sınıf kütüphanesine referans edilmesi gerekmektedir.

Özel olarak hazırlanacak sınıfın sağlaması gereken bazı özellikler vardır. İlk olarak bu sınıfın en azından XML serileştirilebilir(XML Serializable) olması gerekmektedir. Çok doğal olarak bu sınıf içerisinde kullanılacak özelliklerin veri tipleride(data types) serileştirilebilir olmalıdır. (Kendi tiplerimizden özellik türleri yazmadığımızda çoğunluklu ilkel tipleri(primitive types) kullanırız. Bu tiplerin çoğu zaten serileştirilebilir olduğundan sorun çıkma olasılığı azalmaktadır. Ancak kendi tiplerimizi ele aldığımızda serileştirilebilir olmalarına dikkat etmek gerekmektedir.) İkinci olarak özel tip içerisinde, web uygulamalarındaki kullanıcılar için gerekli profil bilgisini oluşturacak özellikler(property) ayrı ayrı tanımlanmalıdır. Bu özelliklerin kullanımı sırasında base anahtar kelimesi ile üst sınıfa aktarma yapılmasına dikkat edilmelidir. Üçünü olarak elbetteki özel sınıfın ProfileBase tipinden türetilmiş olması gerekmektedir. Bu türetmenin doğal sonucu olarak ProfileBase sınıfı içerisinde tanımlanmış bazı üyelerin ezilebileceği(override) ortadadır.

NOT : Hatırlanacağı üzere üst sınıfta(base class) sanal(virtual) olarak tanımlanış olan üyelerin, türeyen sınıflarda(derived class) ezilme(override) zorunluluğu yoktur. Eğer bir zorunluluk getirilmesi isteniyorsa abstract üyelerin yer alabildiği abstract sınıflar veya arayüzler(interface) kullanılmalıdır.

Aşağıdaki şekilde türeyen sınıf(derived class) içerisinde ezilebilecek üyeler gösterilmektedir. Doğal olarak herkes bir Object olduğundan, object sınıfından gelen bazı virtual üyelerde bu listede yer almaktadır.

Örnek olarak MyProfile sınıfı aşağıdaki gibi tasarlanabilir. Sınıf içerisinde kontak bilgilerini saklamak amacıyla Contact isimli bir sınıf daha kullanılmaktadır. Bu sınıfa ait nesne örneklerini kullanan özellikler MyProfile sınıfı içerisinde ele alınarak, kullanıcı tanımlı tiplerin durumuda rahatlıkla incelenebilir.

Contact Sınıfı;

public class Contact
{
    private string _ad;
    private string _soyad;
    private string _email;

    public string Email
    {
        get { return _email; }
        set { _email = value; }
    }

    public string Soyad
    {
        get { return _soyad; }
        set { _soyad = value; }
    }

    public string Ad
    {
        get { return _ad; }
        set { _ad = value; }
    }
    public Contact(string ad, string soyad, string mail)
    {
        Ad = ad;
        Soyad = soyad;
        Email = mail;
    }
    // XML Serileştirme kuralı olarak parametresiz bir yapıcı metod(constructor) olması gerekmektedir.
    public Contact()
    {
    }
}

Burada dikkat edilmesi gereken noktalardan biriside Contact sınıfının varsayılan yapıcı metodunun(Default Constructor) yazılmış olmasıdır. Profil bilgilerinde kendi tiplerimizi kullandığımız durumlarda varsayılan olarak XML serileştirme gerçekleştirilmektedir. Dolayısıyla yine varsayılan olarak AspNetDb veritabanındaki aspnet_Profile tablosuna yazılacak olan özellik değerlerinin XML formatında serileştirilebilir olması gerekmektedir. Bu sebepten ilgili tipin varsayılan yapıcı metoda sahip olması gerekir ki bu XML serileştirmenin kurallarından birisidir. Eğer varsayılan yapıcı metodu yazmassak çalışma zamanında profil bilgisini kaydederken aşağıdaki ekran görüntüsünde yer alan istisnayı(exception) alırız.

 

MyProfile Sınıfı;

public class MyProfile:ProfileBase
{
    public int SonStokDurumu
    {
        get { return Convert.ToInt32(base["SonStokDurumu"]); }
        set { base["SonStokDurumu"] = value; }
    }   
    public DateTime SonGirisZamani
    {
        get { return Convert.ToDateTime(base["SonGirisZamani"]); }
        set { base["SonGirisZamani"] = value; }
    }
    public Contact Contact2
    {
        get { return (Contact)base["Contact2"]; }
        set { base["Contact2"] = value; }
    }
    public Contact Contact1
    {
        get { return (Contact)base["Contact1"]; }
        set { base["Contact1"] = value; }
    }
    public string Bayi
    {
        get { return base["Bayi"].ToString(); }
        set { base["Bayi"] = value; }
    }
}

Dikkat edilecek olursa sınıf içerisinde tasarlanan özelliklere ait get ve set bloklarında base anahtar kelimesi ile ProfileBase sınıfına çıkılmakta ve indeksleyici operatöründen yararlanılarak ilgili alanlara erişilmesi sağlanmaktadır. Elbetteki base ile ulaşılan referansın özellikleri object veri türü ile çalıştığından set blokları içerisinde uygun tür dönüşümlerinin bilinçli(explicit) olarak yapılması şarttır. MyProfile sınıfı içerisinde tanımlanacak özel alanlara(private fields) değer atanması durumunda veritabanına herhangibir şekilde bilgi yazılmayacaktır. Böyle bir işlem için Save metodunun bu sınıf içerisinde ezilemesi ve kodlanması gerekmektedir.

Şimdi MyProfile sınıfını test etmek amacıyla basit bir web uygulaması tasarlayalım. Bu web uygulamasında standart olarak AspNetDb veritabanı kullanılabilir. Form tabanlı doğrulama(Form Based Authentication) sisteminin yer aldığı web uygulamasında standart olarak Login.aspx sayfası kullanıcı girişi için kullanılırken, Default.aspx sayfası içerisinde örnek test kodları yer almaktadır. Web uygulamasına ait konfigurasyon dosyasının(Web.config) içeriği aşağıdaki gibi olmalıdır.

web.config;

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <compilation debug="true" />
        <authentication mode="Forms" />
        <authorization>
            <deny users="?"/>
        </authorization>
        <profile enabled="true" inherits="CustomProfileLibrary.MyProfile"/>
    </system.web>
</configuration>

profile elementi içerisinde inherits niteliğine(attribute) atanan değer ile Profile tipinin ne olduğu söylenir. Burada İsimAlanıAdı.TipAdı(NamespaceName.TypeName) notasyonu kullanılarak profile bilgilerinin yönetiminin MyProfile sınıfı tarafından yapılacağı belirtilmektedir. Form tabanlı doğrulama(Form Based Authentication) kullanıldığından authentication elementinin mode niteliğine Forms değeri atanmıştır. İsimsiz kullanıcıların(anonymous users) siteye giriş yapması istenmediğinden authorization elementi içerisinden söz konusu kullanıcılar ? karakteri ile deny edilmiştir. Gelelim uygulamanın örnek web sayfasına. Sayfanın tasarımı aşağıda görüldüğü gibidir.

Default.aspx sayfası;

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <asp:LoginName ID="LoginName1" runat="server" /><asp:LoginStatus ID="LoginStatus1" runat="server" />
                <br />
                <table>
                    <tr>
                        <td colspan="2" valign="top">Profil Özellikleri&nbsp;<asp:Button ID="btnGetir" runat="server" OnClick="btnGetir_Click" Text="Bilgileri Getir" /></td>
                    </tr>
                    <tr>
                        <td style="width: 164px" valign="top">Bayi</td>
                        <td style="width: 100px"><asp:TextBox ID="txtBayi" runat="server"></asp:TextBox></td>
                    </tr>
                    <tr>   
                        <td style="width: 164px" valign="top">Kontak 1</td>
                        <td style="width: 100px">
                        <table>
                            <tr>
                                <td style="width: 100px">Ad</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak1Ad" runat="server"></asp:TextBox></td>
                            </tr>
                            <tr>
                                <td style="width: 100px">Soyad</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak1Soyad" runat="server"></asp:TextBox></td>
                            </tr>
                            <tr>
                                <td style="width: 100px">Email</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak1Email" runat="server"></asp:TextBox></td>
                            </tr>
                        </table>
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 164px" valign="top">Kontak 2</td>
                        <td style="width: 100px">
                        <table>
                            <tr>
                               <td style="width: 100px">Ad</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak2Ad" runat="server"></asp:TextBox></td>
                            </tr>
                            <tr>
                                <td style="width: 100px">Soyad</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak2Soyad" runat="server"></asp:TextBox></td>
                            </tr>
                            <tr>
                                <td style="width: 100px">Email</td>
                                <td style="width: 100px"><asp:TextBox ID="txtKontak2Email" runat="server"></asp:TextBox></td>
                            </tr>
                        </table>
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 164px; height: 10px" valign="top">Son Giriş Zamanı</td>
                        <td style="width: 100px; height: 10px"><asp:Label ID="lblSonGirisZamani" runat="server" Text="Label"></asp:Label></td>
                    </tr>
                    <tr>
                        <td style="width: 164px; height: 26px" valign="top">Stok Durumu</td>
                        <td style="width: 100px; height: 26px"><asp:TextBox ID="txtStokDurumu" runat="server"></asp:TextBox></td>
                    </tr>
                    <tr>
                        <td colspan="2" valign="top"><asp:Button ID="btnKaydet" runat="server" OnClick="btnKaydet_Click" Text="Bilgileri Kaydet" /></td>
                    </tr>
                </table>
                <br />
                <br />
            </div>
        </form>
    </body>
</html>

Default.aspx.cs;

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using CustomProfileLibrary;

public partial class _Default : System.Web.UI.Page
{
    protected void btnGetir_Click(object sender, EventArgs e)
    {
        Getir();
    }

    protected void btnKaydet_Click(object sender, EventArgs e)
    {
        Kaydet();
    }

    private void Getir()
    {
        try
        {
            txtBayi.Text = Profile.Bayi;
            txtKontak1Ad.Text = Profile.Contact1.Ad;
            txtKontak1Email.Text = Profile.Contact1.Email;
            txtKontak1Soyad.Text = Profile.Contact1.Soyad;
            txtKontak2Ad.Text = Profile.Contact2.Ad;
            txtKontak2Email.Text = Profile.Contact2.Email;
            txtKontak2Soyad.Text = Profile.Contact2.Soyad;
            lblSonGirisZamani.Text = Profile.SonGirisZamani.ToString();
            txtStokDurumu.Text = Profile.SonStokDurumu.ToString();
        }
        catch (Exception excp)
        {
            Response.Write(excp.Message);
        }
    }

    private void Kaydet()
    {
        Profile.Bayi = txtBayi.Text;
        Contact kontak1 = new Contact(txtKontak1Ad.Text, txtKontak1Soyad.Text, txtKontak1Email.Text);
        Profile.Contact1 = kontak1;
        Contact kontak2 = new Contact(txtKontak2Ad.Text, txtKontak2Soyad.Text, txtKontak2Email.Text);
        Profile.Contact2 = kontak2;
        Profile.SonGirisZamani = DateTime.Now;
        int stok = 1;
        Int32.TryParse(txtStokDurumu.Text, out stok);
        Profile.SonStokDurumu = stok;
        Profile.Save();
    }
}

Sayfa basit olarak profil bilgilerinin getirilmesi veya kaydedilmesi için gereken kodları içermektedir. Dikkat edilirse Profile tipi kullanılmaktadır. Nitekim web.config dosyasındaki bildirim nedeni ile Profile özelliği üzerinden MyProfile içerisinde tanımlanmış olan özelliklere erişilebilmektedir. Aşağıdaki şekilde bu durum daha net bir şekilde görülebilir.

Çok doğal olarak daha önceden bir profil bilgisi oluşturulmaması durumuna karşılık bilgiler getirilirken Null Reference Exception alınma olasılığı vardır. Bu nedenle profil bilgileri ortama çekilirken bir try...catch bloğu kullanılması yararlı olabilir. Sayfa üzerinde test bilgilerinin kaydedilmesi sonrasında aspnet_profile tablosunun içeriğine bakılırsa profil bilgisinin başarılı bir şekilde kaydedildiği görülebilir. Dikkat edileceği üzere bilgiler XML serileştirmeye uygun olacak şekilde XML formatında PropertyValueString alanına yazılmıştır.

Çalışma zamanında(run-time) Bilgileri Getir başlıklı düğmeye basıldığında ise söz konusu verilerin ilgili kontrollere doldurulduğu görülecektir. Aşağıda bu duruma ait örnek bir ekran çıktısı yer almaktadır.

Profil Bilgilerini Kod Üzerinden Yönetebilmek;

Bazı durumlarda çalışma zamanında(run-time) uygulamada kullanılan profil bilgilerinin yönetilmesi istenebilir. Söz gelimi profilde kayıtlı bilgilerin gösterilmesi, belirli bir tarihten öncekilerin kaldırılması, bir kullanıcının profil bilgisinin silinmesi, aktif olmayan profillerin elde edilmesi vb... işlemler yapılabilir. Bu aslında basit olarak veritabanına ulaşmak ve ilgili tablonun alanlarına bakmaktan başka bir şey değildir. Ne varki Asp.Net Profile API içerisinde söz konusu yönetsel işlemlerin daha kolay yapılmasını sağlayan ProfileManager sınıfı mevcuttur. Bu sınıfın diagram görüntüsü aşağıdaki gibidir.

Dikkat edileceği üzere ProfileManager, static bir sınıftır(static class).

NOT : Static sınıf kavramı C# 2.0 ile birlikte gelmiştir. Static sınıflar sadece static üyeler(static members) içerebilir, türetme(Inheritance) amacıyla kullanılamaz veya örneklenemezler. Normal sınıflara göre daha hızlı ve performanslı çalıştıkları ortadadır. Bununla birlikte C# 3.0 ile birlikte gelen extension methods kavramında önemli bir yerede sahiptir.

Sınıfın önemli metodları ve yaptığı işler ise aşağıdaki tabloda görüldüğü gibidir.

Metodlar(Methods) Açıklama
GetAllProfiles İki versiyonu olan bu metod sayesinde bir uygulamadaki profil bilgilerinin tamamı ProfileInfoCollection tipi içerisinde olacak şekilde elde edilebilir. Her iki versiyonda ProfileAuthenticationOption enum sabiti tipinden bir parametre almaktadır. Bu parametrenin alabileceği değerler All, Anonymous, Authneticated' dır. Bir başka deyişle tüm kullanıcıların, sadece isimsiz kullanıcıların veya sadece doğrulanmış kullanıcıların profile bilgilerinin elde edilmesi sağlanabilir. Ayrıca bu metod ile sayfalamalara uygun olacak şekilde veri çekilmeside sağlanabilmektedir.
GetNumberOfProfiles Veritabanında kayıtlı olan profil sayısının elde edilmesini sağlayan bu metod yine ProfileAuthenticationOption enum sabiti tipinden bir parametre alır. Buna göre sadece isimsiz kullanıcıların(anonymous users), doğrulanmış kullanıcıların(authenticated users) yada tüm kullanıcıların(all users) kayıtlı olan profil sayıları elde edilebilir.
GetAllInactiveProfiles Profil sahibi kullanıcıların LastActivityDate özelliklerinin değerlerine göre parametre olarak verilen tarih ve öncesindeki tüm profillerin elde edilmesini sağlayan metoddur. İki versiyonu vardır ve her ikisinin ilk parametresi aynıdır. İlk parametrede ProfileAuthenticationOption değeri ikincisinde ise tarih bilgisi belirlenir.
GetNumberOfInactiveProfiles Profil sahibi kullanıcıların LastActivityDate özelliklerinin değerlerine göre parametre olarak verilen tarih ve öncesindeki tüm profillerin sayısının integer olarak elde edilmesini sağlayan metoddur.
FindProfilesByUserName Belirlenen kullanıcı adıyla eşleşen profil bilgilerinin ProfileInfoCollection tipinden döndürülmesini sağlamaktadır. İki farklı versiyonu vardır. Her ikiside ilk iki parametresinde sırasıyla ProfileAuthenticationOption ve kullanıcı adı bilgilerini almaktadır. Sayfalama yapılmasıda sağlanabilmektedir. Burada kullanıcı adı girilirken % karakteri kullanılarak Like benzeri bir sorgulama yapılabilmektedir.
FindInactiveProfilesByUserName Profil sahibi kullanıcıların LastActivityDate özelliklerinin değerlerine göre belirtilen tarih ve öncesinde olanların parametre olarak verilen kullanıcı adı ile eşleşenlerinin bulunmasını sağlayan metoddur. Burada kullanıcı adı girilirken % karakteri kullanılarak Like benzeri bir sorgulama yapılabilmektedir. Örneğin kullanıcı adı içerisinde ma geçenlerin elde edilip belirtilen bir tarhiten öncesine kadar aktivitesi olmayanlar listelenebilir.
DeleteProfile Parametre olarak  verilen kullanıcıya ait profile bilgisinin silinmesini sağlamaktadır. Bu metod silme işlemi başarılı ise true değerini döndürür.
DeleteProfiles Bu metodun iki farklı versiyonu vardır. Bunlardan birisi ProfileInfoCollection tipinden diğeri ise string dizisi tipinden parametre almaktadır. Dolayısıyla profile bilgilerinin silinmesi istenen kullanıcı tipleri iki farklı şekilde yüklenebilir. 
DeleteInactiveProfiles Profil sahibi kullanıcıların LastActivityDate özelliklerinin değerlerine göre belirtilen tarih ve öncesinde olanların silinmesini sağlayan metod ProfileAuthenticationOption enum sabiti tipinden değerde almaktadır.

Şimdi bu sınıfın kullanıldığı örnek bir web sayfasını projeye dahil edelim. Sayfanın tasarım zamanındaki(design-time) görüntüsü ve kodları aşağıdaki gibidir.

ProfilYoneticisi.aspx;

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ProfilYoneticisi.aspx.cs" Inherits="ProfilYoneticisi" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Profil Yönetim Ekranı</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Doğrulama Tipi :<asp:DropDownList ID="ddlDogrulamaTipi" runat="server"></asp:DropDownList>
                <asp:Button ID="btnTumProfiller" runat="server" OnClick="btnTumProfiller_Click" Text="Tüm Profilleri Getir" /><br />
                Kullanıcı Adı(Benzeri) :
                <asp:TextBox ID="txtKullaniciAdi" runat="server"></asp:TextBox>
                <asp:Button ID="btnIsmeGoreGetir" runat="server" OnClick="btnIsmeGoreGetir_Click" Text="İsme Göre Getir" /><br />
                Aktivite Tarihi :
                <br />
                <asp:Calendar ID="dtTarih" runat="server"></asp:Calendar>
                <br />
                <asp:Button ID="btnPasifleriGetir" runat="server" OnClick="btnPasifleriGetir_Click" Text="Pasifleri Getir" /><br />
                Bulunan :
                <asp:Label ID="lblKullaniciSayisi" runat="server" Text="Label"></asp:Label><br />
                <br />
                <asp:GridView ID="grdTumProfiller" runat="server" OnRowCommand="grdTumProfiller_RowCommand" OnRowDeleting="grdTumProfiller_RowDeleting">
                    <Columns>
                        <asp:CommandField ButtonType="Button" DeleteText="Sil" ShowCancelButton="False" ShowDeleteButton="True" />
                    </Columns>
                </asp:GridView>
            </div>
        </form>
    </body>
</html>

ProfilYoneticisi.aspx.cs;

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Profile;

public partial class ProfilYoneticisi : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            // ProfileAuthenticationOption enum sabitinin içeriğini Enum sınıfın GetNames metodu ile alarak DropDownList kontrolüne dolduruyoruz.
            ddlDogrulamaTipi.DataSource = Enum.GetNames(typeof(ProfileAuthenticationOption));
            ddlDogrulamaTipi.DataBind();
        }
    }

    // Bu metod var olan tüm profil bilgilerini getirmek üzere tasarlanmıştır
    private void TumProfilleriGetir()
    {
        // DropDownList kontrolünden seçilen doğrulama kriterinin enum sabitindeki karşılığı seçiliyor.
        ProfileAuthenticationOption dogrulamaKriteri=(ProfileAuthenticationOption)Enum.Parse(typeof(ProfileAuthenticationOption), ddlDogrulamaTipi.SelectedValue);
        // GetAllProfiles metodu ile doğrulama kriterine uygun olan profil bilgileri ProfileInfoCollection tipinden bir koleksiyona aktarılır.
        ProfileInfoCollection tumProfiller = ProfileManager.GetAllProfiles(dogrulamaKriteri);
        grdTumProfiller.DataSource = tumProfiller;
        grdTumProfiller.DataBind();
        // Söz konusu doğrulama kriterine uyan profillerin sayısı elde edilir.
        lblKullaniciSayisi.Text = ProfileManager.GetNumberOfProfiles(dogrulamaKriteri).ToString();
    }

    // GridView kontrolünde Delete işlemi gerçekleştirildiğinde çalışan olay metodudur.
    protected void grdTumProfiller_RowCommand(object sender, GridViewCommandEventArgs e)
    {
        if (e.CommandName == "Delete")
        {
            int rowIndex=Convert.ToInt32(e.CommandArgument);
            string userName=grdTumProfiller.Rows[rowIndex].Cells[1].Text;
            // DeleteProfile metodu ile seçilen satırdaki UserName bilgisine ait Profil silinir.
            ProfileManager.DeleteProfile(userName);
            TumProfilleriGetir();
        }
    }
    protected void btnTumProfiller_Click(object sender, EventArgs e)
    {
        TumProfilleriGetir();
    }

    // TextBox içerisine girilen kullanıcı adına benzer olanların Profil bilgilerini getirir.
    protected void btnIsmeGoreGetir_Click(object sender, EventArgs e)
    {
        ProfileAuthenticationOption dogrulamaKriteri = (ProfileAuthenticationOption)Enum.Parse(typeof(ProfileAuthenticationOption), ddlDogrulamaTipi.SelectedValue);
        // % kullanılması sayesinde içerisinde TextBox' taki bilgi geçen kullanıcı adlarını elde ederiz.
        ProfileInfoCollection profiller = ProfileManager.FindProfilesByUserName(dogrulamaKriteri, "%" + txtKullaniciAdi.Text + "%");
        grdTumProfiller.DataSource = profiller;
        grdTumProfiller.DataBind();
        lblKullaniciSayisi.Text = profiller.Count.ToString();
    }
    protected void grdTumProfiller_RowDeleting(object sender, GridViewDeleteEventArgs e)
    {
       
    }

    /* aspnet_Users tablosunda LastActivityDate değeri, seçilen tarihten daha önce olanların elde edilmesini sağlar. Bunun için GetAllInactiveProfiles metodunun ikinci parametresi kullanılır. */
    protected void btnPasifleriGetir_Click(object sender, EventArgs e)
    {
        ProfileAuthenticationOption dogrulamaKriteri = (ProfileAuthenticationOption)Enum.Parse(typeof(ProfileAuthenticationOption), ddlDogrulamaTipi.SelectedValue);
        grdTumProfiller.DataSource=ProfileManager.GetAllInactiveProfiles(dogrulamaKriteri, dtTarih.SelectedDate);
        grdTumProfiller.DataBind();
        lblKullaniciSayisi.Text=ProfileManager.GetNumberOfInactiveProfiles(dogrulamaKriteri, dtTarih.SelectedDate).ToString();
    }
}

Söz konusu örnekte profil yönetimi adına basit işlemler yapılmaktadır. Örnek olması açısında var olan tüm profillerin elde edilmesi, seçilen profillerden herhangibirinin silinmesi, içerisinde belirtilen ada benzer olan profillerin çekilmesi veya belirli bir tarihten öncesine kadar aktivitesi olmayan profillerin getirilmesi gibi işlemler yapılmaktadır. Örneğin tüm profillerin listelenmesi istendiğinde aşağıdakine benzer bir ekran görüntüsü ile karşılaşılır.

Kullanıcı adı benzer olanların listelenmesine örnek olarak aşağıdaki ekran görüntüsünde olduğu gibi içerisinde ma kelimesi geçenler listelenebilir.

Belirli bir süreden öncesine kadar pasif olan kullanıcıların profil listesini elde etmek istediğimizde ise aspnet_User tablosunda yer alan LastActivityDate değeri baz alınır. Bir başka deyişle aktivite durumuna göre hareket eden ProfileManager sınıfı metodları tarih parametresinin değerlerini, söz konusu tablodaki alan ile kıyaslayarak çalışmaktadır. Bu tabloya ait örnek bir ekran görüntüsü aşağıda görüldüğü gibidir.

Çalışma zamanında örneğin 16.09.2007 tarihi ve öncesindeki profil bilgileri elde edilmek istendiğinde aşağıdakine benzer bir ekran görüntüsü ile karşılaşırız.

İsimsiz(Anonymous) kullanıcılar için profil bilgilerini kullanabilmek;

Bazı durumlarda siteyi ziyaret eden isimsiz kullanıcılar(anonymous users) içinde profil bilgilerinin tutulması ve saklanması istenebilir. Profile API, isimsiz kullanıcılar için oldukça güçlü bir hizmet sağlamaktadır. Sistemin çalışması aslında son derece basittir. Gerekli konfigurasyon ayarlarının yapılmasının ardından siteye bağlanan isimsiz kullanıcılar için birer GUID üretilmektedir. Bu GUID(Global Unique IDentifier) değerleri bir çerez(cookie) yardımıyla istemcinin bilgisayarında saklanırlar. Böylece isimsiz kullanıcıların sunucu tarafında tespit edilebilmesi kolay bir şekilde sağlanmaktadır. Dikkat edilmesi gereken durumlardan birisi istemcilerin oturum(session) açışları arasında farklı tarayıcılar kullanabilecek olmasıdır. Bu durumu ilerleyen kısımlarda analiz etmeye çalışacağız. İsimsiz kullanıcılara destek sağlayabilmek için web.config dosyası içerisinde anonymousIdentification elementinin ilgili özelliklerinin değerlerinin atanması gerekmektedir. Bir önceki örnek ile karışmaması açısından bu kez yeni bir web uygulaması ile devam edelim. Bu seferki örnekte profil özelliklerini web.config dosyası içerisinde tanımlıyor olacağız. Bu amaçla web.config dosyasının içeriğini aşağıdaki gibi tasarladığımızı varsayalım.

Web.config;

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <compilation debug="true"/>
        <authentication mode="Forms"/>
        <authorization>
            <allow users="*"/>
        </authorization>
        <anonymousIdentification enabled="true" cookieSlidingExpiration="true" cookieTimeout="3"/>
        <profile enabled="true">
            <properties>
                <add name="SonGirisZamani" type="System.DateTime" allowAnonymous="true"/>
                <group name="Urun">
                    <add name="UrunAdi" type="System.String" allowAnonymous="true"/>
                    <add name="UrunFiyati" type="System.Double" allowAnonymous="true"/>
                </group>
            </properties>
        </profile>
    </system.web>
</configuration>

anonymousIdentification elementi içerisinde tanımlanabilecek pek çok nitelik(attribute) yer almaktadır. Bu niteliklerden bazıları yukarıdaki örnekte kullanılmıştır. cookieSlidingExpiration niteliğine true değeri atandığı için istemciler, timeout süresi içerisinde talepte bulundukça çerezlerin(cookies) istemci bilgisayarda kalma süresi uzamaya devam edecektir. (Bu tipik olarak Caching mimarisinde kullanılan Sliding Expiration çalışma sistemi ile aynıdır.) Bununla birlikte cookieTimeout niteliğine atanan 3 değeri ile çerezin istemci bilgisayarda 3 dakika süreyle saklanacağı belirtilmektedir. (Uygulamanın doğrulanmış kullanıcılar(authenticated users) ile birlikte isimsiz kullanıcılarada(anonymous users) hizmet vermesi için authorization elementi içerisinde bilinçli olaraktan tüm kullanıcılar(*) allow edilmiştir.)

İlk olarak isimsiz kullanıcılara ait profil bilgilerinin saklandığını ispat etmeye çalışalım. Bu amaçla default.aspx sayfasının içeriğini ve kodlarını aşağıdaki gibi tasarladığımızı düşünelim.

Default.aspx;

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <asp:LoginName ID="LoginName1" runat="server" />
                <asp:LoginStatus ID="LoginStatus1" runat="server" />
            <br />
            <br />
            Son Giriş Zamanı :<asp:Label ID="lblSonGirisZamani" runat="server" Text="Label"></asp:Label><br />
            <br />
            Urun Adı :<asp:TextBox ID="txtUrunAdi" runat="server"></asp:TextBox><br />
            <br />
            Urun Fiyatı :&nbsp;<asp:TextBox ID="txtUrunFiyati" runat="server"></asp:TextBox>
            <br />
            <br />
            <asp:Button ID="btnProfilGetir" runat="server" OnClick="btnProfilGetir_Click" Text="Profil Getir" />
            <asp:Button ID="btnProfilKaydet" runat="server" OnClick="btnProfilKaydet_Click" Text="Profil Kaydet" /></div>
        </form>
    </body>
</html>

Default.aspx.cs;

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class _Default : System.Web.UI.Page
{
    protected void btnProfilGetir_Click(object sender, EventArgs e)
    {
        try
        {
            lblSonGirisZamani.Text = Profile.SonGirisZamani.ToString();
            txtUrunAdi.Text = Profile.Urun.UrunAdi;
            txtUrunFiyati.Text = Profile.Urun.UrunFiyati.ToString("C2");
        }
        catch (Exception exp)
        {
            Response.Write(exp);
        }
    }
    protected void btnProfilKaydet_Click(object sender, EventArgs e)
    {
        Profile.SonGirisZamani = DateTime.Now;
        Profile.Urun.UrunAdi = txtUrunAdi.Text;
        double urunFiyati = 1;
        Double.TryParse(txtUrunFiyati.Text, out urunFiyati);
        Profile.Urun.UrunFiyati = urunFiyati;
        Profile.Save();
    }
}

Uygulamayı çalıştırdığımızda isimsiz bir kullanıcı ile default.aspx sayfasını açabildiğimizi görürüz. Bu aşamada örnek bir kaç veri girip Profil Kaydet başlıklı düğmeye bastığımızda ise bilgilerin başarılı bir şekilde kaydedildiğini görebiliriz. Örneğin aşağıdaki veriler girildiğinde veritabanındaki aspnet_profile tablosunada bir satır eklendiği görülecektir.

Dahası kullanıcı siteye 3 dakikalık süre zarfı içerisinde yeni bir talepte(request) bulunduğunda ve Profil Getir başlıklı düğmeye bastığında ilgili bilgileri elde edebiliecektir.

Çok doğal olaraktan timeout süresi sona erdikten sonra profil bilgileri getirilmek istenirse aşağıdaki gibi bir ekran görüntüsü alınacaktır.

Bir başka deyişle istemci bilgisayardaki çerez(cookie) silindiğinden sunucuda eşleşen bir GUID bulunamamaktadır. (GUID' in bulunamaması isimsiz kullanıcının aspnet_users tablosundan silindiği anlamına gelmemektedir) Buda istenen bilgilerin elde edilemeyeceği anlamına gelir. Elbetteki veriler aspnet_profile tablosunda durmaya devam edecektir. Bu noktada ProfileManager sınıfının ilgili metodlarından yararlanarak belirli periyodlarda söz konusu verilerin temizlenmesi sağlanabilir.

Gelelim kullanıcının aynı uygulamaya isimsiz olarak farklı tarayıcılar ile erişmesi durumuna. Bu amaçla örneği önce Internet Exlplorer ile açıp profil bilgilerini kaydediyor olacağız. Sonrasında ise 3 dakikalık zaman dilimi sona ermeden Firefox ile aynı sayfayı yeniden talep edeceğiz. Örnek olarak ürün adını Vazo, ürün fiyatını 4.5 olarak girdiğimizi düşünelim.

Dikkat edileceği üzere Internet Explorer kullanılıp kaydedilen profil bilgilerine 3 dakikalık süre zarfı içerisinde başka bir tarayıcı program olan Firefox Mozilla içerisinde erişilememiştir. Tersi durumda söz konusudur. Buna göre farklı tarayıcı pencereleri ile gelen taleplerde(request) sunucunun farklı bir GUID üreteceğini ve istemci bilgisayara çerez olarak yazacağını göz önüne almalıyız.

İsimsiz kullanıcılar(anonymous users) ile ilgili bir diğer durumda var olan doğrulanmış bir kullanıcı ile birleştirilmeleridir(Migration). Bu durumu daha kolay anlayabilmek için amazon.com sitesinin işleyiş şeklini göz önüne alabiliriz. Bu siteye giren kayıtlı bir kullanıcı login olmadan sepete ürünler ekleyebilmektedir. Diğer taraftan kullanıcı alışveriş safhasına geçip sayfaya Login olduğunda, isimsiz kullanıcı olarak sepete attığı bilgiler var olan kullanıcı hesabındaki sepet bilgilerine eklenebilmektedir. Asp.Net 2.0 mimarisinde bu tarz bir işlemi belirli bir ölçüde kontrollü olarak gerçekleştirebilmek için için Global.asax.cs dosyasında Profile_OnMigrateAnonymous olay metodunun yüklenmesi yeterlidir. Bu metodun aldığı ProfileMigrateEventArgs tipinden parametre sayesinde isimsiz kullanıcı(anonymous user) için üretilen GUID değerine erişilebilir ve ilgili değerlerin alınarak, login olan kullanıcıya aktarılması sağlanabilir. Yanlız bu noktada daha öncede login olup profil bilgisi kaydedilmiş bir kullanıcının bilgileri üzerine yazılmamaya çalışılmasına özen gösterilmelidir. Bu durumu analiz edebilmek için global.asax.cs dosyasına aşağıdaki kod parçasını eklediğimizi göz önüne alabiliriz.

<%@ Application Language="C#" %>

<script runat="server">

    void Profile_OnMigrateAnonymous(object sender, ProfileMigrateEventArgs e)
    {
        /* Önce isimsiz kullanıcının profile bilgisi elde ediliri. GetProfile metodunun dönüş değeri ProfileCommon tipinden olacağı için buna göre bir atama yapılır. */
        ProfileCommon isimsizProfil = Profile.GetProfile(e.AnonymousID);
        /* Login olan doğrulanmış kullanıcının halen aktiv olup olmadığı öğrenilir. Eğer aktiv ise profil bilgileri mevcut demektir ve üstüne yazılması istenmez. Bu nedenle LastActivityDate değerine bakılarak bir kontrol yapılması gerekmektedir. */
        if (Profile.LastActivityDate.Date == DateTime.MinValue.Date)
        {
            // İsimsiz kullanıcının profil özelliklerinin değerleri ile login olan doğrulanmış kullanıcının profil özellikleri eşleştirilir
            Profile.SonGirisZamani = isimsizProfil.SonGirisZamani;
            Profile.Urun.UrunAdi = isimsizProfil.Urun.UrunAdi;
            Profile.Urun.UrunFiyati = isimsizProfil.Urun.UrunFiyati;
            Profile.Save(); // Login olan kullanıcı için değiştirilen profil değerleri kaydedilir.
        }
        AnonymousIdentificationModule.ClearAnonymousIdentifier();// isimsiz kullanıcı için üretilen çerezin silinmesi sağlanırki bu olay tekrar tetiklenmesin
        ProfileManager.DeleteProfile(e.AnonymousID); //isimsiz kullanıcıya ait profil değerleri veritabanından silinir.
    }

</script>

Yukarıdaki örnek kod parçasında öncelikli olarak isimsiz kullanıcının profil bilgileri elde edilmektedir. Bu amaçla GetProfile metodu kullanılmıştır. Sonrasında ise, o an Login olmuş kullanıcının bir profil bilgisi olup olmadığı tespit edilir. Bu kontrolde LastActivityDate özelliğinden yararlanılır. Burada amaç isimsiz kullanıcının(anonymous users) profil bilgilerini, login olmuş kullanıcının var olan profil bilgileri üzerine yazmamaktır. (Burada global bir kontrol yapılıp istemciden bilgilerin üstüne yazmak isteyip istemeyeceği ele alınaraktanda ilerlenebilir. Bu tamamen uygulamanın veya sürecin ihtiyaçları doğrultusunda verilebilecek bir karardır.) Sonrasında login olan kullanıcının profile kaydettiği bir bilgi yoksa isimsiz kullanıcı için oluşturulan değerlerin ataması yapılır ve kaydetme işlemi(save) gerçekleştirilir. Son olarak olay metodunun tekrardan tetiklenmemesi amacıyla isimsiz kullanıcıya ait bilgi veritabanından silinir. Ayrıca istemcideki çerez bilgiside AnonymousIdentificationModule sınıfının static ClearAnonymousIdentifier metodu ile geçersiz kılınır. Sonuç olarak isimsiz kullanıcı giriş yapıp profil bilgisini kaydettikten sonra daha önceden profil bilgisi bulunmayan bir kullanıcı gibi Login olursa, var olan bilgileri doğrulanmış kullanıcınınkine aktarılacaktır.

Böylece geldik uzun bir makalemizin daha sonuna. Bu makalemizde profile yönetimini biraz daha güçlü ve esnek hale getirmek için neler yapabileceğimizi incelemeye çalıştık. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

ProfileManagement.rar (27,77 kb)

Tags:  
Categories:   Asp.Net 2.0
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Asp.Net Temelleri : Derinlemesine Download/Upload İşlemleri

Çarşamba, 15 Ağustos 2007 17:59 by bsenyurt

Tatile çıkan herkes, iyi ve dinlendirici geçen günlerin ardından tekrar hayatın akışına kapıldığında kısa süreliğinede olsa adaptasyon problemi yaşar. Tatildeyken hatırlayacağınız gibi hafif ve dinlendirici bir Asp.Net konusu ile ilgilenmeye çalışmıştık. Tatil dönüşündeki adaptasyon sürecinde de benzer nitelikte bir konuyu incelemenin uygun olacağı kanısındayım. Bu yazımızda Asp.Net uygulamalarında sıklıkla başvurduğumuz temel dosya giriş/çıkış (Input/Output -IO) işlemlerinden yararlanarak Download ve Upload işlemlerinin nasıl yapılabileceğini ele almaya çalışacağız.

Özellikle web tabanlı içerik yönetim sistemlerinde (Content Management System), kullanıcıların sunucu üzerinde dökümanlar ile etkin bir şekilde çalışabilmeleri sağlanmaktadır. Bu sistemlerde genel olarak kullanıcı kimliği veya rolüne göre istemci bilgisayarlara indirilebilen(Download). Hatta çoğu içerik yönetim sisteminde, istemciler herkesin okuyabileceği yada belirli kişilerin görebileceği şekilde sunucuya döküman aktarma (Upload) işlemleride yapabilirler. Söz gelimi bir yazılım şirketinin içerik yönetim sistemi göz önüne alındığında, yazılım departmanındaki geliştiricilerin hazırladıkları teknik dökümantasyonları Upload veya Download edebilecekleri bir ortam hazırlanabilir.

Hangi açıdan bakılırsa bakılsın, web tabanlı olarak yapılan bu işlemler için şirketler büyük ölçekli sistemler tasarlayıp geliştirmiştir. Fakat temel ilke ve yaklaşımlar benzerdir. Dosya indirme veya gönderme işlemleri, web tabanlı bir sistem göz önüne alındığında HTTP kurallarına bağlıdır. Dolayısıyla bu kuralların sadece uygulanma ve ele alınma biçimleri programlama ortamları arasında farklılıklar gösterebilir. İşte biz bu makalemizde, Asp.Net 2.0 tarafından olaya bakmaya çalışıyor olacağız. İlk olarak dosya indirme işlemlerini ele alacağız. Sonrasında ise Asp.Net 2.0 ile gelen FileUpload aracı yardımıyla sunucuya dosya gönderme(Upload) işlemlerinin nasıl yapılabileceğini inceleyeceğiz. Ek olarak, upload edilen bir dosyanın kaydedilmeden, sunucu belleği üzerinde canlandırılıp işlenmesinin nasıl gerçekleştirilebileceğine bakacağız. Son olarakta, Upload edilen dosyaların bir veritabanı tablosunda alan(Field) olarak saklanması için gereken adımları göreceğiz. Dilerseniz vakit kaybetmeden dosya indirme süreci ile işe başlayalım.

Dosya indirme (Download) işlemlerinde bilinen IO tiplerinden ve Response sınıfının ilgili metodlarından yararlanılır. Hatırlanacağı gibi herhangibir resim dosyasını bir web sayfası içerisinde göstermek için üretilen HTML içeriği ile oynamak gerektiğinden daha önceki makalemizde bahsetmiştik. Dosya indirme(Download) işlemindede içeriğin tipi(Content-Type), uzunluğu(Content-Length) gibi bilgiler önem kazanmaktadır. İlk örneğimizde, IIS üzerinde yayınlanan bir web projesindeki Dokumanlar isimli bir klasörde yer alan dosyaların indirilme işlemlerini gerçekleştirmeye çalışacağız. Bu amaçla web uygulamasına ait dokumanlar klasörü altına aşağıdaki şekildende görüldüğü üzere farklı formatlarda örnek dosyalar atılmasında fayda vardır.

Web uygulamamızın ilk amacı Dokumanlar klasöründeki dosyaların listelenmesini sağlamak olacak. Bu amaçla sayfada bir GridView kontrolü kullanılabilir. Hatta bu kontrolün içeriği FileInfo tipinden nesnelerden oluşan generic bir liste koleksiyonundan (List<FileInfo>) gelebilir. Böylece istemciler indirebilecekleri dosyalarıda görebilir. Download işleminin gerçekleştirilmesi için GridView kontrolünde bir Select Button' dan faydalanılabilir. İndirme işlemi sırasında indirilmek istenen dosyanın fiziki adresi, uzunluğu gibi bilgiler önemlidir. Bu bilgileri ve fazlasını FileInfo sınıfına ait bir nesne örneği yardımıyla elde edebiliriz. Uygulamamıza ait Default.aspx sayfasının içeriği aşağıdaki gibi olacaktır.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.IO" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        string[] dosyalar = Directory.GetFiles(Server.MapPath("Dokumanlar"));
        List<FileInfo> dosyaBilgileri = new List<FileInfo>();

        foreach (string dosya in dosyalar)
        {
            dosyaBilgileri.Add(new FileInfo(dosya));
        }
        grdDosyalar.DataSource = dosyaBilgileri;
        grdDosyalar.DataBind();
    }
}

protected void grdDosyalar_SelectedIndexChanged(object sender, EventArgs e)
{
    string dosyaAdi = Server.MapPath("dokumanlar") + "\\" + grdDosyalar.SelectedRow.Cells[0].Text;
    FileInfo dosya = new FileInfo(dosyaAdi);

    Response.Clear(); // Her ihtimale karşı Buffer' da kalmış herhangibir veri var ise bunu silmek için yapıyoruz.
    Response.AddHeader("Content-Disposition","attachment; filename=" + dosyaAdi); // Bu şekilde tarayıcı penceresinden hangi dosyanın indirileceği belirtilir. Eğer belirtilmesse bulunulan sayfanın kendisi indirilir. Okunaklı bir formattada olmaz.
    Response.AddHeader("Content-Length",dosya.Length.ToString()); // İndirilecek dosyanın uzunluğu bildirilir.
    Response.ContentType = "application/octet-stream"; // İçerik tipi belirtilir. Buna göre dosyalar binary formatta indirilirler.
    Response.WriteFile(dosyaAdi); // Dosya indirme işlemi başlar.
    Response.End(); // Süreç bu noktada sonlandırılır. Bir başka deyişle bu satırdan sonraki satırlar işletilmez hatta global.asax dosyasında eğer yazılmışsa Application_EndRequest metodu çağırılır.
}
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Dosya Indirme Islemleri</title>
    </head>
<body>
    <form id="form1" runat="server">
        <div>
            <h2>Dosyalar</h2>
            <asp:GridView ID="grdDosyalar" runat="server" AutoGenerateColumns="False" SelectedRowStyle-BackColor="Gold" OnSelectedIndexChanged="grdDosyalar_SelectedIndexChanged">
                <Columns>
                    <asp:BoundField DataField="Name" HeaderText="Dosya Adı" />
                    <asp:BoundField DataField="Length" HeaderText="Dosya Uzunluğu" />
                    <asp:BoundField DataField="Extension" HeaderText="Uzantısı" />
                    <asp:BoundField DataField="CreationTime" HeaderText="Oluşturulma Zamanı" DataFormatString="{0:dd.MMMM.yy}" HtmlEncode="False" />
                    <asp:BoundField DataField="LastWriteTime" HeaderText="Son Yazılma Zamanı" DataFormatString="{0:dd.MMMM.yy}" HtmlEncode="False" />
                    <asp:CommandField ButtonType="Button" SelectText="indir" ShowSelectButton="True" />
                </Columns>
            </asp:GridView>
        </div>
    </form>
</body>
</html>

Öncelikli olarak sayfamızda neler yaptığımıza kısaca bakalım. Default.aspx sayfası ilk yüklendiğinde (bunu sağlamak için IsPostBack özelliği ile kontrol yapılmıştır) Dokumanlar klasöründeki dosyaların adları elde edilmektedir. Bu işlem için Directory sınıfının GetFiles metodu kullanılmaktadır. Bir web uygulaması söz konusu olduğu için, sanal klasörün karşılık geldiği fiziki adresi bulmak adına Server.MapPath metodu ele alınmaktadır. GetFiles metodu parametre olarak belirtilen klasördeki dosya isimlerinin elde edilmesini sağlamaktır. Bu nedenle geriye string tipinden bir dizi döndürür. GridView kontrolü içerisinde, elde edilen bu dosyalara ait bazı temel bilgilerin gösterilmesi hedeflenmiştir. Örnekte dosyanın adı(Name), uzunluğu(Length), uzantısı(Extension), oluşturulma(CreationTime) ve son yazılma zamanı(LastWriteTime) bilgileri ele alınmaktadır. Elde edilen dosya adları aslında fiziki adresleride içermektedir. Bu nedenle ilgili dosya adları FileInfo tipinden örneklerin oluşturulmasında kullanılır. Bu nesne örnekleride generic koleksiyonda toplanır. Son olarak GridView kontrolüne veri kaynağı olarak generic liste koleksiyonu atanır. FileInfo ile gelen bilgilerden bazılarını GridView kontrolünde göstermek istediğimizden, AutoGenerateColumns özelliğine false değeri atanmış ve Columns elementi içerisinde ilgili alanlar açık bir şekilde yazılmıştır. BoundField elementlerine ait DataField niteliklerinin değerleri FileInfo ile gelen nesne örneklerindeki özellik adları olarak ayarlanmıştır.

İndirilmek istenen dosya için GridView kontrolüne bir adet CommandField elementi dahil edilmiştir. Burada seçme işlemi ele alınarak aslında küçük bir hile yapılmaktadır. GridView kontrolünde seçim düğmesine basıldıktan sonra devreye giren SelectedIndexChanged olayı içerisinde dosya indirme(Download) işlemi başlatılmaktadır. Teorik olarak Response sınıfının WriteFile metodu ile parametre olarak verilen dosya istemci bilgisayara indirilebilirmektedir. Ancak ön hazırlıklar yapılması gerekmektedir. Bu amaçla, indirilmek istenen dosya adı, GridView kontrolünde seçilen satıra ait ilk hücreden seçildikten sonra, Response sınıfının ilgili metodları ile ön hazırlıklar yapılır. İndirilecek dosya üretilen çıktının Header kısmında ele alınmaktadır. Benzer şekilde indirilecek dosyanın uzunluğuda Header kısmına eklenir. Daha sonra içerik tipi belirlenir. application/octet-stream değeri, dosyanın ikili(binary) formatta indirileceğini belirtmektedir. Bu işlemlerin arkasındanda Response sınıfının WriteFile ve End metodları sırasıyla çalıştırılır. End metodu, o anki sayfaya ait yaşam sürecinin kesilmesini sağlamaktadır. Bir başka deyişle Response.End çağrısından sonra herhangibir kod satırı var ise işletilmeyecektir. Hatta, global.asax dosyasında yer alan Application_EndRequest metoduda devreye girecektir. Bu durumu analiz etmeden önceği örneğimizi test edelim. Uygulama çalıştırıldığında aşağıdakine benzer bir ekran görüntüsü ile karşılaşılacaktır.

Burada pek çok ek özellik tasarlanabilir. Örneğin uzantıya göre içerik tipi değiştirilebilir. Hatta download işlemi yerine örnek bir dökümanın sayfaya çıktı olarak verilmesi sağlanabilir. PDF içeriklerinin tarayıcıda gösterilmesi buna örnek olarak verilebilir. Bunların dışında uygulamanın kullanıcıya göre yetkilendirilmesi ve sadece ele alabileceği dosyaları indirebilmesi sağlanabilir. Bu tamamen projenin ihtiyaçlarına ve geliştiricinin kullanıcılara sunmak istediklerine bağlı olarak gelişebilecek bir modeldir. indir başlıklık düğmelerden herhangibirine bastığımızda indirme işleminin aşağıdaki ekran görüntüsünde yer aldığı gibi başladığı görülür.

Şimdi gelelim Response.End metodunun etkisine. Bu durumu analiz etmek için, Response.End sonrasına aşağıdaki gibi örnek bir kod satırı ekleyelim.

protected void grdDosyalar_SelectedIndexChanged(object sender, EventArgs e)
{
    string dosyaAdi = Server.MapPath("dokumanlar") + "\\" + grdDosyalar.SelectedRow.Cells[0].Text;
    FileInfo dosya = new FileInfo(dosyaAdi);

    Response.Clear();
    Response.AddHeader("Content-Disposition","attachment; filename=" + dosyaAdi);
    Response.AddHeader("Content-Length",dosya.Length.ToString());
    Response.ContentType = "application/octet-stream";
    Response.WriteFile(dosyaAdi);
    Response.End();
    Response.Write("Dosya indirildi");
}

Hemen arkasından bilinen yaşam döngüsünü izlemek adına default.aspx sayfasını aşağıdaki gibi değiştirelim.

protected void Page_PreInit(object sender, EventArgs e)
{
    Debug.WriteLine("Page_PreInit metodu");
}
protected void Page_Init(object sender, EventArgs e)
{
    Debug.WriteLine("Page_Init metodu");
}
protected void Page_Load(object sender, EventArgs e)
{
    // Diğer kod satırları
    Debug.WriteLine("Page_Load metodu");
}
protected void Page_PreRender(object sender, EventArgs e)
{
    Debug.WriteLine("Page PreRender Metodu");
}
protected void Page_Unload(object sender, EventArgs e)
{
    Debug.WriteLine("Page Unload Metodu");
}

protected void grdDosyalar_SelectedIndexChanged(object sender, EventArgs e)
{
    Debug.WriteLine("Dosya indirme işlemi başlıyor");
    // Diğer kod satırları
    Response.End();
    Debug.WriteLine("Response.End metodu çağırıldı");
    Response.Write("Dosya indirildi");
}

Bu değişikliklere ek olarak projeye global.asax dosyası ekleyip içerisine Application_EndRequest metodunu aşağıdaki gibi dahil edelim.

void Application_EndRequest(object sender, EventArgs e)
{
    Debug.WriteLine("Application EndRequest Metodu Çağırıldı");
}

Şimdi uygulamayı Debug modda çalıştırıp output penceresindeki çıktıları izleyebiliriz. Bir dosya indirme işlemi gerçekleştirildikten sonra sayfanın yaşam döngüsü aşağıdaki gibi çalışacaktır.

Page_PreInit metodu
Page_Init metodu
Page_Load metodu
Dosya indirme işlemi başlıyor
   A first chance exception of type 'System.Threading.ThreadAbortException' occurred in mscorlib.dll
   An exception of type 'System.Threading.ThreadAbortException' occurred in mscorlib.dll but was not handled in user code

Page Unload Metodu
Application EndRequest Metodu Çağırıldı

Çok doğal olarak GridView kontrolü üzerindeki düğmeye basıldığında sayfanın sunucuya tekrar gitmesi ve işlenmesi söz konusudur. Bu nedenle süreç Page_PreInit ile başlamaktadır. Ancak dikkat edilecek olursa Response.End çağrısından sonraki satırlar devreye girmemiştir. Debug penceresine ve tarayıcıdaki çıktıya herhangibir kod yazılmamıştır. Dahası, bir exception(System.Threading.ThreadAbortException) fırlatılmış ve sayfa yaşam döngüsü Page_PreRender metodunu işletmeden doğrudan Page_Unload olayını işletmiş ve arkasından global.asax dosyasındaki Application_EndRequest devreye girmiştir. Elbetteki üretilen istisna(exception) Asp.Net çalışma ortamı tarafından görmezden gelinmektedir. Bu nedenle istemci herhangibir şekilde hata mesajı ile karşılaşmaz.

Gelelim makalemizin ikinci konusuna. İndirme işlemleri kadar Upload işlemleride önemlidir. Tabi burada istemcilerin her dosya tipini veya çeşidini sunucuya göndermesi doğru olmayabilir. Kapalı ağ(intranet) sistemlerinde bu söz konusu olabilir. Nitekim kimin handi dosyayı Upload ettiğinin belirlenmesi dışında, bu kişiye ulaşılmasıda kolaydır :). Ancak internet tabanlı daha geniş sistemlerde her ne kadar kullanıcılar tespit edilebilsede, kötü niyetli istemcilerin varlığı nedeniyle sistemin genelini tehlikeye atmamak adına tedbirler almak doğru bir yaklaşım olacaktır. Biz tabiki basit olarak Upload işlemlerini ele alacağız ve bahsettiğimiz güvenlik konularını şimdilik görmezden geleceğiz.

Upload işlemlerini kolaylaştırmak adına Asp.Net 2.0, FileUpload isimli bir kontrol getirmektedir. Bu kontrol basit olarak istemcinin Upload etmek istediği dosyayı seçebilmesini sağlamaktadır. Bu seçim işlemi ile birlikte, sunucuya gönderilmek istenen dosyaya ait bir takım bilgilerde FileUpload kontrolünce elde edilir. Örneğin içeriğin tipi kontrol edilerek sadece bazı dosyaların gönderilmesine izin verilebilir. İlk olarak web uygulamamıza aşağıdaki gibi Default2.aspx sayfasını ekleyelim.

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void btnGonder_Click(object sender, EventArgs e)
    {
        if (uplDosya.HasFile)
        {
            uplDosya.SaveAs(Server.MapPath("Dokumanlar") + "\\" + uplDosya.FileName);
        }
        else
            Response.Write("Upload edilecek dosya yok");
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Upload Islemleri</title>
    </head>
<body>
    <form id="form1" runat="server">
        <div>
            Dosyayı Seçin : <asp:FileUpload ID="uplDosya" runat="server" />
            <br />
            <asp:Button ID="bntGonder" runat="server" Text="Gönder" OnClick="btnGonder_Click" />
        </div>
    </form>
</body>
</html>

Öncelikli olarak neler yaptığımıza bir bakalım. FileUpload kontrolü ile dosya seçilmesi, gönderme işlemi için yeterli değildir. Sayfanın sunucuya doğru gönderilmesi gerekmektedir. Bu işi basit olarak bir Button kontrolü üstelenebilir. Button kontrolümüze ait Click olay metodunda ise öncelikli olarak seçili bir dosya olup olmadığı HasFile özelliği ile kontrol edilmektedir. Bu kontrol, dosya seçmeden gönderme işlemi yapıldığı takdirde oluşacak hataların önüne geçilmesini sağlamaktadır. Eğer seçili olan bir dosya var ise basit olarak FileUpload sınıfının SaveAs metodu çağırılır. SaveAs metoduna dosyanın yol adresinin fiziki olarak verilmesi gerekmektedir. Bu nedenle yine Server.MapPath ile Dokumanlar klasörünün fiziki adresi elde edilir. FileUpload kontrolünün FileName özelliği ile seçilen dosyanın adı yakalanmakta ve fiziki adresin sonuna eklenmektedir. Eğer kod Debug edilirse, dosya seçildikten sonra Upload işlemi için düğmeye basıldığında FileUpload kontrolü üzerinde, seçilen dosyaya ilişkin çeşitli ek bilgilere ulaşılabildiği görülür.

Söz gelimi dosyanın tipi hakkında fikir elde etmek için ContentType özelliğine bakılabilir. Buna göre belirli dosya tiplerinin indirilmesine izin verilmesi istenen durumlara karşı tedbirler alınabilir. Gönderilecek dosyanın boyutu ile ilgili olarak bir kısıtlama getirilmek isteniyorsa içerik uzunluğu ContentLength özelliği ile tedarik edilerek gerekli değişiklikler yapılabilir. Şimdi örneği deniyerek devam edelim. Basit olarak bir döküman dosyasını aşağıdaki gibi seçip Upload etmeyi deneyeceğiz.

Görüldüğü üzere Browse işleminden sonra otomatik olarak standart Choose File iletişim penceresi (Dialog Box) ile karşılaştık. Örnekte bir resim dosyası seçilmiştir. Seçim işleminden sonra Gönder düğmesine basılırsa dosyanın başarılı bir şekilde Dokumanlar klasörü altına yazıldığı görülecektir.

NOT : Upload işlemleri sırasında yetki problemi nedeni ile IIS altındaki herhangibir klasöre dosya yazma işlemi sırasında hata mesajı alınabilir. Bu durumda ASPNET kullanıcısına ilgili klasöre yazma hakkı verilmesi gerekebilir.

Upload işlemleri sırasında dikkat edilmesi gereken kritik bir sayı vardır. 4096 byte. Yani 4 megabyte. Boyutu bu değerin üzerindeki bir dosyayı Upload etmek istediğimizde Asp.Net ortamı bir hata üretir ve dosyanın sunucuya gönderilmesine izin vermez. Ne yazıkki hata üretimi kodların işletilmesinden önce gerçekleşir. Bu nedenle kullanıcıya anlamlı bir mesaj gösterilmeside pek mümkün olmamamaktadır. Eğer 4 megabyte üzerinde dosyaların upload edilebilmesini başka bir deyişle izin verilen limite kadar olan dosyaların gönderilebilmesini istiyorsak web.config dosyası içerisinde httpRuntime elementinin ayarlanması gerekmektedir. system.web boğumu içerisinde yer alan httpRuntime elementi sayesinde, http çalışma zamanına ait çeşitli ayarlamalar yapılabilmektedir. Bizim ihtiyacımız olan dosya büyüklüğü sınıfı ayarı için örneğimizde web.config dosyasını aşağıdaki gibi düzenlememiz yeterlidir.

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <httpRuntime maxRequestLength="51200"/>
        <compilation debug="true"/>
        <authentication mode="Windows"/>
    </system.web>
</configuration>

Burada yapılan ayarlamaya göre istemciler 50 megabyte' a kadar dosyaları sunucuya gönderebilecektir. Upload işlemlerini içerik yönetim sistemlerinde resim, döküman veya örnek uygulamaların, programların gönderilmesinde, grafik kütüphanesi tarzındaki sistemlerde çeşitli formatta resim veya akıcı görüntü formatlarının gönderilmesinde ve buna benzer durumlarda kullanmak yaygındır.

Gelelim makalemizin üçüncü konusuna. Yine istemciden sunucuya doğru bir dosya gönderme işlemi gerçekleştirmeyi hedeflediğimizi düşünelim. Ancak bu sefer XML tabanlı bir dosyayı gönderiyor olacağız. Bu dosyanın özelliği, bizim istediğimiz şekilde tasarlanmış olması dışında, sunucu tarafında anında işlenecek olmasıdır. Söz gelimi, XML dosyası içerisinde dinamik olarak sayfaya yüklenmesi istenen kontrollere ait bilgiler yer alabilir. Bu durumda Upload edilen XML dosyasının sunucu tarafında işlenerek bir çıktı üretilmesi gerekmektedir. Bir başka deyişle istemciden sunucuya gönderilen sayfayı, fiziki olarak yazmadan işlemek ve kullanmak istediğimizi göz önüne alıyoruz. Peki bu sistemi nasıl yazabiliriz? İlk olarak istemcinin göndereceği basit XML dökümanını hazırlayarak başlayalım. Örnek olarak aşağıdaki gibi bir içerik düşünülebilir.

<?xml version="1.0" encoding="utf-8"?>
<Kontroller>
    <Kontrol tip="MetinKutusu" id="metinKutusu1"/>
    <Kontrol tip="Dugme" metin="Gonder" id="gonder1"/>
    <Kontrol tip="Label" metin="Ad" id="ad1"/>
</Kontroller>

Olayı basit bir şekilde ele almak adına Xml içeriğini mümkün olduğu kadar basit düşünmeye çalıştık. Elbetteki çok daha karmaşık ve daha çok parametre sunan bir Xml dökümanı söz konusu olabilir. Şimdi bu dosyayı nasıl ele alacağımıza bakalım. Dikkat edilmesi gereken noktalardan birisi, Upload edilecek dökümanın XML formatında olması gerekliliğidir. Bunu sağlamak için, içerik tipine(ContentType) bakmak gerekecektir. Sonrasında ise Upload edilen dosyanın Framework içerisinde yer alan XML tipleri yardımıyla ele alınması yeterlidir. Sonuç olarak Default3.aspx dosyamızın içeriği aşağıdaki gibi olacaktır.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Xml" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void btnGonder_Click(object sender, EventArgs e)
    {
        if (uplKontroller.HasFile)
        {
            HttpPostedFile dosya = uplKontroller.PostedFile;
            if (dosya.ContentType == "text/xml")
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(dosya.InputStream);
                XmlNodeList kontroller=doc.GetElementsByTagName("Kontrol");
                foreach (XmlNode kontrol in kontroller)
                {
                    switch (kontrol.Attributes["tip"].Value)
                    {
                        case "Dugme":
                            Button btn = new Button();
                            btn.ID = kontrol.Attributes["id"].Value;
                            btn.Text = kontrol.Attributes["metin"].Value;
                            phlKontroller.Controls.Add(btn);
                            break;
                        case "MetinKutusu":
                            TextBox txt = new TextBox();
                            txt.ID = kontrol.Attributes["id"].Value;
                            phlKontroller.Controls.Add(txt);
                            break;
                        case "Label":
                            Label lbl = new Label();
                            lbl.ID = kontrol.Attributes["id"].Value;
                            lbl.Text = kontrol.Attributes["metin"].Value;
                            phlKontroller.Controls.Add(lbl);
                            break;
                    }
                }
            }
            else
                Response.Write("Dosya içeriği XML olmalıdır");
        }
        else
            Response.Write("Dosya bulunamadı");
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Upload Edilen Dosyayi O Anda Islemek</title>
    </head>
<body>
    <form id="form1" runat="server">
        <div>
            Xml Dosayasını Seçin : <asp:FileUpload ID="uplKontroller" runat="server" />
            <br />
            <asp:Button ID="btnGonder" Text="Gönder" runat="server" OnClick="btnGonder_Click" />
            <br />
            Yüklenen Kontroller:
            <asp:PlaceHolder ID="phlKontroller" runat="server" />
        </div>
    </form>
</body>
</html>

Dilerseniz kod içerisinde neler yaptığımıza kısaca bakalım. İlk olarak düğmeye basıldığında HasFile özelliği ile seçilen bir dosya olup olmadığını kontrol ediyoruz. Sonrasında ise FileUpload kontrolünün PostedFile özelliğinden yararlanıp seçilen dosyaya ait bazı temel bilgileri taşıyan HttpPostedFile tipine ait nesne örneğini elde ediyoruz. Bu nesne örneği üzerinden ContentType özelliğine geçerek içeriğin XML olup olmadığını text/xml eşleştirmesi ile kontrol ediyoruz. Sonrasında ise okunan dosya içeriğini XmlDocument nesnesine yüklüyoruz. XmlDocument nesne örneğine ait Load metodunun parametresi olarak bir Stream kullanılabildiğinden, HttpPostedFile nesne örneğinin InputStream özelliğinden yararlanıyoruz. Son olarak yüklenen XML dökümanı içerisinde dolaşarak ilgili nitelikleri okuyor ve dinamik olarak oluşturulan kontrolleri, sayfadaki PlaceHolder kontrolünün Controls koleksiyonuna ekliyoruz.

NOT : İşlemlerin daha sağlıklı olması açısından yüklenen XML içeriğinin belirli kurallara uygun olup olmadığı bir şema dosyası (XSD olabilir örneğin) yardımıyla kontrol edilebilir. Söz gelimi gelen XML dökümanının yapısı, element adları, nitelik adları veya tipleri bu şema yardımıyla denetlenip, kontrollerin belirli standartlara göre okunabilmesi sağlanmış olunur. Şema kontrolünün nasıl yapılabileceğine dair daha önceki bir makalemizden yararlanabilirsiniz.

Uygulamayı çalıştırdığımızda ve istemci tarafından Kontrollerim.xml dosyasını yüklediğimizde aşağıdaki ekran görüntüsünde olduğu gibi kontrollerin başarılı bir şekilde üretilip sayfaya yüklendiğini görebiliriz.

Sıra geldi makalemizin son konusuna. İçerik yönetimi adına, istemcinin sunucuya gönderdiği dosyaların veritabanı üzerindeki bir tabloda tutulması istenebilir. Eğer ilişkisel veritabanı sistemi (Relational Database Management System) söz konusu ise, dosyaların tabloda tutuluyor olması taşıma işlemlerini kolaylaştırabileceği gibi, içerik güvenliğininde de daha etkin bir seviyede yapılabilmesini sağlayacaktır. (Tabi tersine saklanacak dosya boyutlarına göre veritabanı daha hızlı şişecektir.) Bu tarz bir ihtiyacın çıkış noktası son derece basittir. Her zaman olduğu gibi bir FileUpload kontrolü ile istemciye dosya seçtirilmeli daha sonra ilgili içerik sunucu tarafında işlenerek veritabanındaki ilgili tabloya yazdırılmalıdır. Burada çalıştıralacak komut dışında tablodaki alan tipide önemlidir. Text tabanlı bir içerik gönderilecek olsada, tablo tarafında image veya VarBinary tipinden alanlar tutmak Unicode tutarlılığı açısından daha doğru bir yaklaşım olacaktır. Dilerseniz bir örnek ile bu durumu incelemeye çalışalım. İlk olarak içeriği saklayacağımız basit bir tablo oluşturalım. Bunun için SQL Server 2005 üzerinde aşağıdaki gibi bir tablo göz önüne alınabilir.

Dosyalar isimli tabloda, sunucuya gönderilen dosya içeriğini saklamak için image tipinden bir alan kullanılmaktadır. Bunlara ek olarak dosyanın eklenme tarihi,içeriğin tipi ve dosya adı bilgileride yer almaktadır. Söz konusu alanlar dışında, web sitesinde kullanılan doğrulama(Authentication) sistemine göre, Upload işlemini yapan kullanıcının bilgilerinin saklanması hatta varsa Membership gibi kullanıcı tablo sistemleri ile ilişkilendirilmeside mümkün olabilir. Gelelim yükleme işlemini tabloya yazacak kodlarımıza.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void btnGonder_Click(object sender, EventArgs e)
    {
        if (uplDosya.HasFile)
        {
            string conStr = ConfigurationManager.ConnectionStrings["ConStr"].ConnectionString;
            using (SqlConnection conn = new SqlConnection(conStr))
            {
                SqlCommand cmd = new SqlCommand("Insert into Dosyalar (EkleNmeTarihi,DosyaIcerigi,DosyaAdi,IcerikTipi) Values    (@EklenmeTarihi,@DosyaIcerigi,@DosyaAdi,@IcerikTipi)", conn);
                cmd.Parameters.AddWithValue("@EklenmeTarihi", DateTime.Now);
                cmd.Parameters.AddWithValue("@DosyaIcerigi", uplDosya.FileBytes);
                cmd.Parameters.AddWithValue("@DosyaAdi", uplDosya.FileName);
                cmd.Parameters.AddWithValue("@IcerikTipi", uplDosya.PostedFile.ContentType);
                conn.Open();
                int sonuc=cmd.ExecuteNonQuery();
                Response.Write(sonuc + " dosya aktarıldı");
            }
        }
        else
            Response.Write("Dosya seçmelisiniz");
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Upload Edilen Dosyayı Veritabanına Yazma</title>
    </head>
<body>
    <form id="form1" runat="server">
        <div>
            Dosyayı Seçin : <asp:FileUpload ID="uplDosya" runat="server" />
            <br />
            <asp:Button ID="btnGonder" Text="Gönder" runat="server" OnClick="btnGonder_Click" />
        </div>
    </form>
</body>
</html>

Yine kodları kısaca incelemekte fayda var. Her zamanki gibi, istemcinin bir dosya seçtiğinden emin olduktan sonra gerekli işlemleri yapıyoruz. Burada önemli olan nokta çalıştırılacak SQL cümlesinde kullanılan parametrelerin değerlerinin nasıl verildiği. Dikkat edilecek olursa, image tipindeki alanın içeriğini verirken FileUpload kontrolünün FileBytes özelliğinden yararlanıyoruz. Burada dikkat edilmesi gereken noktalardan biriside çok büyük boyutlu dosyaların aktarılması sırasında yaşanabilecek timeout sorunudur. Böyle bir durumda kalındığı takdirde bağlantı için timeout sürelerinin arttırılması yoluna gidilebilir yada dosyanın parçalanarak sunucuya gönderilmesi ve burada o şekilde ele alınması sağlanabilir. Uygulama çeşitli tipteki dosyalar ile test edildiğinde başarılı bir şekilde çalıştığı görülecektir. Aşağıdaki ekran görüntüsünde bir kaç dosya tipinin upload edilmesi sonrasındaki durum vurgulanmaktadır.

Her dosya eklenme işleminden sonrada tarayıcı penceresindeki görüntü aşağıdaki gibi olacaktır.

Elbette Upload edilen içeriklerin, istemciler tarafından indirilmeside gerekecektir. Bu durumda tablo alanındaki dosya içeriğinin stream olarak yazdırılması gerekir. Tabi bunun için Response sınıfının WriteFile metodu yerine BinaryWrite metodunu tercih edeceğiz. (Alternatif bir yaklaşım olarak, tablodan okunan dosya içeriğinin bir temp dosyaya atılması ve oradanda WriteFile metodu ile yazdırılmasıda düşünülebilir) Nitekim dosya içerikleri tabloda binary olarak tutulmaktadır. Öyleyse son olarak bu işlemide nasıl yapabileceğimizi inceleyeceğimiz bir örnek sayfa daha ekleyelim. Default5.aspx sayfamızın içeriği aşağıdaki gibi olacaktır.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConStr"].ConnectionString))
        {
            SqlCommand cmd=new SqlCommand("SELECT DosyaAdi,DosyaIcerigi,IcerikTipi FROM Dosyalar Where Id=@Id",conn);
            cmd.Parameters.AddWithValue("@Id", GridView1.SelectedValue);
            conn.Open();
            SqlDataReader reader=cmd.ExecuteReader();
            if (reader.Read())
            {
                Response.Clear();
                Response.AddHeader("Content-Disposition", "attachment; filename=" + reader.GetString(0));
                byte[] dosyaIcerigi = reader.GetSqlBinary(1).Value;
                Response.AddHeader("Content-Length", dosyaIcerigi.Length.ToString());
                Response.ContentType = "application/octet-stream";
                Response.BinaryWrite(dosyaIcerigi);
            }
            reader.Close();
        }
        Response.End();
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
<body>
    <form id="form1" runat="server">
        <div>
            Dosyalar<br />
            <br />
            <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataSourceID="dsDosyalar" OnSelectedIndexChanged="GridView1_SelectedIndexChanged" DataKeyNames="Id">
                <Columns>
                    <asp:BoundField DataField="DosyaAdi" HeaderText="DosyaAdi" SortExpression="DosyaAdi" />
                    <asp:BoundField DataField="EklenmeTarihi" HeaderText="EklenmeTarihi" SortExpression="EklenmeTarihi" />
                    <asp:BoundField DataField="IcerikTipi" HeaderText="IcerikTipi" SortExpression="IcerikTipi" />
                    <asp:CommandField ButtonType="Button" SelectText="indir" ShowSelectButton="True" />
                </Columns>
            </asp:GridView>
            <asp:SqlDataSource ID="dsDosyalar" runat="server" ConnectionString="<%$ ConnectionStrings:ConStr %>" SelectCommand="SELECT Id,DosyaAdi, EklenmeTarihi, IcerikTipi FROM Dosyalar">
            </asp:SqlDataSource>
        </div>
    </form>
</body>
</html>

Tasarladığımız sayfayı dilerseniz inceleyelim. Basit olarak Dosyalar tablosunun içeriğini göstermek amacıyla SqlDataSource ve GridView kontrollerinden faydalanıyoruz. Yine ilk örneğimizde olduğu gibi indirme işlemini başlatmak adına küçük hilemizi yaptık ve bir Select düğmesi kullandık. Burada dosya içeriği tabloda alan olarak tutulduğu için, SqlCommand ile verinin çekilmesi gerekiyor. Bunu kolaylaştırmak adına GridView içerisinde seçilen satıra ait Id alanının değerini almalıyız. Bu amaçla GridView kontrolünün DataKeyNames özelliğine Id değerini verdik. Bu değeri SelectedIndexChanged metodu içerisinden alarak sorgu cümlesinde parametre olarak kullanıyor ve böylece indirilmek istenen dosyaya ait içeriğin olduğu tablo satırını çekebiliyoruz. Bizim için önemli olan nokta, binary içeriği okumak için SqlDataReader sınıfının GetSqlBinary metodunu kullanıyor olmamız. Bu metod ile dönen tipin Value özelliğinden faydalanıp elde edilen byte[] dizisini Response sınıfının BinaryWrite metoduna parametre olarak verdiğimizde yazma işlemi gerçekleştirilmiş oluyor. Sonuç olarak çalışma zamanında istediğimiz sonuca ulaşıyor ve dosya indirme işlemlerini gerçekleştirebiliyoruz.

Bu makalemizde Asp.Net uygulamalarında Download ve Upload işlemlerini detayları ile incelemeye çalıştık. İlk olarak bir dosyanın indirilme işleminin nasıl yapılabileceğine baktık. Sonrasında ise basit olarak bir Upload işlemi ile sunucuya dosya gönderme olayını ele aldık. Upload işleminin farklı yönlerini ele almak adına, anında sunucu tarafında işleme ve tabloya satır olarak ekleme işlemlerini inceledikten sonra, tablodaki bir binary içeriği indirme sürecine göz attık. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Örnek Uygulama için Tıklayın (Dosyanın çok yer tutmaması açısından mdf dosyası çıkartılmıştır)

Asp.Net 2.0 URL Rewriting Hakkında Gerçekler

Salı, 7 Ağustos 2007 18:11 by bsenyurt

Çok kısa süreliğinede olsa tatilde olduğum şu günlerde yazılım dünyasından kopmak hiç içimden gelmedi. Bu nedenle dinlendiğim zamanlardan arta kalan sürelerde azda olsa bir şeyler karalamak istedim. Sonuç olarak daha hafif ve tatil moduna uygun olacak bir yazı ile yeniden beraberiz. Bu makalemizde Asp.Net 2.0 ile geliştirilen web uygulamalarında, URL eşleştirmelerinin (Url Mapping) nasıl düzenlenebileceğini, bir başka deyişle nasıl özelleştirilebileceğini incelemeye çalışacağız. Son kullanıcılar web ortamında, kendi tarayıcı(browser) uygulamalarında yer alan adres satırlarında zaman zaman karışık ve uzun URL bilgileri ile karşılaşırlar. Genellikle sorgu katarlarının(QueryString) kullanıldığı ve bunların sayılarının çok olduğu durumlarda adres satırlarını okumak gerçekten güçleşebilir. Söz gelimi aşağıdaki URL bilgisini göz önüne alalım.

http://www.azonsitesi.com/urunler.aspx?urunKategori=1&urunAdi=Bilgisayar%20Kitaplari&Sinifi=Ingilizce&BasimYili=2006

Bunun yerine aşağıdaki gibi bir URL bilgisi çok daha kullanışlı ve son kullanıcı açısından okunaklı olabilir.

http://www.azonsitesi.com/Ingilizce/BilgisayarKitaplari/2006Basimi/Goster.aspx

Örnekler çoğaltılabilir. Söz gelimi blog sitelerinde, adres satırlarında okunan içeriğe ait bilgilerin QueryString şeklinde durması yerine örneğin http://buraginblogu/Agustos/7/2007/UrlEslestirme/Oku.aspx gibi bir formata sahip olması son kullanıcı açısından çok daha cezbedicidir.

Bu ve benzer durumlarda, adres satırındaki bilginin daha kolay anlaşılabileceği hale getirilmesi son kullanıcı(End User) için önemli bir hizmettir. Peki bu tarz bir ihtiyaç nasıl karışalanabilir. İlk olarak istemciden gelen adres talebine eş düşecek yeni URL bilgisinin sunucu tarafında ele alınıyor olması gerekir. Sonrasında ise sonuçlar istemci tarayıcı programına istenen formatta gönderilir. Asp.Net 1.1 kullanıldığı takdirde bu işin çözümü özel HttpHandler ve HttpModule sınıflarının yazılması ile mümkün olabilir.(Kendi HttpHandler yada HttpModuler tiplerimizi nasıl yazabileceğimize dair bilgileri daha önceden yayınlanan makalemizden takip edebilirsiniz) Asp.Net 2.0 mimarisindeyse, sadece URL eşleştirilmelerinin daha kolay yapılabilmesini sağlamak amacıyla web.config dosyasında yer alan system.web boğumu(node) içerisinde ele alınabilecek bir urlMappings elementi ve bunun Configuration API' sinde karşılğı olan UrlMappingsSection sınıfı geliştirilmiştir. Bu sayede konfigurasyon bazında URL eşleştirmeleri yapılabilmekte ve özel HttpHandler yada HttpModule tipleri yazılmasına gerek kalmamaktadır.

NOT : Her ne kadar urlMappings elementi veya UrlMappingsSection tipi sayesinde, URL eşleştirmelerinin yapılması kolaylaşmışsada bazı özel durumlarda yine HttpHandler veya HttpModule tipleri geliştirmek gerekebilir. Söz gelimi, urlMappings elementinin doğrudan bir regular expression desteği yoktur. Dolayısıyla benzer yazıma sahip URL bilgileri için ortak eşleştirme yapmak adına element bazında bir hamle yapılması zordur. Bunu sağlamak için HttpModule ve HttpHandler yazmak gerekmektedir. Böyle bir ihtiyaç için kendi HttpModule ve HttpHandler tipinizi yazmaya çalışmanız önerilir.

Aslında Asp.Net 2.0 mimarisinde yer alan URL eşleştirme sistemi aşağıdaki grafikte görüldüğü gibi çalışmaktadır.

Buna göre istemciden gelen talepler sonrası, ilgili web uygulaması Asp.Net çalışma zamanı içerisinde normal sürecine devam eder. Taki sayfanın son hali Render işlemine tabi tutulup istemciye gönderilene kadar. Bir başka deyişle Render işleminde önce, Asp.Net çalışma zamanı(Asp.Net RunTime) web.config içerisinde herhangibir eşleştirme olup olmadığına bakar. Eğer talep edilen URL için bir eşleştirme varsa buna göre HttpContext tipinin RewritePath metodu işletilir ve URL adresi değiştirilir. Sonrasında ise sayfa istemciye gönderilir.

Dilerseniz örnek bir senaryo üzerinden hareket ederek URL eşleştirmelerinin Asp.Net 2.0 mimarisinde nasıl yapıldığını yakından incelemeye çalışalım. Senaryo gereği kullanıcının alt kategorisi ve sınıfına göre bazı ürünleri listelediğini düşünebiliriz. Bu amaçla SQL Server 2005 ile gelen AdventureWorks veritabanındaki ProductSubCategories ve Product tablolarını göz önüne alalım. Bu tablolara arasında aşağıdaki şekilde görülen bire-çok(one to many) ilişki vardır.

İlk olarak default.aspx sayfamızı aşağıdaki gibi geliştirelim.

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>URL Mapping Ornegi</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:GridView ID="GridView1" runat="server" DataSourceID="dsCategories" AllowPaging="True" AutoGenerateColumns="False">
            <Columns>
                <asp:HyperLinkField DataNavigateUrlFields="Name,Class" DataNavigateUrlFormatString="~/Urunler/{0}/{1}/Goster.aspx" DataTextField="Title" HeaderText="Alt Kategori ve Sınıfı" />
            </Columns>
        </asp:GridView>
        <asp:SqlDataSource ID="dsCategories" runat="server" ConnectionString="<%$ ConnectionStrings:AdvConStr %>" SelectCommand="SELECT DISTINCT PSC.ProductSubcategoryID, Replace(PSC.Name,' ','') AS Name, RTRIM(PRD.Class) as Class, PSC.Name+' '+PRD.Class AS Title FROM Production.ProductSubcategory AS PSC INNER JOIN Production.Product AS PRD ON PSC.ProductSubcategoryID = PRD.ProductSubcategoryID WHERE (PRD.Class IS NOT NULL)">
        </asp:SqlDataSource>
    </div>
    </form>
</body>
</html>

Default.aspx sayfası içerisinde yer alan GridView kontrolü, Product ve ProductSubCategory tablolarının birleşiminden bir sonuç kümesine ait satırları göstermek üzere tasarlanmıştır. Söz konusu sonuç kümesi elde edilirken Name ve Class alanındaki boşlukların alınması için Replace ve RTrim isimli T-SQL fonksiyonlarına başvurulmaktadır. GridView bileşenine dikkat edilecek olursa içeride HyperLinkField tipinden bir kontrol kullanılmaktadır. Bu kontrolün dikkate değer özelliği ise DataNavigateUrlFormatString niteliğidir. Sayfa çalışma zamanında aşağıdakine benzer bir sonuç verecektir.

Dikkat edilecek olursa bağlantıların(Links) hedef URL bilgisi, HyperLinkField kontrolünün DataNavigateUrlFormatString niteliğinin değerine göre şekillenmektedir. Söz gelimi, örnek ekran görüntüsünde yer aldığı gibi M sınıfındaki Bottom Brackets ürünleri için URL bilgisi aşağıdaki gibidir.

http://localhost:1292/UrlRewriting/Urunler/BottomBrackets/M/Goster.aspx

Dikkat edilecek olursa bu URL bilgisinin Urunler kelimesinden itibaren olan kısmı çok daha okunaklı ve anlamlıdır. Peki biz bu linke tıkladığımızda Bottom Brackets kategorisinde ve M sınıfında yer alan ürünlerin listesi nasıl elde edilebilir. Bu işlemin Urunler.aspx gibi bir sayfa içerisinde ele alınması düşünüldüğü takdirde, seçilen bağlantı bilgisine göre ilgili kategori ve sınıf bilgilerinin sorgu katarı(QueryString) ile diğer sayfaya gönderilmesi gerekmektedir. Buda çok doğal olarak, default.aspx sayfasında seçilen URL bilgisine eş düşecek asıl URL bilgisinin tanımlanması ile mümkün olabilir. İşte bu noktada, Urunler.aspx sayfasını tasarlamadan önce, web.config içerisinde aşağıdaki ilaveler yapılmalıdır.

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings>
        <add name="AdvConStr" connectionString="Data Source=.;Initial Catalog=AdventureWorks;Integrated Security=True" providerName="System.Data.SqlClient"/>
    </connectionStrings>
    <system.web>
        <urlMappings enabled="true">
            <add url="~/Urunler/BottomBrackets/H/Goster.aspx" mappedUrl="~/Urunler.aspx?AltKategoriId=5&amp;AltKategoriAdi=Bottom%20Brackets&amp;Sinifi=H"/>
            <add url="~/Urunler/BottomBrackets/L/Goster.aspx" mappedUrl="~/Urunler.aspx?AltKategoriId=5&amp;AltKategoriAdi=Bottom%20Brackets&amp;Sinifi=L"/>
            <add url="~/Urunler/BottomBrackets/M/Goster.aspx" mappedUrl="~/Urunler.aspx?AltKategoriId=5&amp;AltKategoriAdi=Bottom%20Brackets&amp;Sinifi=M"/>
            <add url="~/Urunler/Cranksets/H/Goster.aspx" mappedUrl="~/Urunler.aspx?AltKategoriId=5&amp;AltKategoriAdi=Cranksets&amp;Sinifi=H"/>
        </urlMappings>
        <compilation debug="true"/>
        <authentication mode="Windows"/>
    </system.web>
</configuration>

URL eşleştirmeleri için system.web elementi içerisinde yer alan urlMappings boğumu kullanılmaktadır. Bu boğumda, add elementi içerisinde yer alan url ve mappedUrl nitelikleri ilede gereken eşleştirmeler yapılmaktadır. Buna göre, ~/Urunler/BottomBrackets/M/Goster.aspx gibi bir talep geldiğinde bu, ~/Urunler.aspx?AltKategoriId=5&amp;AltKategoriAdi=Bottom%20Brackets&amp;Sinifi=M olarak algılanacaktır. & operatörünün ele alınması sırasında &amp; ifadesinin kullanılmasına dikkat etmek gerekir. Eğer & işareti kullanılırsa derleme zamanıda hata mesajı alınır.

Artık gerekli bildirimler yapıldığına göre Urunler.aspx sayfası aşağıdaki gibi tasarlanabilir.

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        lblAltKategoriAdi.Text = Request.QueryString["AltKategoriAdi"]!=null?Request.QueryString["AltKategoriAdi"].ToString():"";
        lblSinifi.Text = Request.QueryString["Sinifi"]!=null?Request.QueryString["Sinifi"].ToString():"";
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Kategori ve sınıf bazlı ürünler</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="lblAltKategoriAdi" runat="server" Font-Bold="True" Font-Size="X-Large" Font-Underline="True" ForeColor="#C00000"></asp:Label>
        Alt Kategorisi
        <asp:Label ID="lblSinifi" runat="server" Font-Bold="True" Font-Size="X-Large" Font-Underline="True" ForeColor="#C00000"></asp:Label> sınıfı<br />
        <br />
        <asp:GridView ID="grdUrunler" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductId" DataSourceID="dsProducts">
            <Columns>
                <asp:BoundField DataField="ProductId" HeaderText="ProductId" InsertVisible="False" ReadOnly="True" SortExpression="ProductId" />
                    <asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
                    <asp:BoundField DataField="ListPrice" HeaderText="ListPrice" SortExpression="ListPrice" />
                    <asp:BoundField DataField="Class" HeaderText="Class" SortExpression="Class" />
                    <asp:BoundField DataField="SellStartDate" HeaderText="SellStartDate" SortExpression="SellStartDate" />
            </Columns>
        </asp:GridView>
        <asp:SqlDataSource ID="dsProducts" runat="server" ConnectionString="<%$ ConnectionStrings:AdvConStr %>" SelectCommand="Select ProductId,Name,ListPrice,Class,SellStartDate From Production.Product Where ProductSubCategoryId=@SubCatId and Class=@Class">
            <SelectParameters>
                <asp:QueryStringParameter DefaultValue="1" Name="SubCatId" QueryStringField="AltKategoriId" />
                <asp:QueryStringParameter DefaultValue="M" Name="Class" QueryStringField="Sinifi" />
            </SelectParameters>
        </asp:SqlDataSource>
    </div>
    </form>
</body>
</html>

Bu sayfa sadece QueryString ile gelen parametreler değerlendirmekte ve buna göre belirli bir alt kategori ve sınıfa ait ürünlerin bazı alanlarının listelenmesini sağlamaktadır. Bu amaçla yine GridView ve SqlDataSource kontrollerinden yararlanılmış ve uygun sorgu cümleleri kullanılmıştır. Örnek senaryoda yer alan Bottom Brockets alt kategorisi ve M sınıfı seçilirse, Urunler.aspx sayfasında aşağıdaki ekran görüntüsünde yer alan çıktılar elde edilecektir.

Yukarıdaki çıktıya dikkat edilecek olursa URL satırı bizim belirlediğimiz şekilde kalmıştır. Bu çıktının aynısını elde etmek için halen daha sorgu katarı(QueryString) ifadeleride açıkça kullanılabilir. Aynı web uygulamasında aşağıdaki ekran görüntüsünde yer alan URL talebi, aynı sonuçları verecektir.

Ne varki halen daha bazı problemler vardır. Herşeyden önce en azından söz konusu senaryo göz önüne alındığında, var olabilecek tüm olasılıklar için web.config dosyasına teker teker URL eşleştirmelerinin eklenmesi gerekmektedir. Bir geliştirici olarak bunun kod içerisinden daha etkili bir şekilde ele alınması çok doğal bir istektir. Neyseki Asp.Net 2.0 ile gelen güçlü Configuration API alt yapısı sayesinde istersek, kod içerisinde urlMappings kısmına dinamik olarak yeni elemanlar(elements) ekleyebilir, düzenleyebilir ve çıkartabiliriz. Bu amaçla bir admin sayfası veya farklı bir uygulama olacak şekilde bir admin paneli dahi göz önüne alınabilir.(Configuration API' sinin yönetiminin daha detaylı bir şekilde öğrenmek isterseniz daha önce yazılmış olan bir makaleden yararlanabilirsiniz.) Bunun için aşağıdaki gibi bir sayfa tasarladığımızı göz önüne alabiliriz.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Web.Configuration" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    private static void AddMappings()
    {
        Configuration cfg = ConfigurationManager.OpenExeConfiguration("");
        UrlMappingsSection urlMapSct = (UrlMappingsSection)cfg.GetSection("system.web/urlMappings");
        urlMapSct.UrlMappings.Clear();

        string query = "SELECT DISTINCT PSC.ProductSubcategoryID, PSC.Name,PRD.Class FROM Production.ProductSubcategory AS PSC INNER JOIN Production.Product AS PRD ON PSC.ProductSubcategoryID = PRD.ProductSubcategoryID WHERE (PRD.Class IS NOT NULL) ORDER BY PSC.Name, PRD.Class";

        using (SqlConnection conn = new SqlConnection(cfg.ConnectionStrings.ConnectionStrings["AdvConStr"].ConnectionString))
        {
            SqlCommand cmd = new SqlCommand(query, conn);
            conn.Open();
            string url = "", mappedUrl = "";
   
            SqlDataReader reader = cmd.ExecuteReader();
            while (reader.Read())
            {
                url = "~/Urunler/" + reader.GetString(1).Replace(" ","") + "/" + reader.GetString(2).Replace(" ","") + "/Goster.aspx";
                mappedUrl = "~/Urunler.aspx?AltKategoriId=" + reader["ProductSubCategoryID"].ToString() + "&AltKategoriAdi=" + reader["Name"].ToString() + "&Sinifi=" + reader["Class"].ToString();
                UrlMapping map = new UrlMapping(url, mappedUrl);
                urlMapSct.UrlMappings.Add(map);
            }
            reader.Close();
        }
        cfg.Save();
    }

    protected void btnMappEkle_Click(object sender, EventArgs e)
    {
        AddMappings();
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Yonetici Sayfasi</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <asp:Button ID="btnMappEkle" runat="server" Text="Güncel URL Eşleştirmelerini Ekle" OnClick="btnMappEkle_Click" />
            </div>
        </form>
    </body>
</html>

Şimdi burada geliştirdiğimiz kodları kısaca inceleyelim. Configuration alt yapısına göre, web.config dosyası içerisinde bilinen hemen her boğumun(Node) birer yönetimli tip(Managed Type) karşılığı vardır. Buradaki tipimiz UrlMappingsSection sınıfıdır. UrlMappingsSection sınıfıda Asp.Net 2.0 ile birlikte gelmiş bir sınıftır. Sınıf diagramından ele alındığında UrlMappingsSection sınıfının Framework içerisindeki yeri aşağıdaki şekilde görüldüğü gibidir.

Görüldüğü gibi UrlMappingsSection sınıfının UrlMappings özelliği aslında UrlMapping tipinden elemanlar taşıyan özel bir koleksiyonu(UrlMappingCollection) işaret etmektedir. Yeni bir UrlMapping nesne örneği eklenmek istendiğinde bu koleksiyondan yararlanılır. Bu koleksiyondaki elemanları oluşturan UrlMapping tipinin Framework içerisindeki yeri ise aşağıdaki gibidir.

Bu tipi o anki web.config dosyasından elde etmek amacıyla Configuration nesne örneğinin GetSection metodu kullanılmaktadır. Bu metod bilindiği gibi XPath ifadelerini parametre olarak alır.(XPath ile ilgili detaylı bilgiyi daha önceki bir makalemizden edinebilirsiniz) Sonrasında ise veritabanından yapılan sorgu sonucu elde edilen veri kümesine göre url ve mappedUrl değerleri oluşturulur. Bu değişkenler, her bir satır için web.config dosyasına eklenecek UrlMapping tiplerinin yapıcı metodlarına(Constructor) parametre olarak verilmektedir. Son olarak oluşan UrlMapping nesne örneği, Add metodu ile web.config/urlMappings elementi içerisindeki yerini almaktadır.

Elbette, bellek üzerinde yapılan bu değişikliklerin kalıcı olması adına Configuration tipinin Save metodu kullanılmaktadır. Dikkat edilmesi gereken noktalardan biriside URL değeri oluşturulurken Replace metodu ile Name ve Class alanlarındaki boşlukların alınmasıdır. Eğer söz konusu boşluklar alınmassa (özellike Class alanlarındakiler alınmassa) bu linklere tıklandığında çalışma zamanında sayfaların bulunamadığına dair hata mesajları alınabilir. Bu aynı zamanda, URL içerisinde geçersiz olabilecek karakterler var ise bunlarında çıkartılması veya değiştirilmesi gerektiği anlamınada gelmektedir. Örneğin boşlukar çoğunlukla URL satırına %20 şeklinde aktarılırlar. Ancak örnekteki URL bilgisinde klasör tabanlı bir yaklaşım tercih edildiğinden bütün boşlukların çıkartılması yolu tercih edilmiştir. Bu işlemlerin arkasından Admin sayfası çalıştırılır ve düğme tıklanırsa web.config dosyasının aşağıdaki ekran görüntüsünde olduğu gibi değiştiği görülür.

Dikkat edilecek olursa, elde edilen veri kümesine göre, söz konusu olabilecek tüm URL eşleştirmeleri ilave edilmiştir. Configuration API'si sağolsun :) Şimdi default.aspx sayfası çağırılırsa, artık GridView kontrolü içerisindeki her bir bağlantının karşılığının olduğu ve çalıştığı görülür. Default.aspx için örnek görüntü aşağıdaki gibidir.

İlgili bağlantının tıklanması sonrası ise aşağıdaki sonuçlara benzer çıktılar elde edilebilir.

Buraya kadar örnek bir senaryo üzerinden URL eşleştirmesini incelemeye çalıştık. Son teknikte dinamik olarak URL eşleştirmelerini ekledik. Bu teknik her ne kadar göze hoş gelsede dezavantajıda vardır. Öyleki, veri kaynağında değişiklikler yapıldığında örneğin Alt kategori adı değiştiğinde yada yenileri eklendiğinde web.config dosyası içerisinde yeni düzenlemelerin yapılması, bir başka deyişle ilgili fonksiyonun tekrardan çağırılması gerekcektir. Bu çeşitli teknikler ile çözülebilir ama yinede düşünülmesi gereken bir adımdır.

Sonuç olarak Asp.Net 2.0 ile birlikte gelen urlMappings elementi her ne kadar bazı avantajlar sağlasada çeşitli kısıtlamalarda içermektedir. Regular Expression ifadeleri kullanılamadığından, her bir URL için gereken eşleştirmeler teker teker web.config dosyası içerisinde yazılmalıdır. Gerçi Configuration API' si yardımıyla bu bir nebzede olsa aşılabilmektedir ancak Regular Expression'ın yerini tam anlamıyla tutmamaktadır. Regular Expression desteği için geliştiricinin özel HttpModule ve HttpHandler tiplerini geliştirmesi gerekliliği ise bir zorluktur. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Örnek Uygulama için Tıklayın

Tags:  
Categories:   Asp.Net 2.0
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Asp.Net Uygulamalarında Tablo Bazlı Resimleri Ele Almak

Perşembe, 26 Temmuz 2007 20:25 by bsenyurt

Yazın bu sıcak günlerinde daha hafif konularla web maceralarımıza devam ediyoruz. Geçtiğimiz makalemizde Asp.Net uygulamalarında ektin hata yönetiminin nasıl yapılabileceğini incelemeye çalışmıştık. Bu kez veritabanı tablolarında çoğunlukla binary alanlarda saklanan resimlerin, Asp.Net uygulamalarında nasıl ele alınabileceğini örnek projeler üzerinden incelemeye çalışacağız. Bir Windows uygulaması göz önüne alındığında, resimleri gösterebilecek bir PictureBox kontrolünün çeşitli özellikleriden yararlanarak herhangibir tabloda tutulan binary içeriği kullanmak ve bu içeriğin işaret ettiği resmi göstermek son derece kolaydır. Ne varki Asp.Net uygulamalarında her zaman için, render edilerek istemciye gönderilen bir sayfa içeriği mevcuttur. Bu içeriğin tipi(Content Type) daha farklıdır. Dolayısıyla binary formatta tutulan resimleri ele almak için farklı bir yaklaşım gerekmektedir.

Tabloda binary formatta tutulabilen resimleri Asp.Net uygulamalarında ele almak amacıyla, gösterilmek istenen resmi tek başına yorumlayan bir Asp.Net sayfası mevcuttur. Bu sayfanın tek bir görevi vardır o da ilgili resmi image formatlarından uygun olana göre sayfaya Render etmektir. Bunu incemelek için örnek bir senaryo göz önüne almakta fayda olacağı kanısındayım. Bu amaçla SQL Server 2005 ile birlikte gelen ve Production şemasında(Schema) bulunan Product, ProductPhoto ve ProductProductPhoto tabloları göz önüne alınabilir. Bu tablolar arasındaki ilişki kısaca aşağıdaki şekilde görüldüğü gibidir.

ProductPhoto isimli tabloda yer alan ThumbNailPhoto ve LargePhoto isimli alanlarda binary olarak ürün resimleri saklanmaktadır(tam olarak varbinary tipinde). Buna göre ilk örnek senaryomuzda kullanıcılar ürünlerin listelendiği bir sayfadan detay bilgilerini almak için başka bir sayfaya geçiş yapacaklardır. Detayların verildiği sayfada ürüne ait ThumbNailPhoto içeriğide bir resim olarak sayfa gösterilecektir. Başlamadan önce ProductPhoto tablosundaki herhangibir ThumbNailPhoto alanının içeriğini resim olarak nasıl gösterebileceğimize bakalım. Bu amaçla ResimGoster.aspx isimli aşağıdaki gibi bir sayfa tasarlanarak işe başlanabilir.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ResimGoster.aspx.cs" Inherits="ResimGoster" %>

Eminimki ResimGoster isimli web sayfasının içeriği son derece ilginç gelmiştir. Nitekim herhangibir HTML elementi yer almamaktadır. Aslında bu sayfanın tek amacı yüklenirken(bir başka deyişle Page_Load olay metodu çalışırken), ürün resmini ekrana binary olarak yazdırmaktır. Burada elbetteki hangi resmin gösterileceğide önemlidir. Bunun için sayfaya bir şekilde ProductPhotoID alanının değerinin gelmesi gerekmektedir. Bunun için en güzel yol QueryString kullanımıdır. Öyleyse bu sayfanın kodlarını yazarak işe devam edelim.

protected void Page_Load(object sender, EventArgs e)
{
    string resimId = Request.QueryString["ResimId"];
    if (!String.IsNullOrEmpty(resimId))
    {
        byte[] resimBytes=null;
        using (SqlConnection conn = new SqlConnection("data source=localhost;database=AdventureWorks;integrated security=SSPI"))
        {
            SqlCommand cmd = new SqlCommand("Select ThumbNailPhoto From Production.ProductPhoto Where ProductPhotoId=@PhotoId", conn);
            cmd.Parameters.AddWithValue("@PhotoId", resimId);
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
            if(reader.Read())
                resimBytes = reader.GetSqlBytes(0).Value;
            reader.Close();
            if (resimBytes != null)
            {
                Response.ContentType = "image/gif";
                Response.BinaryWrite(resimBytes);
            }
            else
                Response.Write("Resim gösterilemiyor.");
        }
    }
    else
        Response.Write("ResimId parametresi eksik yada hatalı.");
}

Page_Load olay metodu içerisinde ilk olarak HttpRequest sınıfının static QueryString özelliği yardımıyla sayfaya gelen ResimId değeri alınmaktadır. Kullanıcıların bu sayfayı doğrudan talep etme ihtimaline göre ResimId değerinin boş gelme olasılığı bulunmaktadır. Bu nedenle String sınıfının IsNullOrEmpty metodu ile bir kontrol gerçekleştirilir. Sonrasında ise Production şemasındaki ProductPhoto tablosunda gelen değere göre ilgili ThumbNailPhoto alanı çekilir. Eğer gelen ResimId ile eşleşen bir ProductPhotoId alanı var ise bu satırın ThumbNailPhoto alanının değeri SqlDataReader nesne örneğine ait Read metodu yardımıyla okunur. Okuma işlemi sırasında GetSqlBytes metodu kullanılmakta ve Value özelliği ile elde edilen byte dizisi resimBytes isimli alana aktarılmaktadır. Diğer taraftan HttpResponse sınıfının ContentType özelliği ile render edilecek sayfanın içeriği belirlenmektedir. Burada image/gif değeri ile basılacak içeriğin gif formatında bir resim olacağı belirtilmektedir.

NOT: ContentType özelliğinin varsayılan değeri text/HTML dir. Bu değer, tahmin edileceği üzere sayfanın çıktısının HTML olarak üretileceğini işaret etmektedir. Yaygın olarak kullanılan diğer versiyonlar aşağıdaki gibidir.

  • image/gif
  • image/jpeg
  • text/plain
  • application/vnd.ms-excel (çıktının excel dökümanı olmasını sağlar)
  • application/vnd.ms-word (çıktının word dökümanı olmasını sağlar)

Son olarak, elde edilen byte dizisinin çıktıya aktarılmasını sağlamak için yine HttpResponse sınıfının static metodlarından BinaryWrite çağırılmaktadır.

Artık sayfayı test ederek işlemlerimize devam edebiliriz. Elbette doğru sonuçları görebilmek için ResimId parametresini Url satırından göndermekte fayda vardır. Aşağıda örnek olarak 120 numaralı ProductPhotoId değerine sahip satır için elde edilen çıktı görülmektedir.

Burada, üretilen HTML sayfasının kaynak kodlarına bakılmak istenirse tarayıcı buna izin vermeyebilir(Örneğin Microsoft Internet Explorer 7.0 View Source buna izin vermemiştir). Bu sebepten çıktıyı Save As ile kaydetmek gerekebilir. Kaydedilen çıktının içeriği aşağıdaki ekran görüntüsünde yer aldığı gibi olacaktır. Dikkat edilecek olursa sayfanın çıktısı sonucu oluşturulan içerikte img elementi ve src niteliği yer almaktadır.

Elbette satır olarak karşılığı olmayan bir ResimId değeri girilirse sayfa çıktısı tarayıcı penceresinde aşağıdaki gibi olacaktır. Örneğin ProductPhotoId değeri 17 olan bir satır bulunmamaktadır.

Bununla birlikte kullanıcı bu sayfayı doğrudan talep eder ve ResimId parametresini kullanmassa aşağıdaki ekran çıktısını elde eder.

Burada olası bazı hataların önüne geçilmek amacıyla basit tedbirler alınmış ve ekrana bilgi mesajları verilmiştir. Gerçek hayat uygulamalarında son kullanıcıların daha doğru ve etkin bir şekilde uyarılması bir başka deyişle oluşan hatalar konusunda bilgilendirilmesi gerekmektedir.

Artık tek yapılması gereken senaryoyu biraz daha kullanışlı hale getirmektir. Bu amaçla ürünlerin gösterildiği Urunler.aspx isimli basit bir web sayfası tasarlanarak devam edilbilir. Bu sayfada ürünlere ait bir kaç temel bilgi bulunacak ama detayları için başka bir sayfaya yönlendirmede bulunulacaktır. Yönlendirilme yapılan sayfa tahmin edileceği üzere ürüne ait resmide içeren bir detay sayfasıdır. Urunler.aspx sayfasında basit olarak bir SqlDataSource kontrolü ve bu kontrolü ele alan bir GridView bileşeni düşünülebilir. Buna göre Urunler.aspx sayfasının kaynak kod tarafı aşağıdaki gibi tasarlanabilir.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Urunler.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Urunler</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="dsProducts">
                <Columns>
                    <asp:HyperLinkField DataNavigateUrlFields="ProductId" DataNavigateUrlFormatString="UrunDetay.aspx?PrdId={0}" DataTextField="Name" HeaderText="Urun Adı" />
                    <asp:BoundField DataField="ListPrice" DataFormatString="{0:C}" HeaderText="Liste Fiyatı" HtmlEncode="False" SortExpression="ListPrice" />
                </Columns>
            </asp:GridView>
            <asp:SqlDataSource ID="dsProducts" runat="server" ConnectionString="<%$ ConnectionStrings:AdvConStr %>" SelectCommand="SELECT Top 20 ProductID, Name, ListPrice FROM Production.Product Where ListPrice>=1000">
            </asp:SqlDataSource>
        </div>
    </form>
</body>
</html>

Urunler.aspx sayfasında yer alan GridView kontrolünde HyperLinkField kontrolü kullanılmaktadır. Bu alan, ürünün adını(Name) göstermekte olup üzerine tıklandığında kullanıcıyı UrunDetay.aspx sayfasına göndermektedir. Bu işlem sırasında da PrdId isimli bir QueryString parametresi ProductId alanının değerini detay sayfasına taşımaktadır. Urunler.aspx isimli sayfanın çalışma zamanındaki çıktısı aşağıdaki ekran görüntüsünde yer aldığı gibidir.

Gelelim UrunDetay.aspx sayfasına. Bu sayfada basit olarak Urunler.aspx sayfasında seçilen ürünlere ait detay bilgileri gösterilecektir. Ancak önemli olan, seçilen ürünün resmininde ProductPhoto tablosundan binary olarak çekilerek ekrana bastırılacak olmasıdır. Bu amaçla tasarlanan Urunler.aspx sayfasında yine bir SqlDataSource kontrolü ve detaylar için DetailsView bileşeni aşağıdaki gibi kullanılabilir.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UrunDetay.aspx.cs" Inherits="UrunDetay" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Urun Detaylari</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>Ürün Detayları :<br />
            <asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataSourceID="dsProductDetails" Height="50px" Width="294px">
                <Fields>
                    <asp:BoundField DataField="Name" HeaderText="&#220;r&#252;n Adı" SortExpression="Name" />
                    <asp:BoundField DataField="ProductNumber" HeaderText="&#220;r&#252;n Numarası" SortExpression="ProductNumber" />
                    <asp:BoundField DataField="SafetyStockLevel" HeaderText="Stok Seviyesi" SortExpression="SafetyStockLevel" />
                    <asp:BoundField DataField="ReorderPoint" HeaderText="Sipariş Noktası" SortExpression="ReorderPoint" />
                    <asp:BoundField DataField="ListPrice" HeaderText="Liste Fiyatı" SortExpression="ListPrice" DataFormatString="{0:C}" HtmlEncode="False" />
                    <asp:BoundField DataField="StandardCost" HeaderText="Standart Maliyet" SortExpression="StandardCost" DataFormatString="{0:C}" />
                    <asp:TemplateField HeaderText="Urun Resmi">
                        <ItemTemplate>
                            <img alt="Ürün Resmi" runat="server" src='<%#"ResimGoster.aspx?ResimID="+DataBinder.Eval(Container.DataItem, "ProductPhotoID") %>' id="urunResmi" />
                        </ItemTemplate>
                    </asp:TemplateField>
                </Fields>
            </asp:DetailsView>
            <asp:SqlDataSource ID="dsProductDetails" runat="server" ConnectionString="<%$ ConnectionStrings:AdvConStr %>" SelectCommand="SELECT Production.Product.ProductID, Production.Product.Name, Production.Product.ProductNumber, Production.Product.SafetyStockLevel, Production.Product.ReorderPoint, Production.Product.ListPrice, Production.Product.StandardCost, Production.ProductProductPhoto.ProductPhotoID FROM Production.Product INNER JOIN Production.ProductProductPhoto ON Production.Product.ProductID =Production.ProductProductPhoto.ProductID WHERE (Production.Product.ProductID = @PrdId)">
                <SelectParameters>
                    <asp:QueryStringParameter DefaultValue="1" Name="PrdId" QueryStringField="PrdId" />
                </SelectParameters>
            </asp:SqlDataSource>
        </div>
    </form>
</body>
</html>

UrunDetay.aspx isimli web sayfasında dikkat edilmesi ve üzerinde durulması gereken bazı noktalar vardır. Öncelikli olarak SqlDataSource kontrolünde kullanılan sorgu basit olarak aşağıdaki şekilde yer aldığı(Query Builder ile elde edilmiştir) gibi Product ve ProductProductPhoto tablolarının join ile birleştirilmiş bir halidir ve ProductId değerinin where ifadesinde ele almaktadır. Nitekim ürün resminin ProductPhoto tablosundan tedariki için ProductPhotoID değerinin bilinmesi gerekmektedir. Bu nedenle Product ve ProductProductPhoto tabloarı Join ile birleştirilmiştir.

Diğer taraftan DetailsView kontrolü içerisindede resmin gösterilebilmesi için bir TemplateField kullanılmış ve ItemTemplate şablonu içerisinde img elementi aşağıdaki gibi kullanılmıştır.

<asp:TemplateField HeaderText="Urun Resmi">
                        <ItemTemplate>
                            <img alt="Ürün Resmi" runat="server" src='<%#"ResimGoster.aspx?ResimID="+DataBinder.Eval(Container.DataItem, "ProductPhotoID") %>' id="urunResmi" />
                        </ItemTemplate>
                    </asp:TemplateField>

Burada dikkat edilmesi gereken en önemli nokta src niteliğine(attribute) değer atamasının nasıl yapıldığıdır. DataBinder sınıfının Eval metodunu kullanarak o anki satırın içerisinde yer alan ProdcutPhotoId değeri ResimGoster.aspx sayfasına ResimID adlı parametre ile gönderilmektedir. Buda yazımızın başında tasarladığımız ResimGoster sayfasının çağırılması ve bir resim içeriğinin elde edilerek buradaki img kontrolü içerisinde gösterilmesi anlamına gelmektedir. Sonuç itibariyle UrunDetay.aspx sayfası test edildiğinde aşağıdakine benzer bir ekran çıktısı ile karşılaşılacaktır.

Buraya kadar yaptıklarımızı özetleyecek olursak eğer, binary olarak tutulan resimlerin gösterilmesi için izlenebilecek yollardan birisinin adımları aşağıdaki gibi olacaktır.

  • İlk olarak resim içeriğini binary olarak tarayıcıya basabilecek bir aspx sayfası tasarlanır.
  • Sayfanın amacı gereği aspx kaynağında(Source) sadece Page direktifi bırakılır ve diğer içerik silinir. Bu zorunlu değildir. Ancak tavsiye edilen yoldur.
  • Sayfanın Page_Load olay metodu kodlanır.
  • Page_Load olay metodunda gösterilmek istenen resme ait satırın bulunabilmesi için QueryString' den yararlanılabilir.
  • Resme ait binary içeriğin kod tarafında byte dizisi(byte[]) şeklinde ele alınması sağlanır.
  • Elde edilen byte dizisinin sayfaya resim olarak basılmasını sağlamak için önce ContentType özelliğinin değeri image/gif veya image/jpeg olarak belirlenir.
  • Resmi yazdırmak içinse BinaryWrite metodu çağırılır.

Buradaki örnek göz önüne alındığında istemci sayısının fazla olacağı düşünelecek olursa performansı arttırmak adına parametre bazlı olacak şekilde ön bellekleme(Caching) yapılması sağlanabilir. Böylece ResimGoster.aspx sayfasının sürekli olarak Page_Load kodlarını çalıştırmasının önüne geçilmiş olunur.

NOT : Resimlerin çok sık değişmediği düşünülüyorsa ve SQL Server kullanılıyorsa tablo bağımlı bir ön belleklemede yapılabilir(Sql Cache Dependency).

Söz gelimi ResimGoster.aspx dosyasının içeriği aşağıdaki gibi değiştirilerek sayfanın çıktısının belirli bir süreliğine(örneğin 60 saniye boyunca) ön bellekte tutulması sağlanabilir.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ResimGoster.aspx.cs" Inherits="ResimGoster" %>
<%@ OutputCache Duration="60" VaryByParam="ResimID" %>

Gelelim resimlerin işlenmesi ile ilgili diğer bir yaklaşıma. Örnekte resmi gösteren img elementinin src niteliğinde bir url adresinden yararlanılmaktadır. Elbetteki bu işlem programatik olarak kod üzerindende geliştirilebilir. Örneğin ürün bilgilerini bir DataList kontrolü üzerinde göstermek istediğimizi varsayalım. Bu kez resim alanlarının DataList kontrolü içerisindeki img elementinin src niteliğine bağlanmasını kod tarafından gerçekleştirmeye çalışacağız. Bu amaçla aşağıdaki gibi UrunListesi.aspx isimli bir web sayfası hazırlanarak işlemlere devam edilebilir.

<form id="form1" runat="server">
    <div>
        <asp:DataList ID="DataList1" runat="server" DataSourceID="dsProducts" Width="600px">
            <ItemTemplate>
                <table>
                    <tr>
                       <td colspan="2">
                            <asp:Label ID="NameLabel" runat="server" Font-Italic="True" ForeColor="#000040" Text='<%# Eval("Name") %>'></asp:Label>
                        </td>
                        <td style="width: 100px; text-align: right">
                            <asp:Label ID="ProductIDLabel" runat="server" Font-Bold="True" ForeColor="#C00000" Text='<%# Eval("ProductID") %>'></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td rowspan="3" style="width: 100px">
                            <img runat="server" id="urunResmi" alt="Urun Resmi" src="" />
                        </td>
                        <td style="width: 100px">Standart Maliyet</td>
                        <td style="width: 100px">
                            <asp:Label ID="StandardCostLabel" runat="server" Text='<%# Eval("StandardCost", "{0:C}") %>'></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 100px">Liste Fiyatı</td>
                        <td style="width: 100px">
                            <asp:Label ID="ListPriceLabel" runat="server" Text='<%# Eval("ListPrice", "{0:C}") %>'></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 100px">Sınıf</td>
                        <td style="width: 100px">
                            <asp:Label ID="ClassLabel" runat="server" Text='<%# Eval("Class") %>'></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="3"><hr /></td>
                    </tr>
                </table>
            </ItemTemplate>
        </asp:DataList>
        <asp:SqlDataSource ID="dsProducts" runat="server" ConnectionString="<%$ ConnectionStrings:AdvConStr %>" SelectCommand="SELECT Top 20 P.ProductID, P.Name, P.StandardCost, P.ListPrice, PP.ProductPhotoID, P.Class FROM Production.Product P INNER JOIN Production.ProductProductPhoto PP ON P.ProductID = PP.ProductID WHERE P.ListPrice>1400">
        </asp:SqlDataSource>
    </div>
</form>

UrunListesi.aspx sayfasında yer alan DataList kontrolü Production şemasındaki Product ve ProductProductPhoto tablolarının birleşiminden oluşan sonuç kümesinden ilk 20 satırı göstermek üzere tasarlanmıştır ve hatta ListePrice alanına göre filtreleme eklenmiştir. Burada özellikle üzerinde durmamız gereken nokta, img elementidir. ItemTemplate üzerindeki tablo içerisine yerleştirilen img elementinin src niteliğini kod tarafında ele alabilmek için DataList kontrolünün ItemDataBound olayından yararlanılabilir. Bu olay metodu içerisinde söz konusu img elementi bulunmalı ve src niteliğine o anki satırın ProductPhotoId değeri QueryString parametresi olarak aktarılmalıdır. O halde bu amaçla aşağıdaki kod parçasını yazmamız yeterli olacaktır.

protected void DataList1_ItemDataBound(object sender, DataListItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item
        || e.Item.ItemType == ListItemType.AlternatingItem)
    {
        // Önce ProductPhotoId değeri bulunmalı
        string resimId = DataBinder.Eval(e.Item.DataItem, "ProductPhotoId").ToString();
        // img elementi bulunmalı ve src niteliğinin değeri değiştirilmeli.
        HtmlImage resimElementi = (HtmlImage)e.Item.FindControl("urunResmi");
        resimElementi.Src = "~/ResimGoster.aspx?ResimId=" + resimId;
    }
}

ItemDataBound olayı DataList içerisindeki her bir satır için çalışacağından, işlemleri sadece Item ve AlternatingItem tipindeki satırlarda yapmakta fayda vardır. Bu amaçla DataListItemEventArgs tipinden olan e isimli parametrenin özelliklerinden faydalanılmaktadır. Sonrasında ise o anki satır ile gelen ProductPhotoId değerinin elde edilmesi gerekmektedir. Bu amaçlada DataBinder sınıfının Eval metodu ele alınmaktadır. İlk parametre ile o anki veri satırı yakalanmakta, ikinci parametre ilede söz konusu veri satırındaki ProductPhotoId değeri istenmektedir. Eval metodu geriye Object tipinden bir değer döndürdüğü için bilinçli olarak string tipine dönüştürülmüştür. Nitekim url katarında kullanılacak bilgi string' dir.

Bu işlemlerin ardından img kontrolünün elde edilmesi sağlanır. Bunun içinde e.Item üzerinden FindControl metodu çağırılmıştır. FindControl metodunun parametresi kontrolün id değerinin işaret etmektedir. Hatırlanacağı üzere img kontrolünün id niteliğine kaynak tarafında urunResmi adı verilmiştir. img kontrolü HtmlImage tipinden bir kontroldür ve FindControl metodu geriye Control tipinden bir referans döndürdüğünden sonuç referansı bilinçli olarak HtmlImage tipine dönüştürülmüştür. Son olarak elde edilen HtmlImage kontrolüne ait referans üzerinden Src niteliğinin değeri değiştirilir. Burada yapılan işlem tüm yazı boyunca üzerinde durduğumuz konudur. Uygulamayı bu haliyle çalıştırdığımızda aşağıdakine benzer bir ekran görüntüsü elde ederiz.

Bu makalemizde tablolarda binary olarak tutulan resim alanlarını web sayfalarında küçük bir hile ile nasıl işleyebileceğimizi incelemeye çalıştık. Görsellik hemen hemen tüm uygulamalarda önemli bir faktör olduğundan resim alanlarının bu şekilde ele alınıyor olmasını bilmek önemli bir avantaj sağlamaktadır. Kullanılan teknikte önemli olan nokta binary alanın içeriğini tek başına ele alıp resim formatında çıktı veren bir sayfanın var olmasıdır. Ayrıca resmi gösteren kontrolün basit bir img bileşeni olduğuna ve src niteliğinin önemine dikkat edilmelidir. Bu sayfanın çıktısı örneklerden de görüldüğü gibi pek çok farklı biçimde kullanılabilir ve son kullanıcıya görsel olarak daha doyurucu bir içerik sağlanabilir. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Örnek Uygulama için Tıklayın

Asp.Net için Etkili Hata Yönetimi

Çarşamba, 18 Temmuz 2007 20:45 by bsenyurt

Uzun süredir Windows Communication Foundation ile ilgili yazılar yayınlıyoruz. Sanıyorumki biraz hava değişimine ihtiyacımız olacak. Bu nedenle bu haftaki yazımızda biraz daha hafif ama önemli olan bir konu üzerinde durmaya çalışacağız. Web uygulamalarında sunucu taraflı hata yönetimi (Server Side State Management). .Net ortamında hataların ele alınmasında kullanılan en bilinen yol try...catch...finally bloklarıdır. Ne varki uygulama ortamları çeşitlilik göstermektedir. Bir sınıf kütüphanesi içerisinde yapılan hata kontrolü ile dağıtık mimari uygulamaları(distributed applications) içerisinde yapılan hata yönetimi farklıdır.(Örneğin WCF içerisindeki Fault Management konusunu hatırlayalım) Bu sebepten Asp.Net uygulamalarındada farklı bir yaklaşımı ele almak gerekmektedir. Web uygulamalarında oluşan hatalar sonucu çok hoş olmayan hata ekranları ile karşılaşmak mümkün olabilmektedir. Ancak hatalar kontrollü bir şekilde yönetilebilirlerse, son kullanıcıyı bilgilendirebilecek şekilde mesajlar verilip hataların düzeltilmesi yönünde daha sağlam ve güçlü adımlar atılabilir. Bu aynı zamanda uygulamanın tutarlılığı ve güvenilirliği açısındanda önemlidir.

Asp.Net ortamı, hataların yönetimi amacıyla istisna(Exception) tiplerini ve hataları yakalayıcı olay(event) metodların göz önüne alır. Söz konusu hata yönetimi metod seviyesinde(Method Level), sayfa seviyesinde(Page Level) ve uygulama seviyesinde(Application Level) gerçekleştirilebilir. Aşağıdaki tabloda söz konusu seviyeler ve aralarındaki temel farklar vurgulanmaya çalışılmaktadır.

Asp.Net Hata Yönetim Seviyeleri (Error Management Levels)
Metod Seviyesinde Sayfa Seviyesinde Uygulama Seviyesinde
  • Toparlanabilir veya bir başka deyişle kurtarılabilir hatalar çoğunlukla metod seviyesinde ele alınır.
  • Eğer olası hatalar toparlanamayacak cinsten ise bir üst seviyeye yönlendirilir.
  • Bir sayfa ile ilgili tüm hataların tek bir merkezden yönetilebilmesi sağlanır.
  • Olası hatalar sonrasında kullanıcılar çoğunlukla özel sayfalara yönlendirilir.
  • Bu seviyede sayfaların Page_Error olay metodları ele alınır.
  • Hata sayfasına yönlendirilmeden önce sayfa ile ilgili log bilgisi yazdırma, fiziki dosyalara bilgi atma veya adminlere mail gönderme gibi işlemler yapılabilir.
  • Uygulama içerisinde herhangibir sayfada meydana gelen hataların yakalanması sağlanır.
  • Tüm web uygulamasının hata yönetiminin tek bir merkezden kontrol edilebilmesi sağlanmış olur.
  • Bu seviyede global.asax dosyasındaki Application_Error olay metodu ele alınır.
  • Hata sayfasına yönlendirilmeden önce log bilgisi yazdırma, fiziki dosyalara bilgi atma veya adminlere mail gönderme gibi işlemler yapılabilir.

Metod seviyesinden, uygulama seviyesine doğru çıkıldıkça hataların merkezi olarak yönetilmesi ve tek bir merkezden ele alınması dahada kolaylaşmakta ancak, detay bilgilerinden gittikçe uzaklaşılmaktadır. Nitekim bir metod içerisinde meydana gelecek bir hata ile ilişkili yakalanan detayın, sayfa veya uygulama seviyesine aktarılmadığı sürece merkezi olarak ayrıştırılması zor olmaktadır.

Şimdi gelin bu seviyeleri örnekler yardımıyla incelemeye çalışalım. Metod seviyesinde hata yönetiminde try...catch...finally blokları büyük önem arz etmektedir. Ancak bu bloklar istenirse try...catch veya try...finally şeklindede yazılabilir. Çok doğal olarak bunladan hangisinin kullanılacağının kararını vermek için bazı vakkaların göz önüne alınması gerekmektedir. İlk olarak basit bir örnek ile başlayalım. Bu amaçla kullanıcının bölme işlemi yaptığı aşağıdaki gibi bir aspx sayfası olduğunu göz önüne alabiliriz.

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void Hesapla_Click(object sender, EventArgs e)
    {
        double deger1 = Convert.ToDouble(txtDeger1.Text);
        double deger2 = Convert.ToDouble(txtDeger2.Text);
        double sonuc = deger1 / deger2;
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Hata Yonetimi (Metod Seviyesinde)</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Birinci Değer :
                <asp:TextBox ID="txtDeger1" runat="server"></asp:TextBox> <br />
                İkinci Değer :
                <asp:TextBox ID="txtDeger2" runat="server"></asp:TextBox>
                <br />
                <asp:Button ID="btnHesapla" runat="server" Text="Hesapla" OnClick="Hesapla_Click" />
            </div>
        </form>
    </body>
</html>

Öncelikle hata kontrolü yapmadan sayfayı ele almaya çalışacağız. Bu nedenle örnek olarak ikinci kutucuğu boş bırakıp Hesapla isimli düğmeye basıyoruz. Sonuç olarak kullanıcı açısından pekte hoş olmayacak aşağıdaki ekran görüntüsü ile karşılaşırız. (Elbette geliştirme aşamasında bu mesajlar developer açısından daha kıymetli olabilir ;)

Dolayısıyla try...catch blokları yardımıyla Hesapla_Click isimli olay metoduna ait kodların aşağıdaki hale getirilmesi daha doğru olacaktır.

protected void Hesapla_Click(object sender, EventArgs e)
{
    try
    {
        double deger1 = Convert.ToDouble(txtDeger1.Text);
        double deger2 = Convert.ToDouble(txtDeger2.Text);
        double sonuc = deger1 / deger2;
    }
    catch (FormatException err)
    {
        Response.Write("<b>Değerler sayısal olmalıdır. Lütfen girdiğiniz değerleri kontrol ediniz.</b><br/>Detaylı Mesaj : " + err.Message);
    }
    catch (Exception err)
    {
        Response.Write("<b>Beklenmeyen bir hata oluştu.</b><br/>Detaylı Mesaj : " + err.Message);
    }
}

Bunun sonucunda aynı hata tekrar edilmeye çalışılırsa bu sefer mantıklı bir ekran ile karşılaşılacak ve kullanıcı hata ile ilişkili olarak daha doğru bir şekilde bilgilendirilebilecektir.

Elbette metod seviyesinde hata yönetimi adına dikkat edilmesi gereken bazı vakkalarda vardır. Eğer oluşan hataların kurtarılabilme(toparlanabilme) ihtimali varsa try...catch blokları döngüler (while, for gibi) içerisinde ele alınabilir. Böylece tekrar sayısına göre istenen rutin bir kaç kez üst üste denenebilir. Diğer taraftan oluşan istisnalar ile ilişikili olarak ekstradan verilebilecek yada kullanılabilecek bilgiler varsa bunların bir üst seviyede (sayfa seviyesinde-page level) ele alınması için catch bloğu içerisinde throw anahtar kelimesine başvurulabilir. Burada özellikle Exception sınıfının aşırı yüklenmiş(overload) yapıcı(constructor) metodlarından faydalanılmaktadır.

Diğer bir vakka metod içinde kullanılan dış kaynakları ele alır. Örneğin ilgili rutinler içerisinde kaynak temizlenmesi gerekiyorsa (bağlantıların veya dosyaların kapatılması, yönetimsiz-unmanaged nesnelerin serbest bırakılması gibi) finally bloklarını kullanmak doğru olacaktır. Burada finally bloklarının kullanılması şart değildir. Nitekim using blokları yardımıylada, IDisposable arayüzünü uygulayan tipler için blok sonunda Dispose çağrıları gerçekleştirilebilir. Son olarak olası hatalar ilgili metod içerisinde ele alınamıyorsa metodu çağıran yerde yakalanmalıdır.

Görüldüğü üzere vakkaların sayısı ve metod içerisindeki hata yönetimi çeşitli şekillerde yapılabilmektedir. Bu sebepten karar verirken aşağıdaki gibi tablodan faydalanmakta yarar vardır.

Öneri Kaynak temizlemesi gerekiyor mu? Olası hata var mı? Olası hatalar kurtarılabilir mi? Eklenecek ilave hata bilgisi var mı?
Hiç bir kontrole gerek yok hayır yok hayır yok
hayır var hayır yok
try...finally evet yok hayır yok
evet var hayır yok
try...catch hayır var hayır var
try...catch...finally evet evet hayır var

Örneğin kaynak temizlenmesi gerekiyorsa, olası hatalar var ise ve hatta olası hatalara eklenebilecek ekstra bilgiler var ise try..catch...finally bloklarını kullanmak daha mantıklıdır. Ne varki olası hatalarda, hata mesajına ilave bilgiler eklemek catch blokları içerisinde throw kullanmak ile mümkün olabilir. Çünkü amaç, bu hatayı ele alan bir üst seviyeye bilgi göndermektir. Bunun ele alınabileceği en güzel yer sayfa seviyesidir (Page Level). Öyleyse sayfa seviyesinde hata yönetiminin nasıl yapılacağını inceleyerek devam edelim.

Özellikle belirli bir sayfada meydana gelebilecek tüm hataların tek bir merkezden kontrolünün sağlanması gerektiği durumlarda sayfa seviyesinde hata yönetimi gerçekleştirilebilir. Bu teknikte önemli olan nokta, Page_Error olay metodunun etkin bir şekilde kullanılmasıdır. Aslında sayfa seviyesinde hatalar ele alınırken izlenen basit bir yol vardır. Aşağıdaki tabloda bu yol gösterilmektedir.

Sayfa Seviyesinde Hata Kontrolü için Tavsiye Edilen Yol
Madde 0 Bir hata sayfası tasarlanır. :)
Madde 1 Sayfaya Page_Error olay metodu eklenir.
Madde 2 Sayfanın ErrorPage özelliğine hata sayfasının Url bilgisi Page direktifi içerisinde eklenir.
Madde 3 Page_Error olay metodu içerisinde Server sınıfının static GetLastError() metodu ile son oluşan istisna nesne örneği ele alınır.
Madde 4 İstenirse ErrorPage özelliğine burada değer ve hatta querystring yardımıyla bilgi aktarılması sağlanabilir. Böylece hata sayfasına bazı ekstra bilgilerin taşınmasıda sağlanmış olur.
Madde 4.5 Gerekirse bu aşamada loglama (özellikle sistemdeki event loglara bilgi yazma), fiziki dosyalara bilgi yazdırma, yönetici veya ilgili kişilere mail gönderme gibi işlemler yapılabilir.

Page_Error isimli olay metodu sayfa seviyesinde ele alınır. Normal şartlarda sayfada ele alınmayan bir hata oluştuğunda bu metod otomatik olarak çağırılacaktır. Biz bu metod içerisinde yönlendirmeler yaparak kullanıcıları daha akıllı hata bilgilendirme sayfalarına yönlendirebilir ve loglama gibi işlemleri gerçekleştirebiliriz. Page_Error olay metodu içerisinden ilgili hata sayfasına yönlendirme yaparken Page sınıfının ErrorPage özelliğine değer atamak gerekebilir. Bu daha çok querystring yardımıyla hata sayfasına ekstra bilgi gönderileceği durumlarda ele alınır. Aksi durumlarda metod içerisinde değilde Page direktifinde bu özelliğin değerinin belirlenmesi yeterlidir. Ancak burada dikkat edilmesi gereken bir nokta vardır. Eğer metod içerisinde ErrorPage özelliğine hata sayfasını atarken querystring kullanılmassa Asp.Net, çalışma zamanında aspxerrorpath isimli bir anahtarı ve değerini otomatik olarak ekleyecektir. Ki buda hatanın oluştuğu sayfanın yakalanabilmesi ve belkide dinamik bir linkin üretilerek tekrar geri gidilebilmesinide sağlayacaktır.

Şimdi yukarıdaki örneğimizi sayfa seviyesinde ele almaya çalışalım. Madde 0' da değindiğimiz gibi öncelikli olarak bir hata sayfası tasarlamakta fayda var. Bu hata sayfası aşağıdaki gibi tasarlanabilir.

HataSayfasi.aspx;

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        string ekBilgi = Request.QueryString["EkBilgi"];
        string sayfa=Request.QueryString["Sayfa"];
        string hataMesaji = Request.QueryString["HataMesaji"];
        Response.Write("<b>Hata sayfası : </b>" + sayfa+"<br/>");
        Response.Write("<b>Ek bilgi : </b>" + ekBilgi + "<br/>");
        Response.Write("<b>Hata Mesajı : </b>" + hataMesaji);
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Hata Sayfası</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
            </div>
        </form>
    </body>
</html>

Default.aspx sayfasındaki kodları ise aşağıdaki gibi değiştirebiliriz.

protected void Page_Error(object sender, EventArgs e)
{
    Exception olusanHata = Server.GetLastError();
    ErrorPage = "HataSayfasi.aspx?EkBilgi=" + olusanHata.Message + "&HataMesaji=" + olusanHata.InnerException.Message + "&Sayfa="+Page.AppRelativeVirtualPath;
}

protected void Hesapla_Click(object sender, EventArgs e)
{
    try
    {
        double deger1 = Convert.ToDouble(txtDeger1.Text);
        double deger2 = Convert.ToDouble(txtDeger2.Text);
        double sonuc = deger1 / deger2;
    }
    catch (Exception excp)
    {
        throw new Exception("Sayısal değer girişinde hata oluştu", excp);
    }
}

Hesapla_Click olay metodu içerisinde yer alan catch bloğunda throw anahtar sözcüğü kullanılarak bir Exception nesnesi daha fırlatılmıştır. Bu istisna nesnesinin yakalanacağı yer sayfanın Page_Error isimli olay metodudur. Bu metod içerisinde, sayfada oluşan son hatayı yakalayabilmek için Server sınıfının GetLastError metodu kullanılmıştır. Dikkat edilmesi gereken noktalardan birisi, throw ile fırlatılan Exception nesnesi örneklenirken ilk parametreye örnek bir ekstra veri konulmasıdır. Eklenen bu bilgi GetLastError ile yakalanan Exception nesne örneğinin Message özelliği ile elde edilebilir. Bu durumda orjinal istisna mesajınının nereden alınabileceği bir soru işaretidir. Cevap InnerException özelliğidir. InnerException özelliği ile fırlatılan asıl Exception nesne örneği yakalanabilir. Bunu çalışma zamanında test ettiğimizde aşağıdaki ekran görüntüsünde olduğu gibi FormatException tipinin InnerException özelliğinde saklandığını görebiliriz.

Son olarak ErrorPage özelliği ile hata sayfasına gerekli yönlendirme yapılmaktadır. Elbette ErrorPage kullanılmak zorunda değildir. Bunun yerine Server sınıfının Transfer metodu veya Response sınıfının Redirect metodlarının kullanımıda tercih edilebilir. Özellikle Server sınıfının Transfer metodu gereksiz roundtrip' lerin önüne geçilmesini sağlamakta ama url satırına bakıldığında halen daha aynı sayfada olunduğu izlenimini vermektedir.

Örnek geliştirilirken dikkat edilmesi gereken bir nokta vardır. Eğer örneği aynı makine üzerinde (localhost) test ediyorsak beklediğimiz yönlendirme sayfasına gidemediğimizi hatta eski sarı ekranın (orjinal hata mesajının basıldığı sayfadan bahsediyoruz) üretildiğini görürüz. Bunun nedeni web uygulamasının özel hata modunun aktif olmayışıdır. Bir başka deyişle web.config dosyasında yer alan customErrors elementinin mode niteliğine On değerinin verilmesi gerekir.

NOT : Aslında mode özelliğinin 3 farklı değeri vardır. RemoteOnly, On ve Off. RemoteOnly modu aktif iken özel hata sayfalarını sadece istemciler görebilir. On modunda hem istemciler hemde localhost kullanıcısı özel hata sayfalarını görebilir. Biz örneklerimizdeki sayfalarımızı aynı makine üzerinden test ettiğimiz için bu modu On olarak belirledik. Gerçek bir uygulama ortamına çıkıldığında On yerine RemoteOnly kullanılması tavsiye edilir.

<customErrors mode="On"/>

customErrors elementi içerisine error isimli alt elementlerde konulabilir. Bu element sayesinde sunucu seviyesinde meydana gelen hatalar var ise bunların sonucunda özel hata sayfalarına yönlendirmeler yapılabilir. Söz gelimi aşağıdaki bildirimleri ele alalım.

<customErrors mode="On">
    <error statusCode="404" redirect="SayfaYok.aspx"/>
</customErrors>

Buna göre sitede olmayan bir sayfa talep edilirse kullanıcılar SayfaYok.aspx' e yönlendirilirer.

NOT : Yanlız burada dikkat edilmesi gereken bir nokta vardır. Sonradan ele alacağımız gibi global.asax dosyasındaki Application_Error olayı yazılmışsa, uygulama SayfaYok.aspx' e yönlendirilmeden önce buradaki olay metoduna uğrayacaktır ki burada bir yönlendirme yapıyorsak SayfaYok.aspx yerine oraya gidilebilir. Hatta Server.ClearError metodu kullanılmışsa hata sayfasına gidilmeyedebilir. Buda sistemin istediğimiz şekilde çalışmaması anlamına gelmektedir. Gerçi Application_Error içerisinde oluşan hata örneğin sayfa yok hatası yinede yakalanabilir. Örneğin Debug modda bu aşağıdaki şekilde olduğu giri görünecektir.

Dolayısıyla bu noktalara dikkat etmekte fayda vardır.

Örneğin şu aşamada iken web uygulamasını çalıştırdıkdan sonra Giris.aspx isimli yazmadığımız bir sayfayı talep edersek aşağıdaki ekran görüntüsü ile karşılaşırır.

Özellikle SayfaYok.aspx' ten sonra gelen querystring parametresine dikkat edelim. aspxerrorpath ile gelen değer alınıp kullanıcıya daha anlamlı bir hata sayfası gösterilebilir. Biz tekrardan konumuza geri dönelim ve customErrors elementini aşağıdaki haliyle bırakalım.

<customErrors mode="On"/>

Bu değişiklikten sonra uygulama çalışma zamanında test edilirse default.aspx sayfasında hata oluştuktan sonra HataSayfasi.aspx' e gidildiği görülebilir. Tarayıcı penceresindeki url satırına dikkat edilecek olursa querystring parametreleri ve değerleride başarılı bir şekilde aktarılmıştır.

Şunu itiraf etmeliyim ki sarı ekranın görüntüsü buradakinden daha güzeldir. Dolayısıyla hata sayfaları hazırlanırken biraz daha özenilmeli, gerektiğinde projenin sahibi olan şirket standartlarına uygun olaraktan tasarlanmalı ve zengin bir bilgi sunacak hale getirilmelidir.

Oluşan hatalara ilişkin kullanıcılara bilgi verilmesi dışında, siteyi tasarlayan veya yönetenlerinde bilgilendirilmesi gerekebilir. Bu bilgilendirme farklı şekillerde yapılabilir. Örneğin var olan işletim sistemi loglarına bilgi yazılabilir. Örneğin Application loglarına. Ya da daha basit olarak fiziki bir dosyaya hatalar ile ilişkili bazı bilgiler gönderilebilir. Hatta gerektiği yerlerde çok kritik hatalar söz konusu ise ilgili kişilere mail bile gönderilebilir. Söz gelimi aşağıdaki kod parçası ile Page_Error metodu içerisinde, oluşan son hataya ait bilgi fiziki bir dosyaya eklenmektedir.

protected void Page_Error(object sender, EventArgs e)
{
    Exception olusanHata = Server.GetLastError();
    ErrorPage = "HataSayfasi.aspx?EkBilgi=" + olusanHata.Message + "&HataMesaji=" + olusanHata.InnerException.Message + "&Sayfa="+Page.AppRelativeVirtualPath;

    using (FileStream stream = new FileStream("C:\\HataLogDosyasi.txt", FileMode.Append, FileAccess.Write))
    {
        StreamWriter writer = new StreamWriter(stream);
        writer.WriteLine("Hata Zamanı " + DateTime.Now.ToString() + " Hata Sayfası " + Page.AppRelativeVirtualPath + " Hata Mesajı " + olusanHata.InnerException.Message);
        writer.Close();
    }
}

Burada basit olarak FileStream ve StreamWriter tiplerinden yararlanılarak hata bilgileri C: klasörü altındaki bir text dosyasına yazdırılmaktadır. Sonuç olarak uygulama test edildiğinden oluşturulan hata sonrasında ilgili dosyaya aşağıdaki ekran görüntüsünde olduğu gibi bazı bilgiler eklenecektir.

Page_Error metodu içerisinde hata sayfasına herhangibir şekilde yönlendirme yapılmamasına rağmen gidilmektedir. Bu metodun bir özelliğidir. Metod sonuna gelindiğinde, ErrorPage ile belirlenmiş sayfaya otomatik olarak gidilir. ErrorPage değerinin programatik olarak belirlenmesi haricinde Page direktifi içerisinde ayarlanabileceğini daha önceden söylemiştik. Bu aşağıdaki ekran görüntüsünde olduğu gibi düzenlenebilir.

Elbette metod içerisinde ErrorPage özelliği belirtilmişse Page direktifinde yapılan tanımlama geçersiz sayılacaktır.

Gelelim uygulama seviyesinde hata yönetimine. Bu durumda web uygulamasında meydana gelecek hataların ele alınabileceği bir merkez söz konusudur. Söz konusu merkez global.asax dosyası içerisinde yer alan Application_Error isimli olay metodudur. Bildiğiniz gibi global.asax dosyasında, uygulama genelini ilgilendiren bazı olay metodları yer almaktadır. Örneğin uygulama çalışmaya başladığında devreye giren Application_Start, sonlandığında çağrılan Application_End yada kullanıcıların açtıkları oturumlarda(Session) devreye giren Session_Start gibi. Dolayısıyla ilk yapılması gereken işlem web sitesine, eğer yok ise bir global.asax dosyası eklemek olacaktır. Sayfa seviyesindeki hata yönetiminde olduğu gibi, uygulama seviyesinde yapılacak hata yönetimi içinde tavsiye edilen bir yol haritası vardır ve aşağıdaki tabloda olduğu gibidir.

Uygulama Seviyesinde Hata Kontrolü için Tavsiye Edilen Yol
Madde 0 Bir hata sayfası tasarlanır. :)
Madde 1 Sayfalara Page_Error olay metodları eklenir.
Madde 2 Page_Error olay metodlarında, son olarak elde edilen istisna(Exception) nesnesinin referansı aynen metod içerisinde olduğu gibi bilinçli olarak ortama fırlatılır(throw).
Madde 3 global.asax dosyasında yer alan Application_Error olay metodu kodlanır. Bu metod içerisinde son hata bilgisi yine GetLastError metodu ile alınır.
Madde 3.5 Gerekirse bu aşamada loglama (özellikle sistemdeki event loglara bilgi yazma), fiziki dosyalara bilgi yazdırma, yönetici veya ilgili kişilere mail gönderme gibi işlemler yapılabilir. (Sistem loglarına yazma sırasında dikkat edilmesi gereken durumlardan birisi ASPNET(IIS 5.0 için) veya Network Service (IIS 6.0 için) kullanıcısının Application, System ve Security loglarına yazma hakkı olup olmadığıdır. Söz gelimi ASPNET kullanıcısının varsayılan olarak Application loglarına yazma hakkı varken System ve Security loglarına yazma hakkı yoktur. Bu nedenle ilgili kullanıcıların haklarının özellikle loglara yazma işlemleri sırasında dikkate alınması gerekebilir.)
Madde 4 Kullanıcı hata sayfasına yönlendirilmeden önce Server sınıfının ClearError metodunun çağırılması ve hataların temizlenmesi önerilir.
Madde 5 Server.Transfer metodu ile hata sayfasına yönlendirme yapılır.

Buradaki maddelerde dikkat çekici noktalardan biriside son hatanın sayfalara ait Page_Error olay metodları içerisinde tekrardan fırlatılıyor olmasıdır. Bu bir anlamda hatanın bir üst seviyeye aktarılmasıdır. Diğer taraftan bir gerekliliktir. Nitekim, hata bilinçli olarak uygulama seviyesine gönderilmesse Asp.Net çalışma ortamı(Asp.Net RunTime), hatanın ele alınması için HttpUnhandledException tipinden bir nesne örneği üretecektir. Biz hatayı kontrollü bir şekilde ele almak istiyorsak bilinçli bir şekilde fırlatma işlemini üstlenmeliyiz.

Çok doğal olarak web uygulaması içerisinde birden fazla aspx sayfası olduğu düşünülecek olursa, hepsine bir Page_Error metodu eklemek ve kodlamak (en azından GetLastError ile elde edilen istisna nesnelerini fırlatmak) uğraştırıcı ve sabrımızı test edici olabilir. Burada nesne yönelimli mimarinin avantajlarından faydalanmak çok daha akılcı bir çözüm olacaktır. Bir başka deyişle tüm sayfaların türediği bir taban sayfa (base page) içerisindeki Page_Error metodu ele alınabilir.

NOT : Alternatif bir yaklaşım olarak MasterPage kullanımı düşünülebilir. Her ne kadar MasterPage içerisine Page_Error isimli bir metod yazılabiliyor olsada, aslında metod seviyesinden throw ile exception fırlatıldığında bu metod herhangibir şekilde tetiklenmez. Eğer Application_Error olay metodu yazılmışsa doğrudan buraya düşülür ve HttpUnhandledException tipinden bir istisna nesnesi yakalanır. Bu sebepten bu tip bir durumda MasterPage içerisindeki Page_Error olay metodunun kullanımı şeklinde bir çözüm ne yazıkki söz konusu değildir.

İlk olarak Application_Error olay metodunu nasıl ele alacağımıza bakalım. Öncelikle aspx sayfalarındaki Page_Error olay metodlarından yine bir üst seviyeye hata fırlatmak gerekir. Bu nedenle örnek olarak default.aspx sayfasındaki Page_Error olay metodu aşağıdaki gibi değiştirilmelidir.

protected void Page_Error(object sender, EventArgs e)
{
    Exception olusanHata = Server.GetLastError();
    throw olusanHata;
}

Uygulama seviyesinde hata kontrolü yapılacağından ilgili olay kodunun global application class içerisinde yer alması gerekir. Bu nedenle bir adet global.asax dosyası web sitesine dahil edilmelidir. global.asax dosyasında Application_Error olay metodu içerisinde ise aşağıdakine benzer kodlamalar yapılmalıdır.

void Application_Error(object sender, EventArgs e)
{
    Exception excp = Server.GetLastError();
    // Burada loglama, dosyaya yazma, mail gönderme gibi işlemler yapılabilir.
    Server.ClearError();
    Server.Transfer("GenelHataSayfasi.aspx?EkBilgi="+excp.Message+"&HataMesaji="+excp.InnerException.Message);
}

Bu sefer global.asax dosyası içerisinden GenelHataSayfasi.aspx isimli web sayfasına querystring yardımıyla bilgi taşınmaktadır. GenelHataSayfasi.aspx içeriği, HataSayfasi.aspx' e benzemekle birlikte tek farkı hatanın meydana geldiği sayfa bilgisini ele almıyor oluşudur. Eğer uygulama bu haliyle test edilir ve yine Default.aspx içerisinde hata yaptırılırsa aşağıdaki ekran görüntüsü ile karşılaşılır.

Görüldüğü gibi default.aspx içerisindeki metodda oluşan hata catch bloğunda ek bilgi ile tekrar throw edilmiş ve bunu sayfanın Page_Error olay metodu yakalamıştır. Sonrasında ise Page_Error olay metodunda hata tekrar throw ile fırlatılmış ve bunun sonucunda Application_Error olay metoduna gidilebilmiştir. Aşağıdaki grafikte bu durumun Debug moddaki karşılığı gösterilmektedir.

Önce Metod içi;

Sonra Page_Error;

Son olarak Application_Error;

Çalışma sonrasında dikkati çeken noktalardan birisi tarayıcı penceresindeki url satırında Default.aspx' in görünüyor olmasıdır. Halbuki şu anda üzerinde bulunulan sayfa GenelHataYonetimi.aspx' dir. Bunun sebebi Server.Transfer metodudur. Bunun yerine Response.Redirect' te tercih edilebilir. Ama daha öncedende belirtildiği üzere Transfer metodu ile sunucuya doğru gidiş gelişler azaldığı için özellikle uygulama seviyesindeki hata yönetiminde tercih edilen bir tekniktir. İlgili hata sayfası dahada geliştirilebilir. Örneğin hatanın oluştuğu sayfaya dönülmesi için gerekli bağlantı bilgileri hata mesajı ile birlikte taşınabilir. Bu ve dahası tamamen sizlerin hayal gücüne kalmaktadır.

Böylece geldik bir makalemizin daha sonuna. Bu makalemizde Asp.Net uygulamalarında etkin hata yönetimi adına metod, sayfa ve uygulama seviyesinde neler yapabileceğimizi incelemeye çalıştık. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Örnek Uygulama için Tıklayın

Kendi WebPart Kontrolümüzü Geliştirmek - 2

Cuma, 20 Nisan 2007 20:31 by bsenyurt
Kendi web partlarımızı nasıl geliştirebileceiğimizi ve bu sayede kişiselleştirilebilir web sunucu kontrollerini nasıl yazabileceğimizi bu konu ile ilgili bir önceki makalemizde incelemeye çalışmıştık. Bu makalemizde ise kendi Web Part bileşenlerimize özel fiillerin (Web Part Verbs) nasıl eklenebileceğini ve söz konusu fillerin ne şekilde ele alınabileceğini incelemeye çalışacağız. Web Part kontrollerini herhangibir WebPartZone altında kullandığımızda standart olarak bazı fiilere(Verbs) sahip oluruz. Tahmin edeceğiniz gibi bu değerler aslında WebPartManager tarafından ele alınmakta ve sayfa üzerinde uygun olan web part alanlarının (Web Part Zone) gösterilmesini sağlamaktadır. Söz gelimi kullanıcı Edit isimli fiili(Verb) seçtiğinde WebPartManager bileşeni, EditorZone kontrolünü aktif hale getirmekte ve söz konusu Web Part kontrolünün değiştirilebilen yada düzenlenebilen özelliklerinin(Properties) bulunduğu bir bileşeni (örneğin PropertyGridEditorPart kontrolü) göstermektedir. Bu açıdan bakıldığında fiillerin (Verbs) varsayılan olarak WebPartManager bileşenine ait Display Mode değerleri ile yakın bir ilişkide olduklarını söyleyebiliriz. Elbette farklı şekilde davranabilen fiillerde(Verbs) vardır. Örneğin Minimize fiili, bulunduğu Web Part' ın içerisinde yer aldığı WebPartZone alanının küçülmesini sağlarken, Restore fiili tekrardan eski haline getirilmesine olanak vermektedir.

Kendi Web Part bileşenlerimizi geliştirdiğimizde var olan fiillerin(Verbs) bize yetmediği durumlar söz konusu olabilir. Bu sebepten dolayı istersek kendi Web Part fiillerimizi (Web Part Verbs) oluşturabilir ve kod tarafında ele alarak farklı aksiyonların gerçekleştirilmesini sağlayabiliriz. Burada temel dayanak noktası WebPartVerb isimli sınıftır(class). Bu sınıf IStateManager isimli arayüzü uyarlayan (implement) bir tiptir. Kullanım amacı, Web Part bileşeni için özel bir fiili tanımlamaktır. Fiile ait isim (Name), açıklama (Description), seçilme durumu(Checked), resmi(ImageUrl) vb gibi bilgileri içerisinde barındıran bir sınıftır. Kullanıcılar bu fiili seçtiklerinde bir aksiyonun gerçekleştirilmesi gerektiği açık bir şekilde ortadadır. Buda doğal olarak bir metodun çalışma zamanında (run time) tetiklenmesi anlamına gelmektedir. İşte bu sebeple tanımlanan fiilerin gerçekleşmesi halinde çalıştırılacak olan metodları işaret eden WebPartEventHandler temsilcilerinden (delegates) faydalanılmaktadır. Bu temsilcinin prototipi ise aşağıdaki gibidir.

public delegate void WebPartEventHandler(object sender, WebPartEventArgs e);

Dikkat ederseniz WebPartEventHandler temsilcisi standart bir olay temsilcisidir. İkinci paramete olay metoduna bazı bilgileri taşımakta olan WebPartEventArgs sınıfına ait bir nesne örneğidir. Bu tipte doğal olarak EventArgs sınıfından türetilmiştir. İlk parametre ise fiili gerçekleştiren referansın bir başka deyişle WebPartVerb nesne örneğinin taşıyıcısıdır.

NOT : Hatırlayalım ; Temsilciler (Delegates) çalışma zamanında metodların bellek üzerindeki başlangıç adreslerini işaret eden tiplerdir(types). Tanımlandıklarında, işaret edebilecekleri metodun yapısınıda (parametreleri ve dönüş tipi) belirtirler. Olay tabanlı programlamada (Event based programming), Asenkron (Asynchronous) mimaride yer alan Polling, Callback, WaitHandle gibi modellerde, çok kanallı uygulamalarda (Multi Thread Applications) kullanılmaktadırlar

Bize gereken tiplerin neler olduğunu öğrendik. Peki bunları kendi Web Part kontrolümüzde nasıl ele alacağız. Bunun için WebPart sınıfından türetme yoluyla kendi Web Part kontrol sınıfımıza gelen Verbs isimli özelliğin ezilmesi (override) gerekmektedir.

public virtual WebPartVerbCollection Verbs { get; }

Yukarıda prototipi görünen bu özellik, yanlız okunabilir(read only) bir özelliktir ve geriye WebPartVerbCollection tipinden bir referans döndürmektedir. WebPartVerbCollection sınıfı türlendirilmiş (strongly typed) bir koleksiyonu temsil etmekte ve Web Part bileşenine eklenecek ekstra fiileri taşımaktadır. Dolayısıyla bu özelliğin get bloğu içerisinde istediğimiz fiileri(Verbs) oluşturmamız ve gereken olay metodu yüklemelerini yapmamız gerekecektir.

Bu makalemizde özellikle üzerinde duracağımız konu kendi fiillerimizi nasıl yazacağımızdır. Konuyu daha iyi anlayabilmek için örnek bir senaryo üzerinden hareket edeceğiz ve göze hoş gelecek bir Web Part kontrolü geliştirmeye çalışacağız. Bu sefer bir önceki Web Part bileşenimizden farklı olarak, Render metodu yerine CreateChildControls metodunu ezip, bileşen içindeki kontrollerin daha kolay bir şekilde nasıl oluşturulabileceğini de göreceğiz. Dilerseniz amacımızdan bahsederek örneğimizi geliştirelim. Sitemizde çeşitli kategorilerde duvar kağıtları(Wallpapers) olduğunu düşünelim. Siteye giren kullanıcılar seçtikleri kategorideki duvar kağıtlarından kaç tane istiyorlarsa görebilecekler ve istediklerine tıkladıklarında büyük versiyonlarına bakıp bilgisayarlarına indirebilecekler. Bu senaryda Web Part kullanacağımız için resim sayısı ve kategori gibi bilgileri kişselleşetirme(Personalization) şansınada sahip olacağız. Web Part kontrolümüz, seçilen kriterlere göre, kontrolün sayfaya her çizilişinde rastgele resimler seçecek ve bunları gösterecektir. Peki  kendi fiillerimizi bu senaryo içerisine nasıl katabiliriz? Kullanıcıların isterlerse Web Part kontrolüne eklenen resimleri yatay veya dikey düzende görebileceklerini göz önüne alalım. Bunun için Yatay Diz ve Dikey Diz başlıklı örnek fiilleri Web Part kontrolümüze ekleyip, resimlerin sayfa üzerindeki diziliş yönlerini belirleyebiliriz. Üstelik bu fiillerin değerlerini kişiselleştirirsek, sayfayı son bıraktığımız haliyle elde edebiliriz. Örnek Web Part kontrolümüzü bitirdiğimizde aşağıdaki ekran görüntülerindekine benzer sonuçlar elde edeceğiz.

Örnek olarak Uçak kategorisinde her ziyaretimizde rastgele 3 resmin yanyana gösterilmesi;

Web Part kontrolümüz için geliştireceğimiz fiiller(Verbs);

Dikey Diz başlıklı Verb seçildiğindeki durum;

Artık kontrolümüzü geliştrimeye başlayabiliriz. Web Part bileşenimizi yine bir Web Control Library kütüphanesinde ele alabiliriz. ResimPart adlı Web Part sınıfımızın WebPart sınıfından türemesi(Inherit) gerektiğini hatırlayalım. Kontrolümüz kendi içerisinde kişiselleştirilebilir (Personalizable) 3 özellik barındırmalıdır. Bunlardan birisi ziyaretçinin görmek istediği resim sayısını tutan GosterilecekResimSayisi özelliğidir. Ziyaretçinin görmek istediği kategorinin bilgisini ise ResimKategori isimli özellik ile tutabiliriz. Son olarak ziyaretçinin seçtiği fiile (Verb) uygun olacak şekilde bir değişkenin de kişiselleştirilmesi önemlidir ki bir sonraki ziyarette son bıraktığımız haliyle bir dizilim elde edebilelim. Bu amaçlada ziyaretçinin son seçtiği fiili kişiselleştirilebilir bir özellik olacak şekilde CizimYonu ismiyle saklayacağız. Dilerseniz bahsetmiş olduğumuz özellikleri(Property) aşağıdaki gibi yazarak makalemize devam edelim.

[ToolboxData("<{0}:ResimPart runat=server></{0}:ResimPart>")]
public class ResimPart:WebPart
{
    #region Kişiselleştirilebilir özellikler için alan tanımlamaları

    private ResimKategorisi _kategori;
    private int _gosterilecekResimSayisi;
    private Yon _cizimYonu;

    #endregion

    #region Kişiselleştirilebilir Özellikler

    [WebBrowsable(true)]
    [WebDescription("Bakmak istediğimiz resimlerin kategorisi")]
    [WebDisplayName("Resim Kategorisi")]
    [Personalizable(PersonalizationScope.User, false)]
    public ResimKategorisi Kategori
    {
        get { return _kategori; }
        set { _kategori = value; }
    }

    [WebBrowsable(true)]
    [WebDescription("Seçilen kategoride gösterilecek resim sayısı")]
    [WebDisplayName("Resim Sayısı")]
    [Personalizable(PersonalizationScope.User, false)]
    public int GosterilecekResimSayisi
    {
        get{return _gosterilecekResimSayisi <= 0 ? 1 : _gosterilecekResimSayisi;}
        set{_gosterilecekResimSayisi = value <= 0 ? 1 : value;}
    }

    [WebBrowsable(true)]
    [WebDisplayName("Resimlerin Yönü")]
    [WebDescription("Resimler dikey veya yatay gösterilebilmesini sağlar")]
    [Personalizable(PersonalizationScope.User, false)]
    public Yon CizimYonu
    {
        get{return _cizimYonu;}
        set{_cizimYonu = value;}
    }

    #endregion
}

Bir önceki makalemizden de hatırlayacağınız gibi, özelliklerimizi kişiselleştirmek için gerekli nitelikler ile işaretliyoruz. Resimlerin yönünü ve var olan resim kategorilerini birer enum sabiti içerisinde saklamaktayız.

NOT : Enum sabitleri bu senaryoda oldukça işe yarar. Ne varki kategori isimlerinin değiştiği yada yeni kategorilerin eklendiği durumlarda koda girip güncelleme yapmak ve uygulamayı tekrardan build etmek gerekecektir. Alternatif bir yol olarak bu tip bir verinin kod dışında bir ortamda, örneğin bir Xml dosyasında veya veritabanındaki bir parametre tablosunda saklanması göz önüne alınabilir.

Normal şartlarda resimlerin kategorisi için daha farklı bir yöntem izlemek sağlıklı olacaktır. Kendi sistemimizde, aşağıdaki ekran görüntüsünde yer alan klasör yapısını baz alacak bir enum sabiti ele alınmaktadır. Resimlerin bir küçük birde orjinal hallerini tutmak için klasörleme mantığını kullanıyoruz. Buna göre söz konusu resimleri kategori adları şeklinde olan klasörler içerisinde yer alan big ve small alt klasörlerinde ayrıştırmış durumdayız. Web Part kontrolümüz küçük boyutlu resimleri small isimli klasörler altından çekerken, üzerlerine tıklandığında orjinal büyüklüklerindeki versiyonları ise boş bir tarayıcı penceresinde açacak şekilde big isimli alt klasörlerden çekmektedir.

Buna göre kullanıcının enum sabiti yardımıyla seçtiği kategorideki resimleri görebilmesi için, klasör adı ile enum sabiti adının aynı olması gerekir. Bu durumda yanlışlıkla klasörün isminin değiştirilmesi sonucu sistem beklediğimiz şekilde çalışmayacaktır. Özellikle istenen kategoriye bağlı klasör bulunamayacağından çalışma zamanı istisnaları (run time exception) alınması kaçınılmazdır. Bu durumun önüne geçmek için neler yapılabileceğini düşünmekte fayda olacağı kanısındayım. Bize yardımcı olacak enum sabitlerimiz aşağıdaki gibidir.

public enum ResimKategorisi
{
    araba,
    ucak,
    manzara,
    komik
}

public enum Yon
{
    DikeyYon,
    YatayYon
}

Şimdide Web Part kontrolümüz için gerekli fiillerimizi (Verbs) geliştirelim. Hatırlayacağınız gibi makalemizin başında bu iş için Verbs özelliğini ezmemiz(override) gerektiğini söylemiştik. Bu özellik içerisinde tanımlayacağımız WebPartVerb tipinden nesne örneklerinin yapıcı metodları(constructors) içerisinde, ilgili fiil(Verb) seçildiği zaman çalıştırılacak olan metodu işaret edecek bir temsilci tanımı yapılmaktadır. Bunları hesaba katarak Web Part kontrolümüzün içeriğini ilk aşamada aşağıdaki gibi geliştirebiliriz.

WebPartVerb vrbYatay, vrbDikey;

public override WebPartVerbCollection Verbs
{
    get
    {
        vrbDikey = new WebPartVerb("DikeyDizilim", new WebPartEventHandler(DikeyDiz));
        vrbYatay = new WebPartVerb("YatayDizilim", new WebPartEventHandler(YatayDiz));

        vrbDikey.Text = "Dikey Diz";
        vrbYatay.Text = "Yatay Diz";

        WebPartVerb[] verbs = new WebPartVerb[2];
        verbs[0] = vrbDikey;
        verbs[1] = vrbYatay;
        WebPartVerbCollection verbCollection = new WebPartVerbCollection(verbs);

        return verbCollection;
    }
}

public void YatayDiz(object sender, WebPartEventArgs e)
{
    CizimYonu = Yon.YatayYon;
}
public void DikeyDiz(object sender, WebPartEventArgs e)
{
    CizimYonu = Yon.DikeyYon;
}

Şimdi neler yaptığımıza kısaca bakalım. Kendi yazacağımız fiillerimizi WebPartVerb tipinden tanımladıktan sonra get bloğu içerisinde oluşturmaktayız. Bu işlemi yaparken ikinci parametre ile bir WebPartEventHandler temsilci örneği tanımladığımıza ve DikeyDiz ile YatayDiz isimli metodları işaret ettiğimize dikkat edelim. Buna göre, kullanıcılar bu fiillerden birisine tıkladığında YatayDiz ve DikeyDiz isimli metodlar çalışacaktır. Bu metodların içerisinde Web Part kontrolümüz için tanımladığımız CizimYonu özelliğinin değerini berlilemekteyiz. Oluşturulan WebPartVerb kontrollerini bir dizi içerisinde topladıktan sonra bir WebPartVerbCollection koleksiyonunun üretilmesinde kullanıyoruz. Son olarak get bloğundan bu koleksiyonu geri döndürmekteyiz. Peki fiilin seçilmesi sonucunda resimleri yatay veya dikey olarak nasıl yerleştireceğiz?

Sonuç itibariyle seçilen resimlerin ekrana alınması ve belirli bir yöne doğru çizilmesi demek, Web Part kontrolünün ekrana çizilmesi sırasında (Render) uygun HTML takılarının(Tag) oluşturulması demektir. Hatırlayacağınız gibi bir önceki makalemizde bu iş için Render metodunu kullanmıştık. Render metodu dışında var olan Web sunucu kontrollerinden yararlanaraktanda aynı işlemi gerçekleştirebilmekteyiz. Sonuç itibariyle Web Part kontrolleride birer taşıyıcı (Container) olduğundan bir Controls koleksiyonuna sahiptir. Dolayısıyla sunucu kontrollerini oluşturup bu koleksiyona ekleyerek HTML elementleri ile fazla uğraşmadan render işlemlerini gerçekleştirebiliriz. Web Part kontrollerinde bu işlem için tek yapmamız gereken CreateChildControls metodunu ezmek olacaktır. Kendi örneğimiz için bu metodu aşağıdaki gibi ezebiliriz.

protected override void CreateChildControls()
{
    string sanalAdres = HttpContext.Current.Request.Url.ToString();
    sanalAdres = sanalAdres.Substring(0, sanalAdres.LastIndexOf('/'));
    string sanalResimAdresi = sanalAdres + ("/images/") + Kategori.ToString();
    string fizikiKlasor = HttpContext.Current.Request.PhysicalPath;
    fizikiKlasor = fizikiKlasor.Substring(0, fizikiKlasor.LastIndexOf('\\'));
    int siraNo = 0;

    try
    {
        FileInfo[] resimDosyalari = DosyalariAl(fizikiKlasor);
        Random rnd = new Random();
        Table tablo = new Table();

        if (CizimYonu == Yon.DikeyYon)
        {
            for (int i = 0; i < GosterilecekResimSayisi; i++)
            {
                TableRow satir = new TableRow();
                siraNo = rnd.Next(0, resimDosyalari.Length);
                TableCell hucre = HucreOlustur(sanalResimAdresi, siraNo, resimDosyalari);
                satir.Cells.Add(hucre);
                tablo.Rows.Add(satir);
            }
        }
        else if (CizimYonu == Yon.YatayYon)
        {
            TableRow satir = new TableRow();
            for (int i = 0; i < GosterilecekResimSayisi; i++)
            {
                siraNo = rnd.Next(0, resimDosyalari.Length);
                TableCell hucre=HucreOlustur(sanalResimAdresi, siraNo, resimDosyalari);
                satir.Cells.Add(hucre);
            }
            tablo.Rows.Add(satir);
        }
        Controls.Add(tablo);
    }
    catch
    {
    }
}

private static TableCell HucreOlustur(string sanalResimAdresi, int siraNo, FileInfo[] resimDosyalari)
{
    string miniResimDosyaAdresi = sanalResimAdresi + "/small/" + resimDosyalari[siraNo].Name;
    string buyukResimDosyaAdresi = sanalResimAdresi + "/big/" + resimDosyalari[siraNo].Name;
    TableCell hucre = new TableCell();

    string resim = "<a href='" + buyukResimDosyaAdresi + "' target='_blank'><img src='" + miniResimDosyaAdresi + "'/></a>";
    hucre.Text = resim;
    return hucre;
}

private FileInfo[] DosyalariAl(string fizikiKlasor)
{
    DirectoryInfo fizikiKlasorBilgisi = new DirectoryInfo(fizikiKlasor + "\\images\\" + Kategori.ToString() + "\\small\\");
    FileInfo[] resimDosyalari = fizikiKlasorBilgisi.GetFiles();
    return resimDosyalari;
}

Burada kendimize göre bir algoritma geliştirdik. Temel olarak CreateChildControls metodu seçilen fiile göre yatay veya dikey dizilime uygun olacak şekilde bir HTML Table üretmekte ve Controls koleksiyonuna eklemektedir. Burada söz konusu olan Table, TableRow ve TableCell tipleri yönetimli kod(managed code) tarafında geliştirilmiş sunucu bileşenleridir ve çalışma zamanında üretilen sayfa içerisinde HTML Table, HTML TR (Satır), HTML TD (Hücre) elementlerine dönüştürülmektedir. Özel olarak, resimlerin küçük hallerini göstermek ve üzerlerine tıklandığında orjinal boyutlarında açmak için a href ve img HTML elementlerinden faydalanılmaktadır. Sizler bu kod parçasını daha efektif hale getirerek (örneğin optimize ederek) daha ölçeklenebilir bir şekle döndürebilirsiniz. Artık geliştirdiğimiz WebPart bileşenini örnek bir web uygulaması üzerinde deneyebiliriz. Bu amaçla geliştireceğimiz web uygulamasının kişiselleştirmeye destek verebilmesi amacıyla Membership ayarlarını içermesi doğru olacaktır. Sonuç olarak aşağıdaki Flash animasyonunda görülen çıktıyı elde ederiz. (Flash dosyasının boyutu 180 Kb olup yüklenmesi zaman alabilir)

Yukarıdaki Flash animasyonundanda gördüğünüz gibi, sisteme giren bir kullanıcı kendisine göre istediği kategorideki resimleri gösterebilmekte ve bunları yatay veya dikey olarak dizebilmektedir. Bizim dikkat etmemiz gereken nokta kendi fiillerimizin burada işlenebilir olmasıdır. Yukarıda geliştirdiğimiz ResimPart isimli Web Part kontrolümüzde fiillerimizi ayrı olay metodlarına yönlendirdik. Dilersek tüm fiillerimizi aynı metod içerisinde ele alabiliriz. Bunun için öncelikli olarak WebPartVerb bileşenlerimizi oluştururken kullanıdığımız WebPartEventHandler temsilcilerini (delegates) aynı metodu işaret edecek şekilde oluşturmalıyız.

vrbDikey = new WebPartVerb("DikeyDizilim", new WebPartEventHandler(VerbUygula));
vrbYatay = new WebPartVerb("YatayDizilim", new WebPartEventHandler(VerbUygula));

Daha sonra VerbUygula isimli metodumuzu aşağıdaki gibi kodlamamız yeterli olacaktır.

public void VerbUygula(object sender, WebPartEventArgs e)
{
    string tetiklenenVerbId=((WebPartVerb)sender).ID;
    if (tetiklenenVerbId == "DikeyDizilim")
        CizimYonu = Yon.DikeyYon;
    else if (tetiklenenVerbId == "YatayDizilim")
        CizimYonu = Yon.YatayYon;
}

Tüm fiiller aynı olay metodu içerisinde ele alınacaklarından, olayı meydana getiren WebPartVerb nesne örneğinin kim olduğunun bilinmesi gerekmektedir. Bunun için olay metodunun ilk parametresinden yararlanılmıştır. sender isimli değişken, olay metodu içerisinde WebPartVerb tipine dönüştürülmüş ve ID özelliğinin değerine bakılarak CizimYonu isimli enum sabitine uygun değeri atanmıştır. Uygulamayı bu haliyle test ettiğimizde ilk versiyondaki ile aynı sonuçları elde ederiz.

Çözmemiz gereken bir durum daha vardır. Bu da hangi Verb seçildiyse bunun başına bir check işaret konulmasının sağlanmasıdır. Bu amaçla WebPartVerb sınıfının Checked özelliğinin değerinin true veya false olarak değiştirilmesi yeterlidir. Lakin asıl problem bununla ilişki kodun nereye yazılması gerektiğidir. Web sayfaları ve içerisinde yer alan kontrollerin yaşam döngüsü olayı biraz karmaşık hale getirmektedir. Senaryomuzda, işaretleme operasyonu için gerekli kod parçasının CreateChildControls metodu içerisine konulması düşünülebilir. Yada bu işlemi fiilleri ele aldığımız olay metodu içerisine koyabiliriz. Ancak hiç birisi işe yaramayacaktır. Bunun sebebi Web Part bileşenimizin, sunucu tarafındaki yaşam döngüsüdür. Dilerseniz yaşam döngüsünü inceleyerek devam edelim. Sayfa ilk kez talep edildiğinde dolayısıyla Web Part kontrolüde ilk kez oluşturulduğundaki olay işleyiş sırası göz önüne alındığında, Web Part kontrolümüzdeki bazı üyelerin işleyiş sırası aşağıdaki gibi olacaktır.

Kullanıcı bir fiil (Verb) seçtikten sonra sayfa sunucuda tekrar oluşturulup, web part kontrolümüzde yeniden oluşturulacaktır. Bu ikinci request sonrasında üylerin işleyiş sırası ise aşağıdaki gibidir.

Görüldüğü gibi her iki yaşam döngüsü sırasında son olarak Verbs isimli özelliğe girilmektedir. Dolayısıyla işaretleme kodlarını buraya dahil edebiliriz. Bununla birlikte göze çarpan bir diğer durum, ikinci talep sonrasında Verb özelliğinin iki kez devreye giriyor olmasıdır. Bu sebepten, Verb özelliğinin get bloğundaki kodlar aşağıdaki gibi optimize edilmeli ve WebPartVerb bileşenleri ile ilgili koleksiyonun oluşturulma işlemlerinin sadece bir kez yapılması garanti altına alınmalıdır.

WebPartVerb vrbYatay, vrbDikey;
WebPartVerbCollection verbCollection;

public override WebPartVerbCollection Verbs
{
    get
    {
        if (vrbYatay == null && vrbDikey == null)
        {
            vrbDikey = new WebPartVerb("DikeyDizilim", new WebPartEventHandler(VerbUygula));
            vrbYatay = new WebPartVerb("YatayDizilim", new WebPartEventHandler(VerbUygula));

            vrbDikey.Text = "Dikey Diz";
            vrbYatay.Text = "Yatay Diz";
   
            WebPartVerb[] verbs = new WebPartVerb[2];
            verbs[0] = vrbDikey;
            verbs[1] = vrbYatay;
            verbCollection = new WebPartVerbCollection(verbs);
        }
        if (CizimYonu == Yon.DikeyYon)
        {
            vrbDikey.Checked = true;
            vrbYatay.Checked = false;
        }
        else if (CizimYonu == Yon.YatayYon)
        {
            vrbDikey.Checked = false;
            vrbYatay.Checked = true;
        }

        return verbCollection;
    }
}

Artık çalışma zamanında seçtiğimiz fiillerin yanında tik işaret aşağıdaki resimdekine benzer bir şekilde çıkacaktır.

Böylece geldik bir makalemizin daha sonuna. Bu makalemizde kendi Web Part kontrollerimize özel fiilleri (WebPartVerb tipinden nesne örneklerini) nasıl ekleyebileceğimizi örnek bir senaryo üzerinden incelemeye çalıştık. Makalemizin sonlarında kontrollerin yaşam döngüsünün ne kadar önemli olabileceğini gördük. Bir sonraki makalemizde görüşmek dileğiyle hepinize mutlu günler dilerim.

Örnek Uygulama İçin Tıklayınız. (Resimlerin boyutlarının büyük olması nedeni ile Big isimli kasörlerlerdeki resim dosyaları ve App_Data altındaki ASPNETDb.mdf dosyaları silinmiştir. Test ederken bunları göz önüne almayı unutmayınız.)

Tags:  
Categories:   Asp.Net 2.0
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Kendi WebPart Kontrolümüzü Geliştirmek - 1

Çarşamba, 28 Mart 2007 20:47 by bsenyurt
Web uygulamalarında var olan bileşenlerin yetersiz kaldığı durumlarda kendi kontrollerimizi geliştirme yoluna gidebiliyoruz. Kendi kontrollerimizi geliştirirken seçebileceğimiz yollar bellidir. Var olan bir web bileşeninden türetme yolunu seçebiliriz (Inherited Controls). Bu durumda kontrolün Html çıktısının ne olacağını bir başka deyişle Render işlemlerini çok fazla düşünmemize gerek kalmaz. Tek yapmamız gereken var olan üyeleri ezmek (override) veya yeni üyeler katmaktır. Bir diğer yol birden fazla kontrolü içeren komposit bir bileşen geliştirmektir(Composite Controls). Bu tekniğe verilebilecek en güzel örnek kullanıcı web kontrolleridir (web user controls). Özel bileşen geliştirmenin belkide en zor seçeneği, kontrolü sıfırdan yazmaktır. Bu durumda, ilgili bileşenin istemci tarayıcılarındaki Html çıktısını düşünmekle kalmayıp, ViewState, Postback, Event Handling gibi temel konularında göz önüne alınması ve düşünülmesi gerekir.

Asp.Net 2.0 ile birlikte WebPart adı verilen bir Framework gelmiştir. Bu Framework istersek kolay bir şekilde kişiselleştirilebilir (Personalizable) web kontrolleri yazabilmemizede olanak sağlamaktadır. Aslında bahsettiğimiz özelleştirilmiş kontrollerin birer WebPart tipi olduğunu söylemek gerekir. Dolayısıyla, kendi WebPart bileşenlerimizi türetme yardımıyla kolay bir şekilde geliştirebilir ve kullanabiliriz. WebPart' lar sıfırdan yazılan bileşen kontrolleri ve web kullanıcı kontrollerine nazaran, WebPart Framework için tam destek sağlarlar. Bu da, kişiselleştirmenin kullanıldığı sayfalarda oldukça önemli bir meziyettir. İşte biz bu makalemizde basit olarak kendi Web Part bileşenlerimizi nasıl geliştirebileceğimizi incelemeye çalışacağız.

Normal şartlar altında sayfada yer alan herhangiri Web Part Zone içerisine alınan her bileşen otomatik olarak Web Part muamalesi görür. Lakin kendi Web Part bileşenlerimizi geliştirdiğimizde, daha öncedende belirttiğimiz gibi Web Part Framework' ü içerisindeki özelliklerin ve fonksiyonelliklerin tamamını kullanabilme şansına sahip oluruz. Öyleyse işe WebPart tipinden türeyen bir sınıf yazmak ile başlamak lazım.

NOT: WebPart tipi abstract bir sınıf olup, bir Web Part bileşeni için gerekli tüm temel alt yapıyı sunmaktadır.

Bildiğiniz gibi, Visual Studio 2005 içerisindeki proje şablonlarından biriside, Web Control Library seçeneğidir. Web Control Library, standart olarak örnek bir özel kontrol (Custom Control) sınıfı içerir. Aynı zamanda Web ortamı için gerekli temel referanslarıda hazır olarak barındırır. (Örneğin System.Web) Biz Web Part bileşenimizi böyle bir kontrol kütüphanesi içerisine dahil edersek, herhangibir web uygulamasında kolayca kullanabiliriz. Dahası, geliştirdiğimiz Web Part bileşenlerini Visual Studio ToolBox içerisinde ele alabiliriz. Bu da Web Part bileşenimizi bir kontrol olarak ToolBox içerisinde tutabileceğimiz anlamına gelmektedir.

Gelelim makalemizin örnek senaryosuna. Kendi Web Part kontrollerimizi geliştirmeyi öğrenirken, standart olarak ele alınan senaryo, RSS bilgilerini tutan bir bileşenin yazılmasıdır. Bizde geleneği bozmayıp bu tip bir Web Part bileşenini nasıl yazabileceğimizi incelemeye çalışacağız. Ancak başlamadan önce RSS ile ilişkili olarak biraz bilgi vermekte fayda olacağı kanısındayım. Günümüzde pek çok web sitesi, güncel olarak yayınlamak istedikleri bilgilerin, başkaları tarafından kolay bir şekilde ele alınabilmesi amacıyla, Xml tabanlı içerikler sunarlar. RSS bu anlamda Xml verisini standardize etmektedir. Böylece, her RSS içeriğinin aynı şemaya sahip olması sağlanmış olur. Bizde bu yaklaşımı kullanacağız. Özellikle .Net Framework, Xml üzerinde son derece etkili yönetimli tipler (managed types) sunmaktadır. Bu tiplerden faydalanarak her hangibir RSS içeriğini kolay bir şekilde ayrıştırabiliriz (parsing). Aşağıdaki ekran görüntüsünde C#Nedir? sitesinin http://www.csharpnedir.com/Rss.xml adresinden yayınlanan RSS dökümanının bir parçasını görmektesiniz.

Dikkat ederseniz, rss isimli root element içerisinde channel isimli tek bir alt boğum (childe node) vardır. Bu node içerisinde RSS dökümanın sahibi ile ilişkili çeşitli bilgiler yer alır. Örneğin, RSS konusunu anlatan başlık(title) ve açıklama(description) bilgisi, RSS sahibinin web adresi(link) gibi. Diğer taraftan item elementleri içerisindede RSS ile yayınlanmak istenen asıl içerik yer alır. Özetle bu RSS dökümanı ile çalışma zamanında C#Nedir? sitesinde yayınlanan son makalelerin listesini elde edebilir, bağlantıları kullanarak buralara geçiş yapabiliriz. Eğer bu RSS dökümanını internet üzerinden talep edersek aşağıdakine benzer bir ekran görüntüsü alırız.

İşte Web Part kontrolümüz, kişi bazında herhangibir RSS bilgisini sunacak şekilde tasarlanacaktır. Burada RSS' in Url bilgisini ve hatta RSS' in sahibi ile ilgili kısa bir bilgiyi, kişi bazında ayrı ayrı saklayabiliriz. Gelin Web Part kontrolümüzü yazarak kişiselleştirilebilir bir RSS okuyucunun nasıl yapılabileceğini görmeye çalışalım. İlk olarak Web Control Library projemize bir sınıf ekleyeceğiz. Sınıfımızın WebPart sınıfından türemiş olması gerekmektedir.

NOT : Bir sınıfı WebPart abstract sınıfından türettiğimizde, söz konusu yeni tip, Web Part Framework' ünü kullanabileceğimiz üyelerede sahip olur. Bu üyeler WebPart sınfınında türediği çeşitli tipler içerisinde toplanmaktadır. Aşağıdaki şekilde, geliştirdiğimiz örnek web part bileşeninin nesne hiyerarşisini görebilirsiniz.

İlk olarak RssPart isimli bir sınıfı WebPart tipinden türeyecek şekilde aşağıdaki gibi tanımlayalım.

[ToolboxData("<{0}:RssPart runat=server></{0}:RssPart>")]
public class RssPart : WebPart
{
}

Geliştirdiğimiz sınıf her ne kadar bir Web Part bileşeni olsada, sunucu taraflı (server side) bir kontroldür. Bu nedenle, aspx dosyalarının kaynak kısmında birer takı(tag) içerisinde ele alınacaktır. ToolboxData isimli niteliğin eklenmesinin sebebi de budur. Buna göre Web Part bileşenimizi herhangibir aspx sayfasına eklediğimizde aşağıdakine benzer bir element içeriği ile karşılaşırız.

<cc1:RssPart ID="RssPart1" runat="server"/>

Burada cc1 ön ekinin (prefix) nereden geldiğini merak ediyor olabilirsiniz. Aslında ToolBox' tan bir WebPart kontrolünü sayfaya sürükleyip bıraktığımızda otomatik olarak sayfanın başına Register direktifi eklenecektir. (Bu ilk kontrol sürüklenip bırakıldığında otomatik olarak oluşur) Bu direktif temel olarak, Asp.Net uygulamasında kullanılacak olan Web Part bileşenini içeren sınıf kütüphanesinin (Contol Library) referans bilgisinide içerecektir. Kısaca Register direktifimizinde aşağıdaki gibi olacağını söyleyebiliriz.

<%@ Register Assembly="MyWebParts" Namespace="MyWebParts" TagPrefix="cc1" %>

Gelelim Web Part kontrolümüz içerisindeki üyelere. Kontrolümüzün RSS belgesine ait Url bilgisini ve kısa bir başlık bilgisini tutacağını varsayabiliriz. Bu bilgiler için yapacağımız normal şartlar altında birer özellik yazmak olacaktır. Lakin bu sefer özelliklerimizin, kişi bazında özelleştirilebilmesini istiyoruz. Bu nedenle Personalizable niteliğini kullanmamız gerekiyor.

private string _Url;
private string _RssOwner;

[WebBrowsable(true)] // PropertyGridEditorPart içerisinde bu özelliğin gösterilip gösterilmeyeceğini belirtir.
[WebDescription("Verilen Url adresine göre Rss bilgisini okur")]
[Personalizable(PersonalizationScope.User)] // Özelliğin değerinin Membership tablolarında kullanıcı bazında tutulacağını belirtir.
[WebDisplayName("Rss Bilgisi Alınacak Url")] // PropertyGridEditorPart içerisinde Url özelliği için gösterilecek bilgi
public string Url
{
    get { return _Url; }
    set { _Url = value; }
}

[WebBrowsable(true)]
[WebDescription("Rss sahibine ait bilgiyi içerir")]
[Personalizable( PersonalizationScope.User)]
[WebDisplayName("Rss Yayımcısı")]
public string RssOwner
{
    set { _RssOwner = value; }
    get{return _RssOwner;}
}

Url ve RssOwner isimli özelliklerimiz, kişiselleştirilebilir üyelerdir. Personalizable niteliğine atanan PersonalizableScop.User değeri sayesinde Url ve RssOwner özelliklerinin işaret ettikleri değerlerin, kişi bazında Membership API' si üzerinden ilgili tablolarda tutulabileceği belirtilmektedir. WebBrowsable niteliği ile, özelliğin bir PropertyGridEditorPart bileşeni içerisinde gösterilip gösterilmeyeceğine karar verilebilir. Buna göre Url ve RssOwner isimli özelliklerimizi, sayfayı ziyaret eden kullanıcılar isterlerse PropertyGridEditorPart içerisinden değiştirebilirler. WebDescription niteliği(attribute), PropertyGridEditorPart içerisinde gösterilecek olan özelliklerin üzerlerine gelindiğinde kısa açıklama kutucukları gösterilmesini sağlar. Son olarak WebDisplayName niteliği sayesinde, özelliklerin ProperyGridEditorPart içerisinde hangi adlar ile sunulacağı belirtilmektedir.

Kişi bazında tutulacak özelliklerimizide belirttikten sonra, Web Part kontrolümüzün ekrana nasıl çizileceğini ayarlayabiliriz. Bildiğiniz gibi, her sunucu web bileşeni istemci tarafında Html çıktıları haline getirilirler (Render işlemi). Burada da, belirtilen Url adresindeki RSS dökümanını ayrıştırdıktan(parsing) sonra örnek olarak linkler halinde göstermek isteyebiliriz. Bu çıktının düzenli bir formatta olmasını sağlamak içinde Html tablolarına başvurabiliriz. Bir sunucu kontrolünde, Html çıktısını oluşturmak için Render metodu göz önüne alınabilir. Diğer taratfan CreateChildControls metoduna başvurup daha kolay bir biçimde çıktı üretebiliriz. CreateChildControls metodunu ilerleyen makalelerimizde ele almaya çalışacağız. Şimdi dilerseniz, Web Part bileşenimiz için Render metodunu ezelim (override) ve Html çıktısını, RSS içeriğine göre hazırlayalım.

protected override void Render(HtmlTextWriter output)
{
    if (!String.IsNullOrEmpty(Url))
    {
        try
        {
            XmlReader reader = XmlReader.Create(Url);

            DataSet ds = new DataSet();
            ds.ReadXml(reader);

            DataTable items = ds.Tables["item"];

            #region Render Table

            // Table elementi render edilmeden önce gerekli style attribute' ları ekleniyor
            output.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "WhiteSmoke"); // Arka plan rengi
            output.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%"); // genişlik belirleniyor
            output.RenderBeginTag(HtmlTextWriterTag.Table); // Table için açılış takısı

                output.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "Gold");
                output.RenderBeginTag(HtmlTextWriterTag.Tr); // Tr açılış takısı (satır)
                    output.RenderBeginTag(HtmlTextWriterTag.Td); // Td açılış takısı (hücre)
                        output.Write(ds.Tables["channel"].Rows[0]["title"].ToString()); // Td içerisine Rss dökümanından title bilgisi alınıyor
                    output.RenderEndTag(); // Td için kapanış takısı
                output.RenderEndTag(); // Tr için kapanış takısı

                // Rss dökümanındaki her bir item için bir Tr (Table Row) ve içerisinde bir Td (hücre) oluşturuluyor
                for (int i = 0; i < items.Rows.Count; i++)
                {
                    output.RenderBeginTag(HtmlTextWriterTag.Tr); // Tr açılış takısı
                        output.RenderBeginTag(HtmlTextWriterTag.Td); // Td açlış takısı
                            output.AddAttribute(HtmlTextWriterAttribute.Href, items.Rows[i]["link"].ToString()); // href isimli attribute sonraki satırda açılacak olan A takısına ilave edilecek. Değeri ise link alanının içeriği olacak
                            output.RenderBeginTag(HtmlTextWriterTag.A);// A takısı açılıyor
                                output.Write(items.Rows[i]["title"].ToString()); // A takısı içine title alanının değeri yazılıyor
                            output.RenderEndTag(); // A takısı kapatılıyor
                        output.RenderEndTag(); // Td takısı kapatılıyor
                    output.RenderEndTag(); // Tr takısı kapatılıyor
                }

            output.RenderEndTag();// Table' ın bitiş takısı </table>
        }
        catch
        {
            output.Write("Adres çözümlenemedi");
        }
   
        #endregion
    }
}

Render metodunda, daha önceden Web sunucu kontrolü yazmak ile ilişkili makalelerimizde kullandığımızdan biraz daha farklı bir yol tercih ettik. Html içeriğini oluştururken, .Net içerisinde yer alan kuvvetli tiplerden faydalanırsak hatalı Html takısı yazma olasılığımız daha da azalacak ve özellikle tarayıcı farklılıklarını bertaraf edeceğizdir. Bu nedenle Render metodu içerisinde, HtmlTextWriter sınıfına ait RenderBeginTag, RenderEndTag, AddAttribute, AddStyleAttribute gibi metodlardan yararlanılmıştır. Örneğimize göre içeriğimiz bir Table elementi içerisinde tek sütundan oluşan bir yapıda olacaktır. Bu amaçla bir table takısı açmak için

output.RenderBeginTag(HtmlTextWriterTag.Table);

kod satırından faydalanılmıştır. Burada RenderBeginTag, bir takının oluşturulmasını sağlarken ne çeşit bir element olacağını parametre olarak gönderdiğimiz HtmlTextWriterTag sabiti belirtmektedir. Html içerisinde açılan her takının kapatılması gerektiğini biliyoruz. Bunu kod tarafında ifade ederkende yine HtmlTextWriter sınıfının RenderEndTag metodundan faydalanmaktayız. Dikkat edilmesi gereken noktalardan biriside, Html elementlerine niteliklerin nasıl eklendiğidir. Dikkat ederseniz, bu amaçla AddStyleAttribute ve AddAttribute metodlarından yararlanılmıştır. Örneğin tablomuza arka plan rengi ve genişlik vermek için

output.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "WhiteSmoke");
output.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");

kod satırlarından faydalanılmaktadır. Hangi niteliğin ekleneceğini belirlemek için ise, HtmlTextWriterStyle sabitinden yararlanılır. AddAttribute metoduda benzer işlevselliğe sahip olmakla birlikte desteklediği nitelik tipleri daha farklıdır.

Gelelim metodun temel olarak yaptığı işe. İlk olarak kişiselleştirilebilen Url özelliğinden değer alınmakta ve XmlTextReader nesnesi elde edilmektedir. Hataların önüne geçmek amacıyla ilk olarak Url özelliğinin değerinin boş olup olmadığına bakılır. Bunun haricinde internet bağlantısının kesik olması gibi hallerde, istenen Url' den RSS bilgileri çekilemeyeceği için bir çalışma zamanı istisnası(runtime exception) alınacaktır. Bununda önüne geçmek için genel bir try-catch bloğu kullanılmıştır. Bu nedenle yükleme ve Render işlemlerini try bloğu içerisinde gerçekleştirmekteyiz. Burada Url adresinden elde edilen Xml içeriğini okumak için farklı yollarda tercih edilebilir. Örneğin doğrudan XmlReader üzerinden hareket edilebilir veya XPathNavigotor tipinden faydalanılabilir ki bunlar performans açısından daha etkili yollardır. Biz kodu çok fazla karmaşıklaştırmamak adına doğrudan DataSet kontrolüne alıyor ve pahalı bir maliyetinde altına giriyoruz :) DataSet içerisinde yer alan channel ve item tablolarından faydalanarak, RSS içeriğindeki bilgilere erişebiliriz. Örneğin RSS sahibinin belirttiği başlığı (title), Html tablomuzun ilk satırındaki ilk hücreye alıyoruz. Her bir item elementinin belirttiği, başlık ve adres bilgilerini ise sırasıyla title ve link elementlerinden alıp bir a href elementi içerisinde gösteriyoruz. Bildiğiniz gibi a href elementi bir link oluşturulmasını sağlamaktadır.

Web Part kontrolümüzün, WebPart sınıfından gelen pek çok özelliğinide istersek ezebiliriz (override). Örneğin, Web Part bileşenimizin başlık bilgisi(title), imge bilgisi (title icon image) gibi değerlerini değiştirmek isteyebiliriz. Lakin bu tip üyelerin değerlerinin kontrolün Html çıktısının üretilmesinden önce atanması gerekir. Bu amaçla yapıcı metoddan, özelliğin set bloğundan yada kontrole ait PreInit olay metodundan faydalanabiliriz. Geliştirdiğimiz örnekte biz yapıcı metod ve özellik(property) kullanmayı tercih edeceğiz. Bu amaçla sınıfımıza aşağıdaki üyeleri eklememiz yeterli olacaktır.

public override string Title
{
    get
    {
        return _RssOwner;
    }
    set
    {
        _RssOwner= value;
    }
}

public RssPart()
{
    TitleIconImageUrl = "Bilgi.gif";
}

Yukarıdaki kod parçasında dikkat ederseniz, WebPart sınıfında yer alan Title özelliği ezilmektedir ve kullanıcı tarafından kişiselleştirilebilen RssOwner özelliğinin kullandığı _RssOwner alanının değerini işaret etmektedir. Diğer taraftan, Web Part bileşenimizde hemen başlık kısmının yanında sembolik bir imge göstermek amacıyla yapıcı metod(constructor) içerisinde TitleIconImageUrl özelliğine bir değer ataması yapılmıştır. Örnek bir Asp.Net sayfasında Web Part bileşenimizi kullandığımızda tasarım zamanında iken, aşağıdakine benzer bir görüntü ile karşılaşırız.

Burada yer alan Bilgi.gif isimli resim, Web Control Library içerisinde yer almaktadır ve herhangibir Asp.Net projesinde ilgili Web Part bileşeni kullandığında otomatik olarak root klasör içerisine taşınacaktır. Bir başka deyişle eklenen ilk kontrol ile birlikte gelen web kontrol kütüphanesi referansı, beraberinde kaynak olarak bu resim dosyasınıda hedef web uygulaması içerisine taşıyacaktır. Yapılan bu son ekelemeler ile birlikte Web Part bileşenimizin yeni hali aşağıdaki sınıf diagramında gösterildiği gibi olacaktır.

Artık Web Part bileşenimizi bu haliyle test edebiliriz. Kişiselleştirmenin tam olarak etkilerini görebilmek için, bileşenimizi Membership ayarları yapılmış örnek bir Asp.Net Web uygulaması üzerinden test etmekte fayda olacaktır. Testleri gerçekleştirmek için ekran görüntüsü aşağıdaki gibi olan bir web sayfasından yararlanabiliriz.

Unutulmamalıdır ki, geliştirdiğimiz Web Part bileşeninin, Web Part Framework özelliklerini etkin bir şekilde kullanabilmesi için bir Web Part Zone içerisinde yer alması gerekir. Bu nedenle, RssPart isimli Web Part bileşenimizi zoneRss isimli bir WebPartZone kontrolü içerisinde ele alıyoruz. Diğer taraftan, Web Part kontrolümüz içerisinde yer alan Url ve RssOwner gibi çalışma zamanında kişiselleştirilebilir özelliklerini ele alabilmek için bir Editor Zone kontrolümüzde yer almaktadır. Örneğimizde sadece editör kısmını göz önüne alacağımızdan, sayfanın Page_Load olay metodu içerisinde WebPartManager bileşenimizin DisplayMode özelliği aşağıdaki gibi EditDisplayMode olarak ayarlanmıştır. Gerçek bir projede elbetteki kullanıcının diğer modlarıda seçebileceği şekilde kod yazmak gerekir.

wpmYonetici.DisplayMode = WebPartManager.EditDisplayMode;

Sonuç olarak uygulamamızı çalıştırdığımızda, aşağıdaki Flash animasyonunda yer alana benzer bir sonuç ile karşılaşırız.(Flash dosyasının boyutu 380 Kb tır. Bu nedenle yüklenmesi zaman alabilir.)

Dikkat ederseniz, iki ayrı kullanıcı için iki ayrı RSS bilgisi ele alınabilmektedir. Burak isimli kullanıcı kendisi için Msdn' e ait RSS içeriğini tutarken, Melike isimli kullanıcıda C#Nedir? sitesine ait RSS bilgisini ele alabilmektedir. Bu bilgilerin arka tarafta nereye yazıldığını kontrol etmek istersek, Membership bilgilerinin tutulduğu veritabanında (ki örneğimizde local AspNetDb.mdf dosyası kullanılmaktadır. Bir başka deyişle üyelik bilgileri web uygulamasın ait App_Data klasöründe yer alan AspNetDb.mdf veritabanı içerisinde saklanmaktadır.) yer alan AspNet_PersonalizationPerUser tablosuna bakmamız yeterli olacaktır.

Kendi Web Part bileşenlerimizi geliştirirken başka üst sınıf özelliklerini ezebilir ve istediğimiz şekilde çalışmalarını sağlayabiliriz. Aynı zamanda, bir Web Part bileşenine çalışma zamanında ele alacağı yeni fiili aksiyonlar (verb) ekleyebiliriz. Bu ve benzeri diğer konuları ilerleyen makalelerimizde ele almaya çalışacacağız. Böylece geldik bir makalemizin daha sonuna. Bu makalemizde basit olarak kendi Web Part bileşenlerimizi nasıl yazabileceğimizi incelemeye çalıştık. Bunun için,

  • kontrolümüzü tanımlayacak olan sınıfı WebPart isimli abstract sınıftan türetmemiz gerektiğini,
  • içeride kullanıcı bazında kişiselleştirme yapacağımız özellikler için Personalizable niteliğini (attribute) kullanmamız gerektiğini,
  • kontrolün kişiselleştirilebilir özelliklerinin çalışma zamanında ele alınması için PropertyGridEditorPart kullanmamız gerektiğini,
  • özelliklerin PropertyGridEditorPart içerisinde gözükmesi için WebBrowsable niteliğinin kullanılması gerektiğini,
  • istersek var olan Web Part özelliklerini ezerek değiştirebileceğimizi,
  • her zaman olduğu gibi kontrolün Html çıktısını manuel olarak düşünememiz ve yazmamız gerektiğini,

öğrendik. Bir sonraki makalemizde görüşmek dileğiyle hepinize mutlu günler dilerim.

Örnek Uygulama İçin Tıklayınız. (Dosya boyutunun küçülmesi için, App_Data klasörüne atılan Aspnetdb.mdf dosyası silinmiştir. Test ederken burada bir AspNetdb.mdf dosyası oluşturmanı gerekecektir. Bunun için Asp.Net Web Site Administration Tool' dan faydalanabilirsiniz.)

Tags:  
Categories:   Asp.Net 2.0
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share