In diesem Artikel

Stockende UI, keine Reaktion nach dem Klick auf einen Button oder einer Eingabe in einem Feld - dies sind nur wenige Beispiele alltäglicher Probleme, die der Nutzung von Client-Anwendungen im Allgemeinen, und bei Webanwendungen im Speziellen, immer wieder auftreten können. In diesem Artikel schauen wir uns an, wie wir komponentenbasierte UIs in Blazor WebAssembly optimieren können, um dadurch eine für die Benutzer zufriedenstellende Geschwindigkeit und ein flüssiges UI zu bekommen.

Version Information:

  • .NET SDK: 5.0.201
  • ASP.NET Core Blazor WebAssembly: 5.0.4
  • MudBlazor: 5.0.5

Der Beispiel-Code für diesen Artikel findet sich hier.

In der Entwicklung von Webanwendungen, und so auch in der Entwicklung von Blazor WebAssembly SPAs, ist es wichtig, die Laufzeit-Performance der Anwendung immer im Auge zu behalten und wenn nötig, zu optimieren. Daher schauen wir uns in diesem Artikel die Optimierungsmöglichkeiten einer Blazor-Komponente etwas genauer an. Bevor wir aber mit der Optimierung von Komponenten beginnen können, ist es wichtig zu verstehen, welchen Lebenszyklus eine Komponente beim Rendern durchläuft und wie dieser ausgelöst werden kann.

Render-Lebenszyklus einer Komponente

Lebenszyklus

Der Lebenszyklus einer Blazor-Komponente beginnt, wenn sie auf der Seite gerendert wird, d.h. sie wird zum ersten Mal sichtbar.

Blazor WebAssembly Component render lifecycle

In der obigen Abbildung sehen wir die verschiedenen Schritte, die durchlaufen werden, sobald das Rendering einer Komponente angestoßen wird.

Im ersten Schritt wird geprüft, ob es sich um das initiale Rendern der Komponente handelt, oder andernfalls die Komponente neu gerendert werden soll. Dies wird anhand von zwei Optionen geprüft:

  • Innerhalb der Basisklasse einer Blazor-Komponente, der ComponentBase-Klasse, wird geprüft, ob es sich um das erste Rendern der Komponente handelt.
  • Handelt es sich nicht um das erste Rendering, wird die Methode ShouldRender(), welche im nächsten Abschnitt näher erläutert wird, der Basisklasse aufgerufen. Die Methode gibt einen bool-Wert zurück der angibt, ob Render-Prozess durchgeführt werden soll. Der Default Rückgabe-Wert ist true.

Trifft keine der beiden Optionen zu, wird der Render-Prozess beendet (2a). Andernfalls wird ein neuer Render-Tree erstellt (2b). Der Render-Tree beschreibt die HTML-Elemente des HTML-Dokuments, welche in der Blazor-Welt aktualisiert werden sollen. Danach wird dieser an das Document Object Model (DOM) weitergeben. Anhand des Render-Trees werden die Änderungen im DOM aktualisiert. Ist das Update abgeschlossen, werden schließlich die Methoden OnAfterRender und OnAfterRenderAsync aufgerufen und der Prozess ist beendet.

Hinweis: Der Render-Tree beinhaltet nur die Änderungen der Komponente, die sich im Vergleich zum aktuell gerenderten Zustand im DOM geändert haben. Dadurch wird nicht bei jedem Render-Prozess, das DOM-Element vollständig ersetzt, sondern nur die aktuellen Änderungen werden im DOM aktualisiert.

Weitere Informationen und Details über den Lebenszyklus von Blazor-Komponenten finden sich bei meinem Kollegen Pawel Gerr in seinem englischen Artikel Blazor Components Deep Dive - Lifecycle Is Not Always Straightforward.

Was kann ein Re-Rendering auslösen?

