22 Juni 2019

ASP.Net Core mit OpenUI5 – DataBinding

In diesem zweiten Artikel zum Thema ASP.Net Core mit OpenUI5 gehe ich im Gegenteil zum letzten Artikel wieder einen Schritt zurück. Es geht hier erstmal um die Erstellung einer Seite mit Eingabe- und Ausgabefeldern – Im Beispiel in Form einer Konfigurationsseite. Für mich interessant an der Stelle ist das DataBinding von OpenUI5 und der Umgang mit den Datentypen von .Net. Strings und Zahlen sind ja i. d. R. kein Problem, interessanter wird es bei Enums oder Zeitstempel.

Das vollständige Beispiel kann von Github unter [1] abgerufen werden. In diesem Artikel gehe ich zunächst auf den Service in ASP.Net Core ein, dann auf den Controller in OpenUI5 und abschließend auf die View selbst. Folgender Screenshot zeigt die fertige Seite in zwei Größenvarianten (Desktop und Mobile).

ASP.Net Core Service

Als Datenbasis dient ein sehr einfacher Service in ASP.Net Core wie in folgendem Code-Snippet dargestellt. Es gibt einen GET-Endpunkt zur Abfrage des Konfigurationsobjekts und einen PUT zum Ersetzen. Als Datenspeicher dient eine schlichte statische Variable.

[Route("api/[controller]")]
[ApiController]
public class ConfigurationController : Controller
{
    private static ConfigurationData s_config = new ConfigurationData();

    [HttpGet]
    public IActionResult Get() => this.Ok(s_config);

    [HttpPut]
    public IActionResult Put([FromBody]ConfigurationData config)
    {
        s_config = config;
        return Ok(config);
    }
}

Das Konfigurationsobjekt ist ebenso einfach aufgebaut. Es verfügt über zwei ReadOnly-Eigenschaften MachineName und StartTimeStamp, weiterhin über einige beschreibbare Eigenschaften wie das Flag AutoRestart. Nachfolgendes Code-Snippet zeigt die zugehörige Klasse.

/// <summary>
/// Model for configuration data
/// </summary>
public class ConfigurationData
{
    public string MachineName => Environment.MachineName;

    [JsonConverter(typeof(IsoDateTimeConverter))]
    public DateTime StartTimeStamp = new DateTime(2019, 6, 18, 13, 23, 10, 315);

    public bool AutoRestart { get; set; } = true;

    public AutoRestartInterval AutoRestartInterval { get; set; } = AutoRestartInterval.Daily;

    [JsonConverter(typeof(JsonTimeSpanConverter))]
    public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30.5);
}

Zwei kleine Besonderheiten sind an der Stelle aber schon dabei, und zwar die eingesetzten JsonConverter bei den Eigenschaften StartTimeStamp und RequestTimeout. Ersterer ist in Json.Net bereits so enthalten und wandelt den Zeitstempel in ein sprachunabhängiges Format. Der JsonTimeSpanConverter bei der Eigenschaft RequestTimeout ist dagegen eine eigene Klasse, da OpenUI5 bei der Eingabebox für Zeitspannen nur bis Sekunden runtergeht – und nicht bis Millisekunden, wie es Json.Net standardmäßig machen würde. Nachfolgendes Code-Snippet zeigt die Konverter-Klasse.

// TimeSpanConverter from https://stackoverflow.com/questions/39876232/newtonsoft-json-serialize-timespan-format

public class JsonTimeSpanConverter : JsonConverter<TimeSpan>
{
    /// <summary>
    /// Compatible format for OpenUI5: hh:mm:ss
    /// </summary>
    public const string FORMAT = @"hh\:mm\:ss";

    public override void WriteJson(JsonWriter writer, TimeSpan value, JsonSerializer serializer)
    {
        var timespanFormatted = $"{value.ToString(FORMAT)}";
        writer.WriteValue(timespanFormatted);
    }

    public override TimeSpan ReadJson(JsonReader reader, Type objectType, TimeSpan existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        TimeSpan parsedTimeSpan;
        TimeSpan.TryParseExact((string)reader.Value, FORMAT, null, out parsedTimeSpan);
        return parsedTimeSpan;
    }
}

Controller in OpenUI5

