Umstieg auf .NET Core, Teil 3: ASP.NET-Webserveranwendungen umstellen

Seite 3: Umstieg von Webforms & Co. auf Blazor

Inhaltsverzeichnis

Nicht mehr als Migration, sondern als ein weitgehendes Neuschreiben sollte man den Umstieg von ASP.NET Webforms, ASP.NET AJAX oder ASP.NET Dynamic Data auf ASP.NET Core verstehen. Es gibt in ASP.NET Core nicht die Webserversteuerelemente, die allen drei genannten Anwendungsframeworks zugrunde liegen. Microsoft empfiehlt für Webforms eine Migration auf ASP.NET Core Blazor Server und bietet dazu auch ein kostenfreies E-Book an , das aber noch einige Lücken aufweist. Insbesondere das spannende Kapitel zur Umstellung der Webserversteuerelemente auf Blazor ist noch leer: "This content is coming soon.", und soll daher hier exemplarisch diskutiert werden.

Man sollte dabei keine Heinzelmännchen erwarten: Es läuft darauf hinaus, die Webserversteuerelemente aus der Webseite zu entfernen und den Rendering-Code mit HTML und Razor-Syntax neu zu schreiben. An Stellen, wo in Webforms nicht mit dem Standardrendering der Webserversteuerelemente gearbeitet wurde, sondern mit Templates wie ItemTemplate, geht der Umstieg etwas schneller von der Hand. Es empfiehlt sich aber, die Gelegenheit der Neuformulierung in Razor-Code auch zu nutzen, um die Gestaltung und Navigation der Anwendung zu modernisieren.

Der Grund, warum Microsoft gerade ASP.NET Core Blazor Server als Anwendungsframework für eine Migration von ASP.NET Webforms empfiehlt, ist, dass Blazor Server genau wie Webforms ein ereignisgetriebenes Konzept und einen eingebauten Mechanismus für die Beibehaltung des Seitenzustandes zwischen mehreren HTTP-Anfragen besitzt. Man kann zwar auch in ASP.NET Core MVC und ASP.NET Core Razor Pages beliebige Zustände behalten, muss dafür aber deutlich mehr selbst programmieren. Bei ASP.NET Core MVC kommt erschwerend die andersartige Interaktion zwischen Controller und View hinzu. Ein weiterer Pluspunkt für ASP.NET Core Blazor Server ist die einfache Umstellbarkeit auf ASP.NET Core Blazor WebAssembly – sobald dies erschienen ist (Version 1.0 ist für Mai 2020 angekündigt).

Das Beispiel einer HTML-Tabelle mit zwei Auswahlfeldern zum Filtern (s. Abb. 2) zeigt, welche Umstellungsarbeiten für Webserver-Steuerelemente von ASP.NET Webforms zu ASP.NET Core Blazor notwendig sind. Dies ist freilich nur ein exemplarischer Aspekt von vielen Arbeitsschritten.

Beispielhafte HTML-Tabelle – wird durch Listing 1 und 2 sowie Listing 3 und 4 gerendert (Abb. 2).

