← Område 3 Guide · Filmarkivet När något går fel

Webbserverprogrammering 1 · område 3 · guide

Filmarkivet – komponenter, routing och layout

Sexton steg som bygger om Filmarkivet från sidor till byggstenar. Ett kort blir en fil, listan av kort blir en fil, och adressen börjar bära data: /filmer/2 visar film nummer två och /filmer/99 svarar 404. Projektet startar inte om – det är samma Filmarkivet som i område 2, ett område längre fram.

Missade du en lektion?

Guiden går att följa på egen hand. Så här hänger stegen ihop med passen:

Steg 8, steg 12 och steg 16 hanns inte alltid med i klassrummet. De är korta och går att ta i vilken ordning som helst efter steg 7.

Ett bygge som går igenom bevisar ingenting

I område 2 sa kompilatorn till. Här kopplas komponenternas parametrar ihop när sidan renderas, inte när projektet byggs. En glömd [Parameter] ger noll fel och noll varningar i terminalen – och 500 när du öppnar sidan.

Vanan att lägga sig till med i det här området: ändrat en komponent? Öppna sidan. Inte bara dotnet build.

Koden går inte att kopiera

Kodblocken på den här sidan går inte att markera, och det är med flit. Att skriva av koden för hand är där en stor del av inlärningen sker: du läser raden, håller den i huvudet och upptäcker vid tangentbordet vad du inte förstod. Klistrar du in den hoppar du över precis det steget.

Guide · Område 3 · Webbserverprogrammering 1

Vad du bygger här

Filmarkivet slutar vara sidor och blir byggstenar. Ett filmkort är en fil, listan av kort är en fil, och betygsstjärnorna är en tredje. Samma kort står på startsidan och på /filmer – skrivet en gång, använt två.

Adressen börjar bära data. /filmer/2 visar film nummer 2, /filmer/99 svarar 404, och /filmer?sok=cykel visar de titlar som innehåller cykel.

Filmarkivet
├── /                     Veckans film, topplistan, statistik
├── /filmer               Alla filmer som kort. Sökruta
├── /filmer/{id:int}      En film. Beskrivning och resten av arkivet
├── /livscykel            Demo: när metoderna faktiskt körs
├── /datatyper            Från område 2. Orörd
├── /villkor              Från område 2. Orörd
└── /loopar               Från område 2. Orörd

Det färdiga projektet ligger i referensprojekt/Filmarkivet. Kör det innan du börjar, så vet du vart du är på väg.

Projektet startar inte om. Filmarkivet från område 2 är utgångsläget, och det växer. Sidorna /datatyper, /villkor och /loopar rörs inte en enda gång i den här guiden. Så kommer det att vara resten av kursen.

Vad du ska göra själv efteråt

Här väljer du din domän. Fram till nu har du övat på en engångsklass som hette Produkt. Nu bestämmer du vad ditt projekt handlar om, och det valet bär du till och med område 8. Kriterierna står i uppgift/inlamning-3-komponentbibliotek.md under Steg 0. Läs dem innan du bestämmer dig – en domän som är för tunn går inte att bygga vidare på, och att byta i område 5 kostar en vecka.

Sedan bygger du samma sak som guiden, i din domän:

Översättningstabellen

Fyll i högerkolumnen innan du börjar koda.

FilmarkivetDitt projekt
Filmdin huvudentitet
Titel (string)ditt namnfält
Ar (int)ditt årtal eller motsvarande heltal
LangdMinuter (int)ett heltal du kan räkna och formatera på
Betyg (int, 1–5)ett fält med ett begränsat intervall
Beskrivning (string)din längre text, den som bara syns på detaljsidan
Tillganglig (bool)ditt ja/nej-fält
Id (int)samma. Detaljsidan behöver något att slå upp på
Filmdata.Alladin hårdkodade lista, minst sex poster
FilmKortditt kort
FilmListadin lista
Betygsstjarnordin komponent för det begränsade intervallet
Statusetikettdin komponent för ja/nej-fältet
Rutasamma. Den är domänfri och heter gärna likadant
/filmer/{id:int}/dina-saker/{id:int}

Har du ingen motsvarighet till Beskrivning blir din detaljsida en kopia av ditt kort, och då finns det ingen anledning att ha två sidor. Lägg till fältet.

Innan du börjar

Du behöver Filmarkivet från område 2, eller ditt eget projekt i samma läge: en modellklass, en lista med objekt och en sida som renderar dem.

Kontrollen tar tio sekunder:

cd Filmarkivet
dotnet run

Gå till /filmer. Ser du en tabell med fem rader är du redo.

Begreppen i det här momentet

BegreppVad det är
KomponentEn .razor-fil med markup och @code. Kan användas inuti andra komponenter
SidaEn komponent som har @page och därför har en egen adress
ParameterEtt värde som skickas in i en komponent utifrån
[Parameter]Attributet som gör en egenskap mottaglig utifrån. Utan det når värdet inte fram
RenderFragmentMarkup som skickas in i en komponent, inte bara ett värde
ChildContentNamnet på den RenderFragment som fylls med det som står mellan taggarna
RuttMönstret i @page som avgör vilken adress som leder till komponenten
RuttparameterEn del av adressen som blir en parameter: {id:int}
Ruttbegränsning:int i {id:int}. Adressen matchar bara om delen är ett heltal
FrågesträngDet som står efter ? i adressen: ?sok=cykel
LayoutRamen runt sidan. @Body är hålet där sidan hamnar
LivscykelDe metoder Blazor kallar på i tur och ordning när en komponent byggs

Steg 1 – Filmerna flyttar ut ur sidan

Vad vi gör. Ger Film ett Id och en Beskrivning, och flyttar listan till en egen fil.

Filen. Models/Film.cs:

namespace Filmarkivet.Models;

