Kanban nástěnka

Website: Moodle ZČU
Kurs: BP Luboš Dokulil Sada rozšiřujících úloh v Blazor pro předmět Programování 2
Buch: Kanban nástěnka
Gedruckt von: Nepřihlášený host
Datum: Samstag, 17. Oktober 2026, 18:16

1. Zadání

Co je cílem zadání?

  • Definice hierarchického datového modelu s využitím abstraktní bázové třídy a polymorfismu pro ukládání různorodých úkolů do jednotné kolekce.
  • Asynchronní naplnění počátečního stavu aplikace stažením a deserializací dat ze vzdáleného rozhraní REST API.
  • Rozdělení sdílené kolekce do vizuálně nezávislých sloupců pomocí dynamického filtrování a prioritního řazení prostřednictvím dotazů LINQ.
  • Implementace uživatelského rozhraní s podporou technologie Drag & Drop pro vizuální přesun objektů mezi jednotlivými zónami.
  • Zajištění správy vnitřního stavu komponenty, kdy modifikace textového atributu objektu vyvolá automatické překreslení a přesun prvku v aplikaci.

Zadání

Vytvořte abstraktní bázovou třídu PracovniUkol, která bude obsahovat vlastnosti Id, Nazev a Stav (s výchozí hodnotou "Nové"), virtuální vlastnost JeKriticky s výchozí hodnotou false a abstraktní metodu ZiskejTypUkolu(). Z této bázové třídy následně odvoďte dvě specializované třídy: ExterniUkol (přepisuje vlastnosti Stav) a KritickaChyba (přepisuje vlastnost JeKriticky).

V Razor komponentě vytvořte interaktivní agilní nástěnku s následující funkcionalitou:

  1. Inicializace dat: Při spuštění komponenty asynchronně stáhněte data ze služby https://jsonplaceholder.typicode.com/todos. Prvních několik záznamů převeďte na instance typu ExterniUkol a vložte je do hlavní sdílené kolekce úkolů.
  2. Rozložení sloupců: Vizuálně rozdělte obrazovku do tří sloupců: Nové, Probíhá a Dokončeno. Zajistěte, aby každý sloupec pomocí technologie LINQ vykresloval pouze úkoly s odpovídajícím stavem. A úkoly se daly mezi jednotlivými sloupci přetahovat pomocí Drag & Drop.
  3. Řazení s prioritou: Ve sloupci Nové řaďte kritické chyby nad ostatní úkoly.
  4. Formulář: Nad nástěnku přidejte jednoduchý textový vstup a tlačítko, kterým uživatel do systému manuálně přidá novou instanci typu KritickaChyba.

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

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

2. Grafická nápověda

Třídy PracovniUkol, ExterniUkol a KritickaChyba

Třída PracovniUkol slouží jako abstraktní bázová šablona, která definuje společné vlastnosti jako Id, Nazev a Stav. Z této třídy dědí specializované modely ExterniUkol a KritickaChyba, které polymorfně přepisují definovanou metodu ZiskejTypUkolu() a v případě kritické chyby i volitelnou virtuální vlastnost JeKriticky.


Po spuštění

Úvodní fáze vykreslení komponenty je zahájena asynchronním získáním dat. Systém nejprve vytvoří prázdnou kolekci a pomocí služby HttpClient stáhne záznamy ze vzdáleného API. Následně iteruje skrze prvních 12 položek a na základě obdržené logické hodnoty completed je transformuje do instancí třídy ExterniUkol s příslušně přiděleným stavem "Nové" nebo "Dokončeno".


Táhnutí kartou

Interakce uživatele při přesouvání položek využívá vestavěných událostí technologie Drag & Drop. Proces začíná zachycením taženého prvku pomocí události @ondragstart a jeho uložením do dočasné proměnné. Jakmile uživatel kartu upustí v cílovém sloupci, událost @ondrop ověří změnu a aktualizuje textovou vlastnost Stav. O vizuální překreslení rozhraní se následně automaticky postará samotný framework Blazor.

3. Řešení úkolu

Abstraktní třída PracovniUkol
namespace Komplexni_ukol_Kanban.Models
{
    public abstract class PracovniUkol
    {
        public int Id { get; set; }
        public string Nazev { get; set; } = "";

        // Výchozí stav pro Kanban nástěnku
        public string Stav { get; set; } = "Nové";
        public virtual bool JeKriticky => false;

