Filmy

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.