Opgave: JSON-file, Background Image, Icons and more Bootstrap features


Dette er den 5. opgave i opgave-serien ItemRazor.
I forrige opgave blev det muligt at Editere (update) og Delete (slette) Items objekter via GetAllItems siden. I denne opgave vil der være fokus på Persistens, mere præcist vil der blive introduceret til brugen af JSON-filer, så Item objekter kan hentes når siden indlæses, og gemmes når listen af Items objekter opdateres. Desuden vil der blive introduceret til brugen af Icons, anvendelse af Background Image samt flere andre Bootstrap features inkl. Grid.

Udgangspunktet er løsningen fra opgave 4 (ItemRazorV4.zip):



Step 1 (Data)
Første step er at oprette en ny mappe Data. Mappen skal ligge i wwwroot.
Denne mappe kommer senere til at rumme Json-filen: Items.json, hvor vi vil gemme (persistere) alle vores Items objekter.


Step 2 (JsonFileItemService)
Næste step er at oprette en ny klasse JsonFileItemService i mappen Services. Dette klasse skal benyttes til at hente (GetJsonItems) og gemme (SaveJsonItems) listen af Items i Json-filen: Items.json.

Add en property IWebHostEnvironment WebHostEnvironment, propertyen skal kun tilbyde get:

public IWebHostEnvironment WebHostEnvironment { get;}

WebHostEnvironment er en service der bl.a. kan benyttes til at få stien til placeringen af vores fil Items.json, men først skal den initialiseres. Det gøres ved "Dependency Injection" af servicen i konstruktøren:

public JsonFileItemService(IWebHostEnvironment webHostEnvironment)
{
WebHostEnvironment = webHostEnvironment;
}

Nu kan servicen benyttes til at konstruere det fulde filnavn (med path (sti) til placering).

Tilføj propertyen:

private string JsonFileName
{
get { return Path.Combine(WebHostEnvironment.WebRootPath, "Data", "Items.json"); }
}

der returnere det fulde filnavn ved at kombinere stien til "wwwroot" med mappen "Data" og filnavnet "Items.json" (dvs ...\wwwroot\Data\Items.json )

 

Step 3 (SaveJsonItems)
Når listen af Items skal persisteres, dvs skrives til en fil for at blive gemt, skal objekterne gemmes i et bestemt format. Formatet hedder json (JavaScript Object Notation) og ser ud som følger:


Et array [...] med objekter {...} bestående af "name":"value" - pair.
Vores liste af Items objekter skal altså konverteres (serialiseres) til dette format - det kan metoden: JsonSerializer.Serialize< >( ) hjælpe med. De serialiserede objekter skal skrives til filen givet ved JsonFileName, det gøres ved hjælp using(...) der åbner en FileStream til filen oprettet med File.Create(...). Når der skal skrives til filen benyttes en Utf8JsonWriter til at "dekorere" FileStreamen med egenskaben "at kunne skrive i json-format". Det er Serialize-metoden der serialisere listen og skriver den til filen:

public void SaveJsonItems(List<Item> items)
{
using (FileStream jsonFileWriter = File.Create(JsonFileName))
{
Utf8JsonWriter jsonWriter = new Utf8JsonWriter(jsonFileWriter, new JsonWriterOptions()
{
SkipValidation = false,
Indented = true
});
JsonSerializer.Serialize<Item[]>(jsonWriter, items.ToArray());
}
}

Step 4 (GetJsonItems)
Når den serialiserede liste skal hentes ind igen, er det den modsatte proces. Først benyttes using(...) til at åbne en StreamReader og File.OpenText(..) til at åbne filen. Det er JsonSerializer.Deserialize<>(...) metoden der læser fra filen og deserialisere json-objekterne til et array af Item objekter.

Bemærk: Deserializeren benytter default (no arg) construktoren til Itemt til først at oprette et tomt Item objekt, Dernæst vil den for hvert "name":"value" - pair, kalde set metoderne med Name=value osv. - derfor er det vigtigt at klassen Item har en tom konstruktør: Item(){...} uden parametre!

Tilføj følgende metode til JsonFileItemService:

public IEnumerable<Item> GetJsonItems()
{
using (StreamReader jsonFileReader = File.OpenText(JsonFileName))
{
return JsonSerializer.Deserialize<Item[]>(jsonFileReader.ReadToEnd());
}
}

 

Step 5 (Startup.cs, ItemService.cs)
Nu er Service klassen, der skal hjælpe med at "persistere" dvs gemme/hente vores Item objekter, på plads. Vi skal nu have vores ItemService til at benytte den nye service klasse i stedet for at anvende MockData (testdata).

