Filmy

Site: Moodle ZČU
Course: BP Luboš Dokulil Sada rozšiřujících úloh v Blazor pro předmět Programování 2
Book: Filmy
Printed by: Nepřihlášený host
Date: Sunday, 18 October 2026, 3:07 AM

1. Zadání

Co je cílem zadání?

  • Vytvoření statického úložiště v paměti pro globální uchovávání a sdílení seznamu filmů napříč celou aplikací.
  • Definice datového modelu s nastavením striktních pravidel pro uživatelské vstupy, včetně vytvoření vlastní validační logiky pro ohlídání smysluplného letopočtu.
  • Zajištění typové bezpečnosti a minimalizace chyb při výběru kritérií pro řazení záznamů využitím vlastního výčtového typu (enum).
  • Pochopení celého životního cyklu objektů a mechanismu automatického uvolňování paměti (Garbage Collector) prostřednictvím implementace destruktoru.
  • Rozdělení aplikace do více samostatných stránek propojených navigačními odkazy a předáváním identifikátorů přes dynamické parametry v URL adrese.
  • Zajištění kompletní správy záznamů zahrnující zakládání nových položek s automatickým výpočtem ID, bezpečné upravování existujících dat přes dočasnou kopii a mazání nechtěných záznamů.
  • Vytvoření interaktivního hlavního katalogu, který uživateli umožňuje dynamicky filtrovat zobrazená data podle žánru a volitelně je seřazovat podle nejrůznějších kritérií.

Zadání

Vytvořte třídu Film, která bude mít vlastnosti Id, Nazev, RokVydani, Reziser, Zanr a Popis. U vlastností nastavte adekvátní validace [Required]. Dále vytvořte vlastní validační atribut [RokDoSoucasnosti], který zajistí, že zadaný rok vydání nebude v budoucnosti. Na samotný konec třídy doplňte destruktor ~Film(), který při uvolnění objektu z paměti vypíše do konzole zprávu o jeho odstranění. Pro bezpečné přepínání způsobů řazení nadefinujte mimo třídu Film také vlastní enum KriteriumRazeni.

Pro dočasné uchování dat během přecházení mezi stránkami vytvořte statickou třídu DatabazeFilmu, která bude obsahovat list těchto filmů. Do složky wwwroot/data připravte lokální soubor filmy.json obsahující výchozí pole alespoň 10 filmů.

V Razor komponentách vytvořte vícestránkovou aplikaci využívající Blazor směrování:

  1. Hlavní stránka (/): Zde při prvním spuštění načtěte data z JSON souboru. Zobrazte je v tabulce. Nad tabulku přidejte ovládací prvky, kterými může uživatel seznam dynamicky filtrovat (podle žánru) a řadit (abecedně, od nejstarších), přičemž k bezpečné aplikaci logiky řazení využijete svůj nově vytvořený enum. Každý řádek tabulky bude mít tlačítka pro detail, úpravu a okamžité smazání filmu. (Tip: Pro snazší demonstraci destruktoru můžete při smazání volitelně vynutit úklid paměti voláním GC.Collect()).
  2. Formulářové stránky (/pridat a /upravit/{Id:int}): Vytvořte komponenty obsahující <EditForm>. Umožněte uživateli přidávat nová data nebo upravovat ta existující s využitím vašich nastavených validací.
  3. Detail filmu (/film/{Id:int}): Stránka, která si z URL adresy převezme ID a z paměti aplikace vypíše kompletní informace o daném filmu, včetně jeho děje (popisu).

Příklad, jak by mohla stránka vypadat:

Pokud se okno nenačetlo, klikněte zde.

2. JSON data

Zde je adresářová struktura.

Zde je seznam filmů zapsaný v JSON formátu.

