Additional Blogs by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member
0 Kudos

Where "++" in EVS++ ends...

If you are developing applications with WebDynpro for Java and haven't read Yet Another EVS Valuehelp: Showing Display Texts for Keys article by Bertram Ganz I strongly recommend to do this. First, the article outlines very useful technique. Second, this post, up to certain extent, is a follow-up for this article, so this is a "must have" reading to know a context of discussion.

What I like most about this article is that every word here counts! Sadly, my writing skills (ok, in English, not in Java 😉 is far more modest then Bertram's. Even worse, my reading skills are not perfect as well, so I misread several extremely important points and even instruct Re: How to make value help input field case insensitive developers to use EVS++ technique in inappropriate place.

Actually, besides displaying user input EVS++ technique still preserve ability for end-user to alter value. But user has to type keys and only valid keys! What is necessary, and what we try to implement is a solution, that allows user to provide both case insensitive keys/display text as input.

Naively, I assumed that this task is just a piece of cake, the only necessary coding is to alter "setter" of calculated attribute. C'mon, this technique helps me well even for more ad-hoc (800)FORMAT-YOUR-WAY! So the code should look out like the following:

public void setColorCalc(IPrivateEVSPPView.IContextElement element, java.lang.String value) { //@@begin setColorCalc(IPrivateEVSPPView.IContextElement, java.lang.String) if ( null == value ) /* If null just set value directly*/ { element.setColor( null ); return; } /* * keys must be either all upper-case * or all lower-case * here we assume they are upper */ value = value.toUpperCase(); final ISimpleType type = element .node() .getNodeInfo() .getAttribute(IPrivateEVSPPView.IContextElement.COLOR) .getSimpleType(); try { /* works if user enter key */ type.checkValid(value); element.setColor( value ); } catch (final DdCheckException ex) { /* this exception is thrown by checkValid */ /* let us try to find key by display text */ final ISimpleValueSet values = type.getSVServices().getValues(); for (final Iterator i = values.entrySet().iterator(); i.hasNext(); ) { final Map.Entry e = (Map.Entry)i.next(); if ( value.equalsIgnoreCase( (String)e.getValue() ) ) { element.setColor( (String)e.getKey() ); return; } } /* Not found, re-throw validation error */ throw new WDNonFatalRuntimeException(ex); } //@@end }

This is exactly the code I suggested to use in aforementioned How to make value help input field case insensitive. But other developers complained that error messages still shown, even if value can be found in display texts. So, after a small ping-pong session ("this should work" - "no, this is not working") I reproduce the sample myself, and, therefore, reproduce the error.


image
 Image 1. Error with modified EVS++ solution
 

After this I re-read the article (this time very carefully) and found the following statement (right atop of figure 2):

The setter-method of a calculated context attribute is only called by the Web Dynpro Runtime when its value has changed on the UI and when this value is valid.

So, the reason is that a) calculated attribute is of enumerated type; and b) WD validates value entered against enumeration before calling "setter" method. Together this forces error message display.

However, with NW04s preview I found the following behavior that hardly could be valid: also validation fails, "setter" method is still invoked; and, more surprisingly, even if I use non-validating action, validation is performed! I would like to see comments from WD team regarding this issue.

Minus EVS

So at this point we know that EVS on calculated attribute is a problem. EVS assumes enumeration and WD validates value entered against this enumeration. Obviously, we have to declare calculated attribute to be of built-in type com.sap.dictionary.string rather then our custom type Color. Right after applying this change to calculated context attribute user is able to enter both key/display text and everything starts to work fine. Except one thing... We removed helper button along with EVS altogether! Not exactly what we need 😉

So I started with a bit childish approach, and added second InputField right after existing one, bound it to "real" EVS-enabled attribute Color. I set width of this InputField to 0px and bingo! My Internet Explorer display two input fields as single one and gives illusion of one single EVS-enabled control.

However, apart from the fact that this approach smells in technical sense, it also invalid in practice. Euphoria ended quickly as long as I'd opened this application in Mozilla. Mozilla browsers have no CSS rendering bugs of Internet Explorer, and treat width CSS property differently (and, btw, in right way). So you may see below how ugly result looks. Worth to mention, that this approach will not work with Table control, while table does not allow 2 controls to be used as single cell editor. So we need a better solution.


image
 Image 2a. Variant with 2 Input Fields, Internet Explorer
 


image
 Image 2b. Variant with 2 Input Fields, Mozilla Firefox
 

