zurück zum Artikel

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

Dr. Holger Schwichtenberg
Umstieg auf .NET Core: ASP.NET-Webserveranwendungen umstellen

(Bild: Andrey Suslov/Shutterstock.com)

Im dritten von fünf Beiträgen zur Migration vom klassischen .NET Framework auf .Net Core, geht es um die Umstellung ASP.NET-basierter Webserveranwendungen.

.NET Core unterstützt in Form von ASP.NET Core seit seiner ersten Version im Jahr 2016 die Entwicklung von Webanwendungen mit serverseitigem HTML-Rendering (alias Multi-Page Web Applications – MPA) genau wie das Erstellen von HTTP-Webservices nach dem REST-Prinzip. Neu seit ASP.NET Core 3.0 ist auch die Unterstützung für Single-Page Web Applications (SPA). In diesem Beitrag geht es um MPAs und SPAs, ein genauerer Blick auf die Webservices folgt im vierten Teil der Beitragsserie.

Umstieg auf .NET Core

In dieser fünfteiligen Artikelserie geht Holger Schwichtenberg der grundsätzlichen Frage nach, ob Organisationen, die zum Teil schon seit Anfang der 2000er Jahre mit dem klassischen .NET Framework entwickeln, ihren bestehenden Programmcode nun auf .NET Core umstellen sollten. Ist die Entscheidung für eine Migration gefallen, dann gilt es, eine Reihe von Herausforderungen zu meistern:

Zunächst einmal ist das Verständnis wichtig, dass ASP.NET und ASP.NET Core kein einheitliches Webserverframework sind, sondern eine Sammlung verschiedener Webframeworks mit verschiedenen Softwarearchitekturen. Beim alten ASP.NET im klassischen .NET Framework finden sich sieben Varianten für MPAs und Webservices:

  1. ASP.NET Webforms ist das ursprüngliche Architekturmodell in ASP.NET auf Basis von Webserver-Steuerelementen in .aspx-Dateien, die HTML, CSS und JavaScript generieren. Wahlweise kann der C#- oder Visual-Basic-.NET-Programmcode in der .aspx-Datei oder einer Code-Behind-Datei (.aspx.cs oder .aspx.vb) enthalten sein. Mit dem in die Seiten hineingerenderten versteckten Feld namens "ViewState" lässt sich ein Seitenzustand zwischen zwei HTTP-Anfragen festhalten. JavaScript kommt bei diesem Architekturmodell nur in geringen Dosen zum Einsatz. Entwickler haben beim Verwenden der Webserver-Steuerelemente nur begrenzten Einfluss auf die HTML-Ausgabe. Vorteil des Modells ist aber seine hohe Produktivität.
  2. ASP.NET AJAX, eine Erweiterung zu Webforms, verschafft mehr Möglichkeiten zur clientseitigen Ausführung im Browser mit JavaScript. Dazu bietet ASP.NET AJAX einige Steuerelemente, die mit viel JavaScript im Browser laufen.
  3. ASP.NET-Dynamic-Data-Websites sind ein metadatengetriebener Ansatz zum automatischen Generieren von Weboberflächen für die Datenansicht und Dateneingabe, vergleichbar mit Ruby-on-Rails. ASP.NET Dynamic Data wurde mit .NET 3.5 Service Pack 1 eingeführt und in .NET 4.0 erweitert.
  4. ASP.NET Model View Controller (MVC) ist seit 2009 das wichtigste Architekturmodell. Der Controller (.cs-Programmcodedatei) auf dem Server füttert den MVC-View mit Daten. Der View (.cshtml) erzeugt aus einer Template-Syntax mit einer wählbaren View Engine (Microsoft liefert ASPX-Syntax <span><%=x%></span> und die Razor-Syntax <span>@x</span>) eine HTML-Seite, die als Ganzes zum Webbrowser übertragen wird. ASP.NET MVC bietet gegenüber den vorgenannten Modellen eine bessere Kompetenztrennung in der Benutzerschnittstelle, eine bessere Kontrolle über die HTML-Ausgabe, eine bessere Integrierbarkeit von JavaScript sowie bessere Testbarkeit. MVC ist aber weniger produktiv als das Webforms-Modell, weil es hier keine Abstraktion durch Webserver-Steuerelemente und ViewState gibt.
  5. ASP.NET Web Pages basieren auch auf der Razor-Syntax, verflechten aber Programmcode und HTML/CSS in einer Datei (.cshtml). Es gibt einen zugehörigen Editor Webmatrix. ASP.NET Webpages und Webmatrix richten sich eher an Gelegenheits- und Hobby-Programmierer. Microsoft zielte mit dieser Variante auf Entwickler, die PHP mögen. In Deutschland hatte dieses Architekturmodell nur geringe Bedeutung.
  6. SOAP-basierte Webservices kann man in ASP.NET seit der Version 1.0 mit ASP.NET Webservices (alias: ASMX, basierend auf der verwendeten Dateinamenserweiterung .asmx) erstellen. Das Architekturmodell gilt jedoch als veraltet, seit .NET Framework 3.0 im Jahr 2006 die Windows Communication Foundation (WCF) eingeführt hat.
  7. REST-basierte Webservices erlaubt ASP.NET WebAPI, das es seit dem Jahr 2012 gibt.

