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.
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> [email protected] </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 [email protected] gibi @
işareti ile yazılmış mail adreslerini sayfamızı indeksleyen örümcek
yazılımlardan koruma için testmaili (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.
Not: Yazıyı hazırlarken http://www.4guysfromrolla.com/articles/120308-1.aspx adresindeki bilgilerden faydalandım.