[
  {
    "id": 1,
    "nazev": "Pelíšky",
    "rokVydani": 1999,
    "reziser": "Jan Hřebejk",
    "zanr": "Komedie",
    "popis": "Kultovní česká komedie odehrávající se na konci 60. let. Sledujeme osudy dvou rodin s odlišnými politickými a životními názory, které se setkávají pod jednou střechou."
  },
  {
    "id": 2,
    "nazev": "Vykoupení z věznice Shawshank",
    "rokVydani": 1994,
    "reziser": "Frank Darabont",
    "zanr": "Drama",
    "popis": "Příběh nespravedlivě odsouzeného bankéře Andyho, který si ve vězení nachází nečekaného přítele a nikdy neztrácí naději na svobodu."
  },
  {
    "id": 3,
    "nazev": "Pán prstenů: Společenstvo Prstenu",
    "rokVydani": 2001,
    "reziser": "Peter Jackson",
    "zanr": "Fantasy",
    "popis": "Hobit Frodo Pytlík se vydává na epickou výpravu s cílem zničit Jeden prsten a zachránit tak Středozem před temným pánem Sauronem."
  },
  {
    "id": 4,
    "nazev": "Temný rytíř",
    "rokVydani": 2008,
    "reziser": "Christopher Nolan",
    "zanr": "Akční",
    "popis": "Batman čelí svému dosud největšímu a nejšílenějšímu nepříteli – Jokerovi, kriminálnímu géniovi, který chce uvrhnout Gotham City do naprostého chaosu."
  },
  {
    "id": 5,
    "nazev": "Forrest Gump",
    "rokVydani": 1994,
    "reziser": "Robert Zemeckis",
    "zanr": "Drama",
    "popis": "Neobyčejný životní příběh prostoduchého, ale dobrosrdečného muže, který se nechtěně stává přímým aktérem klíčových událostí amerických dějin 20. století."
  },
  {
    "id": 6,
    "nazev": "Matrix",
    "rokVydani": 1999,
    "reziser": "Lana a Lilly Wachowski",
    "zanr": "Sci-Fi",
    "popis": "Počítačový hacker Neo zjišťuje šokující pravdu o tom, že lidstvo žije v simulované realitě ovládané inteligentními stroji, a přidává se k odboji."
  },
  {
    "id": 7,
    "nazev": "Samotáři",
    "rokVydani": 2000,
    "reziser": "David Ondříček",
    "zanr": "Komedie",
    "popis": "Hořká komedie o skupině mladých lidí v Praze, kteří se snaží najít smysl života, lásku a své místo ve světě na přelomu tisíciletí."
  },
  {
    "id": 8,
    "nazev": "Počátek (Inception)",
    "rokVydani": 2010,
    "reziser": "Christopher Nolan",
    "zanr": "Sci-Fi",
    "popis": "Profesionální zloděj, který krade cenná tajemství hluboko z podvědomí během spánku, dostane téměř nemožný úkol – myšlenku naopak vnuknout."
  },
  {
    "id": 9,
    "nazev": "Pulp Fiction: Historky z podsvětí",
    "rokVydani": 1994,
    "reziser": "Quentin Tarantino",
    "zanr": "Krimi",
    "popis": "Kultovní mozaika propletených příběhů dvou zabijáků, stárnoucího boxera, gangsterovy znuděné manželky a páru zlodějíčků v podsvětí Los Angeles."
  },
  {
    "id": 10,
    "nazev": "Star Wars: Epizoda V - Impérium vrací úder",
    "rokVydani": 1980,
    "reziser": "Irvin Kershner",
    "zanr": "Sci-Fi",
    "popis": "Povstalecká aliance čelí drtivému protiútoku Impéria, zatímco mladý Luke Skywalker hledá mistra Yodu, aby dokončil svůj výcvik rytíře Jedi."
  }
]

3. Grafická nápověda

Třídy DatabazeFilmu, Film a RokDoSoucasnostiAttribute

Statická třída DatabazeFilmu funguje jako dočasná paměť a ve své kolekci Seznam uchovává všechny aktuální záznamy. Hlavní datová struktura je definována ve třídě Film, která obsahuje vlastnosti jako Id, Nazev, RokVydani a další textové informace.

