28 Mart 2011 Pazartesi

[Yazılım Felsefesi] Asp.Net'de MVVM'i Denemek

Merhaba Arkadaşlar;

Bu makalenin kaynak kodlarını CodePlex'deki Adresinden (AspNetMVVM_TryOut.rar) indirebilirsiniz.

Bu yazı fikirsel çalışmalarımı yayınladığım "[Yazılım Fesefesi] UI Pattern'ler ve MVVM Üzerine Fikirsel Çalışmalar" makalemin uygulama örneğidir.

Önceki yazıda fikirler ortaya atmıştık. Sonra hayal ettim, bir şeyler ortaya çıkarttım. Sizinle paylaşmak istedim.
Asp.Net üzerinde MVVM denemeleri yapmaktaki öncelikli amacım; Code-Behind kodlamadan kurtulmaya ve UI Bussiness'ı bir tür model'e almaya çalışmaktı. Bunu kısmen gerçeklediğimi söyleyebilirim. Peki bu söylediğim şeyi Asp.Net MVC yapmıyor mu? Evet bir kısmını yapıyor. Controller sınıfları, bir tür Façade olarak Bussiness Logic'i View'dan kurtarıyor. Peki yeterli geliyor mu bu? Hayır. Çünkü sayfalarımız çoğu zaman UI tarafında da bir tür bussiness taşıyor. Biz bunu Asp.Net MVC'de javascript ile hallediyoruz. Asp.Net'de ise (iğrenç)Postback mimarisi ile hallediyoruz.Tamam sevmiyorum Postback mimarisini ama yine de işimizi oldukça kolaylaştırıyor. Çoğu intranet tabanlı Web uygulaması da -büyük dezavantajlarına rağmen- Asp.Net'i tercih ediyor.
Ben de Asp.Net'i tercih ettim :)
Tabii ki Asp.Net'i MVVM'e uygun hale getirebilmek için biraz üzerinde çalıştım. Mesela Asp.Net sayfasından Model oluşturabilmek için Custom Control ve bu kontrollerden veri toplayan bir Custom DataBinding mekanizması kullandım. Aslında şu anki haliyle çook basit bir durumda bu proje ama hayal ettiklerimi gerçekleyebilmemi sağladı.
Yeterince geyik yaptım :D Artık koda geçelim..
Mimarimizin ana amacı UI Bussiness'ı ViewModel'e taşımak olunca, basit bir UI Bussiness örneği bulmamız gerekiyordu. Ben de en sık karşılaştığımız (bir o kadar da karmaşık :D) Cascading Dropdown örneği üzerinden gerçekledim. Kaskat olarak birbirine bağlı 3 ComboBox'dan İl-İlçe-Semt seçimini gerçekleyeceğiz. Modelimiz sadece bu 3 kontrolün CanSet property'lerini düzenleyerek, View üzerinde işlem yapmaları sağlanacak. O zaman ViewModel'imize bakalım.
Bu arada mimarinin henüz önemli bir kısmı eksik.. O da Model ve ViewModel tek sınıfta. .Ama mantığı taşıması açısından kodun yeterli olduğunu düşünüyorum ;)

public class AddressViewModel : ViewModelBase<AddressViewModel>
{
    //Default olarak ViewModel'in property'leri Get ve Set edilebilir mi (CanGet)
    protected override bool DefaultAccess { get { return true; } }
    
    //Model üzerindeki propertyler
    public int? IlID { get; set; }
    public int? IlceID { get; set; }
    public int? SemtID { get; set; }

    protected AddressService addressDataService = new AddressService();

    //UI Logic ya da UI Bussiness
    protected override void PrepareBussiness()
    {
        this.CreateAccessRule("IlceID", AllowFor.Get | AllowFor.Set, () => this.IlID.HasValue);
        this.CreateAccessRule("SemtID", AllowFor.Get | AllowFor.Set, () => this.IlceID.HasValue);

        this.AddDatasource("IlID", () => this.addressDataService.IlData());
        this.AddDatasource("IlceID", () => this.addressDataService.IlceData(this.IlID.Value));
        this.AddDatasource("SemtID", () => this.addressDataService.SemtData(this.IlID.Value, this.IlceID.Value));
    }
}
Bizim için buradaki önemli kısım "PrepareBussiness" methodu.. Dikkat ederseniz CreateAccessRule ile IlceID, IlID'nin değeri olup olmadığına bağlanıyor. Hakeza SemtID'de IlceID'ye bağlanıyor. Burada dikkat etmemiz gereken bir nokta var. Default ayarda biz Model'imizi erişime açmış olmamıza rağmen, aşağıda Get ve Set property'lerini bir üst değerin var olmasına bağlıyoruz. Yani IlID'nin bir değeri olmadan IlceID Get(Visible) ve Set(Enabled) edilemez.. Meali: İl seçilmeden Ilce görüntülenmeyecek..

Bir diğer önemli nokta da erişimi denetleyen kısımların de birer delegate(Func<bool>) olmaları.. Böylece her erişim denendiğinde; belirtilen Rule çalıştırılacak ve sonuç dönecektir.Tabii ki bu Rule'lar ViewModelBase<> sınıfında Dictionary<string,Func<bool>> içinde tutuluyor.
Peki Model'in property'leri sayfaya nasıl yerleşecekler?
<p>
    İl:
    <bss:MyDropDownList ID="ddlIl" runat="server" BindColumnName="IlID" DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>

</p>
<p>
            İlçe:
    <bss:MyDropDownList ID="ddlIlce" runat="server" BindColumnName="IlceID" DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>
</p>
 <p>
            Semt:
    <bss:MyDropDownList ID="ddlSemt" runat="server" BindColumnName="SemtID"  DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>
</p>