// En vanlig C#-klass. Inga attribut och ingen databas ännu – det kommer i
// område 5. Här är den bara ett sätt att hålla ihop värden som hör samman.
public class Film
{
    // Id tillkom i område 3. Detaljsidan ligger på /filmer/{id:int} och behöver
    // något att slå upp filmen på.
    public int Id { get; set; }
    public string Titel { get; set; } = "";
    public int Ar { get; set; }
    public int LangdMinuter { get; set; }
    public int Betyg { get; set; }
    public string Beskrivning { get; set; } = "";
    public bool Tillganglig { get; set; }
}

Filen. Models/Filmdata.cs – ny:

namespace Filmarkivet.Models;

public static class Filmdata
{
    public static List<Film> Alla { get; } = new()
    {
        new Film
        {
            Id = 1,
            Titel = "Nyckeln till frihet",
            Ar = 1994,
            LangdMinuter = 142,
            Betyg = 5,
            Beskrivning = "En bankman döms för ett mord han inte begått och bygger under nitton år i Shawshank en vänskap som bär honom.",
            Tillganglig = true
        },
        // … resten av filmerna, se referensprojektet
    };

    // Slår upp en film på id. Returtypen Film? betyder att svaret kan vara null:
    // frågar någon efter /filmer/99 finns ingen film att lämna tillbaka.
    public static Film? Hamta(int id)
    {
        foreach (var film in Alla)
        {
            if (film.Id == id)
            {
                return film;
            }
        }

        return null;
    }
}

Varför det ser ut så. I område 2 låg listan i ett @code-block i Filmer.razor. Det fungerade så länge en enda sida behövde den. Nu ska två sidor se samma filmer: /filmer visar dem och /filmer/2 slår upp en av dem. Ett fält inuti en komponent syns inte från en annan komponent.

static betyder att listan hör till klassen och inte till ett objekt. Det finns exakt en lista, gemensam för alla besökare, och den lever så länge servern gör det. Det är en förenkling. En riktig applikation håller inte data i en statisk lista – i område 4 flyttar innehållet in bakom gränssnittet IFilmService, och då försvinner både static och den här filen. Skälet att den finns nu är att område 3 handlar om komponenter, och att lösa datafrågan ordentligt skulle stjäla hela området.

Film? med frågetecken är en nullbar typ: värdet kan saknas. Utan frågetecknet varnar kompilatorn för return null. Frågetecknet är ditt löfte att den som anropar metoden kommer att kontrollera svaret – och i steg 10 gör vi det.

foreach i stället för LINQ. Alla.FirstOrDefault(f => f.Id == id) gör samma sak på en rad, men LINQ kommer i område 6. Skriv loopen nu, byt sedan.

Kontrollera. Projektet bygger. Ingenting har ändrats på skärmen ännu.

I ditt projekt. Samma två filer, dina namn. Har din huvudentitet inget naturligt Id – ett artikelnummer, ett ISBN – lägg till ett och numrera 1, 2, 3. Minst sex poster, annars blir listsidan svår att se någon poäng med.

Steg 2 – Den första komponenten

Vad vi gör. Bryter ut ett filmkort ur sidan till en egen fil.

Filen. Components/Filmer/FilmKort.razor – ny mapp, ny fil:

<article class="filmkort">
    <h3 class="filmkort-titel">@Filmdata.Alla[0].Titel</h3>

    <p class="filmkort-fakta">
        @Filmdata.Alla[0].Ar
    </p>
</article>

Använd den i Components/Pages/Home.razor:

<h2>Veckans film</h2>

<FilmKort />

Varför det ser ut så. Skillnaden mellan en komponent och en sida är en enda rad: @page. Med @page "/filmer" har komponenten en egen adress och routern kan skicka besökare dit. Utan @page går den bara att använda inuti en annan komponent, som ett <FilmKort /> i markupen.

Taggen är filnamnet. FilmKort.razor blir <FilmKort />. Därför måste komponentfiler börja med stor bokstav – Razor skiljer <filmkort>, som är okänd HTML, från <FilmKort>, som är en komponent.

Mappen Components/Filmer/ är ett val, inte ett krav. Komponenter går att lägga var som helst. Men ett projekt där allt ligger i Pages/ blir oläsligt vid tjugo filer, och du kommer att ha tjugo filer i område 6.

Kontrollera. Gå till /. Kortet syns under rubriken Veckans film.

Och nu det viktiga: lägg <FilmKort /> en gång till, direkt under den första. Du får två kort som visar samma film. Komponenten kan bara visa Alla[0], för det är det enda den vet. Det är problemet som steg 3 löser. Ta bort det extra kortet igen innan du går vidare.

I ditt projekt. Bryt ut det du visar oftast. Är det en rad i en tabell blir det en <Rad>-komponent i stället för ett kort – strukturen är densamma.

Steg 3 – Parametern

Vad vi gör. Låter kortet ta emot vilken film det ska visa.

Filen. Components/Filmer/FilmKort.razor:

<article class="filmkort">
    <h3 class="filmkort-titel">
        <a href="filmer/@Film.Id">@Film.Titel</a>
    </h3>

    <p class="filmkort-fakta">
        @Film.Ar &middot; @Speltid()
    </p>
</article>

@code {
    // EditorRequired gör att editorn varnar om parametern glöms bort. Den är en
    // varning, inte ett fel – koden kompilerar ändå.
    [Parameter]
    [EditorRequired]
    public Film Film { get; set; } = new();

    private string Speltid() => $"{Film.LangdMinuter / 60} h {Film.LangdMinuter % 60} min";
}

Och i Components/Pages/Home.razor:

<FilmKort Film="Filmdata.Alla[0]" />

Varför det ser ut så. [Parameter] är det som gör en egenskap mottaglig utifrån. Utan attributet är egenskapen helt vanlig C# och Blazor vägrar sätta den – med ett felmeddelande som säger precis det: has a property matching the name 'Film', but it does not have [Parameter].

