XML schlägt JSON oder wie Corona alles veränderte

Betreiben Sie ein Schuhgeschäft? Dann sperren sie zu, schicken die Belegschaft in Kurzarbeit und warten bis alles gut ist. Sind Sie vieleicht stolzer Besitzer einer Airline? Dann blöd. Oder Aldis Erben, dann perfekt.

Die allermeisten bemühen sich ihre Geschäftsmodelle zu digitalisieren. Ein lokaler Pizza Service Mama Mia, hat wohl als einziger einen Online Shop, nimmt Kreditkarte und liefert um unsere Restbelegschaft Mittags zu versorgen. Ich mag Pizza wirklich, aber jeden Tag?

Nun sind wir bei ppedv total durchdigitalisert. Alle Mitarbeiter haben Ultrabooks, viele Surface Geräte. Alle Dienste und Apps sind per Web verfügbar. Per VPN und Direct Access, klappt drucken telefonieren. Home Office Ready. Nur was sollen unsere Kollegen im Home Office tun? Defacto gibt es seit 16.März unsere Geschäftsmodell nicht mehr. Praktisch alle Prozesse haben ihre Berechtigung verloren.

Da wir deswegen auch den Gang zum Arbeitsamt bzw Kurzarbeitsformular gehen müssen, stellt sich die Frage, wie verwalte ich das. Büros in Deutschland und Österreich und verteilte Belegschaft. Das alte Reporting als Steuerungsinstrument ungeeignet. Also stricke ich mir was neues ohne eine Spezifikation zu haben. Die Anforderung entsteht, wenn sie da ist.

Im ersten Schritt habe ich mich von einem relationalen Datenbank Modell verabschiedet. Zuerst bin ich mit einer JSON Struktur gestartet. Mit dem neuen System.Text.Json Parser aus .net core 3.1 habe ich versucht, pro Mitarbeiter ein Dokument anzulegen. Im Kopf Teil deklariere ich die Kennzahlen und Typen theoretisch täglich neu und im diese Struktur von Mitarbeiter erfassen zu lassen. Z.B. wieviele Kurse gebucht worden sind.

Leider passt JSON einfach nicht richtig gut zu .NET. Dynamische Objekte gehen zwar, sind aber echt aufwändig zu programmieren. Dazu kommt das JsonDocument nur Readonly funktioniert und das zurückschreiben einer untypiserten Json Struktur unglaublich viel Code benötigt. Ich will noch gar nicht davon reden wie viele Fehler sich einschleichen können. Nach ca 6h coding habe ich aufgegeben. Jedes gelöste Problem hat 2 neue erzeugt.

Wie gut das hierarchisches Parsing mit XML und XLINQ perfekt gelöst ist. Als UI kommt Webforms zum Einsatz, Sprache VB. Jeder Mitarbeiter bekommt ein XML File mit seinem Login Namen spendiert, das in App_data abgelegt wird.

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <Reporting>
   3:    <Ziel>
   4:      <_EintragAm type="Date"></_EintragAm>
   5:      <Datum type="Date"></Datum>
   6:      <Urlaub type="number"></Urlaub>
   7:      <Arbeitstag type="number"></Arbeitstag>
   8:       <Anfragen type="number"></Anfragen>
   9:      <VonOP type="number"></VonOP>
  10:      <VCSalesCall type="number"></VCSalesCall>
  11:      <Kommentar type="text"></Kommentar>
  12:    </Ziel>
  13:    <Tage>
  14:     
  15:    </Tage>
  16:  </Reporting>

Nach anfänglichen voll flexiblen Ansätzen, haben ich mich auf ein paar Konventionen fest gelegt. Aus den Elementen in <Ziel> soll sich automatisch ein Formular generieren, das der Benutzer füllt und dann in Tage speichert. Element mit _ underscore werden im Formular ignoriert.

Der Benutzer klickt auf ein Datum in der Liste, in der Url wird eine Route gebaut und das Datum dann vorbelegt. Ein Eintrag zum Datum deaktiviert den Link. Besonderheit trotzdem kann man das Datum mehrfach belegen, weil letztendlich könnte man sich vertippt haben. So kann man die Historie dank XML Tree wunderbar nachvollziehen.

image