EVS++, OVS--... Hmmm... Minus EVS, Plus OVS!

Among several types of value help available with WebDynpro (I prefer to count SVS with DropDownByKey as value help too), OVS is the most complex one, but the most flexible. And OVS does not require types with enumerations. So the solution could be outlined as the following:

  1. Besides original attribute, you must create calculated attribute (proxy) with type, equals to base built-in type of original attribute (not the same type!!!). In our example, calculated attribute will be just com.sap.dictionary.string rather then Color.
  2. Expose proxy attribute via InputField UI control.
  3. Add custom OVS extension to proxy attribute and save helper object.
  4. Proxy attribute getter should return display text of original attribute value via helper provided.
  5. Proxy attribute setter should accept either key or display text as input, and set value of original attribute via helper provided.


image
 Image 3. Variant with OVS value help
 

One note before we will dive into source code. I try to automate the task as much as possible but preserve flexibility. So, I have to introduce OVS4EVS.IElementResolver interface here to help resolve "element-with-calculated attribute --> element-with-original-attribute" relation.

You know, that sometimes it is possible to add calculated attribute at the same level as original attribute. But if your original attribute resides in model node with structure binding (like Adaptive RFC) then you may not add it here. The well-known trick is to first add 1..1 child node right below model node and then place calculated attribute here (in this case you may use singleton). I guess these 2 approaches covers 99.99% of calculated attribute applications, so they are handled automatically. For more complex (or should I say "esoteric" 😉 cases you have to provide your own resolver implementation.

To add our OVS4EVS support do the following:

  1. Declare private view controller variable per calculated attribute:
    private OVS4EVS.IHelper _colorOvsHelper;
  2. Add OVS extension in wdDoInti method of view controller and save reference to helper:
    final IWDNodeInfo ctx = wdContext.getNodeInfo(); _colorOvsHelper = OVS4EVS.bind ( ctx.getAttribute("ColorCalcOVS"), /* calculated attribute, proxy*/ ctx.getAttribute("Color") /* real attribute */ );
  3. Add the following to generated "getter" of calculated attribute:
    return _colorOvsHelper.getDisplayText( element );
  4. Add the following to generated "setter" of calculated attribute:
    _colorOvsHelper.applyValue( element, null == value ? null : value.toUpperCase());

Note, that all your keys' characters must be in one case (either all upper or all lower). Above sample for "setter" assumes that all of them are in upper case.

Below is code for OVS4EVS and EVSQuery. I will not explain how this code works, if you'd like to know internals, please read my previous posts OVS, Reloaded and String::operator ISimpleType() const {...}.

package com.sap.sdn.samples.evspp.ovs; import java.util.Locale; import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.ArrayList; import java.util.Collections; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import com.sap.dictionary.runtime.ISimpleType; import com.sap.sdn.samples.cmi.CMIBean; import com.sap.sdn.samples.cmi.CMIInfo; import com.sap.sdn.samples.cmi.DictionaryTypes; import com.sap.tc.cmi.exception.CMIException; import com.sap.tc.cmi.metadata.ICMIModelClassInfo; import com.sap.tc.cmi.metadata.ICMIModelObjectCollectionInfo; import com.sap.tc.cmi.model.ICMIQuery; import com.sap.tc.webdynpro.basesrvc.util.StringUtil; public class EVSQuery extends CMIBean implements ICMIQuery { private Collection _result = null; private String _keyFilter = "*"; private String _valFilter = "*"; final private ISimpleType _type; final private Map _evs; final private Comparator _resultComparator; protected EVSQuery(final ISimpleType type) { this(type, (Map)type.getSVServices().getValues(), null ); } protected EVSQuery(final ISimpleType type, final Map evs) { this(type, evs, null); } protected EVSQuery(final ISimpleType type, final Map evs, final Comparator resultComparator) { _type = type; _evs = evs; _resultComparator = resultComparator; } protected void apply(final Object value) { invalidate(); _valFilter = "*"; _keyFilter = "*"; if ( null == value ) return; _keyFilter = _type.format(value); _valFilter = (String)_evs.get( value ); } public String getKeyFilter() { return _keyFilter; } public void setKeyFilter(final String value) { _keyFilter = StringUtil.isEmpty(value) ? "*" : value; } public String getValueFilter() { return _valFilter; } public void setValueFilter(final String value) { _valFilter = StringUtil.isEmpty(value) ? "*" : value; } public Object getAttributeValue(final String name) { if ( "keyFilter".equals(name) ) return getKeyFilter(); if ( "valueFilter".equals(name) ) return getValueFilter(); return super.getAttributeValue(name); } public void setAttributeValue(final String name, final Object value) { if ( "keyFilter".equals(name) ) setKeyFilter((String)value); else if ( "valueFilter".equals(name) ) setValueFilter((String)value); else super.setAttributeValue(name, value); } public ICMIModelClassInfo associatedInputParameterInfo() { return associatedModelClassInfo(); } public ICMIModelObjectCollectionInfo associatedResultInfo() { return RESULT_INFO; } public long countOf() { return -1; } public void invalidate() { _result = null; } final private static Pattern MATCH_ALL = Pattern.compile(".*"); public void execute() throws CMIException { final Pattern keyFilter = compile(_keyFilter); final Pattern valFilter = compile(_valFilter); final ArrayList result = new ArrayList( _evs.size() ); for (final Iterator i = _evs.entrySet().iterator(); i.hasNext(); ) { final Map.Entry e = (Map.Entry)i.next(); final String key = _type.format( e.getKey() ); if ( !keyFilter.matcher( key ).matches() || !valFilter.matcher( (String)e.getValue() ).matches() ) continue; result.add( new Result( e.getKey(), key, (String)e.getValue() ) ); } if ( null != _resultComparator ) Collections.sort( result, _resultComparator); _result = result; } public Object getInputParameter() { return this; } public Collection getResult() { return _result; } public boolean isDirty() { return null == _result; } public ICMIModelClassInfo associatedModelClassInfo() { return INPUT_INFO; } private static Pattern compile(final String mask) throws CMIException { if ( skipFilter(mask) ) return MATCH_ALL; else try { return Pattern.compile ( mask.replaceAll("(*|?)", ".$1"), Pattern.CASE_INSENSITIVE ); } catch (final PatternSyntaxException ex) { throw new CMIException(ex); } } private static boolean skipFilter(final String mask) { return ( StringUtil.isEmpty(mask) || "*".equals(mask) ); } public static class Result extends CMIBean { final private Object _realKey; final private String _key; final private String _value; Result(final Object realKey, final String key, final String value) { _realKey = realKey; _key = key; _value = value; } public Object realKey() { return _realKey; } public String getKey() { return _key; } public String getValue() { return _value; } public Object getAttributeValue(final String name) { if ( "value".equals(name) ) return getValue(); if ( "key".equals(name) ) return getKey(); return super.getAttributeValue(name); } public void setAttributeValue(final String name, final Object value) { if ( "key".equals(name) || "value".equals(name) ) throw new IllegalArgumentException ("Property " + name + " is read-only"); super.setAttributeValue(name, value); } public ICMIModelClassInfo associatedModelClassInfo() { return RESULT_ELEMENT_INFO; } } final private static CMIInfo.GenericClassInfo INPUT_INFO = new CMIInfo.GenericClassInfo( "EVSQuery", Locale.ENGLISH ) { { property("keyFilter", "com.sap.sdn.samples.evspp.types.EVSKey", false ); property("valueFilter", "com.sap.sdn.samples.evspp.types.EVSValue", false ); } }; final private static ICMIModelClassInfo RESULT_ELEMENT_INFO = new CMIInfo.GenericClassInfo( INPUT_INFO.getName() + "_Result", Locale.ENGLISH ) { { property("key", "com.sap.sdn.samples.evspp.types.EVSKey", true ); property("value", "com.sap.sdn.samples.evspp.types.EVSValue", true ); } }; final private static ICMIModelObjectCollectionInfo RESULT_INFO = new CMIInfo.BasicCollectionInfo ( RESULT_ELEMENT_INFO.getName() + "_Collection", RESULT_ELEMENT_INFO ); }
package com.sap.sdn.samples.evspp.ovs; import java.util.Iterator; import java.util.Map; import com.sap.dictionary.runtime.DdCheckException; import com.sap.dictionary.runtime.ISimpleType; import com.sap.tc.webdynpro.progmodel.api.IWDAttributeInfo; import com.sap.tc.webdynpro.progmodel.api.IWDNodeElement; import com.sap.tc.webdynpro.progmodel.api.IWDOVSNotificationListener; import com.sap.tc.webdynpro.progmodel.api.WDValueServices; import com.sap.tc.webdynpro.services.exceptions.WDNonFatalRuntimeException; import com.sap.typeservices.ISimpleValueSet; public class OVS4EVS { public static interface IElementResolver { public IWDNodeElement resolve(IWDNodeElement elementOfProxyAttr); } public static interface IHelper { public String getDisplayText(IWDNodeElement el); public void applyValue(IWDNodeElement el, final Object value); } final public static IElementResolver SAME_NODE_ELEMENT = new IElementResolver() { public IWDNodeElement resolve(final IWDNodeElement elementOfProxyAttr) { return elementOfProxyAttr; } }; final public static IElementResolver PARENT_NODE_ELEMENT = new IElementResolver() { public IWDNodeElement resolve(final IWDNodeElement elementOfProxyAttr) { return elementOfProxyAttr.node().getParentElement(); } }; public static IHelper bind(final IWDAttributeInfo proxyAttr, final IWDAttributeInfo realAttr) { return bind( proxyAttr, realAttr, null ); } public static IHelper bind(final IWDAttributeInfo proxyAttr, final IWDAttributeInfo realAttr, IElementResolver resolver) { if ( null == proxyAttr ) throw new IllegalArgumentException ("proxyAttr parameter is null"); if ( null == realAttr ) throw new IllegalArgumentException ("realAttr parameter is null"); if ( !realAttr.hasSimpleType() ) throw new IllegalArgumentException ( realAttr + " is not of simple type"); if ( null == resolver ) { if ( proxyAttr.getNode().equals( realAttr.getNode() ) ) resolver = SAME_NODE_ELEMENT; else if ( realAttr.getNode().equals( proxyAttr.getNode().getParent() ) ) resolver = PARENT_NODE_ELEMENT; else throw new IllegalArgumentException ( "Null element resolver supplied, and proxy / real attributes " + "resides are neither in same node, nor proxy node " + "direct child of real node" ); } WDValueServices.addOVSExtension ( EVSQuery.class.getName(), new IWDAttributeInfo[]{ proxyAttr }, new EVSQuery( realAttr.getSimpleType() ), new EvsNotificationListener( realAttr, resolver ) ); return new EvsHelper( realAttr, resolver ); } private static class EvsNotificationListener implements IWDOVSNotificationListener { final private String _realAttr; final private IElementResolver _resolver; EvsNotificationListener(final IWDAttributeInfo realAttr, final IElementResolver resolver) { _realAttr = realAttr.getName(); _resolver = resolver; } public void onQuery(final Object query) {} public void applyResult(final IWDNodeElement target, final Object result) { final EVSQuery.Result myResult = (EVSQuery.Result)result; _resolver.resolve(target) .setAttributeValue( _realAttr, myResult.realKey() ); } public void applyInputValues(final IWDNodeElement target, final Object query) { final EVSQuery myQuery = (EVSQuery)query; myQuery.apply ( _resolver.resolve(target).getAttributeValue( _realAttr ) ); } } private static class EvsHelper implements IHelper { final private ISimpleType _type; final private ISimpleValueSet _svs; final private IElementResolver _resolver; final private String _realAttr; EvsHelper(final IWDAttributeInfo realAttr, final IElementResolver resolver) { _type = realAttr.getSimpleType(); _svs = _type.getSVServices().getValues(); _realAttr = realAttr.getName(); _resolver = resolver; } public String getDisplayText(final IWDNodeElement el) { return _svs.getText ( _resolver.resolve(el) .getAttributeValue( _realAttr ) ); } public void applyValue(final IWDNodeElement el, final Object value) { final IWDNodeElement real = _resolver.resolve(el); if ( null == value ) { real.setAttributeValue( _realAttr, null ); return; } try { _type.checkValid(value); real.setAttributeValue( _realAttr, value ); } catch (final DdCheckException ex) { final String strValue = value.toString(); for (final Iterator i = _svs.entrySet().iterator(); i.hasNext(); ) { final Map.Entry e = (Map.Entry)i.next(); if ( strValue.equalsIgnoreCase( (String)e.getValue() ) ) { real.setAttributeValue( _realAttr, e.getKey() ); return; } } throw new WDNonFatalRuntimeException(ex); } } } }

By the way, I've included table control in demo. This control has no event handlers assigned. However, scrolling inside table or selecting row causes some WebDynpro specific events, and you may observer that client-server roundtrip is executed. In this case no validation performed at all, but calculated attribute "setters" are executed.

You may download complete sample application here (rename to *.zip after download).

2 Comments