Nachdem wir gesehen haben, welchen Prozess eine Komponente beim Rendern durchläuft, schauen wir uns jetzt mal an, was den Render-Prozess einer Komponente auslösen kann:

  • Das erste Rendern findet, wie zu erwarten, beim Initialisieren einer Blazor-Komponente statt.
  • Das zweite Szenario, bei der die Komponente neu gerendert wird, ist es, wenn sich Parameter ändern. Dabei wird zwischen eigenen Parametern und Parametern der übergeordneten Komponente unterschieden:

    • SetParametersAsync: Diese Methode wird aufgerufen, sobald Parameter des übergeordneten Elements geändert wurden oder durch das Setzten eines Route-Parameters in der URL.
    • OnParametersSet/OnParametersSetAsync: Diese Methoden werden aufgerufen, sobald sich die Parameter der Komponente ändern oder die übergeordnete Komponente neu gerendert wird.
  • Wird ein DOM-Event ausgelöst, durch beispielsweise ein onlcick-Event, wird die Komponente ebenfalls neu gerendert. Aber nicht nur die Komponente, bei der das Event ausgelöst wurde, sondern auch alle untergeordneten Elemente werden neu gerendert
  • Zuletzt kann der Prozess über die Methode StateHasChanged aus der Basisklasse ComponentBase der Render-Prozess angestoßen werden. Die Methode StateHasChanged() benachrichtigt die Komponente, dass sich der aktuelle Status geändert hat. Dies führt dann dazu, dass die Komponente neu gerendert wird.

Betrachtet man die unterschiedlichen Szenarien, wird ersichtlich, dass der Render-Prozess sehr oft getriggert werden kann. Doch ist das wirklich immer notwendig? Im weiteren Verlauf dieses Artikels schauen wir uns mögliche Optionen an, wie wir den Render-Prozess einer Komponente optimieren können.

Optimierung des Render-Prozesses

ShouldRender überschreiben

Die erste Option, die wir nutzen können den Render-Prozess zu unterbinden, ist die Methode ShouldRender zu überschreiben. Wie wir in der obigen Abbildung gesehen haben, wird in Schritt 2, sobald es sich nicht um das initiale Rendering handelt und die Methode ShouldRender auch false zurückgibt, der Render-Prozess beendet.

Die Methode ShouldRender ist eine Methode der Basisklasse ComponentBase. Der Default Rückgabewert der Methode ist true. Infolgedessen wird bei jedem Render-Vorgang der Prozess durchgeführt. Nehmen wir beispielsweise ein Formular. Wird in einem Formular ein Feld geändert, wird dadurch der Render-Prozess der Komponenten angestoßen. Dies führt dazu, dass zusätzlich auch all die Komponenten neu gerendert werden, die sich im Formular befinden. In einem Formular ist es natürlich wichtig zu beachten, ob das aktuelle Feld Abhängigkeiten zu anderen Feldern hat. Hat das Feld keine Abhängigkeiten, so muss auch nicht bei jeder Änderung des Feldes neu gerendert werden. Um dies zu vermeiden, können wir die ShouldRender-Methode überschreiben.

// CustomInputText.razor.cs

private int _valueHashCode;

protected override bool ShouldRender()
{
    var lastHashCode = _valueHashCode;
    _valueHashCode = Value?.GetHashCode() ?? 0;
    return _valueHashCode != lastHashCode; 
}

Im obigen Code-Beispiel sehen wir, dass anhand des HashCode die Property Value der Komponente geprüft wird, ob sich diese geändert hat. Ist dies nicht der Fall, gibt die Methode false zurück und der Render-Prozess wird beendet. Sollte die Komponente keine Properties haben, die sich ändern, kann hier natürlich auch immer direkt false zurückgegeben werden.

Dies ist eine Möglichkeit den Render-Prozess frühzeitig zu beenden, wenn es nicht zwingend notwendig ist die Komponente neu zu Rendern. Im nächsten Abschnitt schauen wir uns an, wie wir beim Auslösen eines Events den Render-Prozess optimieren können.

IHandleEvent implementieren

Wird ein Event geworfen, wird die Methode StateHasChanged der ComponentBase-Klasse aufgerufen. Das führt dazu, dass der Render-Prozess der Komponente angestoßen wird. Dies hat zwar den Vorteil, dass der Entwickler die Methode StateHasChanged nicht selbst aufrufen muss, wenn ein Event angestoßen wird. Jedoch hat das auch den Nachteil, dass die Komponente auch ohne jegliche Änderungen neu gerendert wird. Um in diesen Prozess einzugreifen, kann das Interface IHandleEvent implementiert werden. Über das Interface wird die Methode HandleEventAsync implementiert, die bei einem Event der Komponente aufgerufen wird.