Diese Entität wird dann im XML so persistiert.

   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <Reporting>
   3:    <Ziel>
   4:      <_EintragAm type="Date"></_EintragAm>
   5:      <Datum type="Date"></Datum>
   6:      <Urlaub type="number"></Urlaub>
   7:      <Kurs type="number"></Kurs>
   8:      <Kommentar type="text"></Kommentar>
   9:    </Ziel>
  10:    <Tage>
  11:      <Tag woche="13" tag="85">
  12:        <_EintragAm>2020-03-25T11:03:27.1154646+01:00</_EintragAm>
  13:        <Datum>2020-03-25</Datum>
  14:        <Urlaub>0</Urlaub>
  15:        <Kurs>0</Kurs>
  16:        <Kommentar>test</Kommentar>
  17:      </Tag>
  18:    </Tage>
  19:  </Reporting>

Dann als ran ans XML parsen und das ist erfreulich einfach, Nur wegen State und Overhead brauche ich 14 Zeilen Vb.NET Code. Das eigentlich coole ist, die Knoten in Tage einfach als Liste zu erhalten (Zeile 14)

   1:  Public Property Ziele As List(Of XNode)
   2:  Public Property Zahlen As List(Of XNode)
   3:  Public Property Xml As XDocument
   4:   
   5:  Private Sub op_EnterReport_Load(sender As Object, e As EventArgs) Handles Me.Load
   6:   Xml = XDocument.Load(Server.MapPath($"~/app_data/{ADUser.Name}.goal"))
   7:   If Page.IsPostBack = False Then
   8:       Ziele = Xml.Descendants("Ziel").
First.Nodes.Where(Function(x) Not x.ToString.Contains("_")).ToList
   9:          If Request.GetFriendlyUrlSegments().Count > 0 Then
  10:             Dim offset = CInt(Request.GetFriendlyUrlSegments(0))
  11:             CType(Ziele.First, XElement).Value = 
Date.Now.AddDays(offset).ToString("yyyy-MM-dd")
  12:           End If
  13:      End If
  14:      Zahlen = Xml.Descendants("Tage").Nodes.ToList
  15:  End Sub

 

Und wer ne Liste hat, kann auch iterieren. In diesem Fall um beliebig viele Input Felder in meinem Formular zu generieren. Sogar mit Bootstrap Optik, sonst wäre der HTML Code noch kürzer.

   1:  <%For Each item As XElement In Ziele%>
   2:   <div class="form-group row">
   3:   <label for=" " class="col-sm-2 col-form-label">
<%= item.Name%> </label>
   4:        <div class="col-sm-10">
   5:         <input type='<%=item.Attribute("type").Value %>' 
class="form-control form-control-sm" name="<%= item.Name%>"
value="<%= item.Value%>" />
   6:         </div>
   7:     </div>
   8:  <% Next %>
   9:  </div>
  10:  <asp:Button ID="_Button1" runat="server" Text="Speichern" 
OnClick="Button1_Click" class="btn btn-primary btn-sm" />

 

Im nächsten Schritt sollen diese dynamischen Formularfelder eingelesen und in die XML Struktur gespeichert werden, Auch hier ist der Kern ganz einfach. In Zeile 12 wird durch alle Post Form Felder iteriert und dies bei Eignung in ein XElement konvertiert und angehängt. Da ich die Kalenderwoche noch brauche und den Empty Case abfange am Ende etwas mehr Codezeilen.

   1:    Protected Sub Button1_Click(sender As Object, e As EventArgs)
   2:          Dim ci = New CultureInfo("de")
   3:          Dim myCal = ci.Calendar
   4:   
   5:          Dim letzter As XElement = Xml.Descendants("Tage").First.LastNode
   6:          Dim xe As XElement = New XElement("Tag")
   7:          Dim d = Date.Parse(Request.Form("Datum"))
   8:          xe.Add(New XAttribute("woche", 
myCal.GetWeekOfYear(d, CalendarWeekRule.FirstDay, DayOfWeek.Monday)))
   9:          xe.Add(New XAttribute("tag", d.DayOfYear))
  10:   
  11:          xe.Add(New XElement("_EintragAm", Date.Now))
  12:          For Each item As String In Request.Form
  13:              If Not item.Contains("_") Then
  14:                  Dim wert = Request.Form(item)
  15:                  xe.Add(New XElement(item, wert))
  16:              End If
  17:          Next
  18:          If IsNothing(letzter) Then  'der leere daten case
  19:              Xml.Descendants("Tage").First.Add(xe)
  20:          Else
  21:              letzter.AddAfterSelf(xe)
  22:          End If
  23:   
  24:          Xml.Save(Server.MapPath($"~/app_data/{ADUser.Name}.goal"))
  25:          Response.Redirect("~/op/reportenter.aspx")
  26:      End Sub

 