Listing 1 (Benutzerschnittstellenbeschreibung in ASPX-Code) und Listing 2 (zugehörige Benutzerschnittstellensteuerung in C#) zeigen das Rendern dieser Tabelle mit ASP.NET Webforms mit DataGridView-Steuerelement, während Listing 3 (Benutzerschnittstellenbeschreibung in Razor-Syntax) und Listing 4 (zugehörige Benutzerschnittstellenbeschreibung in C#) die neue Implementierung mit ASP.NET Core Blazor dokumentieren.

Listing 1: Rendern der Tabelle durch Datenbindung an das DataGridView-Steuerelement in ASP.NET Webforms (Fluege.aspx):

<form id="form1" runat="server">
  <h4>von:
   <asp:DropDownList ID="C_Abflugort" runat="server" AutoPostBack="True" OnSelectedIndexChanged="C_Abflugort_SelectedIndexChanged"></asp:DropDownList>
   &nbsp;nach:
   <asp:DropDownList ID="C_Zielort" runat="server" AutoPostBack="True" OnSelectedIndexChanged="C_Zielort_SelectedIndexChanged"></asp:DropDownList>
   <%= Anzahl %> Fl�ge gefunden</h4>
  <asp:GridView AutoGenerateColumns="False" ID="C_Fluege" runat="server" CellPadding="4" ForeColor="#333333" GridLines="None" >
   <AlternatingRowStyle BackColor="White" />
   <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
   <RowStyle BackColor="#EFF3FB" />
 
   <Columns>
    <asp:BoundField HeaderText="Flug-Nr" DataField="FlugNr" />
    <asp:BoundField HeaderText="Abflugort" DataField="Abflugort" />
    <asp:BoundField HeaderText="Zielort" DataField="Zielort" />
    <asp:BoundField HeaderText="Datum" DataField="Datum" DataFormatString="{0:d}" />
    <asp:CheckBoxField HeaderText="Nichtraucher" DataField="NichtRaucherFlug">
     <ItemStyle HorizontalAlign="Center" />
    </asp:CheckBoxField>
    <asp:TemplateField HeaderText="Auslastung">
     <ItemTemplate>
      <div><%# Eval("FreiePlaetze")%> freie Pl�tze</div>
      <div style="<%# "font-size: smaller;" + (Convert.ToDecimal(Eval("PlatzAuslastung")) > 50 ? "color:red": "color:green") %>">
       (<%# String.Format("{0:00.00}",Eval("PlatzAuslastung")) %>%)
      </div>
     </ItemTemplate>
    </asp:TemplateField>
 
    <asp:TemplateField ShowHeader="False" >
     <ItemTemplate>
      <asp:ImageButton ID="C_Loeschen" CommandArgument='<%#Eval("FlugNr")%>' runat="server" Width="20px" ImageUrl="~/img/delete.png"
       CommandName="C_Loeschen" OnClientClick="return confirm('M�chten Sie diesen Flug wirklich entfernen?');" OnClick="C_Loeschen_Click"
       AlternateText="Flug l�schen" />
     </ItemTemplate>
    </asp:TemplateField>
 
   </Columns>
  </asp:GridView>
  <hr />
  <%=Info %>
 </form>

Listing 2: Code-Behind-Datei zu Listing 1 (Fluege.aspx.cs):

//Praxishinweis: Der Datenzugriffscode liegt in der Praxis in unteren Schichten, also getrennten Assemblies. Er ist hier nur aus Gr�nden der Pr�gnanz und �bersichtlichkeit des Beispiels in der Code-Behind-Datei direkt enthalten.
using DAL;
using System;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
 
namespace WWWingsWebforms
{
 public partial class Fluege : System.Web.UI.Page
 {
  public int Anzahl = 0;
  public string Info = "";
  int skip = 0;
  int take = 10;
 
  protected void Page_Load(object sender, EventArgs e)
  {
   if (!Page.IsPostBack)
   {
    Info = "<b>Browser:</b> " + HttpContext.Current.Request.Browser.Browser + " " + HttpContext.Current.Request.Browser.Version + "<br><b>Server:</b> " + HttpContext.Current.Request.ServerVariables["SERVER_SOFTWARE"] + " mit " + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
    AbflugorteLaden();
   }
  }
 
  protected void C_Abflugort_SelectedIndexChanged(object sender, EventArgs e)
  {
   ZielorteLaden();
  }
 
  protected void C_Zielort_SelectedIndexChanged(object sender, EventArgs e)
  {
   FluegeLaden();
  }
 
  protected void AbflugorteLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    this.C_Abflugort.DataSource = ctx.Flug.Select(x => x.Abflugort).Distinct().OrderBy(x => x).ToList();
    this.C_Abflugort.SelectedValue = "Berlin";
    this.C_Abflugort.DataBind();
    ZielorteLaden();
   }
  }
 
  private void ZielorteLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    this.C_Zielort.DataSource = ctx.Flug.Where(x => x.Abflugort == this.C_Abflugort.SelectedValue).Select(x => x.Zielort).Distinct().OrderBy(x => x).ToList();
    this.C_Zielort.DataBind();
   }
   FluegeLaden();
  }
 
  protected void FluegeLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    var flugSet = ctx.Flug.Where(x => x.Abflugort == this.C_Abflugort.SelectedValue && x.Zielort == this.C_Zielort.SelectedValue && x.FreiePlaetze > 0).Skip(skip).Take(take).OrderBy(x => x.Datum).ToList();
    Anzahl = flugSet.Count;
    C_Fluege.DataSource = flugSet;
    C_Fluege.DataBind();
   }
 
  }
 
  protected void C_Loeschen_Click(object sender, System.Web.UI.ImageClickEventArgs e)
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    ImageButton button = sender as ImageButton;
    int flugNr = Convert.ToInt32(button.CommandArgument);
    ctx.Remove(ctx.Flug.Find(flugNr));
    ctx.SaveChanges();
   }
   FluegeLaden();
  }
 }
}