Burada kontrolü sınıfın ilgili kontorlüne Bind edebilmek için kendi Custom Controllerimizi kullanıyoruz. Önemli nokta BindColumnName niteliği.. Bu özellik sayesinde, rekürsif bir metod ile Page ile Model'i bind edeceğiz.
Şimdi.. İşin kalbine gelelim.. Code-Behind'a..
public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ViewModelHelper.Run<AddressViewModel>(this);
    }
}
Sizi gülümsetebildiğimi umuyorum :) Hayallerimizi azar azar gerçekliyoruz. Peki ne mi var ViewModelHelper sınıfında ?
public static void Run<TViewModel>(Control mainControl) where TViewModel : ViewModelBase<TViewModel>, new()
{
    TViewModel viewModel = DataBindHelper.GetModel<TViewModel>(mainControl);
    viewModel.RunBussiness();
    DataBindHelper.ConfigureView(mainControl, viewModel);
}
Sistemi ayağa kaldıran kısım burada yatıyor.
  • 3. satır: Rekürsif bir method tüm kontrolleri dolaşıp IMyControl'den türeyen kontrollerin verilerini topluyor ve verilen Model'in BindColumnName'lerinde belirtilen property'lerine basıyor.
  • 4. satır: ViewModel sınfımızda yazdığımız RunBussiness() kısmını aktive ediyoruz.
  • 5. satır: ViewModel sınıfında oluşan rule'lar çerçevesinde, View'ı konfigure eder. (Verilerini Bind Eder, CanGet ve CanSet'ler çerçevesinde Visible ve Enabled'ları ayarlar. Varsa Datasource'larını bind eder.)
3. ve 5. satırdaki methodlar birbirine benzer olduğu, sadece ters çalıştıkları için sadece bir ConfigureView methodunu göstereceğim. Elbetteki kaynak kodlardan tüm diğer ayrıntılara ulaşabilirsiniz.
public static void ConfigureView<TViewModel>(Control mainControl, ViewModelBase<TViewModel> viewModel) where TViewModel : ViewModelBase<TViewModel>, new()
{
    Type dataType = viewModel.GetType();
    var properties = dataType.GetProperties();
    foreach (Control ctrl in mainControl.Controls)
    {
        if (ctrl is IMyControl)
        {
            IMyControl myControl = (ctrl as IMyControl);
            if (string.IsNullOrEmpty(myControl.BindColumnName)) continue;

            PropertyInfo propInfo = properties.
                    Where(pi => pi.Name == myControl.BindColumnName).
                            First();

            object dataValue = propInfo.GetValue(viewModel, null);

            myControl.setValue(dataValue);

            myControl.Visible = viewModel.CanGet(myControl.BindColumnName);
            myControl.ControlEnabled = propInfo.GetSetMethod() != null && viewModel.CanSet(myControl.BindColumnName);

            if (myControl is DataBoundControl && viewModel.HasDataSourceFor(myControl.BindColumnName) && myControl.ControlEnabled)
            {
                (myControl as DataBoundControl).DataSource = viewModel.GetDatasourceFor(myControl.BindColumnName);
                (myControl as DropDownList).AutoPostBack = true;
                (myControl as DataBoundControl).DataBind();
            }
        }
        if (ctrl.Controls.Count > 0)
        {
            ConfigureView(ctrl, viewModel);
        }
    }
}

Buradaki önemli nokta baştan beri söylediğimiz CanGet, CanSet methodlarının burada kullanılmış olması.. ViewModelBase sınıfından Datasource atama ile ilgili diğer methodların da burada işlevsel hale geldiğini görmekteyiz.
Ve son olarak da ViewModelBase sınıfımızdan küçük bir kaç nüans gösterelim :)
public abstract class ViewModelBase<TViewModel> where TViewModel : class,new()
{
..........
   private Dictionary<string, Func<bool>> getSecurityTable { get; set; }
   private Dictionary<string, Func<bool>> setSecurityTable { get; set; }

   internal bool CanGet(string propertyName) { return getSecurityTable.ContainsKey(propertyName) ? getSecurityTable[propertyName]() : DefaultAccess; }
   internal bool CanSet(string propertyName) { return setSecurityTable.ContainsKey(propertyName) ? setSecurityTable[propertyName]() : DefaultAccess; }
..........
   protected void CreateAccessRule(string propertyName, AllowFor allowment, Func<bool> allowMethod)
   {
       if ((allowment & Component.AllowFor.Get) > 0)
       {
           if (getSecurityTable.ContainsKey(propertyName))
           {
               getSecurityTable[propertyName] = allowMethod;
           }
           else
           {
               getSecurityTable.Add(propertyName, allowMethod);
           }
       }
       if ((allowment & Component.AllowFor.Set) > 0)
       {
           if (setSecurityTable.ContainsKey(propertyName))
           {
               setSecurityTable[propertyName] = allowMethod;
           }
           else
           {
               setSecurityTable.Add(propertyName, allowMethod);
           }
       }
   }
..........
}
Burada da AccessRule'ları Dictionary'lere atmamızı ve, CanGet ile CanSet canlı kanlı halleriyle ne kadar küçük birer method olduğunu görmenize yetti sanırım :) Nesneye Dayalı Programlama'ya bu yüzden bayılıyorum. İşlerimizi bu kadar kolay hale getiriyor :D Artık sizi kaynak kodlarla başbaşa bırakıyorum..
Umarım [Yazılım Felsefesi]ne bakışınızı değiştirebilecek, genişletebilecek bir makale olmuştur..
Herkese iyi çalışmalar dilerim..

1 yorum:

  1. Hello! Do you know if they make any plugins to safeguard against hackers? I'm kinda paranoid about losing everything I've worked hard on. Any tips? capital one login

    YanıtlaSil