ASP.NET Webforms und Bootstrap Formular

Eine typische Geschäftsanwendung (LOB) enthält eine Reihe typischer Anwendungsfälle. Eine Liste anzeigen und durchsuchen. Eine Master Detail Darstellung von nested Lists. Fehlt noch Daten zu erzeugen bzw. zu editieren. Es ist zwar möglich, direkt in einem Gridview auch die Daten zu editieren, aber ich halte das nicht mehr für zeitgemäß. Meine Entscheidung fällt auf eine übliche Formulardarstellung und hier im speziellen mit dem ASP.NET Formview Control.

Formulare sind in Bootstrap offensichtlich so kompliziert, das es jede Menge von Online Wysiwyg Formular Editor gibt wie hier oder hier. Das Bedürfnis Controls per Drag&Drop zusammen zu klicken (Fachausdruck Mausschubser) scheint nicht zu sterben.

Laut diesen Tools definiert sich eine HTML5 Form im Bootstrap Design wie folgt. Wobei sich die Websites dabei nicht unbedingt völlig einig sind.

   1:  <form class="form-horizontal">
   2:  <div class="control-group">
   3:    <label class="control-label" for="textinput">Text Input</label>
   4:    <div class="controls">
   5:      <input name="textinput" class="input-xlarge" id="textinput" type="text" >
   6:    </div>
   7:  </div>

image

Die Dokumentation von Bootstrap 3 finde ich dazu mangelhaft.

Nun stellt sich die Frage wie man das in ein ASP.NET Webforms format bekommt. Natürlich kann man mit Formview oder Detailsview die Itemtemplates HTML 5 konform schreiben. Ich will aber so wenig wie möglich Aufwand haben um ein einzelnes Formular zu erzeugen. Dieses Vorhaben wird allerdings noch eine Menge Developer Aufwand nach sich ziehen. Mehr als ich geahnt hatte.

Es gibt seit ASP.NET 4 eine Art Daten Magie. Mit hilfe des DynamicEntity Steuerelements zaubert sich ein Formular anhand der Daten, konkret des Models von ganz alleine. Rein konzeptionell sieht der ASP.NET Code ohne Datenbindung so aus

   1:  <asp:FormView runat="server" ID="FormView1"
   2:  RenderOuterTable="false"> 
   3:    <ItemTemplate>
   4:            <asp:DynamicEntity runat="server" /> 
   5:             <asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" /> 
   6:             <asp:LinkButton runat="server" CommandName="Delete" Text="Delete" 
   7:  OnClientClick='return confirm("wirklich?");' />  
   8:  </ItemTemplate> 
   9:  </asp:FormView>  

Wer nicht ganz so viel Magic will, kann auch in der Columns Liste für jedes Feld einzeln einen <asp:DynamicField anlegen. Das aber in einem anderen Blog Post.

Die Daten kommen im VB.NET Beispiel aus einem Entity Framework generierten Modell.

Dazu eine Anmerkung. Häufig sind Datenbanken vorhanden, müssen über Anwendungsgrenzen hinweg geteilt werden oder unterliegen administrativen Einflüssen Dritter. Insofern wird der Code First Ansatz in der Praxis von Brown Field Projekten seltener auftreten.

Allerdings hat das Modell bei weitem nicht genug Informationen über die Daten. Reihenfolge, Beschriftung, Validierungsregeln und einiges mehr. All das kann man per Data Annotations jeder Property hinzufügen.

   1:  <Display(Name:="Product Number")> 
   2:  <Range(0, 5000)> 
   3:  Public Property ProductID() As Integer

Die Eigenschaften und die  Klassen im Model werden automatisch erzeugt. Wenn man den folgenden Screenshot nimmt, aus der Tabelle Employee eine Klasse Employee die die Felder aus der Tabelle als Propertys enthält.

image

Um das überschreiben der Klasse bei bearbeiten des Modells im Visual Studio Desinger zu verhindern, werden die Annotationen per Metadaten ausgelagert.

Zunächst erzeugt man eine zum Model ident benannte Klasse mit dem Zusatz Partial in einer neuen Datei. In diesem Beispiel erhält das Model so eine zusätzliche Eigenschaft.
Weiters erzeugt man eine zweite Klasse mit identen Propertys zur ModelDatenklasse und hinterlässt pro Eigenschaft eins oder mehrere Attribute wie Editable. Dann wird diese Klasse per MetadataType Attribut der Partial Klasse zugewiesen.

   1:  <MetadataType(GetType(ANSPRECHPARTNERMetadata))>
   2:  Partial Class ANSPRECHPARTNER
   3:      Private _anzahl As Integer
   4:      <Display(AutoGenerateField:=False)>
   5:      Public Property anzahl() As Integer
   6:          Get
   7:              Return Me.ANSPRECHPARTNERCOMMENTS.Count
   8:          End Get
   9:          Set(ByVal value As Integer)
  10:              _anzahl = value
  11:          End Set
  12:      End Property
  13:  End Class
  14:   
  15:  Public Class ANSPRECHPARTNERMetadata
  16:      <Editable(False)>
  17:      <Display(Name:=" ")>
  18:      Public Property AnsprechID As Integer
  19:      <Editable(False)>
  20:      <Display(AutoGenerateField:=False)>
  21:      Public Property AdrID As Integer