Auch die Datumsliste, mit der man sieht, welche Daten bereits erfasst wurden, ist ganz easy mit ASP.NET ohne Webforms Control gebaut. Zuerst wird eine Liste der letzten 90 Tage mit for next erstellt. Für jeden Eintrag geprüft ob ein passender Node im XML Document enthalten ist und wenn ja ein weitere Schleife durchlaufen. Sozusagen die Details für die Zeile. Samstag Sonntag sollen draußen bleiben.

   1: <%For i = 0 To -90 Step -1%>
   2:     <%If Date.Now.AddDays(i).DayOfWeek = 0 Then%>
   3:         <hr />
   4:     <%ElseIf Date.Now.AddDays(i).DayOfWeek = 6 %>
   5:     <%Else %>
   6:      <%If Check4Report(i) Then%>
   7:          <div class="row">
   8:               <div class="col-2">
   9:               <%=Date.Now.AddDays(i).ToString("dd.MMM.ddd") %>
  10:               </div>
  11:           <div class="col-10">
  12:                <%ReportDetails.XMLDaten = Daten4Report(i) %>
  13:                <uc1:ReportDetails runat="server" ID="ReportDetails" />
  14:           </div>
  15:           </div>
  16:          <% Else %>
  17:             <a href='<%= FriendlyUrl.Href("~/op/reportenter/", i) %>'>
  18:  <%=Date.Now.AddDays(i).ToString("dd.ddd.MMM") %></a><br />
  19:   
  20:      <% End If %>
  21:  <% End If %>
  22:   
  23:<% Next %>

Um den HTML Code übersichtlich zu halten habe ich zwei Hilfsmethoden gebaut, die die XML Daten filtern (where) auf passende Attribut Werte. Im ersten Fall wir der Hyperlink damit erzeugt und die zweite Methode liefert wiederum eine Liste der Einträge vom Tag.

   1:  Public Function check4report(id As Integer) As Boolean
   2:          '1.Tag des  Jahres anzahl tage
   3:          Dim datum = (Date.Now.AddDays(id + 1) - Date.Parse("1.1.2020")).Days
   4:   
   5:          Dim t = Zahlen.Where(Function(x As XElement) 
x.Attribute("tag").Value = datum.ToString)
   6:          If t.Count > 0 Then Return True
   7:          Return False
   8:  End Function
   9:  Public Function daten4report(id As Integer) As List(Of XElement)
  10:          Dim datum = (Date.Now.AddDays(id + 1) - Date.Parse("1.1.2020")).Days
  11:          Dim t = Zahlen.Where(Function(x As XElement) 
x.Attribute("tag").Value = datum.ToString)
  12:          Dim ret = t.ToList()
  13:          Dim XListe As New List(Of XElement)
  14:          For Each item In ret
  15:              XListe.Add(CType(item, XElement))
  16:          Next
  17:          Return XListe
  18:      End Function
  19:  End Class

 

Zu guter Letzt nutze ich ein ASCX User Control. Bestimmt aufgefallen, das der ASP.NET Code stark an AS.NET Core Razor erinnert. Einem Partial kann man komplexe Datentypen in der HTML deklaration zuweisen, Einem Webforms Benutzerelement nur einfach Datentypen per HTML Attribut. Deswegen in Zeile 12 XmlDaten eingeschoben wurde.

Was macht nun das Usercontrol  daraus? Fast nichts Smileyeinfach eine Liste von Xelementen.

   1:  Partial Class op_ReportDetails
   2:      Inherits System.Web.UI.UserControl
   3:      Public Property XMLDaten As List(Of XElement)
   4:  End Class

 

Der deklarative Teil der ASCX Datei. Samstag Sonntag wird per XLinq Skip übersprungen.

   1:  <%For Each item In XMLDaten%>
   2:  <div class="row">
   3:    <%For Each subitem As XElement In item.Nodes.ToList.Skip(2)%>
   4:            <%= subitem.Name%>:<%= subitem.Value%> 
   5:   <% Next %>
   6:  </div>
   7:  <%  Next %>

 

Wenn man da einmal drin ist, kann man super mit den Daten arbeiten ob Aggregieren per Group by und Sum oder mehr. Macht Spass- Sorry Json. Überleg grad noch ob ich ein Schema draufleg und die VB.NET XML Literals nutze – kidding

Kommentare sind geschlossen