Nachfolgendes Code-Snippet zeigt den Controller in OpenUI5. Hier verwende ich zunächst ein JSONModel, um die Daten vom ASP.Net Core Service an die Oberfläche zu bringen. Die Methode loadData vom JSONModel nimmt dabei die URL zum Endpunkt entgegen und lädt anschließend die Daten asynchron nach. Die Methode onApplyClick wird später an den Speichern-Button in der Oberfläche gebunden. Hierbei wird ein Ajax-Aufruf an den ASP.Net Core Endpunkt ausgelöst, welcher die durch den Benutzer modifizierten Daten aus dem JSONModel wieder abspeichert. Bei der Methode onResetClick werden lediglich die Daten erneut nachgeladen und dabei etwaige Änderungen des Benutzers rückgängig gemacht.

sap.ui.define([
    "rolandk/de/ui5test/util/BaseController",
    "sap/ui/model/json/JSONModel",
    "sap/m/MessageToast"
], function (BaseController, JSONModel, MessageToast) {
	"use strict";
	return BaseController.extend("rolandk.de.ui5test.controller.Configuration",
        {
            onInit: function () {
	        let oModel = new JSONModel();
	        oModel.loadData("/api/configuration");

	        let oView = this.getView();
	        oView.setModel(oModel);

                // Enables automatic property validation on the ui
                //  see https://help.sap.com/doc/saphelp_uiaddon20/2.05/en-US/91/f0652b6f4d1014b6dd926db0e91070/content.htm?no_cache=true
                sap.ui.getCore().getMessageManager().registerObject(oView, true);
            },

            onApplyClick: function() {
                let oView = this.getView();
                let oModel = oView.getModel();

                let oData = oModel.getData();
                let sData = JSON.stringify(oData);
                jQuery.ajax({
                    type: "PUT",
                    contentType: "application/json",
                    data: sData,
                    url: "/api/configuration",
                    dataType: "json",
                    success: function() {
	                    MessageToast.show("Saved", {
		                    duration: 3000,
		                    width: "80%"
	                    });
                    },
                    error: function() {
	                    MessageToast.show("Error!", {
		                    duration: 3000,
		                    width: "80%"
	                    });
                    }
                });
            },

            /**
             * Resets all data to the state at the backend
             */
            onResetClick: function() {
	            let oModel = new sap.ui.model.json.JSONModel();
	            oModel.loadData("/api/configuration");

	            let oView = this.getView();
	            oView.setModel(oModel);
            }
		});
});

Eine kleine Besonderheit an diesem Controller ist, dass er von der eigenen Klasse BaseController erbt. Dies ist ein guter Mechanismus, um einige Funktionalitäten an zentraler Stelle pflegen zu können. Im Beispiel verwende ich den BaseController zur Bereitstellung von Formatters. Formatter sind Methoden, die einen Wert aus einer Datenquelle für die Oberfläche aufbereiten können. Eine gute Beschreibung dazu findet sich direkt bei der SAP [2]. Nachfolgende beiden Code-Snippets zeigen die Klasse BaseController und den Formatter, welcher später für die Formatierung des Zeitstempels auf der Oberfläche dient.

sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"rolandk/de/ui5test/util/CustomFormatters"
], function (Controller, CustomFormatters) {
    "use strict";
        return Controller.extend("rolandk.de.ui5test.util.BaseController", {

            formatters: CustomFormatters,

            getRouter: function () {
                return sap.ui.core.UIComponent.getRouterFor(this);
            },
    });
});
// Custom formatters class like the implementation here: 
//   https://help.sap.com/doc/saphelp_uiaddon20/2.05/en-US/0f/8626ed7b7542ffaa44601828db20de/content.htm?no_cache=true

sap.ui.define([], function () {
	"use strict";
    return {
        /**
         * Converts a string representation of a DateTime object to a readably datetime for the user
         * @param {String} sDateTime The DateTime in typical json format (example: "1995-12-17T02:24:00.000")
         */
        jsonDateTime: function (sDateTime) {
            let oJSDate = new Date(sDateTime);
            return oJSDate.toLocaleString();
        }
	};
});

View in OpenUI5

