Blazor WebAssembly, Teil 2: Eingabesteuerelemente & JavaScript-Interoperabilität

Seite 2: Bearbeitungsformular

Inhaltsverzeichnis

Für die vollständigen Einstellungen der Aufgabe (Text, Fälligkeit, Priorität, Aufwand) gibt es das Eingabeformular rechts in Abbildung 1. Theoretisch könnte man auch die dazu notwendigen Tags und Codeteile in die Dateien Index.razor beziehungsweise Index.razor.cs integrieren. Das Tutorial soll aber auch die Komponentenbildung nach dem "Teile und Herrsche"-Prinzip darstellen.

Entwickler erstellen also eine neue Razor Component mit dem Namen TaskEdit.razor im Ordner /Pages. Dabei ist der erste Großbuchstabe wichtig: Razor Components müssen immer mit einem Großbuchstaben beginnen. Beim Anlegen der Razor Component über Add | Razor Component im Kontextmenü des Ordners Pages ist festzustellen, dass Visual Studio leider immer eine Ein-Datei-Komponente mit Inline-Code erzeugt. Wer Markup und Code trennen will, muss die Code-Behind-Datei manuell hinzufügen. Das soll hier als partielle Klasse (nicht als Vererbung) erfolgen. Dafür gelten folgende Regeln:

  • Der Dateiname ist wie der Name der Template-Datei plus ".cs".
  • Die Code-Behind-Datei muss eine Klasse enthalten, die öffentlich und partiell ist.
  • Der Namensraum der Code-Behind-Klasse muss dem Unterverzeichnis entsprechen. In diesem Projekt ist der globale Namensraum "Web". Die Razor Components im Unterordner /Code liegen also im Namensraum Web.Pages.
  • Eine Vererbung von der Basisklasse ComponentBase kann man angeben, muss man aber nicht, denn die aus der Markup-Datei entstehende andere Hälfte der Klasse erbt bereits von dieser Basisklasse.

Die Listings 2 und 3 enthalten das Aufgabenbearbeitungsformular /Pages/TaskEdit.razor inklusive Code-Behind-Datei. Das Formular entsteht in Listing 2 mit normalen HTML-Eingabesteuerelementen wie <form>, <input>, <textarea>, <select> und <button>. Die Datenbindung erfolgt bei den Eingabesteuerelementen per Zwei-Wege-Bindung mit @bind. Die Schaltflächen Save und Cancel werden mit einer Ereignisbehandlung für das @onclick-Ereignis belegt. Auch in dieser Razor-Vorlage ist es wieder wichtig, die Blöcke nur zu rendern, wenn die verwendeten Properties (hier: task) auch tatsächlich Objekte enthalten.

@if (Task != null)
{
 <div style="padding: 0">
  <div>
   <h4>Task</h4>
   <form>
    <!--Schaltflächen-->
    <button type="button" title="Änderungen speichern"
            @onclick="Save" class="btn btn-success">
     <span class="glyphicon  glyphicon-floppy-save"></span> <span class="hidden-xs" i18n>Save</span>
    </button>
    <button type="button" title="Änderungen verwerfen" @onclick="Cancel" class="btn btn-warning"><span class="glyphicon glyphicon-remove"></span> <span class="hidden-xs" i18n>Cancel</span></button>
    <!--Titel-->
    <div class="form-group">
     <label for="tasktitle">Titel</label>
     <input id="tasktitle" name="tasktitle" type="text" @bind="Task.Title" required class="form-control" />
    </div>
    <div class="row">
     <!--Wichtigkeit-->
     <span class="col-xs-3" style="padding-right: 2px">
      <div class="form-group">
       <label for="taskimportance" i18n>Importance</label>
       <select id="taskimportance" name="taskimportance" @bind="Task.ImportanceNN" class="form-control">
        <option value="A">A</option>
        <option value="B">B</option>
        <option value="C">C</option>
       </select>
      </div>
     </span>
     <!--Aufwand-->
     <span class="col-xs-3" style="padding-left: 2px; padding-right: 2px">
      <label for="taskeffort " i18n>Effort</label>
      <input id="taskeffort" type="number" name="taskeffort" @bind="Task.Effort" class="form-control" />
     </span>
     <!--Fälligkeit-->
     <span class="col-xs-6" style="padding-left: 2px; ">
      <div class="form-group">
       <label for="taskDue" i18n>Due</label>
       <input id="taskdue" name="taskdue" type="date" @bind="Task.DueNN" class="form-control" />
      </div>
     </span>
    </div>  <!--Ende row-->
    <!--Notiz-->
    <div class="form-group ">
     <label for="tasknote" i18n>Note</label>
     <textarea id="tasknote" name="tasknote " rows="5" @bind="Task.Note" class="form-control "></textarea>
    </div>
   </form>
  </div>
 </div>
}

Listing 2: TaskEdit.razor