Attributet står på egen rad ovanför egenskapen. public krävs: en private egenskap går inte att sätta utifrån hur många attribut man än sätter på den.

= new() i slutet är där för att kompilatorn annars varnar för att Film kan vara null innan den satts. Det är ett tomt objekt som aldrig hinner synas.

Att egenskapen heter Film och typen också heter Film är tillåtet i C# och vanligt i Blazor. Inuti komponenten betyder Film egenskapen; typen används bara i deklarationen.

Data flödar nedåt. Sidan bestämmer vilken film kortet ska visa. Kortet kan inte säga något tillbaka, och det ska det inte kunna. Ett kort som ändrade i listan hade gjort att man måste läsa alla komponenter för att veta vad som händer med datan.

Kontrollera. Lägg tillbaka det andra kortet, med Filmdata.Alla[1]. Nu visar de två olika filmer. Samma fil, två resultat – det är vad parametern köpte.

I ditt projekt. En parameter per sak kortet behöver. Skicka hellre in hela objektet än sju enskilda fält: <Kort Bok="bok" /> slår <Kort Titel="…" Forfattare="…" Ar="…" /> på varje punkt utom en, och den enda är att komponenten då blir bunden till din entitet. Här är det rätt pris.

Steg 4 – Två komponenter som inte vet något om filmer

Vad vi gör. Bygger betygsstjärnorna och statusetiketten som egna komponenter.

Filen. Components/Delade/Betygsstjarnor.razor – ny mapp:

@* Visar ett betyg 1–5 som stjärnor. Vet ingenting om filmer – bara om ett tal. *@

<span class="betyg" title="@Varde av 5">
    @for (int i = 1; i <= 5; i++)
    {
        <span class="@(i <= Varde ? "stjarna fylld" : "stjarna")">&#9733;</span>
    }
</span>

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

Filen. Components/Delade/Statusetikett.razor:

@* Ett ja/nej-värde som en läsbar etikett. *@

<span class="etikett @(Tillganglig ? "etikett-ja" : "etikett-nej")">
    @(Tillganglig ? "Tillgänglig" : "Utlånad")
</span>

@code {
    [Parameter]
    [EditorRequired]
    public bool Tillganglig { get; set; }
}

Använd dem i FilmKort.razor, efter fakta-raden:

    <Betygsstjarnor Varde="Film.Betyg" />
    <Statusetikett Tillganglig="Film.Tillganglig" />

Varför det ser ut så. De två ligger i Delade/ och inte i Filmer/ därför att de inte känner till Film. Betygsstjarnor tar ett int, Statusetikett tar en bool. Det gör dem användbara på detaljsidan också, och i vilken framtida komponent som helst.

Varde="Film.Betyg" utan snabel-a framför uttrycket. Blazor läser attributvärdet som C# när parametern inte är en string. Vore parametern en string hade Varde="Film.Betyg" betytt bokstavligen texten Film.Betyg – och då behövs Varde="@Film.Betyg".

Typad parameter betyder att <Betygsstjarnor Varde="fyra" /> inte kompilerar. Kompilatorn stoppar det, precis som den stoppar int x = "fyra";. Det är därför parametrar ska ha rätt typ och inte string överallt.

Kontrollera. Gå till /. Kortet visar fem stjärnor, varav fyra eller fem är gula, och en etikett som säger Tillgänglig eller Utlånad.

I ditt projekt. Leta efter fälten som visas på ett särskilt sätt: ett betyg, en status, ett pris, ett datum. Varje sådan formatering är en kandidat till en egen liten komponent. Du behöver minst två för att nå kravet på fyra komponenter.

Steg 5 – Listan som komponent

Vad vi gör. Gör rutnätet av kort till en egen komponent.

Filen. Components/Filmer/FilmLista.razor:

<section class="filmlista">
    <h2>@Rubrik</h2>

    @if (Filmer.Count == 0)
    {
        <p class="tomt">@TomText</p>
    }
    else
    {
        <div class="filmrutnat">
            @foreach (var film in Filmer)
            {
                <FilmKort Film="film" />
            }
        </div>
    }
</section>

@code {
    [Parameter]
    [EditorRequired]
    public List<Film> Filmer { get; set; } = new();

    [Parameter]
    [EditorRequired]
    public string Rubrik { get; set; } = "";

    // Har ett förvalt värde och är därför frivillig att sätta.
    [Parameter]
    public string TomText { get; set; } = "Inga filmer att visa.";
}

Varför det ser ut så. Komponenten bestämmer hur filmer visas, aldrig vilka. Listan kommer in som parameter. Det gör att samma komponent kan visa topplistan på startsidan, alla filmer på /filmer och sökträffarna – utan att veta något om något av det.

TomText har ett förvalt värde och är därför frivillig. Sätts den inte används "Inga filmer att visa.". Så skiljer man en parameter som måste sättas från en som kan sättas: ett vettigt förval, inte [EditorRequired].

@if (Filmer.Count == 0) finns för att en tom lista annars ger en rubrik som svävar över ingenting. Det tomma fallet är alltid värt en rad kod – det är det som händer när sökningen inte ger träff, och det kommer att hända.

En komponent inuti en komponent: FilmLista använder FilmKort, som använder Betygsstjarnor. Tre nivåer, och varje nivå bryr sig bara om nivån under.

Kontrollera. Byt ut tabellen i Components/Pages/Filmer.razor mot

<FilmLista Rubrik="Alla filmer" Filmer="Filmdata.Alla" />

Gå till /filmer. Sex kort i ett rutnät i stället för tabellen från område 2.

I ditt projekt. Din lista tar din List<T>. Behåller du en tabell i stället för kort blir @foreach en <tr> – strukturen är identisk, bara markupen skiljer.