Total einfach oder? (hab gerade einen leichten Anfall von Ironie)

Fehlt nur noch ein Stück dazwischen. Hier kommt DynamicData zum Einsatz, wie immer per Nuget nachinstalliert.

image

Im Web Projekt finden sich dann mehrere Verzeichnisse wie Entitytemplate oder Fieldtemplates. Darin Dateien wie Default_edit für den Edit Modus des DynamicEntity Steuerelements. Die exakte Darstellung hängt wieder vom Typ des Tabellenfeldes  oder der Annotation ab. Z.B. kann man ein Feld als URL defnieren und dann kommt das Template url.ascx oder url_edit.ascx zum Einsatz.

image

Zurück zu den Bootstrab 3 Formular Anforderungen. Zunächst das default_edit.ascx. Dies wurde in der HTML Struktur angepasst und CSSClass Bootstrap Attribute zugewiesen.

   1:  <asp:EntityTemplate runat="server" ID="EntityTemplate1">
   2:      <ItemTemplate>
   3:          <div class="form-group">
   4:              <asp:Label ID="Label1" runat="server" OnInit="Label_Init"
   5:                  OnPreRender="Label_PreRender" CssClass="col-sm-2 control-label" />
   6:              <div>
   7:                  <asp:DynamicControl runat="server" ID="DynamicControl"
   8:                      Mode="Edit" OnInit="DynamicControl_Init" />
   9:              </div>
  10:          </div>
  11:      </ItemTemplate>
  12:  </asp:EntityTemplate>

Wie man sieht ist darin ein DynamicDatacontrol enthalten. Bei normalen Text wird dadurch das nächste Template text_edit.ascx eingestreut. Verändert wurden die CSSClass Attribute

   1:  <%@ Control Language="VB" CodeFile="Text_Edit.ascx.vb" Inherits="Text_EditField" %>
   2:   
   3:  <asp:TextBox ID="TextBox1" runat="server" Text='<%# FieldValueEditString %>'
   4:       CssClass="form-control"></asp:TextBox>
   5:   
   6:  <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator1" 
CssClass="has-error" ControlToValidate="TextBox1" Display="Dynamic" Enabled="false" />
   7:  <asp:RegularExpressionValidator runat="server" ID="RegularExpressionValidator1" 
CssClass="has-error " ControlToValidate="TextBox1" Display="Dynamic" Enabled="false" />
   8:  <asp:DynamicValidator runat="server" ID="DynamicValidator1" CssClass="has-error"  
ControlToValidate="TextBox1" Display="Dynamic" />
   9:   

Da die Validator Controls extra Platz beanspruchen, wurde Display auf dynamic gesetzt um diese auszublenden und so Platz im Formular zu sparen. Für das Design von Errormessages die beim Validieren anhand des Models auftauchen habe ich mir einen weiteren Blog Artikel aufgehoben.

Nach diesen Umfangreichen Vorarbeiten erfordert das einzelne Formular nicht mehr soviel Aufwand. Für Edit und Insert werden zwei Templates benötigt.

   1:   <div class="row">
   2:          <div class="col-md-6">
   3:              <div class="panel panel-primary">
   4:                  <asp:FormView runat="server" ID="Adresse"
   5:                      DataKeyNames="Adrnr"
   6:                      ItemType="ADRESSEN"
   7:                      SelectMethod="Adresse_GetItem"
   8:                      UpdateMethod="Adresse_UpdateItem"
   9:                      DeleteMethod="Adresse_DeleteItem"
  10:                      InsertMethod="Adresse_InsertItem"
  11:                      DefaultMode="Edit"
  12:                      GridLines="None"
  13:                      RenderOuterTable="false">
  14:                      <EditItemTemplate>
  15:                          <div class="form-horizontal" role="form">
  16:                              <asp:DynamicEntity runat="server" Mode="Edit" />
  17:                              <div class="btn-group">
  18:                                  <asp:Button runat="server" CssClass="btn btn-default" 
ID="UpdateButton" CommandName="Update" Text="Update" />
  19:                                  <asp:Button runat="server" CssClass="btn btn-default" 
ID="CancelButton" CommandName="Cancel" Text="Cancel" CausesValidation="false" />
  20:                              </div>
  21:                          </div>
  22:                      </EditItemTemplate>
  23:   
  24:                      <InsertItemTemplate>
  25:                          <div class="form-horizontal" role="form">
  26:                              <asp:DynamicEntity runat="server" Mode="Insert" />
  27:                              <asp:ValidationSummary runat="server" ShowModelStateErrors="true" />
  28:                              <div class="btn-group">
  29:                                  <asp:Button runat="server" CssClass="btn btn-default" 
