Zum Inhalt springen

Lern APP für SKS Theorie mit Blazor entwickeln

Vor kurzem habe ich meinen SKS-Sportküstenschifferschein erworben. An den vorhandenen Lern-Apps hat mich gestört, dass diese nicht die Möglichkeit boten, eigene zusammengekürzte Antworten abzuspeichern. Das habe ich zum Anlass genommen, eine eigene App zu erstellen, als Übungsprojekt. Entschieden habe ich mich für das Blazor Framework von Microsoft. Da ich mit Visual Studio arbeiten wollte und C# mir mehr zusagt als zum Beispiel JavaScript. Zuerst habe ich mir das Wissen über Google gesucht, doch dann hat Bing, ChatGPT bekommen und alles begann sich zu beschleunigen.

Das Lernen einer Programmiersprache geht mit ChatGPT wesentlich schneller als ohne, das ist meine Erkenntnis. So ein Tool Anfang der 90’er, die Weltherrschaft wäre mein gewesen! Jetzt versuch’ ich eben einen SKS-Theorie-Trainer zu erstellen. Klar ist ich bin kein Webdesigner, die Logik im Hintergrund liegt mir mehr.

Was ist Blazor und C# ?

Zuerst einige Informationen zum Blazor Framework und C#, die mir bei meinem Projekt geholfen haben. Blazor ist ein Webframework, das es ermöglicht, interaktive Webanwendungen mit C# zu erstellen, ohne JavaScript zu verwenden. Blazor basiert auf dem .NET Core Framework und verwendet WebAssembly, um C#-Code im Browser auszuführen. Das bedeutet, dass man die Vorteile von C#, wie starke Typisierung, Objektorientierung, auch für die Frontend-Entwicklung nutzen kann.

C# ist eine moderne, vielseitige und leistungsfähige Programmiersprache, die sowohl für Desktop-Web- als auch Mobile-Anwendungen geeignet ist. Man bekommt von C# eine klare und konsistente Syntax, die für mich leicht zu erlernen war und zu lesen ist. C# bietet viele nützliche Features, die mir vorher völlig unbekannt waren, wie Delegaten, Lambda-Ausdrücke, Exceptions, Generics und Async / Await.

Meine Blazor Entwicklungsumgebung

Um mit Blazor und C# zu arbeiten, braucht man nur Visual Studio oder Visual Studio Code als Entwicklungsumgebung und das .NET Core SDK als Laufzeitumgebung. Mit diesen Tools kann man schnell und einfach eine Blazor-Anwendung erstellen, testen und veröffentlichen. Visual Studio bietet außerdem viele Hilfsmittel, wie IntelliSense, Debugging und Refactoring, die die Produktivität erhöhen. Wie oben schon erwähnt, habe ich mich von vorneherein auf Visual Studio festgelegt und Visual Studio Code nur gelegentlich verwendet.

SKS-Fachgebiete, die abgefragt werden müssen

Der Sportküstenschifferschein (SKS) ist ein amtlicher Führerschein für Segel- und Motorboote in Küstengewässern. Er ist in Deutschland und vielen anderen Ländern anerkannt und gilt als Nachweis der Befähigung zum Führen von Yachten. Um den SKS zu erwerben, muss man eine theoretische und eine praktische Prüfung ablegen. Die Fachgebiete der Theorie umfassen Navigation, Seemannschaft, Schifffahrtsrecht und Wetterkunde.

Aller Anfang ist schwer

Zuerst war ich sozusagen auf mich alleine gestellt und hatte nur Suchmaschinen als Hilfsmittel. Die Antworten in so manchem Forum kamen mir teils kryptischer vor als die Fragen, die ich hatte. Es gibt viele, viele Antworten auf noch viel mehr Fragen. Es ist wahrscheinlich so, dass es auf jede Code-Technische Frage von mir da draußen in den weiten des Netzes eine Antwort gibt, finden muss man die richtige und dann noch verstehen, hier liegen die wahren Knoten begraben. Zu guter Letzt habe ich einfach angefangen. Meiner Auffassung nach an der richtigen Stelle, bei der Aufbereitung von den eigentlichen Frage- und Antwortdaten.

Wie die Fragen und Antworten speichern?