Steg 6 – Komponenten som räknar

Vad vi gör. Flyttar metoderna från område 2 in i en komponent som äger dem.

Filen. Components/Filmer/Filmstatistik.razor:

<ul class="statistik">
    <li>Antal filmer: @Filmer.Count</li>
    <li>Snittbetyg: @Snittbetyg().ToString("0.0")</li>
    <li>Längsta filmen: @LangstaFilmen().Titel, @LangstaFilmen().LangdMinuter minuter</li>
    <li>Tillgängliga just nu: @AntalTillgangliga() av @Filmer.Count</li>
</ul>

@code {
    [Parameter]
    [EditorRequired]
    public List<Film> Filmer { get; set; } = new();

    private double Snittbetyg()
    {
        int summa = 0;

        foreach (var film in Filmer)
        {
            summa = summa + film.Betyg;
        }

        return (double)summa / Filmer.Count;
    }

    private Film LangstaFilmen()
    {
        Film langst = Filmer[0];

        foreach (var film in Filmer)
        {
            if (film.LangdMinuter > langst.LangdMinuter)
            {
                langst = film;
            }
        }

        return langst;
    }

    private int AntalTillgangliga()
    {
        int antal = 0;

        foreach (var film in Filmer)
        {
            if (film.Tillganglig)
            {
                antal++;
            }
        }

        return antal;
    }
}

I Home.razor:

<h2>Om samlingen</h2>

<Filmstatistik Filmer="Filmdata.Alla" />

Varför det ser ut så. Metoderna är oförändrade från område 2 – de har bara flyttat. Skillnaden är att de nu räknar på Filmer, parametern, i stället för på ett fält i sidan. Samma komponent kan räkna på hela arkivet eller på ett urval.

Det här är sista gången uträkningarna bor i en komponent. I område 4 flyttar de en gång till, in i en tjänst, och då är argumentet att en komponent ska visa och inte beräkna. Lägg märke till att flytten dit blir lätt just för att metoderna redan står samlade på ett ställe och tar sin data som parameter.

Kontrollera. Gå till /. Under Om samlingen: fyra rader med siffror. Snittet ska visas som 4,5, inte 4. Blir det 4 har (double) fallit bort.

I ditt projekt. Minst en metod som räknar på hela listan. Snitt, summa, antal eller största – vad som är meningsfullt i din domän.

Steg 7 – Innehåll som parameter

Vad vi gör. Bygger en ruta där den som använder komponenten bestämmer vad som står inuti.

Filen. Components/Delade/Ruta.razor:

@* En ram med rubrik. Vad som står inuti bestämmer den som använder komponenten:
   allt mellan <Ruta> och </Ruta> hamnar i ChildContent. *@

<section class="ruta">
    <h2>@Rubrik</h2>
    @ChildContent
</section>

@code {
    [Parameter]
    [EditorRequired]
    public string Rubrik { get; set; } = "";

    // RenderFragment är markup som skickas in utifrån. Heter parametern
    // ChildContent fylls den automatiskt med det som står mellan taggarna.
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}

I Home.razor:

<Ruta Rubrik="Vad som är nytt i område 3">
    <p>
        Sidorna byggs av komponenter i stället för av markup. Ett kort är en fil,
        en lista är en fil, och betygsstjärnorna är en tredje.
    </p>
</Ruta>

Varför det ser ut så. En string-parameter skickar in ett värde. En RenderFragment skickar in markup: stycken, listor, länkar, till och med andra komponenter. Det gör skillnaden mellan en komponent som visar en text och en som är en ram runt vad som helst.

Namnet ChildContent är inte fritt valt. Blazor letar efter exakt det namnet när det står markup mellan öppnings- och stängningstaggen. Heter parametern något annat får du felet does not have a property matching the name 'ChildContent' – och det är ett körfel, inte ett kompileringsfel. Sidan bygger, och kraschar när någon öppnar den.

Frågetecknet i RenderFragment? betyder att den kan saknas. Skriver du <Ruta Rubrik="Tom" /> utan innehåll är ChildContent null och @ChildContent renderar ingenting alls. Ingen krasch.

Kontrollera. Gå till /. Rutan har en blå ram, rubriken står överst och ditt stycke inuti. Byt ut stycket mot en <ul> med tre punkter och ladda om – samma komponent, annat innehåll.

I ditt projekt. Ruta är domänfri och kan heta likadant hos dig. Den räknas som en av dina fyra komponenter och uppfyller kravet på RenderFragment.

Steg 8 – En fotnot bara ibland

Vad vi gör. Ger FilmKort en andra RenderFragment, som bara vissa användningar fyller.

Filen. Components/Filmer/FilmKort.razor, sist i markupen och i @code:

    @if (Fotnot is not null)
    {
        <div class="filmkort-fotnot">@Fotnot</div>
    }
    // En namngiven RenderFragment. Den som använder kortet kan skicka in markup
    // med <Fotnot>...</Fotnot>. Utelämnas den är Fotnot null och raden uteblir.
    [Parameter]
    public RenderFragment? Fotnot { get; set; }

I Home.razor:

<h2>Veckans film</h2>

<FilmKort Film="veckansFilm">
    <Fotnot>
        Vald för att den är kortast i arkivet. <strong>@veckansFilm.LangdMinuter</strong>
        minuter hinns med en tisdagskväll.
    </Fotnot>
</FilmKort>

Varför det ser ut så. En komponent kan ha flera RenderFragment-parametrar. Den som heter ChildContent fylls automatiskt; alla andra fylls med en tagg som har parameterns namn. <Fotnot> är alltså inte en komponent utan namnet på en parameter i FilmKort.

FilmKort har bara en RenderFragment, och därför räcker <Fotnot> ensam. Ger du komponenten både ChildContent och en namngiven måste båda skrivas ut som taggar – lös markup bredvid <Fotnot> ger kompileringsfelet RZ9996. Det är områdets enda felmeddelande som kommer före körningen, och det säger rakt ut vilka namn komponenten accepterar.

@if (Fotnot is not null) gör att <div class="filmkort-fotnot"> bara skapas när det finns något att lägga i den. Utan villkoret hade varje kort i listan fått en tom div med en kantlinje överst.

**Detta är hela poängen med RenderFragment:** samma kort, olika mycket innehåll, beroende på sammanhanget. Kortet i listan har ingen fotnot. Kortet på startsidan har en, och den innehåller till och med ett uträknat värde från sidan.

Kontrollera. Gå till /. Kortet under Veckans film har en avdelad rad längst ned. Korten under Högst betyg har det inte. Samma fil.

I ditt projekt. En namngiven RenderFragment någonstans där du ibland vill säga något extra: varför en post är utvald, en varning, ett datum.

Steg 9 – Detaljsidan och ruttparametern

Vad vi gör. Bygger sidan som visar en enda film, med id:t hämtat ur adressen.

Filen. Components/Pages/Filmdetalj.razor – ny:

@page "/filmer/{id:int}"

<PageTitle>@(film?.Titel ?? "Filmen finns inte") – Filmarkivet</PageTitle>

@if (film is not null)
{
    <p class="brodsmula"><a href="filmer">&larr; Alla filmer</a></p>

    <h1>@film.Titel</h1>

    <p class="filmkort-fakta">
        @film.Ar &middot; @film.LangdMinuter minuter
    </p>

    <Betygsstjarnor Varde="film.Betyg" />
    <Statusetikett Tillganglig="film.Tillganglig" />

    <p class="beskrivning">@film.Beskrivning</p>
}

@code {
    // Namnet måste stämma med {id:int} i @page-raden. Stor eller liten
    // begynnelsebokstav spelar ingen roll, stavningen gör det.
    [Parameter]
    public int Id { get; set; }

    private Film? film;

    protected override void OnParametersSet()
    {
        film = Filmdata.Hamta(Id);
    }
}

Varför det ser ut så. {id:int} i @page säger två saker. Att den delen av adressen ska bli en parameter, och att den bara matchar om den är ett heltal. Utan :int matchar /filmer/abc också, och Blazor försöker då stoppa in texten abc i en int. Resultatet är ett körfel: Unable to cast object of type 'System.String' to type 'System.Int32'.

Med :int matchar rutten helt enkelt inte, och /filmer/abc blir en 404 – innan en enda rad i din komponent har körts. Ruttbegränsningen är den billigaste felhanteringen som finns.

Namnet Id i @code måste stämma med {id:int} i rutten. Blazor jämför utan hänsyn till stor eller liten bokstav, men inte till stavning: heter egenskapen Nummer får du does not have a property matching the name 'id'.

OnParametersSet och inte OnInitialized: uppslagningen beror på Id, och Id är en parameter. Steg 15 går igenom skillnaden.

Länken i FilmKort – <a href="filmer/@Film.Id"> – är det som leder hit. Den skrevs redan i steg 3 och pekade fram tills nu på ingenting.

Kontrollera. Gå till /filmer och klicka på en titel. Du landar på /filmer/3 eller motsvarande och ser beskrivningen, som inte syns någon annanstans.

I ditt projekt. @page "/dina-saker/{id:int}". Kontrollera att ditt kort länkar dit, annars går sidan bara att nå genom att skriva adressen för hand.

Steg 10 – När filmen inte finns

Vad vi gör. Får /filmer/99 att svara 404 i stället för en tom sida.

Filen. Components/Pages/Filmdetalj.razor, överst och i OnParametersSet:

@page "/filmer/{id:int}"
@inject NavigationManager Navigering
    protected override void OnParametersSet()
    {
        film = Filmdata.Hamta(Id);

        if (film is null)
        {
            // Ingen film med det id:t. NotFound() gör att Router visar
            // NotFound-sidan och servern svarar 404 i stället för 200.
            Navigering.NotFound();
            return;
        }
    }

Filen. Components/Pages/NotFound.razor – skriv om mallens engelska text:

@page "/not-found"
@layout MainLayout

<PageTitle>Sidan finns inte – Filmarkivet</PageTitle>

<h1>Sidan finns inte</h1>

<p>
    Adressen ledde ingenstans. Det vanligaste är en film som inte finns:
    <code>/filmer/99</code> när arkivet bara har sex filmer.
</p>

<p><a href="filmer">Till alla filmer</a></p>

Varför det ser ut så. @inject NavigationManager Navigering ber Blazor om tjänsten som kan adresser. NavigationManager kan läsa den aktuella adressen, byta adress, och – sedan .NET 10 – säga att det som efterfrågades inte finns.

Navigering.NotFound() gör två saker samtidigt. Router i Routes.razor visar komponenten som står i NotFoundPage, och servern sätter statuskoden till 404.

Statuskoden är det som spelar roll. En sida som skriver "finns inte" men svarar 200 OK ljuger för allt som läser statuskoder i stället för text: sökmotorer, övervakning, andra program. Kontrollera i webbläsarens nätverksflik – står det 200 är det fel, hur rätt texten än är.

return efter anropet: utan det fortsätter metoden med film som null och kraschar på nästa rad som rör den.

Routes.razor har redan NotFoundPage="typeof(Pages.NotFound)" – det kom med mallen i .NET 10. Program.cs har app.UseStatusCodePagesWithReExecute("/not-found", …), som är det som gör att /nagot-som-inte-finns också hamnar rätt.

Kontrollera. Tre adresser:

AdressSka ge
/filmer/3Filmen. Status 200
/filmer/99"Sidan finns inte". Status 404
/filmer/abc"Sidan finns inte". Status 404, stoppad av :int

