ASP.NET Core MVC: kontroleri i Razor pogledi

Lekcija 6 od 7 · ASP.NET Core

Umesto da JavaScript pravi stranicu iz JSON-a, server može sam da napravi HTML – kao Django šabloni. U ASP.NET Core-u za to služe MVC kontroleri i Razor pogledi (.cshtml): C# kod i HTML u istom fajlu. Pravimo katalog proizvoda i formu za dodavanje, u istom projektu.

Pre nego što počneš: potrebno predznanje – JavaScript klijent, šabloni u Django-u (za poređenje).
Kako da učiš: radi u svom projektu korak po korak i posle svake izmene pokreni aplikaciju; zadatak na kraju prvo uradi sam/a. Kod je u C#-u za .NET 8 ili noviji (preporuka: aktuelna LTS verzija); naziv fajla je u komentaru na vrhu svakog primera.

Uključivanje MVC-a

// Program.cs – izmene: AddControllersWithViews umesto AddControllers + podrazumevana ruta
builder.Services.AddControllersWithViews();       // API kontroleri i dalje rade

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapControllers();                             // API: /api/proizvodi
app.MapControllerRoute(                           // MVC: /Katalog, /Katalog/Detalji/2
    name: "default",
    pattern: "{controller=Katalog}/{action=Index}/{id?}");
app.Run();

Ruta {controller=Katalog}/{action=Index}/{id?} znači: /Katalog/Detalji/2 poziva akciju Detalji kontrolera KatalogController sa id = 2; prazna adresa ide na Katalog/Index. Ako u Program.cs postoji UseDefaultFiles iz lekcije 5, ukloni ga ili premesti klijent u wwwroot/admin.html, da bi početna strana bila katalog.

MVC kontroler

// Controllers/KatalogController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Prodavnica.Data;
using Prodavnica.Models;

namespace Prodavnica.Controllers;

public class KatalogController(ProdavnicaContext db) : Controller     // Controller (sa pogledima), ne ControllerBase
{
    // GET /Katalog
    public async Task<IActionResult> Index()
    {
        var proizvodi = await db.Proizvodi.OrderBy(p => p.Naziv).ToListAsync();
        return View(proizvodi);                        // Views/Katalog/Index.cshtml
    }

    // GET /Katalog/Detalji/2
    public async Task<IActionResult> Detalji(int id)
    {
        var p = await db.Proizvodi.FindAsync(id);
        if (p == null) return NotFound();
        return View(p);
    }

    // GET /Katalog/Novi – prazna forma
    public IActionResult Novi() => View(new Proizvod());

    // POST /Katalog/Novi – obrada forme
    [HttpPost]
    [ValidateAntiForgeryToken]                         // zaštita od CSRF napada
    public async Task<IActionResult> Novi(Proizvod p)
    {
        if (!ModelState.IsValid) return View(p);       // ista forma sa porukama o greškama
        db.Proizvodi.Add(p);
        await db.SaveChangesAsync();
        TempData["Poruka"] = $"Dodat je proizvod „{p.Naziv}“.";
        return RedirectToAction(nameof(Index));        // POST → redirect → GET
    }
}

Obrazac je isti kao u Django-u: GET prikazuje formu, POST je obrađuje, posle uspeha se preusmerava. TempData čuva poruku za jedan sledeći zahtev (kao messages u Django-u).

Razor pogledi

@* Views/_ViewImports.cshtml – važi za sve poglede *@
@using Prodavnica.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@* Views/Shared/_Layout.cshtml – zajednički okvir (kao base.html u Django-u) *@
<!DOCTYPE html>
<html lang="sr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>@ViewData["Title"] – Prodavnica</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body>
  <nav class="navbar bg-dark" data-bs-theme="dark">
    <div class="container">
      <a class="navbar-brand" asp-controller="Katalog" asp-action="Index">Prodavnica</a>
      <a class="btn btn-sm btn-success" asp-controller="Katalog" asp-action="Novi">+ Novi proizvod</a>
    </div>
  </nav>
  <main class="container py-4">
    @if (TempData["Poruka"] is string poruka)
    {
      <div class="alert alert-success">@poruka</div>
    }
    @RenderBody()
  </main>
</body>
</html>
@* Views/_ViewStart.cshtml *@
@{ Layout = "_Layout"; }
@* Views/Katalog/Index.cshtml *@
@model List<Proizvod>
@{ ViewData["Title"] = "Katalog"; }