Diese sieben Architekturmodelle lassen sich auch in einem Webprojekt mischen, weil es in ASP.NET einen gemeinsamen Unterbau gibt.

Im neuen ASP.NET Core gibt es auf dem heutigen Stand nur noch fünf Architekturmodelle (s. Abb. 1):

  1. Ganz oben sieht man die Model-View-Controller-Architektur (MVC), die seit Version 1.0 (Jahr 2016) in ASP.NET Core existiert. Allerdings ist die ASPX-Syntax mit <span><%=x%></span> nicht mehr möglich, sondern nur noch die Razor View Engine mit <span>@x</span>.
  2. Seit ASP.NET Core 2.0 (Jahr 2017) gibt es die Razor Pages, die ein Page Model statt eines Controllers besitzen, aber die gleichen Interaktionen mit dem Webbrowser ermöglichen. Lediglich die Architektur auf dem Webserver ist anders, insbesondere die Zusammenarbeit zwischen Page Model und View.
  3. Ebenfalls seit ASP.NET Core 1.0 gibt es das WebAPI-Modell, bei dem der Webserver lediglich Daten per REST-Dienst im JSON-Format an den Webbrowser liefert und das HTML-Rendering sowie die Benutzerinteraktionsereignisbehandlung komplett im Client per JavaScript/TypeScript stattfindet.
  4. An vierter Stelle sieht man nun den ASP.NET Core Blazor Server mit dem kontinuierlichen Zyklus der Übertragung von Interaktionen und DOM-Änderungen via ASP.NET SignalR, sodass der Benutzer das Erlebnis einer Single-Page-Web-Application erhält, wenngleich die Ausführung weiterhin auf dem Server stattfindet. ASP.NET Core Blazor Server ist erstmals in ASP.NET Core 3.0 am 23.9.2019 erschienen [6].
  5. Das fünfte Architekturmodell ist ASP.NET Core Blazor WebAssembly. Die Serverseite bietet hier wie in Modell 3 nur noch WebAPIs an. C# läuft hier auf Basis der Mono-Runtime in WebAssembly innerhalb des Webbrowsers und kann dort .NET-Code ausführen. Blazor WebAssembly erstellt also eine echte SPA. Es befindet sich bisher noch in der Preview-Phase und soll im Mai 2020 erscheinen.
Umstieg auf .NET Core: ASP.NET-Webserveranwendungen umstellen

ASP.NET Core bietet mittlerweile fünf Architekturalternativen (Abb. 1)

Die Unterschiede der verfügbaren Architekturmodelle bei ASP.NET und ASP.NET Core machen deutlich, dass nicht in allen Fällen eine Migration im engeren Sinne möglich ist, sondern in vielen Fällen ein Neuschreiben notwendig wird – zumindest der oberen Anwendungsschichten (Benutzerschnittstellenbeschreibung und Benutzerschnittstellensteuerung). Wie viel des alten Programmcodes sich dabei wiederverwenden lässt, hängt von der Güte der gewählten Softwarearchitektur ab. Je klarer Geschäftslogik und Datenzugriff von der Benutzerschnittstellensteuerung abgetrennt sind, desto einfacher lassen sich diese Schichten Geschäftslogik und Datenzugriff wiederverwenden.