Die Fragen und Antworten hab ich in einer Excel Tabelle aufbereitet. Je Fachgebiet eine eigene Tabelle. Das ganze habe ich so formatiert, dass die Fragen fett und die Antworten normale Schrift hatten. Die Bilder habe ich erst einmal nicht beachtet. Denn zu dem Zeitpunkt wusste ich noch nicht einmal, wie ich die Fragen für die App nutzbar ablege. Eine Frage kann mehrere Zeilen haben, sprich zwei oder mehr Fragen in einer kombiniert. Die vorgegebenen Antworten haben dementsprechend auch mehrere Zeilen und ganz viele Zeilen bei Aufzählungen. Die Rohdaten sahen dann so aus:

Zuerst dachte ich, eine Datenbank wäre das Mittel zur Wahl, aber das entpuppte sich für mich am Anfang als zu komplex. Ich wollte eine einfachere Lösung und habe mich dafür entschieden, je Fachgebiet eine JSON Datei zu erzeugen.

Was ist JSON und Parsen?

JSON steht für JavaScript Object Notation. Es ist ein Format zum Speichern und Übertragen von Daten, das auf der Syntax von JavaScript-Objekten basiert. JSON Parsen ist der Prozess, bei dem eine JSON-Zeichenfolge in ein C#-Objekt umgewandelt wird. Dies kann nützlich sein, wenn man Daten von einer externen Quelle wie einer API oder einer Datei erhält, die im JSON-Format vorliegen.

Wie den Excel Text in JSON Konvertieren?

Um das zu bewerkstelligen habe ich mir für MS Excel einen JSON Konverter gesucht und einen gefunden. Als Konverter habe ich den JSON Converter for VBA Code von Tim Hall verwendet, den gibt es auf GitHub. Jetzt brauchte ich nur noch eine VBA Methode, die diesen Konverter aufruft, die Fragen und Antworten aus der Tabelle übergibt und mir den JSON Code ausgibt. Trotz Konverter, der eben nur das macht, was man ihm anbietet, war es für mich am Anfang schwer, die richtige Reihenfolge bzw. JSON Syntax hinzubekommen. Das war mehr „Trial-and-Error“.

Doch zum Ende hat es funktioniert und hier präsentiere ich stolz die aufrufende Methode für den Konverter:

Sub convertJsonQuestions()
    'Diese Prozedur konvertiert Fragen und Antworten aus einem Arbeitsblatt in ein JSON-Format
    
    'Variablendeklarationen
    Dim fr As Collection 'Sammlung für jede Frage
    Dim fn As Dictionary 'Wörterbuch für Fragendetails
    Dim fb As Collection 'Sammlung für den Fragenbereich
    Dim ab As Collection 'Sammlung für den Antwortbereich
    Dim strTable As String 'Name des Arbeitsblatts
    Dim strText As String 'Textvariable deklarieren
    Dim json As String 'JSON-String
    Dim counter As Integer 'Zähler für die Zeilen
    
    strTable = "Seemannschaft" 'Name des Arbeitsblatts festlegen
    
    Set th = ThisWorkbook.Sheets(strTable) 'Arbeitsblatt festlegen
    
    Set fr = New Collection 'Sammlung für jede Frage initialisieren

    For counter = 1 To lastRow(1, strTable) 'Schleife durch alle Zeilen des Arbeitsblatts
        If Left(th.Cells(counter, 1), 6) = "Nummer" Then 'Überprüfen, ob die aktuelle Zelle eine Fragenummer enthält
            Set fn = New Dictionary 'Wörterbuch für Fragendetails initialisieren
            fn.Add "ID", th.Cells(counter, 1) 'Fragenummer hinzufügen
            
            counter = counter + 1
            
            Set fb = New Collection 'Sammlung für den Fragenbereich initialisieren
            While th.Cells(counter, 1).Font.Bold = True 'Schleife durch alle fett gedruckten Zellen (Fragenbereich)
                strText = Replace(th.Cells(counter, 1), Chr(10), "") 'Text aus der Zelle extrahieren und Zeilenumbrüche entfernen
                fb.Add strText '"F" & marker, strText
                counter = counter + 1
            Wend
            
            Set ab = New Collection 'Sammlung für den Antwortbereich initialisieren
            While Not Left(th.Cells(counter, 1), 6) = "Nummer" And counter < lastRow(1, strTable) 'Schleife durch alle Zellen bis zur nächsten Fragenummer oder bis zum Ende des Arbeitsblatts (Antwortbereich)
                strText = Replace(th.Cells(counter, 1), Chr(10), "") 'Text aus der Zelle extrahieren und Zeilenumbrüche entfernen
                ab.Add strText '"A" & marker, strText
                counter = counter + 1
            Wend
            
            fn.Add "Fragen", fb 'Fragenbereich zum Wörterbuch hinzufügen
            fn.Add "Antworten", ab 'Antwortbereich zum Wörterbuch hinzufügen
            fr.Add fn 'Wörterbuch zur Sammlung hinzufügen
            counter = counter - 1 'Zähler zurücksetzen
        End If
    Next counter
    
    json = JsonConverter.ConvertToJson(fr) 'Konvertieren der Sammlung in JSON

    Debug.Print json 'JSON ausgeben
    