Først skal den nye service configureres. Opdater ConfigureServices metoden i Startup.cs med services.AddTransient<JsonFileItemService>();


public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddSingleton<ItemService, ItemService>();
services.AddTransient<JsonFileItemService>();
}

Overvej/Undersøg:

Nu kan servicesen injiceres ind i ItemService via konstruktøren. Opdater ItemService med:

private JsonFileItemService JsonFileItemService { get; set; }

public ItemService(JsonFileItemService jsonFileItemService)
{
JsonFileItemService = jsonFileItemService;
_items = MockItems.GetMockItems();
//_items = JsonFileItemService.GetJsonItems().ToList();
}

Bemærk: Første gang vi afprøver programmet findes filen ikke endnu og data ligger kun i MockData - derfor udkommenterer vi initialiseringen via JsonFileItemService. Næste gang programmet afvikles er filen dannet og listen gemt (såfremt listen er ændret og save kaldt), derfor udkommenteres initialiseringen via MockData og initialiseringen via JsonFileItemService "indkommenteres" igen.

 

Step 6 (ItemService - AddItem, DeleteItem, UpdateItem)
Hver gang listen ændres skal vi "persistere" ændringen, dvs vi skal kalde SaveJsonItems.

Opdater AddItem, DeleteItem og UpdateItem, så SaveJsonItems( ) kaldes når listen opdateres:

public void AddItem(Item item)
{
_items.Add(item);
JsonFileItemService.SaveJsonItems(_items);
}

 

public Item DeleteItem(int? itemId)
{
Item itemToBeDeleted = null;
foreach (Item item in _items)
{
if (item.Id == itemId)
{
itemToBeDeleted = item;
break;
}
}
if (itemToBeDeleted != null)
{
_items.Remove(itemToBeDeleted);
JsonFileItemService.SaveJsonItems(_items);
}
return itemToBeDeleted;
}

 

public void UpdateItem(Item item)
{
if (item != null)
{
foreach (Item i in _items)
{
if (i.Id == item.Id)
{
i.Name = item.Name;
i.Price = item.Price;
}
}
JsonFileItemService.SaveJsonItems(_items);
}
}

 

Step 7 (Afprøv)
Afprøv at programmet og "persisteringen" virker, dvs. at nyoprettede objekter stadigt vil være i listen/tabellen, når applikationen genstartes (samt at andre opdateringer af listen Edit/Delete bliver gemt).

Husk: Første gang applikationen køres skal data hentes fra MockItem! Efterfølgende skal data hentes fra filen (ps kræver at der er udført en Add, Delete eller Update inden).

Nu vi har CRUD, Search, Filter og Persistensen på plads, er det tid til at gøre lidt ved designet (Bootstrap mm)

 

Step 8 (Bootswatch - Themes, _Layout.cshtml )
Det første vi gør er at ændre på "Themes" dvs det look and feel tema, som vores applikation skal benytte. Det gøres ved i _Layout.cshtml filen at tilføje et <Link>-tag med en CDN reference til det Bootswatch/Bootstrap Themes vi ønsker (her er valgt "slate"). Indsæt følgende link (efter linket til bootstrap):

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@4.5.2/dist/slate/bootstrap.min.css">

Til inspiration kan du finde CDN link til andre Bootswatch themes her: https://www.bootstrapcdn.com/bootswatch/


Step 9 (Afprøv)
Afprøv at linket virker og at applikationen har skiftet Themes til "slate".

 

Step 10 (background-image, ItemRazorStyles.css, _Layout.cshtml)
Den letteste måde, at indsætte et baggrunds billede, er via "styling af <body>-tagget.
Opret en ny css fil: ItemRazorStyles.css i mappen: "wwwroot\css" (højreklik på mappen vælg Add-> New Item -> Style Sheet).

Udskift default stylesheet'et: site.css med det nye:

<link rel="stylesheet" href="~/css/ItemRazorStyles.css" />

Det billede vi gerne vil benytte er:



Hent og gem billedet computergear.jpg i mappen: "wwwroot".

Indsæt følgende css i ItemRazorStyles.css:

body{
/* The image used */
background-image: url("../computergear.jpg");
/* Full height */
height: 100%;
/* Center and scale the image nicely */
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}

 

Step 11 (Afprøv)
Afprøv at baggrunds billedet bliver vist i applikationen.

 

