Makale Özeti

Bu yazıda ASP.NET sayfalarının HTML çıktılarına programatik olarak nasıl erişebileceğimizi ve bu çıktıları nasıl değiştirebileceğimizi göreceğiz.

Makale

Bir web sayfasının HTML çıktısını bazı özel durumlarda programatik olarak elde etmek veya değiştirmek isteyebiliriz. ASP.NET'in sayfa yapısı ve HTML oluşturma şekili ilk bakışta biraz karmaşık görünse de, gelişmiş özellikleri sayesinde bir web sayfasına ait HTML çıktıyı programatik olarak ele almamıza ve değiştirmemize olanak sağlamaktadır. Peki neden HTML çıktıyı değiştirmek isteyelim? Birkaç mantıklı nedeni şöyle sıralayabilirim:

- ASP.NET'in oluşturduğu bazı HTML kodlarına tasarım aşamasında veya sunucu taraflı kodlama ile fazla müdahale edemeyiz. Ancak bazı durumlarda belirli HTML elementlerinin veya bir sunucu kontrolünün oluşturduğu HTML kodunu elde etmek, değiştirmek isteyebiliriz. Örneğin ViewState nesnesinin sayfadaki yerini değiştirmek bu durum için güzel bir örnektir.

- Bir uygulamada fazla sayıda sayfa varsa, sayfaların tamamında belirli değişiklikler yapmak isteyebiliriz. Örneğin HTML standartlarına uymayan bir yazım şeklini farkedip, sonradan tüm sayfalardaki bu hatalı yazımı kolayca düzeltmek...

- Belirli ifadelerin geçtiği sayfalarda değişiklik yapmak istenebilir. Örneğin Ana Sayfa kelimelerinin geçtiği sayfalarda bu ifadeyi Anasayfa olarak değiştirmek...

Bu tip durumlarda tek tek dosyaları güncellemek yerine daha hızlı ve dinamik bir çözüm yolu aranabilir. HTML çıktıyı değiştirmenin yanında, çıktıyı belirli bir kaynağa(fiziksel olarak bir konuma veya veritabanına) kaydetmek de istenilebilir. Özellikle raporlama amaçlı oluşturulan sayfalarda, belirli zaman aralıklarıyla HTML çıktılarını kaydetmek ve ilerleyen tarihlerde incelemek gibi taleplerle karşılaşılabilir. Aslında bu tip ihtiyaçlar duruma göre, geliştirilen projeye göre çok farklı şekillerde günlük hayatımızda karşımıza çıkabiliyor.

Bu tip durumlarda tek tek dosyaları güncellemek yerine daha hızlı ve dinamik bir çözüm üretmek isteyebiliriz. HTML çıktıyı değiştirmenin yanında, çıktıyı belirli bir kaynağa(fiziksel olarak bir konuma veya veritabanına) kaydetmek de isteyebiliriz. Özellikle raporlama amaçlı oluşturulan sayfalarda, belirli zaman aralıklarıyla HTML çıktılarını kaydetmek ve ilerleyen tarihlerde incelemek gibi taleplerle karşılaşılabilir. Aslında bu tip ihtiyaçlar duruma göre, geliştirilen projeye göre çok farklı şekillerde günlük hayatımızda karşımıza çıkabiliyor.

ASP.NET uygulamalarında, bir sayfanın HTML çıktısına Response.Filter özelliği ile erişebiliriz. Bu özellik System.IO.Stream tipinden bir değer döndürmektedir. Her ne kadar Response.Filter gibi yazımı çok kolay olan bir yolla HTML çıktıya erişiyor gibi görünse de, bu çıktıya erişmek, hatta çıktıyı değiştirmek için biraz daha zahmetli bir yol izlememiz gerekecektir. Stream nesnesi ile dosya okuma tecrübesi olanlar ne demek istediğimi daha iyi anlayacaktır. Örneğimize başlamadan önce basit bir sayfa tasarlayıp oluşturacağı HTML çıktıyı inceleyelim. Aşağıda örnek sayfamızın kodları yer almaktadır.

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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        Mail adresim:<br>
        ugur@nedirtv.com
    </div>
    </form>
</body>
</html>

Sayfa üzerinde dinamik olarak bir işlem yapmadığımız için <div> elementinin iç kısmı oluşacak HTML çıktıda aynen yer alacaktır. Sayfadaki amacımız <br> şeklinde yazılmış elementleri <br /> şekline getirmek ve içerisinde test123@nedirtv.com gibi @ işareti ile yazılmış mail adreslerini sayfamızı indeksleyen örümcek yazılımlardan koruma için test123 (at) nedirtv.com şekline getirmek olacak.