        // Abstraktní metoda, kterou musí každý potomek přepsat
        public abstract string ZiskejTypUkolu();
    }
}

Použitím klíčového slova abstract u třídy PracovniUkol vytváříme obecnou šablonu, ze které nelze přímo tvořit instance. Nastavením výchozí hodnoty "Nové" u vlastnosti Stav zajišťujeme automatické zařazení každého nového úkolu do prvního sloupce nástěnky. Pomocí virtuální vlastnosti JeKriticky a abstraktní metody ZiskejTypUkolu() navíc připravujeme datový model pro polymorfismus, čímž si vynucujeme specifické chování u všech budoucích odvozených tříd.


Třída ExterniUkol
namespace Komplexni_ukol_Kanban.Models
{
    // Odvozená třída pro úkoly stažené z API
    public class ExterniUkol : PracovniUkol
    {
        public override string ZiskejTypUkolu() => "Získáno z API";
    }
}

Třída ExterniUkol přímo rozšiřuje bázovou třídu PracovniUkol a získává tak přístup ke všem jejím definovaným vlastnostem. Pomocí klíčového slova override dochází k polymorfnímu přepsání povinné abstraktní metody ZiskejTypUkolu(), jejíž tělo je pro maximální čistotu kódu zapsáno stručně pomocí lambda výrazu (=>) vracejícího statický text.


Třída KritickaChyba
namespace Komplexni_ukol_Kanban.Models
{
    // Odvozená třída pro manuálně zadané chyby
    public class KritickaChyba : PracovniUkol
    {
        public string Priorita { get; set; } = "Vysoká";
        public override bool JeKriticky => true;
        public override string ZiskejTypUkolu() => $"Kritická chyba ({Priorita})";
    }
}

Třída KritickaChyba rozšiřuje datový model o novou vlastnost Priorita a pomocí klíčového slova override přepisuje původní virtuální vlastnost JeKriticky z bázové třídy na hodnotu true.


Třída ApiZaznam
using System.Text.Json.Serialization;

namespace Komplexni_ukol_Kanban.Models
{
    // Pomocná třída pro zachycení surových dat z JSONu
    public class ApiZaznam
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; } = "";

        [JsonPropertyName("completed")]
        public bool Completed { get; set; }
    }
}

Pomocná třída ApiZaznam slouží jako dočasná struktura pro prvotní zachycení surových dat ze vzdáleného rozhraní REST API. Aby bylo zajištěno bezchybné napojení na přijímaný formát JSON, jsou jednotlivé vlastnosti opatřeny atributem [JsonPropertyName]. Tento přístup umožňuje zachovat v kódu C# standardní jmenné konvence a frameworku jasně specifikuje, pod jakými textovými klíči (např. "id", "title", "completed") má reálná data v odpovědi ze serveru hledat.


Razor komponenta
@page "/"
@inject HttpClient Http

<PageTitle>Nástěnka</PageTitle>

<h2>Kanban Nástěnka</h2>