Als vergleichsweise einfache, aber auch recht aufwändige Migration lässt sich der Umstieg vom klassischen ASP.NET MVC auf ASP.NET Core MVC bezeichnen. In diesem Fall können Softwareentwickler zumindest die Razor View-Syntax fast unverändert beibehalten. Die im alten ASP.NET MVC noch erlaubte View Engine mit der klassischen, in den 1990er Jahren mit Active Server Pages eingeführten Platzhalter-Syntax (<% … %>) gibt es in ASP.NET Core allerdings nicht mehr. Wer sie noch verwendet, muss viel umstellen.

In jedem Fall sind Entwickler von der Änderung bei den Webserver-APIs betroffen: Es haben sich alle Klassen zur Interaktion mit dem Webserver geändert, insbesondere für den Zugriff auf die aktuelle URL, Browserinformationen, Cookies und andere Informationen aus HTTP-Anfrage und HTTP-Antwort sowie auch sonstige Klassen für die Zustandsverwaltung (beispielsweise Session-Variablen und Caching) und Konfigurationsdaten. Dies betrifft auch darauf aufbauende Funktionen wie URL-Rewriting, Benutzerverwaltung und Authentifizierung.

Im klassischen ASP.NET wurden diese Webserver-APIs von der Assembly System.Web (System.Web.dll) bereitgestellt, allen voran von dem eingebauten Objekt HttpContext.Current mit Unterobjekten wie Server, Request, Response und Session. Die Assembly System.Web ist aber in ASP.NET Core nur noch ganz rudimentär vorhanden (für URL-Encoding und -Decoding für Nicht-Webanwendungen). Microsoft hat die System.Web-Funktionalität in ASP.NET Core auf viele Assemblies in einzelnen NuGet-Paketen verteilt, darunter

ASP.NET Core arbeitet zudem umfassend mit Dependency Injection (NuGet-Paket Microsoft.Extensions.DependencyInjection). Alle diese API-Änderungen bedeuten konkret, dass der Startcode einer Webanwendung und einiges von dem Programm in den Controllern, gegebenenfalls auch in in Views enthaltenen Programmcode, umgeschrieben werden muss. Das ist manuell zu erledigen.

Es existiert auch kein Werkzeug, das ein ASP.NET-MVC-Projekt automatisch in ein ASP.NET-Core-MVC-Projekt umbaut. Letztlich kommt man auch hier zu dem Vorgehensmodell, das in Teil 2 dieser Serie für WPF- und Windows Forms-basierte Desktopanwendungen ausführlich beschrieben wurde [7] und welches man kurz zusammenfassen kann mit: Neues leeres Projekt anlegen, Dateien für Controller, Views und sonstige Klassen umkopieren und so lange umprogrammieren, bis es wieder läuft. Allerdings ist der bei ASP.NET MVC notwendige Änderungsaufwand bei weitem höher als bei WPF- und Windows-Forms-Anwendungen, denn bei letzteren beiden gibt es keine solch gravierenden API-Änderungen.

Zusätzlich schlagen in Webprojekten noch die Änderungen in den .NET-Basisklassen durch, welche auch bei WPF- und Windows-Forms-Migrationen erforderlich sind. In ASP.NET Core 1.0, 1.1, 2.0, 2.1 und 2.2 hatte ein .NET-Softwareentwickler noch die Option, ASP.NET Core wahlweise auf klassischem .NET Framework oder .NET Core zu betreiben. Mit dem Betrieb auf .NET Framework konnten Entwickler einigen Migrationsaufgaben im Bereich der Basisklassen entgehen beziehungsweise die Migration schrittweise durchführen. Seit ASP.NET Core 3.0 erlaubt Microsoft den Weg über das klassische .NET Framework leider nicht mehr, daher ist nun zwingend .NET Core als Basis zu verwenden. Heute noch auf eine Version vor ASP.NET Core 3.0 zu setzen, ist aber wenig sinnvoll, weil die Versionen 1.0, 1.1, 2.0 und 2.2 nicht mehr von Microsoft unterstützt werden. Für die Long-Term-Support-Version 2.1 endet der Support am 21.8.2021 [8].

Eine Migration von ASP.NET Webpages auf ASP.NET Core Razor Pages ist mit ähnlich hohem Aufwand verbunden wie der Umstieg von ASP.NET MVC auf ASP.NET Core MVC, soll aber an dieser Stelle wegen der geringen Marktrelevanz von ASP.NET Webpages nicht weiter diskutiert werden.

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 [9] 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." [10], 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.