Öppna nätverksfliken (F12) och kontrollera statuskoden, inte bara texten.

I ditt projekt. Samma tre kontroller, dina adresser. Det här är ett av kraven för C – och en av de vanligaste sakerna som saknas.

Steg 11 – Sökningen och frågesträngen

Vad vi gör. Lägger en sökruta på /filmer som filtrerar på titel.

Filen. Components/Pages/Filmer.razor:

@page "/filmer"

<PageTitle>Filmer – Filmarkivet</PageTitle>

<h1>Filmer</h1>

<form method="get" action="filmer" class="sokruta">
    <label for="sok">Sök på titel</label>
    <input type="search" id="sok" name="sok" value="@Sok" />
    <button type="submit">Sök</button>
    <a href="filmer">Visa alla</a>
</form>

<FilmLista Rubrik="@Rubrik()"
           Filmer="traffar"
           TomText="@($"Ingen titel innehåller \"{Sok}\".")" />

@code {
    // Värdet efter ?sok= i adressen. Formuläret ovanför skickar dit det.
    [SupplyParameterFromQuery]
    public string? Sok { get; set; }

    private List<Film> traffar = new();

    // OnParametersSet och inte OnInitialized: träffarna beror på Sok, och Sok är
    // en parameter. Ändras adressen ska listan räknas om.
    protected override void OnParametersSet()
    {
        traffar = new List<Film>();

        foreach (var film in Filmdata.Alla)
        {
            if (string.IsNullOrWhiteSpace(Sok)
                || film.Titel.Contains(Sok, StringComparison.OrdinalIgnoreCase))
            {
                traffar.Add(film);
            }
        }
    }

    private string Rubrik()
    {
        if (string.IsNullOrWhiteSpace(Sok))
        {
            return $"Alla filmer ({traffar.Count})";
        }

        return $"Träffar på \"{Sok}\" ({traffar.Count})";
    }
}

Varför det ser ut så. Två sätt att få data ur adressen, och de gör olika saker:

RuttparameterFrågesträng
Ser ut som/filmer/3/filmer?sok=cykel
Deklareras med{id:int} i @page + [Parameter][SupplyParameterFromQuery]
Måste finnasJa, annars matchar inte ruttenNej, saknas den blir värdet null eller 0
Används tillVilken sak som ska visasHur den ska visas: sökning, filter, sortering

<form method="get"> är ren HTML och kräver ingen interaktivitet. Webbläsaren tar name="sok" och värdet i rutan och bygger adressen /filmer?sok=cykel åt dig. Servern får en helt vanlig ny förfrågan, komponenten byggs om, och Sok är satt. Ingen JavaScript, ingen knapphändelse.

value="@Sok" gör att sökordet står kvar i rutan efter sökningen. Utan den raden töms fältet varje gång, vilket ser trasigt ut.

StringComparison.OrdinalIgnoreCase gör sökningen okänslig för stora och små bokstäver. Contains(Sok) utan den hade krävt exakt Cykel med stort C.

Kontrollera. Fyra sökningar:

AdressSka ge
/filmerAlla filmer (6)
/filmer?sok=cykelTräffar på "cykel" (1)
/filmer?sok=CYKELSamma träff
/filmer?sok=zzzTexten från TomText

Skriv i rutan och tryck på knappen – adressfältet ska ändras. Det är beviset för att det är webbläsaren som bygger frågesträngen.

I ditt projekt. Sök på ditt textfält. Kravet för A är dessutom en sortering, och den byggs likadant: en <select name="sortera"> i samma formulär och en andra [SupplyParameterFromQuery].

Steg 12 – Menyn

Vad vi gör. Uppdaterar navigeringen och tittar närmare på NavLink.

Filen. Components/Layout/NavMenu.razor, i <nav>:

        @* Match="NavLinkMatch.All" på startsidan. Utan den skulle länken räknas
           som aktiv på varje adress, eftersom alla adresser börjar med /. *@
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">Start</NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="filmer">Filmer</NavLink>
        </div>

        <div class="nav-rubrik px-3">Genomgångar</div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="datatyper">Datatyper</NavLink>
        </div>

Varför det ser ut så. NavLink är en komponent som renderar en <a> och dessutom sätter klassen active när adressen stämmer. Det är hela skillnaden mot en vanlig länk, och det är tillräckligt för att det ska vara värt en komponent: alternativet är att varje sida talar om för menyn var den är.

Match="NavLinkMatch.All" betyder att adressen måste stämma helt. Förvalet är NavLinkMatch.Prefix, som räcker för href="filmer" – den ska vara aktiv även på /filmer/3. Men startsidans href="" är prefix till precis varenda adress, så utan All lyser Start överallt.

href="filmer" utan inledande snedstreck. <base href="/" /> i App.razor gör resten. Skriver du /filmer fungerar det också – men relativa adresser är det som gör att applikationen går att lägga i en undermapp i område 8.

Kontrollera. Klicka runt. Exakt en menypost i taget ska vara markerad. Gå till /filmer/3 – Filmer ska fortfarande vara markerad, inte Start.

I ditt projekt. Dina sidor. Kravet är att navigeringen fungerar, och det som oftast fattas är just markeringen: en meny där ingenting eller allting är markerat räknas inte som fungerande.

Steg 13 – Layouten och sidfoten

Vad vi gör. Lägger en sidfot i layouten, så att den syns på varje sida.

Filen. Components/Delade/Sidfot.razor:

@* Ligger i layouten och syns därför på varje sida. *@

<footer class="sidfot">
    <p>
        Filmarkivet &middot; Webbserverprogrammering 1 &middot;
        @DateTime.Now.Year
    </p>
</footer>