Pro zajištění správnosti dat u letopočtu je vytvořeno vlastní pravidlo RokDoSoucasnostiAttribute. To dědí ze systémové třídy ValidationAttribute a přepisuje metodu IsValid pro specifickou kontrolu zadaného roku.


Editování

Po otevření editační stránky se aplikace nejprve pokusí najít požadovaný záznam v paměti. Pokud neexistuje, proces končí zobrazením chyby. Pokud je originál nalezen, vytvoří se dočasná kopie s názvem upravovanyFilm pomocí operátoru new. Zobrazený formulář je napojen na tuto kopii, což chrání původní data před nechtěným přepisem.

Po kliknutí na tlačítko pro uložení se ověří platnost dat. V případě porušení pravidel se zobrazí chyby a uživatel musí formulář opravit. Jsou-li data validní, původní záznam se přepíše hodnotami z upravené kopie a aplikace přesměruje uživatele zpět na seznam voláním metody NavigateTo.


Zobrazení filmů

Při požadavku na zobrazení dat se nejprve načte surový obsah databáze do proměnné dotaz. Poté se vyhodnotí, zda je nastaven filtr v proměnné vybranyZanr. Pokud ano, celá kolekce se zúží pomocí metody Where.

Následně se kontroluje zvolená hodnota v proměnné kriteriumRazeni. Podle ní se na připravená data aplikuje buď metoda OrderBy pro uspořádání vzestupně, nebo OrderByDescending pro uspořádání sestupně. V případě výchozího stavu se neaplikuje žádné řazení. Takto zpracovaná a upravená kolekce se nakonec vrátí k vykreslení do HTML tabulky.

4. Řešení úkolu

Enum KriteriumRazeni
namespace Komplexni_ukol_Filmy.Models
{
    // Výčtový typ pro bezpečnější definici možností řazení
    public enum KriteriumRazeni
    {
        NazevVzestupne,
        NazevSestupne,
        RokVzestupne,
        RokSestupne,
        ReziserVzestupne
    }
}

Enum s názvem KriteriumRazeni.


Třída DatabazeFilmu
namespace Komplexni_ukol_Filmy.Models
{
    // Zde budeme uchovávat všechny filmy po dobu běhu aplikace
    public static class DatabazeFilmu
    {
        public static List<Film> Seznam { get; set; } = new List<Film>();
    }
}

Pomocí modifikátoru static u třídy DatabazeFilmu a vlastnosti Seznam vytváříme v aplikaci sdílenou paměť. Uložená data si díky tomu zachovávají svůj stav po celou dobu běhu programu a při přechodu uživatele mezi stránkami nedochází k jejich ztrátě. Okamžitou inicializací new List<Film>() rovnou vytváříme prázdný seznam, čímž efektivně předcházíme chybám s prázdnou referencí (null) při prvním pokusu o zobrazení nebo přidání nového filmu.


Třída Film
namespace Komplexni_ukol_Filmy.Models
{
    using System.ComponentModel.DataAnnotations; // Nutné pro validace

    public class Film
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Název filmu je povinný.")]
        public string Nazev { get; set; } = "";

        [Required(ErrorMessage = "Rok vydání je povinný.")]
        [RokDoSoucasnosti]
        public int RokVydani { get; set; }

        [Required(ErrorMessage = "Jméno režiséra je povinné.")]
        public string Reziser { get; set; } = "";

        [Required(ErrorMessage = "Žánr je povinný.")]
        public string Zanr { get; set; } = "";

        // Nové políčko pro popis (není povinné, proto nemá [Required])
        public string Popis { get; set; } = "";

        // Destruktor pro uvolnění objektu z paměti
        ~Film()
        {
            //Vypíše do konzole, že objekt filmu byl uvolněn z paměti
            Console.WriteLine($"Objekt filmu {Nazev} byl uvolněn z paměti.");
        }
    }
}