Listing 3: Rendern der Tabelle durch Iteration in ASP.NET Core Blazor (Fluege.razor):

<h4>
 von: <select id="C_JobTitle" @bind="C_Abflugort" class="form-control">
  @foreach (var s in Abflugorte)
  {
   <option value="@s.ToString()">@s.ToString()</option>
  }
 </select>
 nach: <select id="C_JobTitle" @bind="C_Zielort" class="form-control">
  @foreach (var s in Zielorte)
  {
   <option value="@s.ToString()">@s.ToString()</option>
  }
 </select>
 @Anzahl Fl�ge gefunden
</h4>
 
<table cellspacing="0" cellpadding="4" id="C_Fluege" style="color:#333333;border-collapse:collapse;">
 <tr style="color:White;background-color:#507CD1;font-weight:bold;">
  <th scope="col">Flug-Nr</th>
  <th scope="col">Abflugort</th>
  <th scope="col">Zielort</th>
  <th scope="col">Datum</th>
  <th scope="col">Nichtraucher</th>
  <th scope="col">Auslastung</th>
  <th scope="col"></th>
 </tr>
 @{ int rowCounter = 0;}
 @foreach (Flug f in flugSet)
 {
  rowCounter++;
  <tr style='background-color:@(rowCounter % 2 == 0 ? "white" : "#EFF3FB;" )'>
   <td>@f.FlugNr</td>
   <td>@f.Abflugort</td>
   <td>@f.Zielort</td>
   <td>@String.Format("{0:d}", f.Datum)</td>
   <td style="text-align:center"><input class="bigcheckbox" id="C_Fluege_ctl00_@rowCounter" type="checkbox" name="C_Fluege$ctl02$ctl_@rowCounter" @bind="f.NichtRaucherFlug" disabled="disabled" /></td>
   <td>
    <div>@f.FreiePlaetze freie Pl�tze</div>
    <div style='font-size: smaller;color:@(f.PlatzAuslastung > 50 ? "red" : "green")'>
     (@f.PlatzAuslastung%)
    </div>
   </td>
   <td>
    <img ID="C_Loeschen" Width="20" src="/img/delete.png"
         @onclick="() => this.C_Loeschen_Click(f.FlugNr)" title="Flug l�schen" />
   </td>
  </tr>
 }
</table>
<hr />
@((MarkupString)info)

Listing 4: Code-Behind-Datei zu Listing 1 (Fluege.razor.cs):

//Praxishinweis: Der Datenzugriffscode liegt in der Praxis in unteren Schichten, also getrennten Assemblies. Er ist hier nur aus Gr�nden der Pr�gnanz und �bersichtlichkeit des Beispiels in der Code-Behind-Datei direkt enthalten.
using DAL;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.JSInterop;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
 
namespace WWWingsBlazor.Pages
{
 
 public partial class Fluege
 {
  #region DI
  [Inject]
  IHttpContextAccessor httpContextAccessor { get; set; }
  [Inject]
  IJSRuntime _jsRuntime { get; set; }
  #endregion
 
  #region Properties
  List<Flug> flugSet { get; set; } = new List<Flug>();
  string info = "";
  string c_abflugort = "Berlin";
  string C_Abflugort
  {
   get
   { return this.c_abflugort; }
   set
   { this.c_abflugort = value; ZielorteLaden(); }
  }
  string c_zielort = "Berlin";
  string C_Zielort
  {
   get
   { return this.c_zielort; }
   set
   {
    this.c_zielort = value; FluegeLaden();
   }
  }
  List<string> Abflugorte = new List<string>();
  List<string> Zielorte = new List<string>();
  int Anzahl = 10;
  int skip = 0;
  int take = 5;
  #endregion
 
  protected override void OnInitialized()
  {
   var httpContext = httpContextAccessor.HttpContext;
   info = "<b>Browser:</b> " + httpContext.Request.Headers["User-Agent"] + "<br><b>Server</b>: " + httpContext.GetServerVariable("SERVER_SOFTWARE") + " mit " + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
   AbflugorteLaden();
  }
 