Filen. Components/Layout/MainLayout.razor:

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <span class="top-row-text">Webbserverprogrammering 1 &middot; område 3</span>
        </div>

        @* @Body är hålet i layouten. Här renderas den sida rutten pekade ut. *@
        <article class="content px-4">
            @Body
        </article>

        <Sidfot />
    </main>
</div>

Varför det ser ut så. @inherits LayoutComponentBase är det som gör komponenten till en layout. Basklassen ger den egenskapen Body, av typen RenderFragment – **samma typ som ChildContent i steg 7**. En layout är alltså inget nytt begrepp: det är en komponent med en RenderFragment som routern fyller med sidan i stället för att du gör det.

Vilken layout som används står i Routes.razor: DefaultLayout="typeof(Layout.MainLayout)". En enskild sida kan säga emot med @layout NagonAnnanLayout överst – det gör NotFound.razor, eftersom den renderas utanför den vanliga vägen.

Sidfoten skrivs en gång och syns på sju sidor. Skrivs den i stället i varje sida finns den på sju ställen, och den åttonde sidan glömmer den.

Kontrollera. Alla sidor – även /datatyper från område 2 – har fått en sidfot utan att en enda av dem ändrats. Det är vad @Body betyder.

I ditt projekt. Minst en sak i layouten som du inte upprepar per sida. Sidfot, sidhuvud eller en avdelare räcker; kravet är att layouten är gemensam och att du kan peka på vad den ger.

Steg 14 – `_Imports.razor`

Vad vi gör. Slipper skriva samma @using överst i varje fil.

Filen. Components/_Imports.razor, tre rader tillagda sist:

@using Filmarkivet.Models
@using Filmarkivet.Components.Delade
@using Filmarkivet.Components.Filmer

Varför det ser ut så. _Imports.razor gäller för alla .razor-filer i sin egen mapp och i alla undermappar. En rad här ersätter samma rad i varje sida.

Utan @using Filmarkivet.Models måste varje sida som nämner Film ha den raden själv – i område 2 stod den överst i Filmer.razor av precis det skälet.

De två andra raderna gör att <FilmKort /> och <Ruta /> går att skriva utan mappnamn. Blazor letar efter komponenter i de namnrymder som är importerade. Är Filmarkivet.Components.Delade inte importerad blir <Ruta> bara en okänd HTML-tagg – och det ger en varning, inte ett fel, så sidan renderar en tom <ruta>-tagg som inte finns. Det är ett av de svåraste felen att se, eftersom ingenting kraschar.

Filen är inte magisk: den gäller bara .razor. Vanliga .cs-filer använder sina egna using som förut, eller ImplicitUsings i .csproj.

Kontrollera. Ta bort @using Filmarkivet.Models från en sida som använder Film. Projektet bygger ändå.

I ditt projekt. Lägg dina namnrymder här när du har mer än två komponentmappar. Innan dess är det lika lätt att skriva ut dem.

Steg 15 – Livscykeln

Vad vi gör. Tar reda på när OnInitialized och OnParametersSet faktiskt körs.

Filen. Components/Pages/Livscykel.razor – demo:

@page "/livscykel"
@page "/livscykel/{Tal:int}"

<h1>Livscykeln vid statisk serverrendering</h1>

<p>
    Den här sidan har byggts <strong>@minInstans</strong> gånger sedan servern
    startade.
</p>

<ol>
    @foreach (var rad in logg)
    {
        <li><code>@rad</code></li>
    }
</ol>

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

    // static: hör till klassen och överlever mellan förfrågningar. Samma trick
    // som räknaren i område 1. En riktig applikation räknar inte så här.
    private static int antalForfragningar = 0;
    private static int antalAfterRender = 0;

    private int minInstans;
    private List<string> logg = new();

    protected override void OnInitialized()
    {
        antalForfragningar++;
        minInstans = antalForfragningar;
        logg.Add("OnInitialized – objektet är skapat, parametrarna är satta en första gång");
    }

    protected override void OnParametersSet()
    {
        logg.Add($"OnParametersSet – Tal är {Tal}");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        antalAfterRender++;
    }
}

Hela filen med förklaringarna runt omkring ligger i referensprojektet.

Varför det ser ut så. Tre metoder, och de körs inte som man tror.

MetodNärVad den är till för
OnInitializedEn gång, när objektet skapatsSådant som inte beror på parametrarna
OnParametersSetVarje gång parametrarna sättsSådant som beror på parametrarna
OnAfterRenderNär sidan finns i webbläsarenKod som behöver en färdig sida att prata med

**Vid statisk serverrendering körs OnInitialized och OnParametersSet exakt en gång var, i den ordningen, per förfrågan – och OnAfterRender körs inte alls.** Varje adressbyte är en ny förfrågan, ett nytt objekt av komponenten och en ny omgång. Det är därför det i den här kursen aldrig gör någon praktisk skillnad vilken av de två första du väljer.

Men valet betyder något ändå, av två skäl. Det säger åt den som läser koden vad uppslagningen beror på. Och det är rätt när komponenten en dag får nya parametrar utan att skapas om, vilket är precis vad ett interaktivt renderingsläge gör.

Regeln: beror koden på en parameter går den i OnParametersSet. Gör den inte det går den i OnInitialized. Filmdetalj slår upp på Id – OnParametersSet. Home bygger topplistan ur en lista den redan har – OnInitialized.

Kontrollera. Gå till /livscykel. Ladda om fem gånger: räknaren går 1, 2, 3, 4, 5 – varje omladdning är ett nytt objekt. Loggen har alltid exakt två rader, i samma ordning. Och OnAfterRender står kvar på noll, hur många gånger du än laddar om.

Den nollan är det som gör statisk serverrendering konkret. Komponenten finns inte kvar när sidan når webbläsaren.

I ditt projekt. Din detaljsida ska slå upp i OnParametersSet. Kan du säga varför är du klar med det här steget.