Třída Film definuje základní datový model pro uchování informací o konkrétním snímku. Zapojení jmenného prostoru System.ComponentModel.DataAnnotations umožňuje definovat pravidla přímo u jednotlivých vlastností.

Vlastnosti Nazev, Reziser a Zanr jsou označeny anotací [Required]. Uživatel musí tato pole ve formuláři vyplnit. Pokud tak neučiní, systém zablokuje odeslání a vypíše varování definované v parametru ErrorMessage.

Vlastnost RokVydani je chráněna uživatelsky definovaným atributem [RokDoSoucasnosti], který hlídá, aby zadaný letopočet dával logický smysl. Vlastnost Popis žádná omezující pravidla nemá a její vyplnění je dobrovolné.

Na konci třídy je destruktor ~Film. Tuto metodu spouští automaticky Garbage Collector těsně předtím, než objekt trvale odstraní z RAM. Příkaz Console.WriteLine následně propíše do vývojářské konzole zprávu.


Třída RokDoSoucasnostiAttribute
using System.ComponentModel.DataAnnotations;

namespace Komplexni_ukol_Filmy.Models
{
    public class RokDoSoucasnostiAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
        {
            if (value is int rok)
            {
                int aktualniRok = DateTime.Now.Year;
                if (rok < 1895 || rok > aktualniRok)
                {
                    return new ValidationResult($"Rok vydání musí být mezi 1895 a {aktualniRok}.");
                }
            }
            return ValidationResult.Success!;
        }
    }
}

Třída RokDoSoucasnostiAttribute dědí z ValidationAttribute. Funguje jako vlastní validační pravidlo pro aplikaci na vlastnosti v datovém modelu.

Metoda IsValid je přepsána klíčovým slovem override. Ověřuje, zda je vstupní hodnota celé číslo, a při splnění ji rovnou ukládá do proměnné rok.

Proměnná aktualniRok dynamicky získává současný letopočet systémovým voláním DateTime.Now.Year.

Podmínka kontroluje, zda zadaný rok není menší než 1895 nebo větší než aktualniRok. Při porušení rozsahu metoda vrací chybovou zprávu jako novou instanci ValidationResult. Pokud data vyhovují, vrací ValidationResult.Success! a propouští je k dalšímu zpracování.


Razor komponenta - Home
@page "/"
@inject HttpClient Http

<PageTitle>Katalog filmů</PageTitle>

<div class="d-flex justify-content-between align-items-center mb-4">
    <h1>Katalog filmů</h1>
    <a href="pridat" class="btn btn-success">Přidat nový film</a>
</div>