Zur Positionierung der Ein- und Ausgabefelder habe ich mich für ein BlockLayout [3] entschieden. Es hilft dabei, die Konfigurationsseite in mehrere Zellen aufzuteilen, deren Größe und Position sich je nach verfügbarer Auflösung am Endgerät anpassen. Die ersten beiden Textfelder sind reine Ausgabefelder und beziehen sich auf die Eigenschaften MachineName und StartTimeStamp im Datenmodell. Letzterer Zeitstempel wird vom Server im ISO-Format (z. B. 2019-06-22T09:41:02.323T) bereitgestellt und an der Oberfläche mittels obigen Formatters in ein für den Benutzer gut lesbares Format umgewandelt. Nachfolgendes Code-Snippet zeigt genau diesen Teil in der XML-View. Das Databinding wird dabei in geschweifte Klammern gesetzt. Wird der Wert einer Eigenschaft direkt übernommen, so reicht das Eintragen des Pfads zu eben dieser. Falls ein Formatter benötigt wird, so kann das über Eigenschaften eines JSon-Objekts konfiguriert werden, welches direkt inline innerhalb des XML definiert wird.

<l:BlockLayoutCell title="{i18n>configInfo}">
  <!-- Simple display only text -->
  <Label text="{i18n>configMachine}" labelFor="input-machine"/>
  <Input id="input-machine"
         value="{/machineName}"
         editable="false"/>

  <!-- Display DateTime value from .Net, formatted in local language
       (see CustomFormatters.js) -->
  <Label text="{i18n>configStartTimestamp}" labelFor="input-starttimestamp"/>
  <Input id="input-starttimestamp"
         value="{
             path: '/startTimeStamp',
             formatter: '.formatters.jsonDateTime' }"
         editable="false"/>
</l:BlockLayoutCell>

Die Eigenschaft AutoRestartInterval im Datenmodell ist eine Enumeration und wird von Json.Net in einfache Integer-Werte gewandelt. 0 steht dabei für den Wert Daily und 1 für den Wert Weekly. In der XML-View lässt sich genau das mit dem Control Select ausdrücken, wie nachfolgender Code zeigt. Der an der Oberfläche darzustellende Text wird in die Eigenschaft text der ListeItems geschrieben, der tatsächliche Wert in der Datenquelle wird mit der Eigenschaft key angegeben. Die Datenbindung selbst wird auf die Eigenschaft selectedKey des Select-Controls gesetzt. Interessant ist noch die Bindung der Eigenschaft enabled auf AutoRestart, welche weiter oben auch auf das Switch-Control gebunden wird. Hiermit kann der Benutzer dieses Flag beliebig aktivieren und deaktivieren, was sich direkt auf die darunter liegende Box auswirkt.

<l:BlockLayoutCell title="{i18n>configAutoRestart}">

  <!-- Edit for boolean value-->
  <Switch id="input-autorestart" state="{/autoRestart}" />

  <!-- Edit for enum value 
       The property key contains the integer representation of the enum value -->
  <Label text="{i18n>configAutoRestartInterval}" labelFor="input-autorestartinterval" width="100%" />
  <Select id="input-autorestartinterval" width="100%"
          enabled="{/autoRestart}"
          selectedKey="{/autoRestartInterval}">
    <core:ListItem text="Daily" key="0" />
    <core:ListItem text="Weekly" key="1" />
  </Select>
</l:BlockLayoutCell>

Die letzte Eigenschaft im Datenmodell ist das RequestTimeout und hat in .Net den Datentyp TimeSpan. Wie weiter oben beschrieben wird es vom Server durch einen eigenen JsonConverter im Format hh:mm:ss für das Frontend bereitgestellt. Auf OpenUI5 Seite gibt es genau dafür das Control TimePicker. Nachfolgend entsprechend die Stelle in der XML-View.

<l:BlockLayoutCell title="{i18n>configTimeouts}">
  <Label text="{i18n>configRequestTimeout}" labelFor="input-requesttimeout"/>
  <TimePicker id="input-requesttimeout" width="100%"
              value="{/requestTimeout}" />
</l:BlockLayoutCell>

Damit die Daten abschließend auch gespeichert werden können, gilt es noch, entsprechende Schaltflächen auf der View zu platzieren. Diese werden direkt in die footer-Aggregation des Page-Controls gesetzt und mit den zugehörigen Methoden in obigen Controller gebunden.