Çıktıyı değiştirmeden önce çıktıyı nasıl elde edip, string bir değişkene atabileceğimize bakalım. Response.Filter özelliği Stream tipinden bir nesne taşımaktadır. Bu nesnenin içeriğini elde edebilmek için özel bir Stream nesnesi yazmak ve Response.Filter'ın içeriğini bu nesne üzerinden istemciye gitmesini sağlamamız gerekir. Dolayısıyla ilk olarak Filter özelliğine atayabileceğimiz tipten, yani Stream sınıfından kalıtılmış bir sınıf yazacağız. Aşağıda HtmlFilterStream adındaki özel sınıfımıza ait kodlar yer almaktadır. Kodlar kalabalık görünse de, makalenin ilerleyen kısımlarında sadece Flush ve Write metotlarının içeriklerinde değişiklikler yapacağız.

App_Code/HtmlFilterStream.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
 
public class HtmlFilterStream : Stream
{
    Stream _baseStream;
    long _position;
    string _html = "";
 
    public HtmlFilterStream(Stream stream)
    {
        _baseStream = stream;
    }
 
    public override bool CanRead { get { return true; } }
    public override bool CanSeek { get { return true; } }
    public override bool CanWrite { get { return true; } } 
    public override long Length { get { return 0; } }
 
    public override long Position
    {
        get { return _position; }
        set { _position = value; }
    }
 
    public override void Write(byte[] buffer, int offset, int count)
    {
        _baseStream.Write(buffer, 0, buffer.Length);
    }
 
    public override int Read(byte[] buffer, int offset, int count)
    {
        return _baseStream.Read(buffer, offset, count);
    }
 
    public override long Seek(long offset, SeekOrigin origin)
    {
        return _baseStream.Seek(offset, origin);
    }
 
    public override void SetLength(long value)
    {
        _baseStream.SetLength(value);
    }
 
    public override void Flush()
    {
        _baseStream.Flush();
    }
}

Sınıfımızı Stream sınıfından kalıttığımız için Flush, Length, Read, Write gibi abstract üyeleri ezmemiz(override) gerekiyor. Üyelerin içlerini yukarıdaki kodlarda görüldüğü şekilde dolduruyoruz. Response.Filter özelliğinden alacağımız nesnenin kendine ait üyeleri çağırabilmek içinse _baseStream adından bir field tanımlıyor ve sınıfın yapıcı metodunda(constructor) bu nesneyi parametre olarak alıyoruz. Bir de _html adında bir field tanımlamamız var, bu field az sonra okuma işlemlerinde kullanmamız için gerekli olacak.

Gelelim HTML çıktıyı yakalama işlemine. İlk olarak sadece çıktıyı okumaya çalışacağız. ASP.NET sayfalarının çıktıları oluşurken sayfa parçalar halinde render edilmekte ve Write metodu çalışma zamanı içerisinde birden fazla defa tetiklenebilmektedir. Her tetiklenmede HTML çıktının belirli bir parçası elde edildiği için bu metotta HTML kodlarının parçalarını birleştirmemiz gerekecek. Flush metodu ise açılan stream'e ait bilgilerin bellekten kaldırılmasından hemen önce tetikleneceği için bu metotta elde edilen HTML kodlarını tamamına erişebileceğiz. Write ve Flush metotlarında yapacağımız değişiklikler aşağıda görülmektedir.

public class HtmlFilterStream:Stream
{
    ...
 
    public override void Write(byte[] buffer, int offset, int count)
    {
        _html += Encoding.Default.GetString(buffer, offset, count);
 
        _baseStream.Write(buffer, 0, buffer.Length);
    }
 
    ... 
    public override void Flush()
    {
        string output = _html;
        _baseStream.Flush();
    }
}

Write metodunda üretilen HTML çıktıları birleştirdik ve son olarak Flush metodunda tüm çıktıyı okuduk. Eğer çıktıyı belirli bir kaynağa kaydetmek istersek Flush metodu içerisinde _html değişkeninin içeriğini kullanabiliriz. Çıktıyı yakalamak için gerekli kodları hazırladık, ancak halâ ASP.NET sayfamızın HTML çıktısını bu nesne üzerinden stream edilmesini sağlamış değiliz. Bu işlemi gerçekleştirmek için de Default.aspx dosyasının Page_Load metoduna aşağıdaki satırı eklememiz yeterli.

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Filter = new HtmlFilterStream(Response.Filter);
    }
}

Response.Filter'a ait nesne referansının yeni bir Stream nesnesi(HtmlFilterStream) olacağını belirledik. Hatırlayacağınız gibi kendi yazdığımız HtmlFilterStream nesnesini yapıcı metodunda  parametre olarak bir Stream nesnesi istiyorduk. new HtmlFilterStream(Response.Filter) ifadesiyle sayfanın HTML çıktısına ait ana stream'i yeni üretilen nesneye göndermiş olduk.