@if (Models.DatabazeFilmu.Seznam.Count == 0)
{
    <p><em>Načítám data z JSONu...</em></p>
}
else
{
    <div class="row mb-4 bg-light p-3 rounded">
        <div class="col-md-4">
            <label class="form-label"><strong>Filtrovat podle žánru:</strong></label>
            <select class="form-select" @bind="vybranyZanr">
                <option value="">-- Všechny žánry --</option>
                @foreach (var zanr in Models.DatabazeFilmu.Seznam.Select(f => f.Zanr).Distinct().OrderBy(z => z))
                {
                    <option value="@zanr">@zanr</option>
                }
            </select>
        </div>
        <div class="col-md-4">
            <label class="form-label"><strong>Řadit podle:</strong></label>
            <select class="form-select" @bind="kriteriumRazeni">
                <option value="@Models.KriteriumRazeni.NazevVzestupne">Názvu (A-Z)</option>
                <option value="@Models.KriteriumRazeni.NazevSestupne">Názvu (Z-A)</option>
                <option value="@Models.KriteriumRazeni.RokVzestupne">Roku (od nejstarších)</option>
                <option value="@Models.KriteriumRazeni.RokSestupne">Roku (od nejnovějších)</option>
                <option value="@Models.KriteriumRazeni.ReziserVzestupne">Režiséra (A-Z)</option>
            </select>
        </div>
    </div>

    <table class="table table-hover shadow-sm">
        <thead class="table-dark">
            <tr>
                <th>Název filmu</th>
                <th>Rok</th>
                <th>Režisér</th>
                <th>Žánr</th>
                <th>Akce</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var film in ZobrazeneFilmy)
            {
                <tr>
                    <td><strong>@film.Nazev</strong></td>
                    <td>@film.RokVydani</td>
                    <td>@film.Reziser</td>
                    <td><span class="badge bg-secondary">@film.Zanr</span></td>
                    <td>
                        <a href="film/@film.Id" class="btn btn-sm btn-outline-primary">Detail</a>
                        <a href="upravit/@film.Id" class="btn btn-sm btn-outline-warning">Upravit</a>
                        <button class="btn btn-sm btn-outline-danger" @onclick="() => SmazatFilm(film.Id)">Smazat</button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    // Proměnné napojené na výběrová menu (selecty)
    private string vybranyZanr = "";
    private Models.KriteriumRazeni kriteriumRazeni = Models.KriteriumRazeni.NazevVzestupne;

    protected override async Task OnInitializedAsync()
    {
        if (Models.DatabazeFilmu.Seznam.Count == 0)
        {
            var data = await Http.GetFromJsonAsync<List<Models.Film>>("data/filmy.json");
            if (data != null) Models.DatabazeFilmu.Seznam = data;
        }
    }

    // Tato vypočítaná vlastnost automaticky vrací seřazená a vyfiltrovaná data
    private IEnumerable<Models.Film> ZobrazeneFilmy
    {
        get
        {
            // Vezmeme celou databázi
            var dotaz = Models.DatabazeFilmu.AsEnumerable();

            // 1. Krok: Filtrace podle žánru (pokud je nějaký vybrán)
            if (!string.IsNullOrEmpty(vybranyZanr))
            {
                dotaz = dotaz.Where(f => f.Zanr == vybranyZanr);
            }

            // 2. Krok: Řazení pomocí nového C# "switch expression" s využitím Enum
            return kriteriumRazeni switch
            {
                Models.KriteriumRazeni.NazevVzestupne => dotaz.OrderBy(f => f.Nazev),
                Models.KriteriumRazeni.NazevSestupne => dotaz.OrderByDescending(f => f.Nazev),
                Models.KriteriumRazeni.RokVzestupne => dotaz.OrderBy(f => f.RokVydani),
                Models.KriteriumRazeni.RokSestupne => dotaz.OrderByDescending(f => f.RokVydani),
                Models.KriteriumRazeni.ReziserVzestupne => dotaz.OrderBy(f => f.Reziser),
                _ => dotaz
            };
        }
    }

    private void SmazatFilm(int idFilmu)
    {
        // Najdeme film podle ID a rovnou ho odstraníme z naší paměti
        var filmKSmazani = Models.DatabazeFilmu.Seznam.FirstOrDefault(f => f.Id == idFilmu);
        if (filmKSmazani != null)
        {
            // Odstranění ze seznamu zruší referenci. Objekt následně 
            // zachytí Garbage Collector a zavolá jeho destruktor ~Film().
            Models.DatabazeFilmu.Seznam.Remove(filmKSmazani);
        }
    }
}

Tato komponenta představuje hlavní stránku aplikace a kombinuje uživatelské rozhraní s logikou pro zobrazení, filtraci a řazení dat. Pomocí direktivy @inject je připojena služba HttpClient pro asynchronní stahování dat. Metoda OnInitializedAsync ověřuje, zda je statická kolekce DatabazeFilmu.Seznam prázdná. Pokud ano, asynchronně stáhne úvodní data ze souboru filmy.json a naplní jimi paměť.

O zobrazení dat v tabulce se stará vypočítaná vlastnost ZobrazeneFilmy. Ta nepoužívá statické pole, ale dynamicky sestavuje LINQ dotaz. Nejprve aplikuje metodu Where pro vyfiltrování záznamů podle žánru v proměnné vybranyZanr. Následně využívá C# konstrukci switch, která již nevyužívá nespolehlivé textové řetězce, ale bezpečné hodnoty z výčtového typu Models.KriteriumRazeni. Na základě vybrané hodnoty pak aplikuje správnou metodu pro seřazení (OrderBy nebo OrderByDescending).

