Windows Forms Aplikacije - Best Practicies (MVP pattern, C#)

gost 390446

Iskusan
Poruka
5.726
Iako je danas WPF maltene potpuno izbacio iz igre Windows Forms aplikacije, mnogima je jos uvek problematicno da se prebace na ovu novu tehnologiju, i MVVM nacin razmisljanja.
Takodje, cesto ljudi postavljaju pitanja kako da nesto urade u Win Forms aplikaciji, a jednostavno ne mogu da im odgovorim jer su od pocetka postavili stvari totalno pogresno, i vrlo je besmisleno i pokusavati dati odgovor na mnoga takva pitanja.
Ako neko programira u Objektno Orijentisanom programskom jeziku (C#, Java, Python....), mora da razmislja objektno orijetnisano prilikom resavanja problema.
A to znaci sledece: uvek i uvek, bez izuzetaka, mora se teziti da se sto vise ispostuju dva principa OOP-a, koja ovde konstantno ponavljam, i koji su uzrok i posledica kompletne OOP filozofije i uzrok i posledica svakog OOP jezika: JAKA KOHEZIJA, i SLABA POVEZANOST (High Cohesion & Low Coupling).

Kako bismo u Windows Forms aplikaciji ispostovali ova dva principa, i kako bismo napravili nasu aplikaiciju preglednom, citkom, jednostavnom za odrzavanje i debug-ovanje, za dalje upgrade-ovanje, i sto je jako vazno - za testiranje, osmisljen je jedan jako lep i jednostavan projektni obrazac (design pattern), koji nam pomaze da iskomponujemo (postavimo) aplikaciju tako da ispunimo gornje zahteve. Taj obrazac se zove MVP = Model - View - Presenter.
Za one koji su se vec susretali sa jednim drugim obrascem, zvanim MVC (Model - View - Controller), ovo ce biti vrlo jednostavno. Zapravo, MVP je varijacija MVC-a, gde prezenter preuzima ulogu kontrolera i zaduzen je za registrovanje dogadjaja (event-a) na UI-u i njihovo preusmeravanje (routing) u odgovarajuce komande koje rade nesto sa Modelom (obrada i priprema podataka), kao i za "routing-back": vracanje obradjenih podataka nazad na UI.

MVP_3.jpg


Nastavak uskoro ;)
 
Poslednja izmena:
NASTAVAK:

Primecujete da se u gornjoj slici View naziva "Passive View". Zasto? Pa zato sto je cela poenta da nas View ne sadrzi nikakvu logiku! Jer uvek zelimo da posebno testiramo UI, a posebno da tesriramo nasu biznis logiku. Kako ovo radi?
Jedan View ima svoj prezenter i svoj model. Kada, na primer, korisnik pokrene neku akciju (klinke neko dugme na View-u), taj request biva prosledjen prema Presenteru, koji trigeruje odredjenu funkciju. Presenter je taj koji je tako implementiran da je u svakom trenutku izvrsenja programa "svestan" stanja korisnickog interfejsa (View), i stanja u modelu podataka (dakle, isto kao ViewModel u MVVM pattern-u). To se postize tako sto View instancira Presenter objekat u svom konstruktoru. Takodje, nas View (nasa windows forma) mora implementirati nas "pasivni interfejs":

Kod:
namespace ModelViewPresenter
{
    public interface IView
    {
        string TextValue { get; set; }
    }
}

Evo i implementacije naseg View-a:

Kod:
namespace ModelViewPresenter
{
    public partial class Form1 : Form, IView
    {
        private Presenter presenter = null;
        private readonly Model m_Model;
 
        public Form1(Model model)
        {
            m_Model = model;
            InitializeComponent();
            presenter = new Presenter(this, m_Model);
            SubscribeToModelEvents();
        }
 
        public string TextValue
        {
            get
            {
                return textBox1.Text;
            }
            set
            {
                textBox1.Text = value;
            }
 
        }
 
        private void Set_Click(object sender, EventArgs e)
        {
            presenter.SetTextValue();
        }
 
        private void Reverse_Click(object sender, EventArgs e)
        {
            presenter.ReverseTextValue();
        }
 
        private void SubscribeToModelEvents()
        {
            m_Model.TextSet += m_Model_TextSet;         
        }
 
        void m_Model_TextSet(object sender, CustomArgs e)
        {
            this.textBox1.Text = e.m_after;
            this.label1.Text = "Text changed from " + e.m_before + " to " + e.m_after;
        }
    }
}

Kao sto rekosmo, prezenter je neka vrsta "coveka u sredini", koji zna trenutno stanje View-a (podatke u kontrolama - labele, textBox, ostalo..), i poziva odredjeni svoj metod koji vrsi konkretnu operaciju koju zelimo da bude izvrsena.
Pored toga, prezenter ce odrzavati stanje View-a i Model-a: nakon sto se operacija zavrsi, i view i model moraju biti svesni promene do koje je doslo, i moraju biti update-ovani.
Evo prezentera za nas konkretni primer.

Kod:
namespace ModelViewPresenter
{
    public class Presenter
    {
        private readonly IView m_View;
        private IModel m_Model;
 
        public Presenter(IView view, IModel model)
        {
            this.m_View = view;
            this.m_Model = model;
        }
 
        public void ReverseTextValue()
        {
            string reversed = ReverseString(m_View.TextValue);
            m_Model.Reverse(reversed);
        }
 
        public void SetTextValue()
        {
            m_Model.Set(m_View.TextValue);
        }
 
        private static string ReverseString(string s)
        {
            char[] arr = s.ToCharArray();
            Array.Reverse(arr);
            return new string(arr);
        }
    }
}

Kao sto se vidi iz primera gore, Prezenter poziva model, tako da model update-uje i cuva svoje stanje. Dok je Model taj koji je zaduzen da pokrene svoj sopstveni event, putem kojeg ce "obavestiti" klijenta o promeni stanja, konkretno, obavestiti UI (View) da je se odredjeni tekst promenio. Evo implementacije modela, za ovaj primer:

Interface:
Kod:
namespace ModelViewPresenter
{
    public interface IModel
    {
        void Set(string value);
        void Reverse(string value);
    }
}

....i implementacija:

Kod:
namespace ModelViewPresenter
{
    public class Model : IModel
    {
        private string m_textValue;
 
        public event EventHandler<CustomArgs> TextSet;
        public event EventHandler<CustomArgs> TextReverse;
 
        public Model()
        {
            m_textValue = "";
        }
 
        public void Set(string value)
        {
            string before = m_textValue;
            m_textValue = value;
            RaiseTextSetEvent(before, m_textValue);
        }
 
        public void Reverse(string value)
        {
            string before = m_textValue;
            m_textValue = value;
            RaiseTextSetEvent(before, m_textValue);
        }
 
        public void RaiseTextSetEvent(string before, string after)
        {
            TextSet(this, new CustomArgs(before, after));
        }
    }
 
    public class CustomArgs : EventArgs
    {
        public string m_before { get; set; }
        public string m_after { get; set; }
 
        public CustomArgs(string before, string after)
        {
            m_before = before;
            m_after = after;
        }
    }
}

Sada je krug potpuno zaokruzen!

Napomena: ovo je samo pokazni primer, da bi se sto lakse demonstrirao osnovni princip.
U real-life aplikacijama, mora se koristiti DI, kako bi se instancirali svi zavisni objekti (registrovati dependency na pocetku izvrsenja aplikacije, i resolve-ovati ih kada nam zatrebaju), a konkretne operacije izdvojiti u posebne klase (po mogucstvu staticke klase), koje, opet, apstrakovati putem interfejsa.

Za dalja pitanje: izvolite, tu sam :)
 
Poslednja izmena:
Lepo ali kako bi izgledao primer sa MzSql bazom? Jednu tabelu ucitaj u DataGrid prikazati pojedine kolone i omoguciti izmenu pojedinih redova ili polja u nekom redu, zatim tako izmenjene podatke vrati u tabelu. Ili kako u tabeli obrisati neki red ili polje, kako insert neki red, update.
Jedno pitanje kako knjigovodsto zahteva da se podaci istovremeno menjaju u 3 povezane tabele, da li se mora da koristi commit - transaction? Da li je bolje koristiti proceduru zbog brzine rada i sigurnosti podataka tj. koegzistentnih u isto vreme vise tabela?
Hvala
 
Nebitan je nosioc podataka. Da li je MySql, ili SQLServer, ili obican XML ili JSON file, ili nesto cetvrto.
Za komunikaciju sa nosiocem podataka (bazom podataka, fajl sistemom, ili nebitno cime vec), kreira se poseban Data Access Layer (DA) koji sadrzi sve neophodne klase za upravljanje podacima.
Sami podaci su predstavljeni klasama koje definises u modelu.
Koliko imas tablea - toliko ces imati klasa, i svaka klasa u modelu mora da implementira interfejs IModel - time se obezbedjuje da se kada se promeni model, okine event i prikaze prikaz na view-u.
Pored toga, preporucljivo je da u okviru modela implementiras i validaciju za svaki podatak.
Naravno - ako koristis neki ORM Framework, ove stvari su mnogo pojednostavljene, ali ako zelis performanse - izbegavaj neki standardni ORM, vec koristi svoju sopstvenu implementaciju ovako kako sam ti opisao.

Tri povezane table menjas tako sto prvo azuriras roditelja pa onda dete - tu nema neke filozofije. Ukoliko je zahtev sistema takav da je potrebno obezbediti "transakcioni tip upisa necega u bazu" (sve ili nista), onda ces koristiti transakciju.
Transakciju mozes implementirati sam na bezbroj nacina, mada cini mi se da C# ima vec neke ugradjene funkcije za implementaciju transakcije.
 
Ma ok to je ok. znam da za svaku tabelu imam poseban klasu za interfejs i poseban wiev. Da znam da prvo se azuzira roditelj tabela pa zatim jedno dete pa drugo.
Pitanje je bilo " Da li je bolje ili ne koristiti proceduru zbog brzine rada i sigurnosti podataka tj. koegzistentnih u isto vreme vise tabela? "

Uvek je bolje koristiti stored procedure. Ali one se koriste iskljucivo zbog performansi, ni zbog cega drugog, i zato sto ti stored procedura omogucuje da napises bukvalno mini-program koji barata upisom podataka, gde mozes imati punu kontrolu.
 

Back
Top