<!-- Buttons for saving and reset -->
<footer>
  <Toolbar>
    <ToolbarSpacer/>
    <Button text="{i18n>configSave}" type="Accept"
            press="onApplyClick" />
    <Button text="{i18n>configReset}" type="Reject"
            press="onResetClick" />
  </Toolbar>
</footer>

Nachfolgend noch der vollständige Code der XML-View.

<mvc:View
   xmlns="sap.m"
   xmlns:l="sap.ui.layout"
   xmlns:core="sap.ui.core"
   xmlns:mvc="sap.ui.core.mvc"
   xmlns:tnt="sap.tnt"
   controllerName="rolandk.de.ui5test.controller.Configuration"
   displayBlock="true">

  <Page showHeader="false"
        enableScrolling="true">

    <!-- Main content of the page-->
    <content>
      <l:BlockLayout background="Default">

        <l:BlockLayoutRow>
          <l:BlockLayoutCell title="{i18n>configInfo}">
            <!-- Simple display only text -->
            <Label text="{i18n>configMachine}" labelFor="input-machine"/>
            <Input id="input-machine"
                   value="{/machineName}"
                   editable="false"/>

            <!-- Display DateTime value from .Net, formatted in local language
                 (see CustomFormatters.js) -->
            <Label text="{i18n>configStartTimestamp}" labelFor="input-starttimestamp"/>
            <Input id="input-starttimestamp"
                   value="{
                       path: '/startTimeStamp',
                       formatter: '.formatters.jsonDateTime' }"
                   editable="false"/>
          </l:BlockLayoutCell>
        </l:BlockLayoutRow>

        <l:BlockLayoutRow>
          <l:BlockLayoutCell title="{i18n>configAutoRestart}">

            <!-- Edit for boolean value-->
            <Switch id="input-autorestart" state="{/autoRestart}" />

            <!-- Edit for enum value 
                 The property key contains the integer representation of the enum value -->
            <Label text="{i18n>configAutoRestartInterval}" labelFor="input-autorestartinterval" width="100%" />
            <Select id="input-autorestartinterval" width="100%"
                    enabled="{/autoRestart}"
                    selectedKey="{/autoRestartInterval}">
              <core:ListItem text="Daily" key="0" />
              <core:ListItem text="Weekly" key="1" />
            </Select>
          </l:BlockLayoutCell>

          <l:BlockLayoutCell title="{i18n>configTimeouts}">
            <Label text="{i18n>configRequestTimeout}" labelFor="input-requesttimeout"/>
            <TimePicker id="input-requesttimeout" width="100%"
                        value="{/requestTimeout}" />
          </l:BlockLayoutCell>

          <l:BlockLayoutCell>
          </l:BlockLayoutCell>
        </l:BlockLayoutRow>

      </l:BlockLayout>
    </content>

    <!-- Buttons for saving and reset -->
    <footer>
      <Toolbar>
        <ToolbarSpacer/>
        <Button text="{i18n>configSave}" type="Accept"
                press="onApplyClick" />
        <Button text="{i18n>configReset}" type="Reject"
                press="onResetClick" />
      </Toolbar>
    </footer>

  </Page>
</mvc:View>

Fazit

Insgesamt ist es schon interessant, sich damit zu beschäftigen, wie die Datentypen von .Net und die Controls von OpenUI5 zusammenfinden. Auf größere Stolpersteine bin ich dabei bis dato noch nicht gestoßen. Hier im Artikel wurden bereits Formatter behandelt, um Daten aus dem Model für die UI aufzubereiten. Ein weiterführendes Konzept ist die Verwendung von Datentyp-Klassen bei der Datenbindung, wodurch nicht nur der Weg von der Datenquelle zur Oberfläche, sondern auch retour behandelt werden kann. Auch automatische Validierung der Benutzereingaben kann damit abgebildet werden.

Verweise
[1] = https://github.com/RolandKoenig/ASPNetCoreWithOpenUI5/tree/master/App_02
[2] = https://help.sap.com/doc/saphelp_uiaddon20/2.05/en-US/0f/8626ed7b7542ffaa44601828db20de/content.htm?no_cache=true
[3] = https://openui5.hana.ondemand.com/#/api/sap.ui.layout.BlockLayout


Schlagwörter: ,
Copyright 2019. All rights reserved.

Verfasst 22. Juni 2019 von Roland in category "ASP.Net Core / OpenUI5

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.