Die Code-Behind-Klasse deklariert zwei Properties als öffentliche Parameter (annotiert mit [Parameter]): eines als Task-Objekt für das zu bearbeitende Aufgabenobjekt und eines als Ereignis mit einem Zahlparameter (Action<int>), das eine erfolgte Änderung an den Aufrufer signalisiert und dabei nur den Primärschlüssel des geänderten Objekts als Parameter mitliefert. Diese beiden Parameter können Nutzer per HTML-Syntax befüllen (hier: Index.razor):

<TaskEdit Task="@task" TaskHasChanged="@ReloadTasks"></TaskEdit>

Sobald die Parameter befüllt sind, löst die Blazor-Infrastruktur in TaskEdit.razor.cs das Lebenszyklusereignis OnParameterSet() in Listing 3 aus. Im Artikelbeispiel ist hier aber nichts zu tun, denn Entwickler bekommen ja vom Aufrufer bereits das komplette Task-Objekt. Anders wäre es, wenn Letzterer nur den Primärschlüssel übergeben würde und die Komponente das zugehörige Datenobjekt selbst laden müsste.

Listing 3 behandelt die Klick-Ereignisse Save() und Cancel(). Im Fall von Save() ruft das Frontend im Backend die Operation /ChangeTask auf. Beim Abbruch wird einfach das aktuelle Objekt mit /Task neu geladen – somit sind alle Änderungen verworfen und man hat auch den aktuellen Stand vom Server. Sowohl beim Speichern als auch beim Abbruch von Änderungen erzeugen Webentwickler dann manuell das Ereignis TaskHasChanged() mit der ID der Aufgabe.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace Web.Pages
{
 public partial class TaskEdit
 {
  [Parameter] // zu bearbeitende Aufgabe
  public BO.Task Task { get; set; }

  [Parameter] // Ereignis, wenn Aufgabe sich geändert hat
  public EventCallback<int> TaskHasChanged { get; set; }

  [Inject]
  MiracleListAPI.MiracleListProxy proxy { get; set; } = null;

  [Inject]
  AuthenticationManager am { get; set; }

  protected override async System.Threading.Tasks.Task OnInitializedAsync()
  {
  }

  // wenn Parameter gesetzt wird
  protected async override void OnParametersSet()
  {
  }

  protected async void Save()
  {
   await proxy.ChangeTaskAsync(this.Task, am.Token);
   await TaskHasChanged.InvokeAsync(Task.TaskID);
  }

  protected async void Cancel()
  {
   await GetTask(Task.TaskID);
   await TaskHasChanged.InvokeAsync(Task.TaskID);
  }

  private async Task GetTask(int id)
  {
    this.Task = await proxy.TaskAsync(id, am.Token);
  }
 } // end class TaskEdit
}

Listing 3: TaskEdit.razor

Was nun noch fehlt, ist das Einbinden der neuen Razor Component TaskEdit.razor in die Index.razor-Datei. Dafür ergänzt man dort vor dem letzten schließenden </div>:

<!-- ### Spalte 3: Aufgabendetails-->
@if (task != null)
{
<div>
<TaskEdit Task="@task" TaskHasChanged="@ReloadTasks"></TaskEdit>
</div>
}

Das sorgt dafür, dass immer, wenn Benutzer eine konkrete Aufgabe angeklickt haben, das Eingabeformular rendert und dabei das Task-Objekt als Datenübergabe erhält. Wenn das Formular eine Änderung persistiert hat, erzeugt es das Ereignis TaskHasChanged(). Beim Aufruf wird das eingehende Ereignis in der Routine ReloadTasks() behandelt, indem das aktuell angewählte Objekt abgewählt wird.

public async Task ReloadTasks(int taskID)
{
this.task = null;
}

Damit verschwindet das Eingabeformular fast schon "magisch" (weil oben die Bedingung task != null nicht mehr erfüllt ist). Die Bedingung in der zweiten Spalte sorgt in Verbindung mit den Bootstrap-CSS-Grid-Klassen dafür, dass nun der freie Raum auf dem Bildschirm sofort von der mittleren Spalte eingenommen wird (s. auch Listing 4 in Teil 1):

<div class="WLPanel @(this.task==null ? "col-xs-8 col-sm-8 col-md-9 col-lg-10 ": "hidden-xs col-sm-6 col-md-5 col-lg-6"  )">

Die übergebene taskID des Objekts wird hier konkret nicht gebraucht. Das Beispiel soll zeigen, wie man grundsätzlich Daten von der aufgerufenen Komponente an den Aufrufer zurückgibt.

Bei der Bedienung der Webanwendung stellt man eine kleine Unschönheit fest: Wenn der Benutzer den Status einer Aufgabe per Checkbox ändert, zeigt sich das Bearbeitungsformular. Das liegt daran, dass das die Aufgabe ein <li>-Element ist, das als Kasten dargestellt wird und die Checkbox im Inhalt des <li>-Elements liegt. Zur Verhinderung der Ereignisweitergabe muss man @onclick:stopPropagation="true" im <input type="checkbox"> ergänzen.

<input type="checkbox" …
@onclick:stopPropagation="true"
@onchange... />