Blazor WebAssembly App
- Inleiding
- Hosted Blazor WebAssembly App
- Componenten
- Modeldefinitie
- Interface
- Klasse
- Host
- Componenten in detail
- Slot
Inleiding
We hadden het in deze post gehad over de host. In ASP .NET Core wordt een host geactiveerd bij het opstarten van een applicatie en de host zet de “dingen” klaar die nodig zijn voor het doen draaien van de applicatie.
De host is slechts het startpunt van een Blazor applicatie en een Blazor applicatie bestaat uit veel meer dan alleen maar een host. Zo zijn er naast de host ook componenten, modeldefinities, interfaces en klassen en een component bestaat op haar beurt ook weer uit drie onderdelen.
We lichten in deze post het één en ander toe aan de hand van een ASP .NET Core hosted Blazor WebAssembly App. We hebben daarbij gebruik gemaakt van Visual Studio 2019 en de voorbeeldcode kun je hier (GitHub) vinden.
Hosted Blazor WebAssembly App
We creëren in Visual Studio een hosted Blazor WebAssembly App. Let op optie “ASP.NET Core hosted”. Vergeet niet die optie aan te vinken.
Het één en ander wordt gegenereerd en we zien in Visual Studio een solution met drie projecten. We zien naast een .client-project ook een .server en een .shared-project.
Componenten
Een Blazor app bestaat uit diverse componenten en we gaan twee componenten toevoegen aan de app. De gegenereerde code laten we daarbij zo veel mogelijk intact.
We breiden de app uit met een component voor het kunnen toevoegen van een nieuwe autobezitter (menukeuze Voeg Toe) en we voegen een component toe voor het doen tonen van de gegevens van de eigenaren van een auto (menukeuze Totaal aantal).
VoegToe
We breiden de applicatie uit met een component waarbij de component bestaat uit een scherm van waaruit een nieuwe autobezitter toegevoegd kan worden (menukeuze Voeg Toe):
TotaalAantal
We voegen ook een component toe dat de gegevens van autobezitters ontvangt en de binnengekomen gegevens als volgt op het scherm toont (menukeuze Totaal aantal):
De componenten zullen we later in deze post in detail bespreken. We gaan eerst in op de zaken die nodig zijn om een component te laten doen draaien.
Modeldefinitie
Componenten doen (meestal) dingen met gegevens en we maken een modeldefinitie voor datgene wat we willen vastleggen. Wat willen we van een autobezitter registreren? We houden het simpel en we registreren van een eigenaar alleen diens naam en de regio waar de autobezitter woont. De modeldefinitie leggen we vast in class definition EIGENAAR in de .shared-project.
Interface
Wat de componenten ontvangen en wat ze opleveren, dat alles moet een bepaalde structuur hebben en de structuur moet bekend zijn binnen de applicatie. Dit om de componenten gegevens te laten doen uitwisselen met de andere onderdelen van de applicatie.
De structuren leggen we vast in een interface. Het één en ander komt tot uiting in interface IserviceInterface die we aanmaken in de .client-project.
De component die belast is met met het doen toevoegen van de gegevens van een nieuwe autobezitter krijgt een object dat zij mag gaan vullen. Het werk houdt voor die component op zodra het object naar behoren is gevuld. Het object wordt door de desbetreffende component “over de schutting gegooid” en het is aan andere onderdelen van de applicatie om ervoor te zorgen dat de gegevens naar behoren worden opgeslagen.
De component die de gegevens van de autobezitters gaat tonen krijgt een List (IEnumerable) met objecten. Voor de component is het niet van belang waar de gegevens vandaan komen. Het enige wat voor haar telt is die List met objecten en het doen tonen van de gegevens op het scherm.
We verpakken het één en ander in taken zodat we dingen asynchroon kunnen laten uitvoeren en schermen niet gaan blokkeren hetgeen bijzonder prettig is voor de “user experience” van de gebruiker.
Klasse
De structuren zijn bekend, maar de invulling die we eraan kunnen geven kan verschillen. M.a.w. je kan het op verschillende manieren implementeren en we bouwen dan ook twee klassen zijnde twee verschillende implementaties van eenzelfde interface. De klassen vinden we terug in de .client-project.
ServiceMemoryClass
Waar de gegevens van de eigenaren vandaan komen en hoe die zijn opgeslagen? We maken “mock” implementatie ServiceMemoryClass waarbij de gegevens opgehaald worden uit een interne List<> en de gegevens “hard” zijn gecodeerd. We zijn voor de “mock” implementatie al snel klaar met onderstaande code, maar we zullen zien dat voor de database implementatie toch wat meer nodig is.
ServiceDbClass
ServiceDbClass is een implementatie waarbij gegevens opgehaald worden uit een SQL Server database. De implementatie maakt daarbij gebruik van een Web API in de .server-project. We gaan alle dingen langs die nodig zijn voor de implementatie om uiteindelijk uit te komen bij klasse ServiceDbClass.
Database
We beginnen met de database die we voor de implementatie willen gebruiken. In dit voorbeeld gaan we uit van een bestaande SQL Server database. We maken geen gebruik van “migraties” die een database en tabellen creëren en weer bijwerken aan de hand van (gewijzigde) modeldefinities. We benaderen de database server met SQL Server Management Studio:
We hebben op de databaseserver een database met de naam VOORBEELD en die gaan we voor onze implementatie gebruiken:
Binnen de database is er een tabel EIGENAAR en daar komen de gegevens van de autobezitters:
Waarbij de tabel de volgende inhoud heeft:
DbContextClass
Voor het ophalen van de gegevens uit de database en het bijwerken van de gegevens in de database is een dbContext-klasse nodig zijnde een intermediair naar de onderliggende database. dbContext-klasse DbContextClass is een klasse dat erft van klasse DbContext:
In klasse dbContextClass geven we bij de DbSet de naam op van de database tabel welke gekoppeld moet worden aan de modeldefinitie. In dit geval hebben de modeldefinitie en de desbetreffende database tabel dezelfde naam. Het beestje heeft in beide gevallen de naam EIGENAAR.
Web Api ServiceDBController
Controller ServiceDBController in de .server-project. is een Web API en via de Web API worden de methoden aangeroepen voor het opvragen en het doen toevoegen van gegevens. De Web API benadert de database niet rechtstreeks, maar maakt gebruik van klasse dbContextClass zijnde de intermediair naar de onderliggende database.
We definiëren in de controller backing variable _dbContextClass (een variabele van het type dbContextClass). De backing variabele wordt door de host via de constructor gevuld waarbij we zullen zien dat de host een connectiestring aanlevert zodat we verbinding met de database kunnen maken.
Tenslotte de methoden voor het opvragen van en het doen toevoegen van gegevens. De methoden maken gebruik van de backing variable _dbContextClass. We zien de methoden TotaalAantal en VoegToe.
ServiceDbClass – implementatie
En uiteindelijk komen we uit bij klasse ServiceDbClass zijnde de implementatie waarbij gegevens opgehaald worden uit een SQL Server database. De implementatie maakt daarbij gebruik van Web API ServiceDBController in de .server-project. In dit voorbeeld wordt gebruik gemaakt van een interne web API, maar een externe Web API kan ook gebruikt worden.
Connectiestring
Alle controllers en klassen zijn gevrijwaard van details als connectiestrings, maar we hebben uiteindelijk toch een keer een connectiestring nodig om bij de onderliggende database te komen en we zullen ergens die connectiestring moeten opgeven zodat bekend is met welke connectiestring precies gewerkt moet worden.
We beginnen met het vastleggen van de connectiestring in configuratiebestand appsetting.json. Daarbij geven we elke connectiestring een naam zodat we de juiste connectiestring gebruiken indien we meerdere connectiestrings hebben en in dit geval hebben we een connectiestring gedefinieerd met de naam VOORBEELDConnection. In het “oude” .NET Framework gebruikten we XML voor de configuratiebestanden, maar we zien dat sinds .NET Core JSON wordt gebruikt.
De connectiestring is gedefinieerd en we moeten nu ergens opgeven dat die connectiestring gebruikt moet worden, maar wie gaat dat doen? Het antwoord is: de host. De host van de .server-project geeft aan dat iets gedaan moet worden met de dbContextClass en we zien dat de host ook meteen connectiestring VOORBEELDConnection doorgeeft aan die dbContextClass:
En daar valt wel wat voor te zeggen. De host mag regelen welke klassen gebruikt worden en als de host toch bezig is dan mag de host ook meteen opgeven welke connectiestring gebruikt moet worden. Je gaat er de klassen en de controllers niet mee lastig vallen.
Host
We hebben een aantal implementaties gebouwd, maar welke implementatie wordt uiteindelijk gebruikt binnen de applicatie? Ook hier kan de host iets betekenen want via de host van de .client-project kunnen we met dependency injection alles zodanig inrichten dat we maar op één plek op hoeven te geven welke implementatie het gaat worden.
De interface wordt dan met de gekozen implementatie een service waarbij de service binnen de applicatie beschikbaar is voor alle componenten van de applicatie.
We gaan in de .client-project naar de host met haar methode Main in Program.cs. In methode Main geven we op welke implementatie van interface IServiceInterface binnen de applicatie gebruikt moet worden als zijnde een service dat overal aanroepbaar is binnen de applicatie:
In dit voorbeeld geven we bij de host op dat we de ServiceMemoryClass “mock” implementatie gaan gebruiken. We “injecteren” de ServiceMemoryClass “mock” implementatie als zijnde de implementatie die gebruikt zal worden binnen de applicatie (dependency injection).
Het is geen probleem als we een andere implementatie willen doorvoeren in de applicatie en we kunnen bijvoorbeeld de ServiceDbClass implementatie injecteren als de gegevens uit een SQL server database moeten komen.
Zo kunnen we ook een implementatie bouwen waarbij de gegevens uit een SQLite database moeten komen. We geven dan bij de host op dat de SQLite-implementatie geïnjecteerd moet worden met als gevolg dat vanaf dat moment de gegevens uit een SQLite database komen.
De componenten in de applicatie zelf hoeven zich niet druk te maken over de te gebruiken implementatie en waar de gegevens vandaan komen en waar ze naartoe moeten. Dat wordt voor ze geregeld binnen de host en de desbetreffende implementatie. De componenten roepen alleen maar methodes aan van een interface.
Componenten in detail
We hadden al eerder aangegeven dat we twee componenten willen toevoegen aan de app en de toe te voegen componenten gaan we nu in detail bekijken.
In het voorbeeld hebben we twee Razor componenten toegevoegd aan de out-of-the-box boilerplate code van Microsoft. Voor componenten wordt in Blazor alles vastgelegd in een bestand met de extensie .razor en zo hebben we in de .client-project de bestanden VoegToe.razor en TotaalAantal.razor.
Zie deze post waarin wordt uitgelegd wat het verschil is tussen razor en blazor. Het zijn twee termen die regelmatig door elkaar gehaald worden en voor veel verwarring zorgen.
De componenten roepen de methoden aan van interface IServiceInterface die als een service beschikbaar is binnen de web applicatie en zo krijgt component TotaalAantal via interface-methode .TotaalAantal() een List met objecten waarvan zij de gegevens kan tonen op het scherm. Component VoegToe vult een object en verstuurt het object met interface-methode .VoegToe() naar een plek waar de gegevens van het object opgeslagen worden.
Welke implementatie van toepassing is en waar de gegevens vandaan komen? Dat wordt door de host en de implementaties geregeld en dat zijn dingen waar de componenten zich niet mee bezig gaat houden. In beide componenten verwijzen we middels de @inject-directive naar interface IServiceInterface.
Directives
In Blazor kan een component opgedeeld worden in drie onderdelen. Allereerst zijn er de directives. Zo hebben we een @page-directive die van belang is voor de navigatie (in dit voorbeeld de @page-directives @page “/voegtoe” en @page ”/totaalaantal”). We zien de waarden van de @page-directive ook terugkomen in de url.
Daarnaast hebben we @using directives voor de te gebruiken libraries plus wat @inject directives voor de te gebruiken interface.
De directives voor de VoegToe-component:
en de directives voor de TotaalAantal-component:
Markup
We hebben als tweede onderdeel van een component HTML-markup en Razor-markup en voor de opmaak wordt verder gebruik gemaakt van Bootstrap en CSS (Cascading Style Sheets). Verder wordt een HTML-form gebruikt met validatie faciliteiten zodat het te verwerken object correcte waarden bevat.
Bootstrap, HTML-forms en validatie van waarden in HTML-controls zijn onderwerpen die we in deze post niet behandelen. Zie deze post waarin wordt ingegaan op Bootstrap en CSS en deze post waarin wordt ingegaan op de EditForm. De HTML markup en de Razor markup voor de VoegToe-component:
en de HTML markup en Razor markup voor de TotaalAantal-component:
Code
Als derde onderdeel van een component hebben we de code en dat is alles wat onder de @code-directive staat. De code is C# en dat is een “unique-selling-point” van Blazor want we kunnen in een component “gewoon” C# gebruiken waardoor we niet meer of veel minder afhankelijk zijn van JavaScript. De code voor de VoegToe-component:
en de code voor de TotaalAantal-component:
Slot
In deze post heb ik het één en ander toegelicht aan de hand van een ASP .NET Core hosted Blazor WebAssembly App waarbij alle zaken die van belang zijn voor een ASP .NET Core hosted Blazor WebAssembly App de revue passeerden. Zo bestaat een Blazor applicatie uit:
- verschillende modeldefinities
- een database
- controllers zijnde de te gebruiken Web APIs
- interfaces en klassen die de interfaces implementeren
- componenten waarbij de componenten uit drie onderdelen bestaan
- een host die opgeeft wat allemaal gebruikt moet worden in de applicatie
We hebben zo doende een applicatie dat uit veel “bouwstenen” bestaat, maar elke bouwsteen kan gezien worden als een afzonderlijke eenheid en dat geeft ook mogelijkheden voor het doen uitvoeren van (geautomatiseerde) testen op die afzonderlijke onderdelen van de applicatie.
Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over Blazor heb geschreven? Hit the Blazor button…