Step 12 (Mere styling, ItemRazorStyle.css, GetAllItems)
Tabellen ser ikke godt ud med det nye baggrunds-billede, men det kan vi gøre noget ved.

Først ændre vi på "opacity"

a) Add følgende css class definition:

.table-opacity {
background-color: #ffffff;
opacity: 0.9;
}

b) Add den nye css class til <table> i GetAllItems:

<table class="table table-bordered table-hover table-striped table-opacity">

Så er vi klar til lidt mere "styling" mm!

c) Add et nyt <th>-tag under de andre table headers

<th>Actions</th>

d) Add class="btn-secondary active" til <tr> under <thead>

<thead>
<tr class="btn-secondary active">
...
</tr>
</thead>

e) Add class="btn-secondary" til <tr> i foreach-løkken:

foreach (var item in Model.Items)
{
<tr class="btn-secondary">
...
</tr>
}


Bemærk: Nu er det filen: _Layout.cshtml, der skal opdateres.

f) Add class="d-flex flex-column min-vh-100" til <body> tagget

<body class="d-flex flex-column min-vh-100">

g) Add class:"mt-auto" til <footer> tagget:

<footer class="border-top footer text-muted mt-auto">

 

Step 13 (Afprøv)
Du skulle gerne få vist tabellen i et lidt pænere design ala:

 

Bemærk: navigations-baren er lys, for at få den dark (som er default for slate) og teksten hvid:

a) Fjern: class: navbar-light bg-white fra <nav>-tag i _Layout.cshtml:

<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">

b) Fjern: class: text-dark fra <a> taggene

<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
<a class="nav-link text-dark" asp-area="" asp-page="/Item/GetAllItems">Items</a>

 

Step 14 (Layout - Bootstrap Grid)
For at kunne styre layoutet på siden benytter vi Bootstrap Grids, hvor hver rows bliver opdelt i 12 coloms, se evt: https://getbootstrap.com/docs/5.0/layout/grid/

a) Først indsættes en "row" med 3 "col" (colums) der indeholde overskrifter til Search og Filter:

<div class="row">
<div class="col-4"><h5>Search Name</h5></div>
<div class="col-4"><h5>Filter Price</h5></div>
<div class="col-4"></div>
</div>

Bemærk: "col-4" betyder at <div> tagget spænder over 4 (af 12) kolonner

Overvej: Hvorfor er der indsat et tomt <div> tag med "col-4" efter de to andre?

b) Add et <div class="row"> - tag om de to <form>-tag samt <div class="col-4"> og <div class="col-5"> om de enkelte forms.

Bemærk: der er tilføjet et tomt <div class="col-2"></div> efter de to forms - hvorfor?

Vi styrer også lige de enkelte kolonners bredde i tabellen, tilføj width="60%" og de 4 <col> tags:

Overvej/Undersøg: Hvad er formålet med de enkelte Bootstrap "klasser", hvad er effekten af dem? - måske du kan anvende dem i dit kommende projekt!


Step 15 (Afprøv)
Du skulle gerne få vist en side ala:

 

Helt korrekt - det ser ikke færdigt ud. Ideen er at knapperne skal udskiftes med icons.

Step 16 (Icons - _Layout.cshtml)
Vi vil gerne benytte Icons, font-awesome har en række gratis iconer vi frit kan benytte (men har også en række iconer der kræver betaling) se evt: https://fontawesome.com/v5.15/icons?d=gallery&p=2

Først skal vi tilføje et CDN-link til font-awesome (_Layout.cshtml):

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">

Når der skal indsættes et icon på et link <a>-tag indsættes et <i>-tag class="fa fa-edit" er en css-class defineret af font-awsome der indsætter et edit-icon.

Add <i class="fa fa-edit"></i> til <a>-tag for edit og <i class="fa fa-trash"></i> til <a>-tagget for delete:

<a class="btn btn-primary btn-sm" type="button" data-toggle="tooltip" title="Edit" asp-page="EditItem" asp-route-id="@item.Id"><i class="fa fa-edit"></i></a>

<a class="btn btn-danger btn-sm " type="button" data-toggle="tooltip" data-placement="top" title="Delete" asp-page="DeleteItem" asp-route-id="@item.Id"><i class="fa fa-trash"></i></a>

Overvej/Undersøg: Hvad er formålet med de enkelte Bootstrap "klasser", hvad er effekten af dem? - måske du kan anvende dem i dit kommende projekt!

 

Step 17 (Afprøv)
Du skulle gerne få vist en side ala:

 


God fornøjelse!
Henrik Høltzer