Metoda SmazatFilm vyhledá konkrétní záznam podle jeho vlastnosti Id pomocí metody FirstOrDefault. V případě nálezu jej odstraní ze statické kolekce zavoláním metody Remove. Tento krok je klíčový pro životní cyklus objektu – odstraněním ze seznamu zaniká poslední reference na daný objekt filmu. Ten se tak stává "neviditelným" pro zbytek aplikace, což umožní mechanismu Garbage Collector uvolnit jeho místo v operační paměti a při tomto procesu automaticky vyvolat dříve definovaný destruktor (metodu ~Film()).


Razor komponenta - Detail filmu
@page "/film/{Id:int}"

<PageTitle>Detail filmu</PageTitle>

@if (hledanyFilm == null)
{
    <div class="alert alert-danger mt-4">Film s tímto ID nebyl nalezen.</div>
}
else
{
    <div class="card shadow mt-4" style="max-width: 600px;">
        <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
            <h2 class="mb-0">@hledanyFilm.Nazev</h2>
            <span class="badge bg-light text-dark fs-6">@hledanyFilm.RokVydani</span>
        </div>
        <div class="card-body fs-5">
            <p><strong>Režie:</strong> @hledanyFilm.Reziser</p>
            <p><strong>Žánr:</strong> @hledanyFilm.Zanr</p>
            <p><strong>Popis:</strong> @hledanyFilm.Popis</p>
            <p class="text-muted fs-6 mt-4">Interní ID v databázi: @hledanyFilm.Id</p>
        </div>
        <div class="card-footer bg-white">
            <a href="./" class="btn btn-secondary">⬅ Zpět na seznam</a>
        </div>
    </div>
}

@code {
    [Parameter]
    public int Id { get; set; }

    private Models.Film? hledanyFilm;

    protected override void OnInitialized()
    {
        // Vyhledání filmu v paměti na základě ID z parametru URL
        hledanyFilm = Models.DatabazeFilmu.Seznam.FirstOrDefault(f => f.Id == Id);
    }
}

Tato komponenta slouží k zobrazení detailu konkrétního filmu. Adresa stránky je definována direktivou @page s parametrem trasy {Id:int}. Tento dynamický parametr z URL adresy je v bloku @code namapován na vlastnost Id pomocí systémového atributu [Parameter].

Životní cyklus komponenty využívá metodu OnInitialized, která je přepsána klíčovým slovem override. Uvnitř této metody se pomocí LINQ funkce FirstOrDefault prohledává statická kolekce DatabazeFilmu.Seznam. Vyhledá se ten záznam, jehož identifikátor se shoduje s předaným parametrem Id. Nalezený objekt se následně uloží do privátní proměnné hledanyFilm.

Prezentační část kódu využívá podmínku @if k ověření stavu dat. Pokud proměnná hledanyFilm zůstane prázdná (obsahuje null), zobrazí se uživateli textové upozornění, že film neexistuje. V opačném případě kód přejde do větve else a vykreslí formátovanou kartu, do které dosadí konkrétní hodnoty z vlastností objektu, jako jsou Nazev, RokVydani, Reziser, Zanr a Popis.


Razor komponenta - Přidat film
@page "/pridat"
@inject NavigationManager NavManager

<PageTitle>Přidat film</PageTitle>

<h1>Přidat nový film</h1>
<hr />