<!-- Pokud ještě nebyla stažena data, zobrazí se informace o načítání -->
@if (databazeUkolu == null)
{
    <div class="alert alert-info">Načítám úkoly ze serveru...</div>
}
else
{
    <!-- Formulář pro manuální vytvoření a zařazení objektu "Kritická chyba" -->
    <div class="mb-4 p-3 bg-light border rounded">
        <h5>Nahlásit kritickou chybu</h5>
        <div class="d-flex gap-2">
            <!-- Obousměrná datová vazba pro textový vstup -->
            <input @bind="novyNazevChyby" class="form-control" placeholder="Popis problému..." />
            <button class="btn btn-danger" @onclick="PridatChybu">Vytvořit chybu</button>
        </div>
    </div>

    <div class="row">
        <!-- SLOUPEC: NOVÉ -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Nové")'>

            <h4 class="text-center bg-secondary text-white p-2 rounded">Nové</h4>

            <!-- Úkoly s vlastností JeKriticky = true seřadí OrderByDescending na vrchol -->
            @foreach (var ukol in databazeUkolu
                    .Where(u => u.Stav == "Nové")
                    .OrderByDescending(u => u.JeKriticky))
            {
                <div class="card mb-2 shadow-sm @(ukol.JeKriticky ? "border-danger" : "")"
                     draggable="true"
                     @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <strong>@ukol.Nazev</strong>
                        <br />
                        <!-- Polymorfní volání metody ZiskejTypUkolu() -->
                        <span class="badge @(ukol.JeKriticky ? "bg-danger" : "bg-primary") mt-2">
                            @ukol.ZiskejTypUkolu()
                        </span>
                    </div>
                </div>
            }
        </div>

        <!-- SLOUPEC: PROBÍHÁ -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Probíhá")'>

            <h4 class="text-center bg-warning text-dark p-2 rounded">Probíhá</h4>

            @foreach (var ukol in databazeUkolu.Where(u => u.Stav == "Probíhá"))
            {
                <div class="card mb-2 shadow-sm border-warning" draggable="true" @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <strong>@ukol.Nazev</strong>
                        <br />
                        <span class="badge bg-primary mt-2">@ukol.ZiskejTypUkolu()</span>
                    </div>
                </div>
            }
        </div>

        <!-- SLOUPEC: DOKONČENO -->
        <div class="col-md-4 kanban-sloupec"
             ondragover="event.preventDefault();"
             @ondrop='() => DokoncitPresun("Dokončeno")'>

            <h4 class="text-center bg-success text-white p-2 rounded">Dokončeno</h4>

            @foreach (var ukol in databazeUkolu.Where(u => u.Stav == "Dokončeno"))
            {
                <div class="card mb-2 shadow-sm border-success text-muted" draggable="true" @ondragstart="() => ZahajitPresun(ukol)">
                    <div class="card-body">
                        <del>@ukol.Nazev</del>
                        <br />
                        <span class="badge bg-secondary mt-2">@ukol.ZiskejTypUkolu()</span>
                    </div>
                </div>
            }
        </div>
    </div>
}

<style>
    /* CSS pro správné fungování cílových zón */
    .kanban-sloupec {
        min-height: 400px;
        background-color: #f8f9fa;
        border: 2px dashed #dee2e6;
        border-radius: 8px;
        padding: 10px;
    }
    .card[draggable="true"] {
        cursor: grab;
    }
    .card[draggable="true"]:active {
        cursor: grabbing;
    }
</style>

@code {
    // Hlavní databáze uchovávající polymorfní objekty (Externí úkoly i Chyby)
    private List<Models.PracovniUkol>? databazeUkolu;
    private Models.PracovniUkol? prenasenyUkol;
    private string novyNazevChyby = "";

    protected override async Task OnInitializedAsync()
    {
        databazeUkolu = new List<Models.PracovniUkol>();
        var dataZApi = await Http.GetFromJsonAsync<List<Models.ApiZaznam>>("https://jsonplaceholder.typicode.com/todos");

        if (dataZApi != null)
        {
            foreach (var polozka in dataZApi.Take(12))
            {
                databazeUkolu.Add(new Models.ExterniUkol
                {
                    Id = polozka.Id,
                    Nazev = polozka.Title,
                    Stav = polozka.Completed ? "Dokončeno" : "Nové"
                });
            }
        }
    }

    private void ZahajitPresun(Models.PracovniUkol ukol) => prenasenyUkol = ukol;

    private void DokoncitPresun(string novyStav)
    {
        if (prenasenyUkol != null && prenasenyUkol.Stav != novyStav)
        {
            prenasenyUkol.Stav = novyStav;
            prenasenyUkol = null;
        }
    }

    private void PridatChybu()
    {
        if (!string.IsNullOrWhiteSpace(novyNazevChyby) && databazeUkolu != null)
        {
            int noveId = databazeUkolu.Any() ? databazeUkolu.Max(u => u.Id) + 1 : 1;
            databazeUkolu.Add(new Models.KritickaChyba { Id = noveId, Nazev = novyNazevChyby, Stav = "Nové" });
            novyNazevChyby = "";
        }
    }
}

Metoda OnInitializedAsync asynchronně stahuje data z API a transformuje je do výchozího stavu. Výpis úkolů do sloupců řídí LINQ metoda Where, přičemž ve sloupci "Nové" se navíc uplatňuje řazení OrderByDescending(u => u.JeKriticky), což zaručuje prioritní zobrazení kritických chyb na vrcholu seznamu. Samotné přesouvání využívá nativní HTML události @ondragstart a @ondrop; po upuštění prvku dojde v metodě DokoncitPresun pouze k přepsání textové vlastnosti Stav, načež Blazor díky své reaktivitě automaticky zajistí vizuální přesun karty do správného sloupce.