ID="InsertButton" CommandName="Insert" Text="Insert" />
  30:                                  <asp:Button runat="server" CssClass="btn btn-default" 
ID="CancelButton" CommandName="Cancel" Text="Cancel" CausesValidation="false" />
  31:                              </div>
  32:                          </div>
  33:                      </InsertItemTemplate>
  34:                  </asp:FormView>
  35:              </div>
  36:          </div>

Tipp: ASP:Formview Tab Tab- Dann eine ID für das Formview vergeben. Als nächses den Itemtype auf ein Entity Framework Item zuweisen. Dann die Methoden für Select, Insert, Update und Delete tippen und Methode erstellen lassen.

image

Die Namen werden dann automatisch erzeugt

   1:   <asp:FormView runat="server" id="myName" ItemType="ADRESSEN"
   2:          SelectMethod="myName_GetItem"
   3:          UpdateMethod="myName_UpdateItem"
   4:          DeleteMethod="myName_DeleteItem"
   5:          InsertMethod="myName_InsertItem"
   6:          >

Richtig hilfreich ist aber der erzeugte Codebehind VB Code so das nur eine Handvoll Zeilen getippt werden müssen.

   1:   
   2:  Partial Class adressen_test
   3:      Inherits System.Web.UI.Page
   4:   
   5:      ' Der ID-Parameter sollte dem DataKeyNames-Wert entsprechen, der für das Steuerelement
   6:      ' festgelegt wurde, oder mit einem Wertanbieterattribut versehen werden, z. B. <QueryString>ByVal id as Integer
   7:      Public Function Unnamed_GetItem(ByVal id As Integer) As Object
   8:          Return Nothing
   9:      End Function
  10:   
  11:      ' Der ID-Parameter sollte dem DataKeyNames-Wert entsprechen, der für das Steuerelement
  12:      ' festgelegt wurde, oder mit einem Wertanbieterattribut versehen werden, z. B. <QueryString>ByVal id as Integer
  13:      Public Function myName_GetItem(ByVal id As Integer) As ADRESSEN
  14:          Return Nothing
  15:      End Function
  16:   
  17:      ' Der Name des ID-Parameters sollte dem für das Steuerelement festgelegten DataKeyNames-Wert entsprechen.
  18:      Public Sub myName_UpdateItem(ByVal id As Integer)
  19:          Dim item As ADRESSEN = Nothing
  20:          ' Element hier laden, z. B. item = MyDataLayer.Find(id)
  21:          If item Is Nothing Then
  22:              ' Das Element wurde nicht gefunden.
  23:              ModelState.AddModelError("", String.Format(
"Das Element mit der ID {0} wurde nicht gefunden.", id))
  24:              Return
  25:          End If
  26:          TryUpdateModel(item)
  27:          If ModelState.IsValid Then
  28:              ' Änderungen hier speichern, z. B. MyDataLayer.SaveChanges()
  29:   
  30:          End If
  31:      End Sub
  32:   
  33:      ' Der Name des ID-Parameters sollte dem für das Steuerelement festgelegten DataKeyNames-Wert entsprechen.
  34:      Public Sub myName_DeleteItem(ByVal id As Integer)
  35:   
  36:      End Sub
  37:   
  38:      Public Sub myName_InsertItem()
  39:          Dim item = New ADRESSEN()
  40:          TryUpdateModel(item)
  41:          If ModelState.IsValid Then
  42:              ' Save changes here
  43:   
  44:          End If
  45:      End Sub
  46:  End Class

Optisch sieht mein Formular dann so aus.

image

Wenn man wenig Platz hat, weil zb nur Smartphone spielt Bootstrap seine responsive Design Fähigkeiten ohne Mehraufwand aus. Die Beschriftungen wandern dann über das Eingabefeld.

Exemplarisch wird noch gezeigt wie ein neuer Datensatz angelegt wird.

   1:   Public Sub Adresse_InsertItem()
   2:          Dim item = New ADRESSEN()
   3:          item.createdby = User.Identity.Name
   4:          item.Datum = Date.Now
   5:          TryUpdateModel(item)
   6:          If ModelState.IsValid Then
   7:              db.ADRESSEN.Add(item)
   8:              db.SaveChanges()
   9:              Response.Redirect(FriendlyUrl.Href("~/adressen/kundenedit/",item.adrID)) 
  10:          End If
  11:  End Sub

Wirklich praktisch ist, das man über item auf das Adressen Objekt vor dem Update zugreifen kann und auch danach. Davor werden fehlende Inhalte ergänzt wie der Bearbeiter. Danach wird die von der SQL Datenbank erzeugte Identity ausgelesen und auf die editier Seite umgeleitet.

Kommentare sind geschlossen