End Sub

Das ist die letzte Version, ich kann gar nicht betonen, wie oft ich die Reihenfolge von Fragen und Antworten vertauscht habe. Da kam anfangs alles raus, aber kein JSON. Ist der Konverter in ein eigenes Modul kopiert und führt man die obige Methode aus, dann wird der konvertierte JSON Text in den Debug Bereich geschrieben. So weit, so gut!

Korrigieren des JSON

Der JSON Text ist unformatiert, also den Text kopieren und am besten VS Code öffnen und eine neue „JSON“ Datei erstellen. Den Text hineinkopieren und die Tastenkombination »Shift-Alt-F« zum Formatieren betätigen. Dann sieht man das nicht der ganze Text korrekt formatiert wurde, manche Zeilen sind umgebrochen und werden nicht korrekt dargestellt. Das muss behoben werden, sonst funktioniert später das Parsen der JSON nicht. In der Methode habe ich in den Zeilen 29 und 36 versucht, die Zeilenumbrüche herauszulöschen, aber es muss noch einen mir unbekannten Grund geben, warum die Zeilen umgebrochen werden.

Das Korrigieren ist in VS Code relativ einfach, denn die fehlerhaften Zeilen werden im Editor rot unterstrichen und auch seitlich in der Minimap rot angezeigt. Die eingefügten Codes in Gelb sind Umlaute, die später beim Parsen wieder in das richtige Zeichen umgewandelt werden.

Ein neues Blazor Projekt erstellen in Visual Studio

Dazu musst Du Visual Studio öffnen und als Projektvorlage die „Blazor-Webassembly App“ auswählen.

Dann auf weiter klicken und das Projekt konfigurieren, hier kannst Du den Projektpfad und den Projektnamen eingeben. Dann wieder auf weiter klicken zu den „Weiteren Informationen“, die habe ich es wie folgt eingestellt:

In das Projekt kopierst Du in „wwwroot“ deine JSON Datei. Dann brauchen wir noch eine neue Razor Komponente, bei mir ist es die Seemannschaft.razor, da meine JSON Datei die Fragen des Fachgebietes „Seemannschaft“ enthält. Dein Projekt Explorer sollte dann in etwa so aussehen:

Blazor Komponente erstellen

Aber was soll in die Komponente? Anfangs musste ich mir alles zusammen suchen, der Chat-Assistent von OpenAI brachte noch mehr Fehler im C# Code als Hilfen. Doch dann wurde ich für Bing Chat freigeschaltet, der ist gefühlt etwas langsamer, aber die Ergebnisse waren wesentlich besser. Das kann sich natürlich schon wieder geändert haben, aber in meinen Anfängen war das so. Jetzt habe ich ChatGPT folgendermaßen befragt:

Ich habe eine JSON-Datei mit Fragen und Antworten, die ich in einer Blazor PWA-Komponente anzeigen möchte. Die JSON-Datei heißt “seemannschaft.json” und hat folgendes Format:
{
    "ID": "Nummer 10:",
    "F1": [
        "1. Wozu dienen Backstagen?",
        "2. Bei welchen Takelungen werden sie vor allem gefahren?"
    ],
    "A1": [
        "1. Zum zus\u00E4tzlichen Abstagen des Mastes nach achtern.",
        "2. Bei 7/8-Takelung und anderen nicht toppgetakelten Yachten."
    ]
},
{
    "ID": "Nummer 11:",
    "F1": [
        "Was ist das \"laufende Gut\"?"
    ],
    "A1": [
        "Tauwerk, das zum Setzen, Bergen oder Bedienen der Segel oder anderer Teile der Takelage dient."
    ]
}
Die Komponente soll die aktuelle Frage und Antwort anzeigen und mir erlauben, zwischen den Fragen zu navigieren. Dazu soll es jeweils einen Button “Nächste Frage” und einen Button “Vorherige Frage” geben.
Die Komponente soll die Antwort erst anzeigen, wenn ich auf einen Button “Antwort zeigen” klicke.
Die Komponente soll die URL “/seemannschaft” haben und den Titel “Fachgebiet Seemannschaft” tragen.

Ok, ich war nicht ganz ehrlich, der Prozess zu einer funktionierenden Komponente war länger. Aber dazu später. Folgende Komponente habe ich mit obigem Text erzeugt bekommen:

@page "/seemannschaft"
@using System.Text.Json

<h1>Fachgebiet Seemannschaft</h1>

@if (_questions == null)
{
    <p>Loading...</p>
}
else
{
    <h2>@_questions[_currentQuestion].ID</h2>
    <ul>
        @foreach (var question in _questions[_currentQuestion].F1)
        {
            <li>@question</li>
        }
    </ul>
    @if (_showAnswer)
    {
        <h3>Antwort:</h3>
        <ul>
            @foreach (var answer in _questions[_currentQuestion].A1)
            {
                <li>@answer</li>
            }
        </ul>
    }
    else
    {
        <button @onclick="() => _showAnswer = true">Antwort zeigen</button>
    }
    <br />
    <button @onclick="PreviousQuestion" disabled="@(_currentQuestion == 0)">Vorherige Frage</button>
    <button @onclick="NextQuestion" disabled="@(_currentQuestion == _questions.Length - 1)">Nächste Frage</button>
}

@code {
    private int _currentQuestion = 0;
    private bool _showAnswer = false;
    private Question[] _questions;

    protected override async Task OnInitializedAsync()
    {
        var response = await Http.GetAsync("seemannschaft.json");
        var json = await response.Content.ReadAsStringAsync();
        _questions = JsonSerializer.Deserialize<Question[]>(json);
    }

    private void NextQuestion()
    {
        _currentQuestion++;
        _showAnswer = false;
    }

    private void PreviousQuestion()
    {
        _currentQuestion--;
        _showAnswer = false;
    }

    public class Question
    {
        public string ID { get; set; }
        public string[] F1 { get; set; }
        public string[] A1 { get; set; }
    }
}

Korrigieren des ChatGPT Komponenten Vorschlags

Nachdem kopieren in die Komponente „Seemannschaft.razor“ erscheint in Zeile 45 ein Fehler CS0103 „Http ist im aktuellen Kontext nicht vorhanden“. ChatGPT vergisst jedes Mal den inject des Http Klienten. Das führte zu Frage Antwort Spielchen zwischen mir und der KI. Aber gerade solche Dinge lassen die Lernkurve steigen. Ich musste in der dritten Zeile noch „@inject HttpClient Http“ einfügen.

Ebenso habe ich ChatGPT darauf hingewiesen, dass die Variablen auf einen möglichen Nullverweis hin zu überprüfen sind und ein möglicher Nullverweis zu verhindern ist, denn das ergibt den Hinweis CS8618. Dieser Hinweis erscheint in der Zeile 41, 48, 64–66. Keine Chance, funktioniert nicht, musste ich alles nachträglich erfragen und selbst ändern. Es gibt mehrere Möglichkeiten, diesen Hinweis zu verhindern oder zu verbessern.

Fangen wir mit Zeile 41 an, dafür bietet ChatGPT oder Bing Chat folgende Lösungen an:

  1. Initialisiere das Feld mit einem Standardwert, z. B. private Question[] questions = Array.Empty<Question>();
  2. Verwende einen nullable Typ, z.B. private Question[]? questions;
  3. Deaktiviere die Warnung für dieses spezifische Feld mit #pragma warning disable CS8618 vor der Deklaration des Feldes und aktiviere sie wieder mit #pragma warning restore CS8618 nach der Deklaration.