  protected void AbflugorteLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    this.Abflugorte = ctx.Flug.Select(x => x.Abflugort).Distinct().OrderBy(x => x).ToList();
    this.C_Abflugort = "Berlin";
    ZielorteLaden();
   }
  }
 
  private void ZielorteLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    this.Zielorte = ctx.Flug.Where(x => x.Abflugort == this.C_Abflugort).Select(x => x.Zielort).Distinct().OrderBy(x => x).ToList();
    this.C_Zielort = this.Zielorte.ElementAt(0);
   }
   FluegeLaden();
  }
 
  protected void FluegeLaden()
  {
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    flugSet = ctx.Flug.Where(x => x.Abflugort == this.C_Abflugort && x.Zielort == this.C_Zielort && x.FreiePlaetze > 0).Skip(skip).Take(take).OrderBy(x => x.Datum)
.ToList();
    Anzahl = flugSet.Count;
   }
  }
 
  protected async Task C_Loeschen_Click(int flugNr)
  {
   // Aufruf von confirm() in JavaScript
   var e = await _jsRuntime.InvokeAsync<bool>("confirm", "M�chten Sie diesen Flug wirklich entfernen?");
   if (!e) return;
 
   using (var ctx = new DAL.Wwwings66_VieledatenContext())
   {
    ctx.Remove(ctx.Flug.Find(flugNr));
    ctx.SaveChanges();
   }
   FluegeLaden();
  }
 }
}

Man sieht deutlich, dass die Steuerelemente in ASP.NET Webforms deklarativer waren, beispielsweise wurden die Elemente für die Auswahllisten (option) sowie die Tabellentags table, th, tr und td automatisch erzeugt. Entwickler konnten dabei die alternierende Tabellenzeilenfarbe durch ein Tag (AlternatingRowStyle BackColor="White" /) ausdrücken. Die deklarativen Fähigkeiten in Webforms hatten jedoch enge Grenzen, sodass man etwa für die verschiedenen Zeichenfarben in der letzten Spalte auf ein ItemTemplate zurückgreifen muss, in dem Webentwickler die HTML-Ausgabe selbst zusammenbauen.

Hingegen muss man sich in ASP.NET Core Blazor (genau wie in anderen Razor-Syntax-basierten Webframeworks wie MVC, Web Pages oder Razor Pages) per se um die HTML-Ausgabe selbst kümmern, beispielsweise für die Tabelle mit alternierenden Zeilenfarben:

<table cellspacing="0" cellpadding="4" id="C_Fluege" style="color:#333333;border-collapse:collapse;">
 <tr style="color:White;background-color:#507CD1;font-weight:bold;">
  <th scope="col">Flug-Nr</th>
�
 </tr>
 @{ int rowCounter = 0;}
 @foreach (Flug f in flugSet)
 {
  rowCounter++;
  <tr style='background-color:@(rowCounter % 2 == 0 ? "white" : "#EFF3FB;" )'>
   <td>@f.FlugNr</td>
�

Es gibt aber auch schon Steuerelemente von Drittanbietern von Blazor, die Komfortfunktionen bieten, die Microsoft nicht in Blazor eingebaut hat. Zu beachten ist aber, dass es für Blazor nicht wie bei Webforms einen grafischen GUI-Designer gibt. Entwickler bekommen bei der Erfassung der HTML-Tags inklusive Razor-Syntax nur Eingabevorschläge (in Visual Studio: IntelliSense).

Ein DataGridView in ASP.NET Webforms rendert immer eine HTML-Tabelle mit table, th, tr und td. Das ist in Zeiten des Responsive Webdesign für unterschiedliche Geräteformen nicht mehr zeitgemäß. Dennoch wurde hier die Umsetzung in Blazor mit den gleichen Tags erledigt (ebenso wie die gleichen Variablennamen verwendet wurden), um den direkten Vergleich zu zeigen. In der Praxis sollte man die Gelegenheit nutzen, die Ausgabe auf ein Responsive Webdesign mit umzustellen (beispielsweise unter Einsatz des CSS-Spaltenframeworks in Twitters Bootstrap) und auch die Variablenbenennung vereinheitlichen.

Ein weiterer Unterschied zwischen den Vorlagenseiten in Listing 1 und 3 ist die Ausgabe der Zeichenkette info, die HTML-Tags enthält: In Webforms kann man die Variable einfach mit der ASPX-Platzhaltersyntax ausgeben (<%=info %>), bei ASP.NET Core Blazor ist als Sicherheitsfunktion eine Konvertierung in die Klasse MarkupString notwendig: @((MarkupString)info). Das hatte Microsoft schon in ASP.NET MVC eingeführt, dort aber syntaktisch anders gelöst: @Html.Raw(info).