<h1 class="h3">Katalog (@Model.Count)</h1>
<div class="row g-3">
  @foreach (var p in Model)
  {
    <div class="col-12 col-sm-6 col-lg-4">
      <div class="card h-100">
        <div class="card-body">
          <h2 class="h5">@p.Naziv</h2>
          <p class="mb-1">@p.Cena din</p>
          @if (p.Kolicina == 0)
          {
            <span class="badge bg-danger">nema na stanju</span>
          }
          else
          {
            <span class="badge bg-success">na stanju: @p.Kolicina</span>
          }
        </div>
        <div class="card-footer">
          <a asp-action="Detalji" asp-route-id="@p.Id">Detalji</a>
        </div>
      </div>
    </div>
  }
</div>
RazorZnačenje
@model List<Proizvod>tip podataka koje pogled dobija od kontrolera (View(proizvodi))
@p.Nazivispis vrednosti – automatski „escape“-ovan (zaštita od XSS)
@foreach, @ifC# petlje i uslovi unutar HTML-a
asp-action, asp-route-idtag helperi: prave ispravnu adresu (/Katalog/Detalji/2)
@RenderBody()mesto gde se ubacuje pogled (kao {% block %} u Django-u)

Forma sa proverom

@* Views/Katalog/Novi.cshtml *@
@model Proizvod
@{ ViewData["Title"] = "Novi proizvod"; }

<h1 class="h3">Novi proizvod</h1>
<form asp-action="Novi" method="post" style="max-width: 420px">
  <div asp-validation-summary="ModelOnly" class="text-danger"></div>
  <div class="mb-3">
    <label asp-for="Naziv" class="form-label"></label>
    <input asp-for="Naziv" class="form-control">
    <span asp-validation-for="Naziv" class="text-danger small"></span>
  </div>
  <div class="mb-3">
    <label asp-for="Cena" class="form-label"></label>
    <input asp-for="Cena" class="form-control">
    <span asp-validation-for="Cena" class="text-danger small"></span>
  </div>
  <div class="mb-3">
    <label asp-for="Kolicina" class="form-label">Količina</label>
    <input asp-for="Kolicina" class="form-control">
    <span asp-validation-for="Kolicina" class="text-danger small"></span>
  </div>
  <button class="btn btn-success">Sačuvaj</button>
</form>

asp-for="Cena" pravi polje sa pravim tipom (type="number"), imenom i vrednošću; asp-validation-for prikazuje poruku iz atributa [Range]. Forma sa tag helperom automatski dobija skriveni token protiv CSRF-a, koji proverava [ValidateAntiForgeryToken].

Web API + JavaScript ili MVC?

Web API + JS klijent (lekcija 5)MVC + Razor (ova lekcija)
ko pravi HTMLpregledač (JavaScript)server
dobro zainteraktivne strane, mobilne aplikacije, više klijenatasajtove sa sadržajem, forme, admin strane
pretraživači (SEO)teželakše – HTML je odmah gotov

Mnoge aplikacije kombinuju oba – kao Prodavnica sada: katalog u MVC-u, a API za mobilnu aplikaciju ili JavaScript. Postoje i Razor Pages (pogled i kod po stranici, bez kontrolera) i Blazor (interaktivne strane u C#-u umesto JavaScript-a).

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje.

ZADATAK 01Težina: lako

Pogled Detalji

Napravi pogled Views/Katalog/Detalji.cshtml koji prikazuje naziv, cenu, količinu i ukupnu vrednost (cena × količina) proizvoda i link nazad na katalog.

Česte greške i saveti
  • Pogled mora biti u Views/ImeKontrolera/ImeAkcije.cshtml (bez reči „Controller“): Views/Katalog/Index.cshtml.
  • Bez @addTagHelper u _ViewImports.cshtml atributi asp-* se ne obrađuju i linkovi su prazni.
  • MVC kontroler nasleđuje Controller, a API kontroler ControllerBase.

Proveri znanje

Otvori kviz

MVC i Razor

1. Šta radi return View(proizvodi) u MVC kontroleru?




2. Čemu služi atribut asp-for="Cena" u formi?




3. Na koju akciju vodi adresa /Katalog/Detalji/5 sa podrazumevanom rutom?