Umstieg auf .NET Core: ASP.NET-Webserveranwendungen umstellen

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 [11]) 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).

In den beiden Code-Behind-Dateien sieht man geringere Unterschiede als bei der Vorlage, weil auch schon die Webforms-Implementierung mit Microsofts Objekt-Relationalem Mapper Entity Framework Core arbeitet. In der Regel haben ältere Webforms-Projekte aber eine ältere Datenzugriffstechnik. Deren Umstellung ist ein Thema in einem weiteren Teil dieser Serie.

Die im Beispiel sichtbaren Unterschiede in den Code-Behind-Dateien sind:

Die zahlreichen Änderungen sowohl an der Benutzerschnittstellenbeschreibung als auch an der Benutzerschnittstellensteuerung zeigen bereits in diesem sehr einfachen Beispiel sehr deutlich den nicht unerheblichen Aufwand eines Wechsels von Webforms auf Blazor. Bei einer Migration auf ASP.NET Core Razor Pages wäre der Aufwand ähnlich, bei ASP.NET Core MVC noch ein klein wenig höher.

In der Praxis gibt es viele Tabellenausgaben mit Sortier-, Gruppier- und Editierfunktionen sowie Detailansichten. Deren Neuprogrammierung ist noch deutlich aufwändiger, kann hier aber aus Platzgründen nicht näher behandelt werden.

Der Aufwand für die Migration einer Webanwendung von ASP.NET zu ASP.NET Core hängt von der verwendeten Variante des Webframeworks ab. Die vergleichsweise einfachste Umstellung ist die von ASP.NET MVC zu ASP.NET Core MVC, wobei auch diese aufgrund der fundamentalen Änderungen im Bereich der Webserver-APIs deutlich mehr Zeit erfordert als die Umstellung einer Desktop-Anwendung.

Wer von ASP.NET Webforms migrieren will, muss zumindest die Benutzeroberflächenbeschreibung auf jeden Fall neu schreiben.

Dr. Holger Schwichtenberg
ist einer der bekanntesten Experten für .NET und Webtechniken in Deutschland. Zusammen mit 35 weiteren Experten unterstützt er im Rahmen der Firma www.IT-Visions.de [12] mittlere und große Unternehmen durch Beratung und Schulungen beim Erstellen von Windows- und Webanwendungen. Zudem hat er einige Fachbücher zu .NET, PowerShell und JavaScript/TypeScript veröffentlicht. Bei Twitter folgen Sie ihm unter @DOTNETDOKTOR [13]
. (map [14])


URL dieses Artikels:
https://www.heise.de/-4687175

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Umstieg-auf-NET-Core-Migrieren-oder-nicht-migrieren-4628946.html
[2] https://www.heise.de/hintergrund/Umstieg-auf-NET-Core-Desktop-Anwendungen-mit-WPF-und-Windows-Forms-umstellen-4640058.html
[3] https://www.heise.de/hintergrund/Umstieg-auf-NET-Core-ASP-NET-Webserveranwendungen-umstellen-4687175.html
[4] https://www.heise.de/hintergrund/Umstieg-auf-NET-Core-Teil-4-SOAP-und-REST-Webservices-umstellen-4705706.html
[5] Datenzugriff auf .NET Core umstellen
[6] https://www.heise.de/news/Microsoft-laeutet-mit-NET-Core-3-0-und-C-8-0-neues-Zeitalter-ein-4535758.html
[7] https://www.heise.de/hintergrund/Umstieg-auf-NET-Core-Desktop-Anwendungen-mit-WPF-und-Windows-Forms-umstellen-4640058.html
[8] https://dotnet.microsoft.com/platform/support/policy/dotnet-core
[9] https://docs.microsoft.com/de-de/dotnet/architecture/blazor-for-web-forms-developers/
[10] https://docs.microsoft.com/en-us/dotnet/architecture/blazor-for-web-forms-developers/migration#migrate-built-in-web-forms-controls
[11] https://getbootstrap.com/docs/4.0/layout/grid/
[12] https://www.it-visions.de/
[13] http://twitter.com/dotnetdoktor
[14] mailto:map@ix.de