<div class="card shadow-sm p-4" style="max-width: 600px;">
    <EditForm Model="novyFilm" OnValidSubmit="UlozitFilm">
        <DataAnnotationsValidator />

        <div class="mb-3">
            <label class="form-label">Název filmu:</label>
            <InputText @bind-Value="novyFilm.Nazev" class="form-control" />
            <ValidationMessage For="@(() => novyFilm.Nazev)" class="text-danger" />
        </div>

        <div class="mb-3">
            <label class="form-label">Rok vydání:</label>
            <InputNumber @bind-Value="novyFilm.RokVydani" class="form-control" />
            <ValidationMessage For="@(() => novyFilm.RokVydani)" class="text-danger" />
        </div>

        <div class="mb-3">
            <label class="form-label">Režisér:</label>
            <InputText @bind-Value="novyFilm.Reziser" class="form-control" />
            <ValidationMessage For="@(() => novyFilm.Reziser)" class="text-danger" />
        </div>

        <div class="mb-3">
            <label class="form-label">Žánr:</label>
            <InputText @bind-Value="novyFilm.Zanr" class="form-control" />
            <ValidationMessage For="@(() => novyFilm.Zanr)" class="text-danger" />
        </div>

        <div class="d-flex gap-2 mt-4">
            <button type="submit" class="btn btn-success">Uložit film</button>
            <a href="./" class="btn btn-outline-secondary">Zrušit</a>
        </div>
    </EditForm>
</div>

@code {
    private Models.Film novyFilm = new Models.Film() { RokVydani = 2024 };

    private void UlozitFilm()
    {
        // Generování nového ID pomocí LINQ
        novyFilm.Id = Models.DatabazeFilmu.Seznam.Any() 
            ? Models.DatabazeFilmu.Seznam.Max(f => f.Id) + 1 
            : 1;

        // Přidání do paměti statické databáze
        Models.DatabazeFilmu.Seznam.Add(novyFilm);

        // Automatické přesměrování zpět na hlavní stránku
        NavManager.NavigateTo("./");
    }
}

Tato komponenta zajišťuje vytvoření a uložení nového záznamu. Pomocí direktivy @page je dostupná na adrese /pridat. Kód využívá direktivu @inject k připojení systémové služby NavigationManager, která je uložena do proměnné NavManager a slouží k programovému přesměrování uživatele.

Uživatelské rozhraní staví na komponentě <EditForm>. Ta je napojena na instanci objektu novyFilm, která je v bloku @code inicializována a předvyplněna rokem vydání 2024. Značka <DataAnnotationsValidator /> aktivuje kontrolu validačních pravidel definovaných v datovém modelu. Jednotlivé vstupy, jako <InputText> nebo <InputNumber>, jsou obousměrně svázány s vlastnostmi objektu pomocí parametru @bind-Value. Případné chyby při vyplňování se vykreslují pomocí <ValidationMessage>.

Při úspěšném a validním odeslání formuláře (událost OnValidSubmit) se zavolá metoda UlozitFilm. Ta nejprve pomocí LINQ funkcí Any a Max zjistí aktuálně nejvyšší hodnotu vlastnosti Id ve statické kolekci DatabazeFilmu.Seznam. Novému záznamu přiřadí hodnotu o jedna vyšší, případně hodnotu 1, pokud je kolekce prázdná. Následně je objekt vložen do paměti voláním metody Add. Nakonec metoda NavigateTo přesměruje uživatele zpět na úvodní stránku katalogu.


Razor komponenta - Upravit film
@page "/upravit/{Id:int}"
@inject NavigationManager NavManager

<PageTitle>Upravit film</PageTitle>

<h1> Upravit film</h1>
<hr />

@if (upravovanyFilm == null)
{
    <p>Načítám nebo film neexistuje...</p>
}
else
{
    <div class="card shadow-sm p-4" style="max-width: 600px;">
        <EditForm Model="upravovanyFilm" OnValidSubmit="UlozitZmeny">
            <DataAnnotationsValidator />

            <div class="mb-3">
                <label class="form-label">Název filmu:</label>
                <InputText @bind-Value="upravovanyFilm.Nazev" class="form-control" />
                <ValidationMessage For="@(() => upravovanyFilm.Nazev)" class="text-danger" />
            </div>

            <div class="mb-3">
                <label class="form-label">Rok vydání:</label>
                <InputNumber @bind-Value="upravovanyFilm.RokVydani" class="form-control" />
                <ValidationMessage For="@(() => upravovanyFilm.RokVydani)" class="text-danger" />
            </div>

            <div class="mb-3">
                <label class="form-label">Režisér:</label>
                <InputText @bind-Value="upravovanyFilm.Reziser" class="form-control" />
                <ValidationMessage For="@(() => upravovanyFilm.Reziser)" class="text-danger" />
            </div>

            <div class="mb-3">
                <label class="form-label">Popis (děj):</label>
                <InputTextArea @bind-Value="upravovanyFilm.Popis" class="form-control" rows="3" />
            </div>

            <div class="d-flex gap-2 mt-4">
                <button type="submit" class="btn btn-warning">Uložit změny</button>
                <a href="./" class="btn btn-outline-secondary">Zrušit</a>
            </div>
        </EditForm>
    </div>
}

@code {
    [Parameter]
    public int Id { get; set; }

    private Models.Film? upravovanyFilm;

    protected override void OnInitialized()
    {
        // Najdeme film v naší "databázi"
        var original = Models.DatabazeFilmu.Seznam.FirstOrDefault(f => f.Id == Id);
        if (original != null)
        {
            // UDĚLÁME KOPII (Cloning), abychom neupravovali data přímo v paměti před kliknutím na uložit
            upravovanyFilm = new Models.Film
            {
                Id = original.Id,
                Nazev = original.Nazev,
                RokVydani = original.RokVydani,
                Reziser = original.Reziser,
                Zanr = original.Zanr,
                Popis = original.Popis
            };
        }
    }

    private void UlozitZmeny()
    {
        // Po odklepnutí najdeme původní film a přepíšeme jeho hodnoty těmi z formuláře
        var original = Models.DatabazeFilmu.Seznam.FirstOrDefault(f => f.Id == Id);
        if (original != null && upravovanyFilm != null)
        {
            original.Nazev = upravovanyFilm.Nazev;
            original.RokVydani = upravovanyFilm.RokVydani;
            original.Reziser = upravovanyFilm.Reziser;
            original.Zanr = upravovanyFilm.Zanr;
            original.Popis = upravovanyFilm.Popis;
        }

        // Přesměrujeme zpět na úvod
        NavManager.NavigateTo("./");
    }
}

Tato komponenta zajišťuje úpravu existujícího záznamu. Adresa je definována direktivou @page s dynamickým parametrem trasy {Id:int}, který se v bloku kódu mapuje na vlastnost Id pomocí atributu [Parameter]. K programovému přesměrování uživatele slouží připojená služba NavigationManager uložená v proměnné NavManager.

V metodě OnInitialized se pomocí LINQ funkce FirstOrDefault vyhledá původní záznam ve statické kolekci DatabazeFilmu.Seznam. Následně se vytvoří jeho přesná kopie (klon), která se uloží do proměnné upravovanyFilm. Tento krok zabraňuje úpravě dat přímo v živé paměti – pokud uživatel začne psát, ale formulář neodešle (například klikne na tlačítko Zrušit), originální data v databázi zůstanou nedotčena.

Uživatelské rozhraní využívá prvek <EditForm> napojený na klonovaný objekt v proměnné upravovanyFilm. Validace dat je aktivována značkou <DataAnnotationsValidator />. Změny v polích, jako je <InputText> nebo <InputTextArea>, se přes direktivu @bind-Value okamžitě propisují do vlastností klonu (například Nazev, RokVydani nebo Popis).

Po odeslání platného formuláře se spustí metoda UlozitZmeny. Ta znovu vyhledá původní záznam v kolekci DatabazeFilmu.Seznam a ručně přepíše jeho původní vlastnosti novými hodnotami z proměnné upravovanyFilm. Po dokončení zápisu zavolá metoda NavigateTo přesměrování zpět na hlavní stránku katalogu.