SAP / openui5

OpenUI5 lets you build enterprise-ready web applications, responsive to all devices, running on almost any browser of your choice.
http://openui5.org
Apache License 2.0
2.95k stars 1.24k forks source link

Way to prevent model path eval in XML binding parts #1019

Closed tstorch closed 8 years ago

tstorch commented 8 years ago

OpenUI5 version: all

Browser/version (+device/version): all

I wanted to reproduce the getText method of bundels in xml. The following code is taken from here and works quiet well, but I'm very unhappy about the JSONModel bending. In short: An model is introduced which returns on getProperty(someText) just someText without any evaluation.

// formatter
getText: function() {
    var args = [].slice.call(arguments);
    if (args.length >= 2) {
        var sBundle = args.shift();
        var oBundle = this.getOwnerComponent().getModel("i18n").getResourceBundle();
        var key = args.shift();
        return oBundle.getText(key, args);
    }
    return "";
}
// Component.js
var oVerbatimModel = new sap.ui.model.json.JSONModel({});
oVerbatimModel.getProperty = function(sPath) {
    return sPath;
};
oVerbatimModel.setDefaultBindingMode("OneWay");
this.setModel(oVerbatimModel, "verbatim");
<!--  some View-->
<Text text="{
    parts: [
        'verbatim>masterTitleCount',
        'masterView>/itemCount'
    ],
    formatter: '.formatter.getText'
}" />

This code (currently) seems to must be that complicated (read: it's a hack) because there is no way to prevent model path evaluation (getProperty) in the parts part of the xml binding.

What could be done? There are two feasable options I see:

  1. provide a dedicated model in UI5, which just returns the path string on getProperty, i.e. no more JSONModel bending
  2. resolve the root of the problem: somehow allow non evaluated string in parts in xml bindings.

I personally would prefer 2. This would also allow for more general use cases of formatters. My assumption though is, that this would me more complex to implement than option 1.

Michadelic commented 8 years ago

If I understood you would like to use static strings in the data binding syntax. This is currently a well-known limitation in SAPUI5, all parts of the data binding syntax have to be loaded from a model. We have internally discussed it already and we think that the separation between UI is more clear if no data is defined on the view directly so we encourage you to define the data in the controller.

But nstead of your workaround there is a lot easier solution to this, you can simply define a view model in the controller as a JSONModel and load the properties from there. You can see this in action for example in the worklist tempalte or the corresponding tutorial: https://sapui5.hana.ondemand.com/explored.html#/sample/sap.m.tutorial.worklist.03/code/webapp%252Fcontroller%252FWorklist.controller.js

Check the onInit method, there the view model is set up, also as in your use case with i18n model and used in the Worklist.view.xml file.

Also, in your case, you can use an i18n text with a placeholder and use the "jQuery.sap.formatMessage" formatter with no controller code at all. Check the worklist tutorial, it is all in the code of that one.

Hope that solves your issue.

Michael

tstorch commented 8 years ago

Sorry for being imprecise. This was not my problem. What wanted to do is something like this only with i18n

<!-- view -->
<!-- ... -->
<Text text="Some text with {viewModel>specificData}"/>
<Text text="Some other text with {viewModel>specificData}"/>
<!-- ... -->

As you see in my example I already use a view model in the view, which is here called masterView. In it I store some data (above /itemCount). Now I would like to use /itemCount in two places of an xml view as part of a binding. This bindings should load each a different i18n string an stick the same model data (/itemCount) in it (like with getText combined with getProperty). To illustrate it here an example of how it would be solved currently:

Warning: Code only for demo purposes and _not_ tested
// controller
// ...
onInit: function() {
  var oBundle = this.getResourceBundle(),
    specificData = getCurrentSpecificData(),
    viewModel = new JSONModel({
    textA: oBundle.getText("MessageA", specificData),
    textB: oBundle.getText("MessageB", specificData) // same data as in textA
  });

  this.setModel(viewModel, "viewModel");
}
// ...
updateText: function() {
  var oViewModel = this.getModel("viewModel"),
    oBundle = this.getResourceBundle(),
    specificData = getCurrentSpecificData();
  oViewModel.setProperty("textA", oBundle.getText("MessageA", specificData));
  oViewModel.setProperty("textB", oBundle.getText("MessageB", specificData));
}
// ...
<!-- view -->
<!-- ... -->
<Text text="{viewModel>/textA}"/>
<Text text="{viewModel>/textB}"/>
<!-- ... -->

As you see, if specificData changes, I alwaya have to update two properties. This seems very unnatural and tedious. I figured something like this would be more fitting:

// controller
// ...
onInit: function() {
  var specificData = getCurrentSpecificData(),
    viewModel = new JSONModel({
    specificData: specificData
  });

  this.setModel("viewModel");
}
// ...
updateText: function() {
  var oViewModel = this.getModel("viewModel"),
    specificData = getCurrentSpecificData();

  oViewModel.setProperty("specificData", specificData);
}
// ...
<!-- view -->
<!-- ... -->
<!-- this is one possibility of syntax and arguably not the best/most intuitive -->
<Text text="{ parts: [ 'textA', 'viewModel>specificData' ], formatter: '.formatter.i18n' }"/>
<Text text="{ parts: [ 'textB', 'viewModel>specificData' ], formatter: '.formatter.i18n' }"/>
<!-- ... -->
codeworrior commented 8 years ago

I think, @Michadelic mentioned a solution for your problem already:

Also, in your case, you can use an i18n text with a placeholder and use the "jQuery.sap.formatMessage" formatter with no controller code at all. Check the worklist tutorial, it is all in the code of that one.

The trick is to use a ResourceModel (a wrapper around ResourceBundles that makes i18n texts available in data binding). Essentially, it used a given binding path as key into the resource bundle and returns the associated text (in the current session locale).

To get the formatting capabilities of getText you can use jQuery.sap.formatMessageas a formatter. getText internally uses the same method.

Combining all this leads to the following declarative binding:

<mvc:View
    resourceBundleName="sap.ui.core.messagebundle"
    resourceBundleAlias="i18n"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns:l="sap.ui.layout"
    xmlns="sap.m">
        <l:VerticalLayout>
            <Text text="raw: '{/fileSize}'" />
            <Text text="{parts:['i18n>FileSize.Kilobyte', '/fileSize'], 
                        formatter: 'jQuery.sap.formatMessage'}" />
            <Text text="{parts:['i18n>FileSize.Byte', '/fileSize'], 
                        formatter: 'jQuery.sap.formatMessage'}" />
        </l:VerticalLayout>
</mvc:View>

For illustration purposes, I've used a resource bundle from UI5 and text keys from that bundle for which the texts contain placeholders.

A working example can be found here: http://jsbin.com/xamicofogi/edit?html,output

One pitfall I should mention: in contrast to all other models, a binding path for a ResourceModel must not start with a slash.

tstorch commented 8 years ago

OK. Now I got it. Sorry for my confusion! I did not find the formatter: 'jQuery.sap.formatMessage'}" part in the tutorial. Thanks a lot, @Michadelic @codeworrior !