Steg 16 – Utseendet

Vad vi gör. Lägger till klasserna komponenterna använder.

Filen. wwwroot/app.css, sist i filen:

/* Område 3: komponenterna */
.filmrutnat {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
    margin: 1rem 0;
}

.filmkort {
    border: 1px solid #d0d0d0;
    border-radius: .3rem;
    padding: .8rem 1rem;
    background: #fff;
}

.betyg .stjarna {
    color: #cfcfcf;
}

.betyg .stjarna.fylld {
    color: #e0a300;
}

.etikett-ja {
    background: #e3f5e3;
    color: #1c5c1c;
}

Resten av klasserna – .filmkort-titel, .filmkort-fakta, .filmkort-fotnot, .etikett, .etikett-nej, .sokruta, .sidfot, .nav-rubrik, .beskrivning, .tomt – ligger i referensprojektets wwwroot/app.css.

Varför det ser ut så. app.css gäller hela applikationen. Alternativet är en FilmKort.razor.css bredvid komponenten, vilket Blazor stöder och kallar isolerad CSS: reglerna gäller då bara den komponenten. MainLayout.razor.css och NavMenu.razor.css i projektet är just sådana.

Här räcker app.css, eftersom klasserna används av flera komponenter – .filmkort-fakta står både i kortet och på detaljsidan.

auto-fill med minmax(15rem, 1fr) gör att antalet kolumner följer fönstrets bredd utan en enda mediefråga.

Kontrollera. Ladda om och dra ihop fönstret. Korten går från tre i bredd till två till ett.

I ditt projekt. Dina klasser, ditt utseende. Utseendet bedöms inte, men ett projekt där man inte ser var ett kort slutar och nästa börjar är svårt att läsa även för den som rättar.

När något går fel

Felen i det här området är nästan alla körfel, inte kompileringsfel. Projektet bygger, och sidan svarar 500. Meddelandet står i terminalen där dotnet run körs, och i webbläsaren om du kör i utvecklingsläge.

Slå upp felmeddelandet här innan du fastnar. Skriv en bit av det du ser.

10 rader

FelmeddelandeVad det betyderGör så här
Object of type '…' does not have a property matching the name 'X'Du sätter en parameter som komponenten inte harStavfel, eller så glömdes parametern. Jämför taggen med @code-blocket
…does not have a property matching the name 'ChildContent'Det står markup mellan taggarna, men komponenten har ingen ChildContentLägg till [Parameter] public RenderFragment? ChildContent { get; set; }. Namnet måste vara exakt det
has a property matching the name 'X', but it does not have [Parameter]Egenskapen finns men saknar attributetSätt [Parameter] på raden ovanför. Kontrollera också att den är public
does not have a property matching the name 'id'Ruttens namn och egenskapens namn skiljer sig{id:int} kräver en egenskap som heter Id
Unable to cast object of type 'System.String' to type 'System.Int32'Rutten saknar :intSkriv {id:int}, inte {id}
AmbiguousMatchException: The request matched multiple endpointsTvå komponenter har samma @pageSök i projektet efter adressen. Oftast en gammal sida som skulle tagits bort
RZ9996: Unrecognized child content inside component 'X'Komponenten har en namngiven RenderFragment, och du blandar den med lös markupSkriv ut <ChildContent> också. Används en namngiven måste alla skrivas ut. Det här är ett av områdets få kompileringsfel
Found markup element with unexpected name 'Ruta' (varning)Komponenten hittades inte, taggen behandlas som HTML@using saknas, eller filnamnet är felstavat. Sidan renderar tomt utan att krascha
The type or namespace name 'Film' could not be found@using Filmarkivet.Models saknasLägg till den i _Imports.razor
Sidan är tom, inget felEn @if som aldrig blir sannÄr film null? Kontrollera vad Hamta fick för id

exempel/felsokning/ innehåller sex filer som var och en ger ett av felen ovan. Kör dem om du vill se meddelandena innan du råkar ut för dem.

Det som är lätt att missa när du gör det själv

Fyra komponenter, inte fyra filer. Kravet är fyra komponenter som gör något. Fyra kopior av samma kort med olika namn räknas inte, och det syns direkt.

Din detaljsida måste visa något som listsidan inte visar. Är kortet och detaljsidan lika finns ingen anledning att klicka. Det är därför Beskrivning finns i modellen – lägg in motsvarigheten i din.

Kortet måste länka till detaljsidan. Guidens <a href="filmer/@Film.Id"> skrevs i steg 3, långt innan sidan fanns. Gör du stegen i annan ordning är det lätt att detaljsidan bara går att nå genom att skriva adressen för hand. Det räknas inte som "kopplade med ruttparameter".

**{id:int} och inte {id}.** Utan begränsningen kraschar sidan på varje adress som inte är ett tal – och sådana kommer, från länkar och sökmotorer.

**Parametrar är public.** private int Varde med [Parameter] ovanför ger ett kompileringsfel som handlar om helt andra saker och är svårt att tyda.

**Namnet ChildContent går inte att välja fritt.** Vill du kalla den något annat måste den som använder komponenten skriva ut namnet som en tagg, som <Fotnot> i steg 8. Blandar man ihop de två blir felet ett körfel.

Sökningen behöver inget klick. Ett <form method="get"> räcker. Kod som lyssnar på @onclick gör ingenting alls vid statisk serverrendering – knappen händer bara ingenting när man trycker på den, utan felmeddelande.

Statuskoden är en del av kravet. Kontrollera 404 i nätverksfliken, inte genom att läsa texten på sidan.

Checklista

Guidens egen checklista. Den är inte betygskraven – de står i uppgiften och är uppdelade på E, C och A. Den här listan säger bara om du gjort klart det guiden går igenom.

Klart när allt är ikryssat

Kryssen sparas i din webbläsare.

Vill du mer