Ich habe mich für Lösung 2 entschieden, das gleiche für Zeile 64. Natürlich hat mich interessiert, was das Fragezeichen bedeutet.

Das Fragezeichen in private Question[]? _questions; gibt an, dass der Typ Question[] nullable ist. Das bedeutet, dass die Variable _questions entweder einen Wert vom Typ Question[] oder null enthalten kann. In diesem Fall wird _questions Initial auf null gesetzt und später, wenn die JSON-Datei geladen wurde, auf einen Wert vom Typ Question[] gesetzt. Dies ermöglicht es, den Ladezustand der Komponente zu überprüfen und eine Ladeanzeige anzuzeigen, während die JSON-Datei geladen wird. 

Für Zeile 65–66 habe ich mich für die erste Lösung entschieden. Meine finale Komponente sieht so aus:

@page "/seemannschaft"
@using System.Text.Json
@inject HttpClient Http

<h1>Fachgebiet Seemannschaft</h1>

@if (_questions == null)
{
    <p>Loading...</p>
}
else
{
    <h2>@_questions[_currentQuestion].ID</h2>
    <ul>
        @foreach (var question in _questions[_currentQuestion].F1)
        {
            <li>@question</li>
        }
    </ul>
    @if (_showAnswer)
    {
        <h3>Antwort:</h3>
        <ul>
            @foreach (var answer in _questions[_currentQuestion].A1)
            {
                <li>@answer</li>
            }
        </ul>
    }
    else
    {
        <button @onclick="() => _showAnswer = true">Antwort zeigen</button>
    }
    <br />
    <button @onclick="PreviousQuestion" disabled="@(_currentQuestion == 0)">Vorherige Frage</button>
    <button @onclick="NextQuestion" disabled="@(_currentQuestion == _questions.Length - 1)">Nächste Frage</button>
}

@code {
    private int _currentQuestion = 0;
    private bool _showAnswer = false;
    private Question[]? _questions;

    protected override async Task OnInitializedAsync()
    {
        var response = await Http.GetAsync("seemannschaft.json");
        var json = await response.Content.ReadAsStringAsync();
        _questions = JsonSerializer.Deserialize<Question[]>(json);
    }

    private void NextQuestion()
    {
        _currentQuestion++;
        _showAnswer = false;
    }

    private void PreviousQuestion()
    {
        _currentQuestion--;
        _showAnswer = false;
    }

    public class Question
    {
        public string? ID { get; set; }
        public string[] F1 { get; set; } = Array.Empty<string>();
        public string[] A1 { get; set; } = Array.Empty<string>();
    }
}

Damit die Seite auch aufgerufen werden kann, muss noch ein zusätzlicher Navigationseintrag hinzugefügt werden, dazu öffnet man die NavMenu.razor im Shared Ordner. Und fügt die Zeilen 27–31 hinzu.

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorApp3</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="seemannschaft">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Seemannschaft
            </NavLink>
        </div>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Der erste Start

Nun die Blazor PWA starten, dann sollte im Browser folgendes erscheinen.

Die Buttons funktionieren, Du kannst innerhalb der Fragen navigieren und die Antwort wird nach Anklicken des Buttons „Antwort zeigen“ angezeigt. Jetzt wäre es möglich, die Komponente für jedes Fachgebiet zu kopieren und abzuändern. Die jeweiligen JSON Dateien musst Du natürlich noch erstellen und in wwwroot kopieren. Das war’s aber auch schon!

Schon ganz schönes Stück würde ich behaupten, ohne ChatGPT wäre ich aber bei weitem nicht so weit. Das tolle ist, dass diese PWA auch installiert werden kann, wenn Sie auf einem Webspace mit HTTPS abgelegt ist. Jetzt kann schon unterwegs mit der eigenen App gelernt werden!

Das war der erste Teil, es kann schon gelernt werden, aber es fehlt noch so einiges.

Ein Gedanke zu „Lern APP für SKS Theorie mit Blazor entwickeln“

  1. Pingback: SKS Theorie Trainer PWA App

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert