📖 Arhitektura ViewModela
ViewModel je specijalizovana C# klasa dizajnirana isključivo za specifičan pogled (View). Dok domenski modeli (entiteti) predstavljaju strukturu baze podataka (npr. `Zaposlenik`, `Sektor`), ViewModel predstavlja strukturu podataka okrenutu korisniku.
Zašto je zabranjeno (pogrešno) slati Entitete direktno na View?
Mnogi početnici čine grešku i šalju Entity Framework klase direktno u View. U sistemima javne uprave ovo je ogroman sigurnosni rizik i arhitektonski problem:
- Mass Assignment / Overposting napadi: Ako pošaljete `Zaposlenik` entitet u formu, haker može ručno izmijeniti HTML kako bi poslao skrivena polja poput `IsAdministrator = true` ili `Plata = 10000`. Binder će ovo pročitati i sačuvati u bazu!
- Eksponiranje osjetljivih podataka: Entitet `Korisnik` sadrži `PasswordHash` ili `Token`. Ako ga šaljete na View (čak i ako ih ne ispisujete), postoji rizik da ti podaci procure kroz API ili JSON serijalizaciju.
- Neusklađenost podataka (Mismatched shapes): View često zahtijeva podatke iz više tabela (npr. forma za kreiranje zaposlenika treba ime i prezime, ali i padajuću listu svih radnih mjesta). Entitet `Zaposlenik` nema mjesto za spisak radnih mjesta.
🏗️ Primjer iz Praxe Javne Uprave
Posmatrajmo situaciju gdje kreiramo novog državnog službenika. Potrebno nam je ime, JMBG, ali i lista dostupnih sektora unutar ministarstva kako bi ga dodijelili pravom sektoru.
public class SluzbenikCreateViewModel
{
// Samo polja koja nam stvarno trebaju na formi
[Required(ErrorMessage = "Ime zaposlenika je obavezno.")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "Ime mora imati između 2 i 50 karaktera.")]
[Display(Name = "Ime zaposlenika")]
public string Ime { get; set; }
[Required(ErrorMessage = "Prezime je obavezno.")]
[StringLength(50)]
public string Prezime { get; set; }
[Required(ErrorMessage = "JMBG je obavezan.")]
[RegularExpression(@"^\d{13}$", ErrorMessage = "JMBG mora sadržavati tačno 13 cifara.")]
public string JMBG { get; set; }
[Required(ErrorMessage = "Odabir sektora je obavezan.")]
[Display(Name = "Sektor rasporeda")]
public int OdabraniSektorId { get; set; }
// Ovo koristimo da napunimo Dropdown (Select listu) na View-u.
// Korisnik ovo NE popunjava, ovo šalje server kontroleru za prikaz.
public IEnumerable DostupniSektori { get; set; }
}
✅ DataAnnotations: Moćan i Čist način Validacije
Data Annotations je .NET mehanizam za deklarativnu validaciju podataka koristeći atribute iznad property-ja. MVC Framework automatski "čita" ove atribute, generiše klijentski JavaScript kod za validaciju u pretraživaču, i provodi serversku validaciju u Kontroleru.
| Atribut | Opis i Upotreba | Primjer |
|---|---|---|
[Required] |
Polje ne smije ostati prazno (ili null). | [Required(ErrorMessage="Obavezno polje")] |
[StringLength] |
Ograničava minimalni i maksimalni broj karaktera stringa. | [StringLength(10, MinimumLength=2)] |
[RegularExpression] |
Validacija pomoću složenih Regex paterna (email, JMBG, šifre, MAC adresa). | [RegularExpression(@"^[a-zA-Z]+$")] (Samo slova) |
[Range] |
Broj (ili datum) mora biti unutar određenog raspona. | [Range(18, 65, ErrorMessage="Godine od 18 do 65")] |
[Compare] |
Upoređuje vrijednost sa drugim poljem u ViewModelu (najčešće za lozinke). | [Compare("Password", ErrorMessage="Lozinke se ne slažu")] |
[EmailAddress] |
Provjerava da li je format validan email (osnovna provjera). | [EmailAddress] |
🛠️ Pisanje Custom Validation Atributa
Šablon atributa često nije dovoljan za specifčne državne sisteme. Na primjer, želimo validirati da je unesen datum zaposlenja poslovni dan (ne vikend) ili želimo složenu validaciju JMBG algoritma po modulusu 11.
Lako možemo kreirati vlastiti atribut nasljeđivanjem klase ValidationAttribute.
public class SamoRadniDanAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return true; // Required rješava null. Ostavi true ovdje.
DateTime datum;
if (DateTime.TryParse(value.ToString(), out datum))
{
if (datum.DayOfWeek == DayOfWeek.Saturday || datum.DayOfWeek == DayOfWeek.Sunday)
{
return false; // Validacija pada
}
return true; // Validacija uspješna
}
return false; // Nije poslan ispravan tip datuma
}
}
// KORIŠTENJE U VIEW MODELU:
public class RjesenjeOZapocetomRaduVM
{
[Required]
[SamoRadniDan(ErrorMessage = "Početak angažmana mora biti radni dan, a ne vikend!")]
[DataType(DataType.Date)]
public DateTime DatumPocetkaRada { get; set; }
}
🛡️ Alternativa ViewModelu: [Bind] Atribut (Nije preporučljivo)
Ako insistirate na upotrebi domenskih entiteta umjesto ViewModela, barem se osigurajte od
Overposting napada korištenjem [Bind] atributa kod primanja parametara u
Kontroleru.
⚠️ Upozorenje
Korištenje [Bind] je "flaster" rješenje. Za produkcijske aplikacije u velikim sistemima uvijek, ali baš uvijek, koristite čiste ViewModel klase.
[HttpPost]
[ValidateAntiForgeryToken]
// Bind striktno definiše "SMIJEŠ MI POSLATI SAMO OVA 3 POLJA, SVE OSTALO IGNORIŠI"
// Korisnik pokušava poslati "Plata=5000", ali MVC to ignoriše jer nije u Include listi.
public ActionResult Create([Bind(Include = "NazivSektora,SifraSekotra,Budzet")] Sektor sektorEntity)
{
if (ModelState.IsValid)
{
sektorEntity.Aktivan = true; // Ovo mi ručno postavljamo, korisnik ne može.
sektorEntity.VrijemeKreiranja = DateTime.Now;
db.Sektori.Add(sektorEntity);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(sektorEntity);
}
✅ Zaključak
ViewModeli štite vašu bazu podataka, skrivaju arhitekturu API-ja i olakšavaju prenos tačno onih podataka
koji su potrebni pogledu. U kombinaciji sa DataAnnotations i vlastitim validacijskim
atributima, dobijate moćan mehanizam za zaštitu kvalitete podataka.