Model bağlayıcı nedir?
Bİldiğiniz gibi ASP.NET MVC çatısında yönetici (controller) aksiyonlarına (action) gelen tüm parametreler aslen string veri tipindedir. Gelen değerin string veri tipinden, aksiyon parametresinin sahip olduğu veri tipine döndürülmesinden ve istemciden gelen değerin bu parametre içinde saklanmasından sorumlu olan sınıf(lar) model bağlayıcı (model binder) olarak isimlendirilmektedir. Karmaşık veri tipleri, listeler, koleksiyonlar, diziler, binary byte dizileri standart model bağlayıcılar tarafından tanınabilir.
MVC'de talep (request) önce MVCHandler nesnesine gelir. MVCHandler, bir yönetici fabrikası (ControllerFactory) aracılığıyla bir yönetici elde eder ve talebi yöneticiye yönlendirir. Gelen talebe göre yönlendirme (routing) kayıtlarına bakılarak ilgili yöneticinin ilgili aksiyonu ControllerActionInvoker nesnesi tarafından çağrılır. Bu çağrı sırasında hem aksiyon için kayıt edilmiş aksiyon filtrelerine (action filter) göre işlem gerçekleştirilir hem de aksiyon parametrelerine uygun bir şekilde gelen talep içerisinden veriler ayıklanır ve en başta da anlattığım gibi string veriler aksiyon parametrelerine uygun veri tiplerine döndürülerek veri atamaları gerçekleşir. Bu veri tipi dönüşümü ve veri atamalarından sorumlu olan en önemli bileşen ise bir DefaultModelBinder nesnesidir. DefaultModelBinder, IValueProvider nesnelerinden faydalanarak gelen değerleri okur ve hem standart .Net veri tiplerine sahip değerleri hem de karmaşık nesneleri oluşturabilir. MVC paketinde hazır gelmekte olan dört farklı IValueProvider nesnesi vardır. Bunlar talep geldiğinde işleme alınma sıralarına göre şu şekilde listelenebilir;
Değer Sağlayıcı |
Değer Kaynağı |
Üst Sınıf |
FormValueProvider |
Request.Form |
NameValueCollectionValueProvider |
RouteDataValueProvider |
RouteData.Values |
IDictionaryValueProvider<object> |
QueryStringValueProvider |
Request.QueryString |
NameValueCollectionValueProvider |
HttpFileCollectionValueProvider |
Request.Files |
DictionaryValueProvider<HttpPostedFileBase> |
Şimdi farklı aksiyon parametrelerine göre model bağlama nasıl çalışıyor inceleyelim.
Primitif veri tipleri ve model bağlama
public ActionResult Login(string userName, string eMail, bool persist) {}
Yukarıdaki aksiyonda talep içinden bilgilerin alınması ve aynı isimdeki parametrelere atanması DefaultModelBinder için en kolay işlemlerden biri. Burada dikkat edilmesi gereken noktalardan biri, bool persist olarak tanımlanmış parametrenin Nullable olmamasıdır. Dolayısıyla eğer DefaultModelBinder model bağlama sırasında hiçbir değer sağlayıcı nesne ile persist parametresine değer ataması gerçekleştiremezse (ki bu sadece istemciden bu isimde bir değer gelmemesi sonucunda olur) bu durumda bir InvalidOperationException ile karşılaşırız. Nullable olmayan veri tipleri için CSharp 4.0 opsiyonel parametre özelliğinden faydalanabilirsiniz. Yani yukarıdaki aksiyon tanımını;
public ActionResult Login(string userName, string eMail, bool persist = false) { … }
şeklinde yazarak, persist değeri gelmediğinde ön ayarlı değer olarak false olmasını sağlayabilirsiniz.
Kullanıcı tanımlı tipler ve model bağlama
Tanımı aşağıdaki gibi olan bir modelimiz olsun;
public class SuperModel { public string FirstName { get; set; } public string LastName { get; set; } public int Height { get; set; } public int Bust { get; set; } public int Waist { get; set; } public int Hips { get; set; } }
Bu modeli bir aksiyon parametresi olarak kullandığımız takdirde (tabi ki bir form postalama senaryosu dahilinde bu olmalı), DefaultModelBinder devreye girecek ve bir SuperModel nesnesi oluşturacak, istemciden postalanan verilerle SuperModel nesnesinin tüm özelliklerini dolduracaktır. Böylece okumak istediğiniz her değer için model.FirstName = Request.Form["FirstName"] benzeri gereksiz tekrarlamalara girmemiş olacağız.
Aşağıda SuperModel modeliyle çalışan bir aksiyon tanımı mevcut;
public ActionResult SaveModel(SuperModel superModel) { ... }
Model bağlama işleminde seçici geçirgenlik
Veritabanına süper model kayıt etmeyi sağlayan bir html formumuz olduğunu varsayalım. Sorumlu kişi bu form ile ad, soyad, boy ve beden ölçüleriyle birlikte süper model kayıtları oluşturuyor. Süper modelin boyunun sorumlu kişi tarafından kayıt edilmesini istemiyoruz. Aksiyonumuz hemen yukarıdaki şekilde tanımlanmış yani SuperModel parametresiyle çalışıyor. Height bilgisinin DefaultModelBinder tarafından algılanmasını engellemek için görünümdeki html formundan Height için oluşturulmuş olan input kontrolünü kaldırıyoruz. Yani form aşağıdaki şekilde tanımlanıyor;
<h2>SaveModel</h2>
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<%: Html.LabelFor(model => model.FirstName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.FirstName) %>
<%: Html.ValidationMessageFor(model => model.FirstName) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.LastName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.LastName) %>
<%: Html.ValidationMessageFor(model => model.LastName) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Bust) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Bust) %>
<%: Html.ValidationMessageFor(model => model.Bust) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Waist) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Waist) %>
<%: Html.ValidationMessageFor(model => model.Waist) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Hips) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Hips) %>
<%: Html.ValidationMessageFor(model => model.Hips) %>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
Form tanımında Height parametresine ait bir tanım olmadığına dikkat edin. Kullanıcı tarafından Height değerinin girilmesini engellemiş oluyoruz, ancak gerçekten Height değerinin alınmasına engel oluyor muyuz? Hayır olmuyoruz. Kullanıcı kayıt formunu postalarken, iyi niyetli bir kullanıcı(!) ise, action url parametresine ?Height=178 parametresini ekleyerek kayıt ettiği süper modelin 1.78 boyunda olmasını sağlayabilir. DefaultModelBinder, Request.Form içinde bulamadığı Height değerini, Request.QueryString içinden alacak ve dinamik olarak oluşturduğu SuperModel nesnesine bağlayacaktır. DefaultModelBinder'a Height parametresini bağlama işleminin dışında tutmasını bildirmek için Bind öz niteliğini kullanıyoruz. Güncel aksiyon tanımımız şu şekilde;
public ActionResult SaveModel([Bind(Exclude = "Height")]SuperModel superModel) { ... }
Eğer Height parametresini sadece SaveModel aksiyonu için değil, SuperModel nesnesinin kullanıldığı her aksiyonda bağlama işlemi dışında bırakmak istiyorsak, bu durumda Bind öz niteliğini sınıf üzerinde kullanabiliriz;
[Bind(Exclude = "Height")] public class SuperModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Height { get; set; }
public int Bust { get; set; }
public int Waist { get; set; }
public int Hips { get; set; }
}
Model bağlama ve koleksiyonlar
DefaultModelBinder sadece primitif ve kullanıcı tanımlı veri tipleri ile çalışmıyor. Kullanıcı tarafından post edilen verilerden diziler, koleksiyonlar ve sözlükler oluştururabilme yeteneğine de sahiptir. Birkaç örnekle bu özellikleri inceleyelim.
Aşağıda üç adet checkbox içeren bir form tanımı görüyorsunuz. Üç checkbox'ın da name özelliği, yani isimleri aynı; source.
<% using (Html.BeginForm()) { %>
Gazete: <input type="checkbox" name="source" value="gazete" /><br />
Arama Motoru: <input type="checkbox" name="source" value="arama_motoru" /><br />
Reklam: <input type="checkbox" name="source" value="reklam" /><br />
<input type="submit" value="Gönder Bakalım" />
<% } %>
Formdan gelen bilgiyi alacak olan aksiyon tanımı da hemen aşağıda:
public ActionResult Registration(List<string> source) { ... }
Bu sayfaya gidip, karşımıza gelen formdaki üç checkbox'tan örneğin sadece Arama Motoru ve Reklam kutucuklarını işaretleyip formu postaladığımızda, aksiyondaki source parametresinde iki farklı string olduğunu ve sırasıyla değerlerinin arama_motoru ve reklam olduğunu göreceğiz. DefaultModelBinder, formdan gelen ve aynı isme sahip olan değerleri FormValueProvider kullanarak bir string listesine attı ve böylece biz de teker teker seçilmiş olan değerlere ulaşabildik.
Son bir örnek olarak, az önce yaptığımız string listesi uygulamasını süper modellerimizle gerçekleştirelim. Bir dizi süper model bilgisini form üzerinden sunucuya postalayan bir görünüm ve postalanmış bilgiler içinden süper modelleri ayıklayabilecek bir aksiyon tanımı yapalım.
<h2>SaveModels</h2>
<% using (Html.BeginForm()) { %>
<% for (int i = 0; i < 10; i++) { %>
<fieldset>
<legend><%: i %>. Süper Model</legend>
<div class="editor-label">
<%: Html.Label(string.Format("model[{0}].FirstName", i)) %>
</div>
<div class="editor-field">
<%: Html.TextBox(string.Format("model[{0}].FirstName", i)) %>
</div>
<div class="editor-label">
<%: Html.Label(string.Format("model[{0}].LastName", i)) %>
</div>
<div class="editor-field">
<%: Html.TextBox(string.Format("model[{0}].LastName", i)) %>
</div>
<div class="editor-label">
<%: Html.Label(string.Format("model[{0}].Bust", i)) %>
</div>
<div class="editor-field">
<%: Html.TextBox(string.Format("model[{0}].Bust", i)) %>
</div>
<div class="editor-label">
<%: Html.Label(string.Format("model[{0}].Waist", i)) %>
</div>
<div class="editor-field">
<%: Html.TextBox(string.Format("model[{0}].Waist", i)) %>
</div>
<div class="editor-label">
<%: Html.Label(string.Format("model[{0}].Hips", i)) %>
</div>
<div class="editor-field">
<%: Html.TextBox(string.Format("model[{0}].Hips", i)) %>
</div>
</fieldset>
<% } %>
<p>
<input type="submit" value="Create" />
</p>
<% } %>
Yukarıdaki form tanımında bir döngü ile 10 adet süper model kaydı yapılabilmesi için kontrol serisi oluşturuyorum. Kontrollerin isimlendirmesine lütfen dikkat. Her bir model için model[0].FirstName, model[5].Waist şeklinde isimleri olan input kontrolleri oluşturmuş oluyorum. input kontrollerine bu şekilde isim vermek tamamen kurallar dahilinde. DefaultModelBinder, dizi şeklinde bir isimlendirmeyle karşılaştığında, aksiyon parametresi ile eşleştirmek için indeksleyicilerin hemen önündeki ismi parametre ismi olarak alıyor, yani bizim senaryomuzda aksiyon parametresinin adı model olmalı.
Aşağıda aksiyon tanımı mevcut:
[HttpPost] public ActionResult SaveModels(List<SuperModel> model)
{
return View();
}
Form postalandığında, DefaultModelBinder devreye giriyor, postalanan verilerden sırasıyla 10 adet SuperModel nesnesi oluşturuyor ve her bir fieldset içinden gelen veriyi bir SuperModel nesnesine bağlıyor. Gördüğünüz gibi çok dinamik ve esnek bir yapı mevcut. ASP.NET Webforms yapısından çok daha esnek.
Konumuz burada bitmez. Zira model bağlayıcı ve değer sağlayıcı konusu üzerinde daha konuşulabilecek bir konu. İlerleyen yazılarda, MVC'nin her noktasında olduğu gibi bu sistemi nasıl genişletebiliriz, IValueProvider ve IModelBinder arayüzlerini kullanarak nasıl model bağlayıcı ve değer sağlayıcı geliştirebiliriz inceleyeceğiz.