HtmlFilterStream.cs dosyasında Flush metodunun içerisindeki bir satıra breakpoint ekleyip sayfayı çalıştıracak olursak _html değişkeninde üretilen sayfanın HTML kodlarını görebiliriz.


Resim: Debug modda _html değişkeni içerisindeki HTML kodları

Write metodu HTML çıktının stream'e aktarıldığı yerdir. Dolayısıyla HTML çıktıyı değiştirmek için Write metodunda değişiklikler yapmamız gerekiyor. HTML çıktının Response'a yazıldığı kısım ise şu anki Write metodunun en alt satırında yer alan _baseStream.Write metodu aracılığıyla yapılmaktadır. Bu metodun aldığı buffer parametresi çıktıyı içerisinde saklayan byte dizisidir. O halde çıktıyı değiştirmek için öncelikle _html değişkeninde değişiklikler yapmak, sonra da bu değişiklikleri buffer dizisine aktarmak gerekecek. Aşağıda Write metodunun yeni hali görülmektedir.

...
using System.Text;
using System.Text.RegularExpressions;
 
public class HtmlFilterStream:Stream
{
    ...
 
    public override void Write(byte[] buffer, int offset, int count)
    {
        _html += Encoding.Default.GetString(buffer, offset, count);
 
        // <br> şeklinde yazılmış elementleri <br/> biçimine dönüştürüyoruz
        _html = _html.Replace("<br>""<br/>");
        
        // E-posta adreslerini değiştirmek için gerekli metin formatını belirliyoruz
        string emailPattern = @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";
 
        Regex objRegex = new Regex(emailPattern);
        MatchCollection objCol = objRegex.Matches(_html); // Formata uyan metinleri buluyoruz
 
        foreach (Match item in objCol)
        {
            //İfade içerisindeki @ karakterlerini (at) haline dönüştürüyoruz
            string newValue = item.Value.Replace("@"" (at) ");
            _html = _html.Replace(item.Value, newValue);
        }
 
        buffer = Encoding.Default.GetBytes(_html); // Güncel _html içeriğini byte dizisine çeviriyoruz
        _baseStream.Write(buffer, 0, buffer.Length); // Stream'i yeni içeriğiyle yazdırıyoruz
    }
 
}

Hatırlanacağı gibi dosyadaki amacımız <br> elementlerini <br/> şekline dönüştürmek ve e-posta adreslerinin içerisindeki @ karakterini (at) şeklinde yazdırmaktı. Bu doğrultuda iki değişiklik için gerekli düzenlemeleri yaptık. Son olarak Encoding.Default.GetBytes metodunu kulllanarak _html string değişkenindeki bilgileri buffer dizisine aktardık ve çıktıyı stream'e bastık. Sayfamızı çalıştırdığımızda değişikliklerin yapıldığını görebileceğiz.


Resim: Sağ kısımda HTML çıktının değiştiği görülmektedir

Yaptığımız HTML çıktıyı değiştirme işlemi sadece Default.aspx sayfası ile ilgiliydi. Default.aspx sayfasının Page_Load metodunda Response.Filter özelliğini yeni bir stream nesnesi üzerinden ele aldığımız için sadece bu sayfa değişiklikten etkilenecektir. Bu değişikliğin uygulamadaki tüm sayfalar için yapılmasını istiyorsak özel bir HttpModule nesnesi yazmamız veya Global.asax dosyası içerisinden ilgili Application olay metodunda(BeginRequest veya PostReleaseRequestState gibi) gerekli gerekli düzenlemeleri yapmamız gerekiyor. Aşağıda Global.asax dosyasına ekleyeceğimiz kodlar yer almaktadır.

<%@ Application Language="C#" %>
 
<script runat="server">
 
    ...
    void Application_PostReleaseRequestState(object sender, EventArgs e)
    {
        if(Response.ContentType == "text/html")             Response.Filter = new HtmlFilterStream(Response.Filter);
    }
</script>

Bu güncellemeyi yaptıktan sonra projemize farklı .aspx dosyaları ekleyip HTML çıktının düzenlendiğini gözlemleyebiliriz. İlgili olay metodunda uzantıya yönelik değil, çıktının tipine göre(text/html kontrolü) işlem yapıldığı için .ashx gibi dosyalarında çıktıları ele alınacaktır.

Bu makalede bir ASP.NET sayfasının HTML çıktısını programatik olarak nasıl elde edebileceğimizi ve değiştirebileceğimizi inceledik. Bir başka yazıda görüşmek üzere.

Not: Yazıyı hazırlarken http://www.4guysfromrolla.com/articles/120308-1.aspx adresindeki bilgilerden faydalandım.


Uğur UMUTLUOĞLU
Microsoft MVP (ASP.NET)
www.umutluoglu.com
twitter.com/umutluoglu