// HandleEventInputText.razor.cs

public partial class HandleEventInputText : ComponentBase, IHandleEvent
{
    private bool _preventRender;

    public Task HandleEventAsync(EventCallbackWorkItem item, object? arg)
    {
        try
        {
            var task = item.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                                  task.Status != TaskStatus.Canceled;

            if (!_preventRender)
            {
                StateHasChanged();
            }

            return shouldAwaitTask
                ? CallStateHasChangedOnAsyncCompletion(task, _supressRender)
                : Task.CompletedTask;
        }
        finally
        {
            _preventRender = false;
        }
    }

    private async Task CallStateHasChangedOnAsyncCompletion(Task task, bool preventRender)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        if (!preventRender)
        {
            StateHasChanged();
        }
    }

    void PreventRender()
    {
        _preventRender = true;
    }
}
<!-- HandleEventInputText.razor -->
<label>
    @Label <input class="form-control @CssClass" placeholder="Override ShouldRender" id="@Id" @bind="CurrentValue" @oninput="PreventRender"/>
</label>

Im obigen Code-Beispiel sehen wir die Klasse HandleEventInputText, die von der Basisklasse ComponentBase ableitet und das Interface IHandleEvent implementiert. In der Methode HandleEventAsync wird anhand der Property _preventRender geprüft, ob die Komponente gerendert werden soll oder nicht. Dadurch haben wir in der Komponente die Möglichkeit, bei einem Event die Methode PreventRender aufzurufen, wie wir im obigen Code-Beispiel beim Event oninput sehen können. Infolgedessen bekommt die Property _preventRender den Wert true zugewiesen. Solange die Property den Wert true besitzt, wird der Render-Prozess nicht angestoßen.

Event-Utilities

Eine weitere Variante das erneute Rendern bei Events zu vermeiden, ist der Einsatz der Hilfsklasse EventUtil, die von Microsoft empfohlen wird.

public static class EventUtil
{
    public static Action AsNonRenderingEventHandler(Action callback)
        => new SyncReceiver(callback).Invoke;
    public static Action<TValue> AsNonRenderingEventHandler<TValue>(
            Action<TValue> callback)
        => new SyncReceiver<TValue>(callback).Invoke;
    public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
        => new AsyncReceiver(callback).Invoke;
    public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
            Func<TValue, Task> callback)
        => new AsyncReceiver<TValue>(callback).Invoke;

    private record SyncReceiver(Action callback) 
        : ReceiverBase { public void Invoke() => callback(); }
    private record SyncReceiver<T>(Action<T> callback) 
        : ReceiverBase { public void Invoke(T arg) => callback(arg); }
    private record AsyncReceiver(Func<Task> callback) 
        : ReceiverBase { public Task Invoke() => callback(); }
    private record AsyncReceiver<T>(Func<T, Task> callback) 
        : ReceiverBase { public Task Invoke(T arg) => callback(arg); }

    private record ReceiverBase : IHandleEvent
    {
        public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => 
            item.InvokeAsync(arg);
    }
}

Die Hilfsklasse stellt die Methode AsNonRenderingEventHandler bereit. Die Methode kann genutzt werden, um die Func oder Action zwar auszuführen, aber das erneute Rendering des EventHandlers nicht auszulösen.

<MudButton OnClick="EventUtil.AsNonRenderingEventHandler(TogglePreventRendering)"
           Target="_blank"
           Variant="Variant.Filled"
           EndIcon="@Icons.Material.Filled.Power"
           Color="Color.Secondary">
    Beim Klick auf diesen Button wird kein neues Rendering ausgelöst
</MudButton>

Im obigen Code-Beispiel, sehen wir einen Button, der einen Onclick-EventHandler, mit Hilfe der Methode AsNonRenderingEventHandler, aufruft. Ähnlich wie im vorherigen Abschnitt wird auch hier das IHandleEvent-Interface eingesetzt, wie es beim Datentyp ReceiverBase zu sehen ist. Der Unterschied zum vorherigen Ansatz liegt darin, dass das Rendering direkt vermieden wird und es kann vorab nicht geprüft werden, ob gerendert werden soll oder nicht.

Im nächsten Abschnitt schauen wir uns an, wie wir mit dem Einsatz von JavaScript ein DOM-Event verzögern können, um dadurch die Anzahl der Render-Vorgänge zu reduzieren.

Debounce DOM-Event

Hier vorab ein kleiner Hinweis: In diesem Abschnitt wird Javascript-Interop eingesetzt. Da wir uns in diesem Artikel aber auf die Optimierung von Komponenten fokussieren, gehe ich nicht weiter auf JS-Interop ein. Mehr Informationen hierzu finden sich hier.

Ein aus der JavaScript-Welt bekanntes Verfahren, um das Anstoßen von Events zu verzögern, ist das Debouncing. Beim Debounce-Verfahren wird der EventCallback erst dann ausgeführt, wenn nach einer bestimmten Zeitspanne keine neuen Events mehr geworfen werden. Das heißt, wenn wir z.B. in einem Textfeld tippen, wird das Event oninput erst dann gefeuert, sobald aufgehört wurde zu tippen und eine selbst gesetzte Zeitspanne abgelaufen ist. Dies hat den Vorteil, dass das Event nur einmal ausgeführt wird.

Um das Debounce Verfahren in einer Blazor-WebAssembly-Komponente einzusetzen, müssen wir sowohl im C#-Code als auch im JavaScript-Code, Methoden implementieren.

C#-/Razor-Code

// DebounceTextArea.razor.cs

public partial class DebounceTextArea : IDisposable
{
    //... Code above
    
    [Inject] public IJSRuntime JS { get; set; }

    private IJSObjectReference _module;
    private ElementReference _textareaElement;
    private DotNetObjectReference<DebounceTextArea> _selfReference;


    // JavaScript Datei laden
    protected override async Task OnInitializedAsync()
    {
        _module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/debounce.js");
        await base.OnInitializedAsync();
    }

    // JavaScript Event registrieren    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _selfReference = DotNetObjectReference.Create(this);
            
            // Event wird nach 500 ms geworfen
            var minInterval = 500; 
            
            // JavaScript-Code aufrufen mit dem HTML-Element, einer ObjectReferenz und dem Timeout bis das Event geworfen wird
           await _module.InvokeVoidAsync("onDebounceInput",
                    _textareaElement, _selfReference, minInterval);
        }

        Console.WriteLine("Debounced TextArea: After Render called.");
    }

    
    // Methode die vom JavaScript-Code aufgerufen werden kann. Als Parameter wird der aktuelle Wert der TextArea übergeben.
    [JSInvokable]
    public void HandleOnInput(string value)
    {
        Console.WriteLine($"TextChanged {Value}. JS Value {value}");
        if (Value != value)
        {
            StateHasChanged();
        }
    }
    
    //... Code below   
 
    public void Dispose() => _selfReference?.Dispose();
}
<!-- DebounceTextArea.razor -->
<label>
    @Label <textarea @ref="_textareaElement" placeholder="Debounce input" class="form-control @CssClass" id="@Id" @bind="CurrentValue"></textarea>
</label>

Betrachten wir zunächst den C#-Code im obigen Code-Beispiel. In der Methode OnAfterRenderAsync wird beim initialen Rendering mit Hilfe von JS-Interop die Funktion onDebounceInput im JavaScript aufgerufen. Zum Aufrufen werden zwei Referenzen erwartet:

  • Die erste Referenz ist eine Objektreferenz der Komponente selbst, die mit Hilfe der Klasse DotNetObjectReference erstellt werden kann.

    Hinweis: Wird ein Disposeable-Objekt in einer Komponente genutzt, muss das Interface IDisposable implementiert werden. In der Methode Dispose müssen dann alle Objekte zerstört werden.

  • Die zweite Referenz verweist auf das HTML-Objekt. Im Code-Beispiel wird hierfür im HTML-Code das Attribut @ref genutzt, um eine ElementReference im C#-Code zu erstellen.

Die anderen beiden Parameter, um die JavaScript-Methode aufzurufen, sind natürlich der Name der Methode und das Intervall für das Verzögern des Events.

Weiter schauen wir uns die HandleOnInput-Methode näher an. Diese wird vom JavaScript-Code aufgerufen, sobald das Event geworfen wurde. Damit der JavaScript-Code die Methode aufrufen kann, wird das JSInvokable-Attribut benötigt. In der Methode selbst wird lediglich die Methode StateHasChanged aufgerufen, sobald sich der Wert der Property Value geändert hat.

JavaScript-Code

Um die Methode HandleOnInput, die wir im obigen Abschnitt gesehen haben, aufrufen zu können, müssen wir die JavaScript-Funktion onDebounceInput hinzufügen. Diese wird dann aus dem C#-Code via JS-Interop aufgerufen.

<!--debounce.js-->
export function onDebounceInput(elem, component, interval) {
    elem.addEventListener('input', debounce(e => {
        component.invokeMethodAsync('HandleOnInput', e.target.value);
    }, interval));
}

function debounce(func, timeout = 300) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => { func.apply(this, args); }, timeout);
    };
}

Wie wir im obigen Code-Beispiel sehen, wird im JavaScript-Code die Funktion onDebounceInput implementiert. Diese finden wir auch im C# Code wieder, wenn wir nochmal auf die Methode OnAfterRenderAsync schauen. Hier wird nachdem alle Parameter definiert wurden, um die Funktion mit der folgenden Code-Zeile aufgerufen:

// DebounceTextArea.razor.cs

await JS.InvokeVoidAsync("onDebounceInput", _textareaElement, _selfReference, minInterval);

Nachdem die C#-Methode InvokeVoidAsync aufgerufen wurde, wird via JSInterop die JavaScript-Funktion onDebounceInput aufgerufen. Die Funktion registriert einen EventListener der auf das Event input hört. Mit Hilfe der debounce-Funktion, wird der Aufruf der C#-Methode HandleOnInput solange verzögert, bis der Interval abgelaufen ist, nachdem aufgehört wurde in das Textfeld zu schreiben.

Hinweis: Um den Effekt im Video gut erkennen zu können, sollte das Video im Fullscreen abgespielt werden.

Bevor wir uns nun im nächsten Abschnitt mit dem Darstellen von Listen beschäftigen, hier noch ein Hinweis: Das Überschreiben von ShouldRender oder das Implementieren des Interface IHandleEvent kann mit dem Debounce verfahren auch kombiniert werden.

Listenoptimierung mit Virtualize

Nachdem wir nun Möglichkeiten gesehen haben, um den Render-Prozess von Komponenten in Blazor WebAssembly zu optimieren, schauen wir uns jetzt an was passiert, wenn wir eine Liste rendern. Als Beispiel nutzen wir hier eine Liste, in der wir einzelne Einträge selektieren können.

Wie wir im vorherigen Abschnitt des Artikels schon gesehen haben, wird bei einem Event wie z.b. ein OnClick der Render-Prozess angestoßen. Infolgedessen haben wir bei einer Liste das Problem, dass bei jeder Clickauf ein Listen-Element, alle Einträge neu gerendert werden. Zusätzlich müssen bei einem for-Loop von Anfang an alle Einträge in den DOM geladen werden. Zur Optimierung kann die Virtualize-Komponente, die von Blazor WebAssembly zur Verfügung gestellt wird, genutzt werden. Die Komponente hat den Vorteil, dass nur die Komponenten geladen werden, die sich im Sichtbereich der Anwendung befinden. Dies hat den großen Vorteil das, auch wenn über die API die Daten nicht nach und nach geladen werden können, nicht alle Einträge direkt dem DOM hinzugefügt werden. Zusätzlich kann über den Parameter OverscanCount vor und nach dem Sichtbereich eine gewisse Anzahl an Einträgen im DOM vorgeladen werden. Daher ist eine wichtige Voraussetzung zum Einsetzten der Virtualize-Komponente, dass nicht alle Einträge einer Liste im DOM gerendert sein müssen.

<!-- Contributions.razor -->

<Virtualize Context="contribution" ItemsProvider="@LoadContributions" OverscanCount="10">
    <ItemContent>
        <ContributionCard Contribution="@contribution"></ContributionCard>
    </ItemContent>
    <Placeholder>
        <PlaceholderCard></PlaceholderCard>
    </Placeholder>
</Virtualize>
// Contributions.razor.cs

public partial class Contributions
{
    [Inject] public ContributionService ContributionService { get; set; }

    private async ValueTask<ItemsProviderResult<Contribution>> LoadContributions(
        ItemsProviderRequest request)
    {
        var maxCount = 200;
        var numConfs = Math.Min(request.Count, maxCount - request.StartIndex);
        var contributions =
            await ContributionService.GetContributionsAsync(request.StartIndex, numConfs,
                request.CancellationToken);
        return new ItemsProviderResult<Contribution>(contributions, maxCount);
    }
}

Im obigen Code-Beispiel sehen wir die Virtualize-Komponente. Die Liste an Personen wird mit dem ItemProvider geladen. Damit beim Scrollen der Liste keine zu große Verzögerung auftritt, werden mit dem OverscanCount vor und nach dem Sichtbereich 10 Einträge vorgehalten. Der ItemProvider bietet die Möglichkeit, die Daten via Lazy-Loading nachzuladen. Im obigen Code-Beispiel, sehen wir die Methode LoadContributions, die immer dann aufgerufen wird, sobald neue Einträge geladen werden müssen. Können die Daten nicht schnell genug nachgeladen werden, durch beispielsweise einer langsamen API oder einer schlechten Internetverbindung, bietet die Virtualize-Komponente die Möglichkeit an, einen Placeholder einzusetzen. Dieser wird so lange angezeigt, bis die nächsten Einträge geladen wurden.

Durch den Einsatz der Virtualize-Komponente haben wir also die Möglichkeit die Performance zu optimieren. Jedoch kann dies nicht zwingend mit den Optimierungen der vorherigen Abschnitte verbunden werden. Das liegt daran, dass Komponenten, die nicht im Sichtbereich liegen oder vorgeladen wurden, aus dem DOM entfernt wurden. Wenn diese wieder geladen werden, handelt es sich um das Initialisieren der Komponente, was automatisch zu einem Render-Prozess führt.

Fazit

In diesem Artikel haben wir mögliche Varianten gesehen, den Render-Prozess von Blazor-WebAssembly-Komponenten zu verbessern. Schon mit kleinen Anpassungen können viele Render-Vorgänge vermieden werden, was sich positiv auf die Performance der ganzen Anwendung auswirken kann. Wichtig ist dabei zu beachten, wann welche Komponente gerendert werden muss und wann nicht.

Durch das Überschreiben von der Methode ShouldRender oder das Implementieren des Interface IHandleEvent konnten wir sehen, dass dadurch bereits viele Render-Vorgänge eingespart wurden.

Doch auch wenn ein Rendering nicht vermieden werden kann, haben wir durch den Einsatz von JavaScript bei einem Event, die Anzahl der Render-Vorgänge minimieren können.

Mit Hilfe der Virtualize-Komponente haben wir gesehen, dass bereits viele Funktionen eingesetzt werden konnten, wie beispielsweise das Lazy-Loading oder eine Ladeanimation als Placeholder. Dies trägt nicht nur zur besseren Performance bei, sondern auch einer erhöhten Usability der Anwendung.

Daher lässt sich abschließend sagen, dass wenn man beim Entwickeln von Komponenten darauf achtet, die Notwendigkeit des (Re-)Renderns so gering wie möglich zu halten, die Performance deutlich gesteigert werden kann.

Wenn Sie auch die weiteren Artikel unserer Experten nicht verpassen möchten, melden Sie sich hier zu unserem kostenlosen, monatlichen Dev-Newsletter an.

Related Articles

 | Christian Weyer

Blazor WebAssembly 5 und die Kopplung an .NET 5 - guter Ausblick für die Zukunft Blazor WebAssembly 5 ist im November 2020 als Teil von .NET 5 sechs Monate nach dem Erscheinen der ersten offiziellen Version (3.2.0) released worden und profitiert an diversen Stellen von…

Read article
 | Patrick Jahr

Version Information .NET SDK 5.0.104 ASP.NET Core Blazor WebAssembly: 5.0.4 MudBlazor: 5.0.4 Der gesamte Source Code zur Beispielanwendung findet sich in diesem GitHub Repository. Exception Handling als Aufgabenstellung Der richtige Umgang mit Fehlern ist für die Erstellung einer…

Read article
 | Pawel Gerr

In general, you can divide template engines into two types. The relatively simple ones are using template strings with placeholders to be replaced by some concrete values. The other template engines can do everything the simple ones can but additionally provide means for control…

Read article