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

Background

Before delving into insides of this post answer yourself several questions.

  1. Do you feel yourself comfortable with value nodes?
  2. Do not bypass question [1]. Really, just try to count clicks necessary to bind adaptive RFC model against number of annoying operations to create value nodes of same structure in several disconnected controllers.
  3. Are you using calculated attributes? Have you ever feeling that starting from third or fourth calculated attribute you are broking OO-principles and controller code get bloated?
  4. How many times are you trying to do extra-ordinary context mapping between value nodes? And do you feel that you are trying to fit Procrustean bed of context-mapping restrictions rather then implementing core functionality?
  5. Do you ever have difficulties to express complex validation constraints? Even such obvious (sigh) like regular expression matching? Or really complex as ISBN verification?
  6. Have you ever found context-related WebDynpro APIs rather limiting? No support for attribute change events, arcane and not convenient invalidation rules, etc...

You have answered at least some of questions with "Yes, it happens to me" then this post is definitely for you. If not: Who knows, probably you "will be back" later. Anyway, I'm strongly discouraging you from applying techniques mentioned below if no such problems exists in your current development. Do not introduce complexity just because you think some technique is cool. But "if": then:, well, full speed ahead ๐Ÿ˜‰

Disclaimer / Warranty / Important notes

First, some of techniques mentioned (validation / property constraints) rely on certain behavior of WebDynpro. I really have no clue whether or not it is standard. Probably, moderators will be quite peaceful to not delete the post right after reading this line or blame me for non-standard solutions. I guess functionality existing up to moment is very important, and if some compatibility issues arise it is possible to discuss them in comments.

Second, I've developed my code using NW04s or Paris (yes, I'm definitely a lucky dog ;). I've done all my best to not break compatibility with earlier versions. But if you have any problems back-porting it to SP11+ we may discuss issues in comments as well.

Third ("last but not least"), I will not try to describe how to program JavaBeans; related Java site has enough information as well as JavaDocs bundled with NetWeaver.

Use case

Imaging, that we have to develop component to edit team of employees. Say, we need to create editor for some project's staff. Initially editor is pre-initialized by certain team of users from external party like some sophisticated staff allocation engine (we simply emulate this). User may add / remove employees from the team as well as edit properties of existing ones (we left UME magic outside this sample). While editor provides a great flexibility to final user, it is enforces one important constraint: variation of total team salary should not exceed certain percent of initial team's salary. User should see immediate feedback on this constraint violation. Also when exiting editor we need to inform external component about delta-changes to team - editor has to provide exact information on:

  1. updated employees from original list
  2. deleted employees from original list
  3. newly added employees

Without any additional delay, let us start with implementation.

Model JavaBeans

First, we will create (almost) regular JavaBeans for our use case. Here I will discuss final result, however in reality it is an outcome of several trials-n-refactorings.

Okay, we have to define our Employee JavaBean. It is just regular Java class with corresponding property accessor/mutator methods. By the way, if you are developing your JavaBeans from scratch I recommend the following path:

  1. Create class with private fields for properties.
  2. Use Eclipse's Source / Generate Getter / Setter option to generate accessor / mutator methods.
  3. Adjust return types of methods if they differ from field type. In our case, birthDate property is java.sql.Date but represented by long field. Besides conversion accessors serve as a guard for underlying data. For example, java.sql.Date objects are mutable and there is a risk that someone will modify property without your knowledge if you expose underlying field direclty. For Employee object this lead to inconsistencies with age property as well as by-skipping property change notifications in general. Note, however, that salary property exposes it underlying field directly. This is done because corresponding java.math.BigDecimal type is immutable.
  4. Extract interface from resulting class if necessary. In our case I've extracted IEmployee just for sake of sample completeness, but in real life programming against interface rather then against implementation is a "GoodThing(TM)"

Creating JavaBeans is as natural as drinking cup of java at morning (along with spending hour or more reading email/rss or blogging instead of coding/bug-fixing ๐Ÿ˜‰ Here I just emphasize several important things with sample model beans:

  1. Employee class has a super-class ObservableBean this handles adding/removing listeners for property change notifications. Sure, I may include this logic right into Employee class, but I hope (sincerely ๐Ÿ˜‰ that you will reuse this sample for something real.
  2. One type here (Gender) requires an enumeration dictionary tye. This can be done easily:
    • Create enumeration type in Local Dictionary (it has to be string-based!!!)
    • Enable "Generate a class representation of the enumeration"
    • As far as this class is not creating symbolic constants for enumeration values, declare these constants yourself somewhere. I've chosen IEmployee interface, you may see MALE and FEMALE constants of type Gender here.
    • Enemies are all around. Do not trust anyone. Validate your input in property mutator methods. You see that bean itself has not strictly valid state after construction. But it knows how to handle this not-so-valid state. And it enforces strict contract for any new value assigned - if it not satisfies restrictions corresponding exception is thrown.
  3. According to JavaBeans guidelines we should notify property change listeners only after we assign valid value. Notification event contains reference to our bean so (theoretically) listener may access any of bean properties (including changing one). Therefore it is important that at notification phase state of our bean will be valid.

Here is one of mutator methods exposed (changing US SSN, social security number):

public void setSsn(final String value) { if ( isEmpty(value) ) throw new InvalidPropertyValueException("SSN may not be empty"); if ( !SSN.matcher(value).matches() ) throw new InvalidPropertyValueException("Invalid SSN format, should be like 123-45-6789"); final String prev = _ssn; _ssn = value; _pcs.firePropertyChange("ssn", prev, value); }

First, we check whether or not this number is not empty and satisfies SSN pattern (XXX-XX-XXXX, where X is any digit). If any of constraints fails we throw an exception. Next we notify any listener registered about property change. Pay attention that before firing notification we set property state to the supplied value (side note: java.beans.VetoableChangeListeners should be notified before actual object state is modified). Hint: avoid passing mutable object to listeners if the same object is assigned as field value - for the very same reasons as with accessor return value.

Now it is time to discuss our exception class. You may note from supplied sample project that it extends com.sap.tc.webdynpro.services.exceptions.WDNonFatalException. And this is extremely important point. Regular JavaBeans may declare checked property exceptions on their methods. JavaBeans designed for WebDynpro models may not. This is due to fact that WebDynpro generator fails to generate typed IWDNodeElement sub-interface for model class with "exceptional" mutators. You may ask why I choose exactly this class rather then generic java.lang.RuntimeException? For good reason, of course ๐Ÿ˜‰

Our hand-written code is not the only part that invokes mutator method on model JavaBean. The other part is WebDynpro framework itself. During transporting user input from client to server WebDynpro assign changed (and only changed, btw) values to IWDNodeElement's attributes. As far as the node in question is model node, attribute setter merely delegates call to corresponding model class mutator method. Next, if this method throws generic unchecked exception (checked is declined by generator, if you remember), WebDynpro data container is not able to deal with it. Unless... Unless its type or its super-type is com.sap.tc.webdynpro.services.exceptions.WDNonFatalRuntimeException. Here WebDynpro shines in all colors and respect us for gracefully handling invalid user input ๐Ÿ˜‰ Moreover, when transporting user input WebDynpro framework keeps track of attribute in question. And if our model class declines change with this exception, WebDynpro assigning automatically error to corresponding IWDNodeElement attribute as with IWDMessageManager.reportInvalidContextAttribute(...) method!!! Get your money for nothin', get your chicks for free... To sweet to be true, but that's it.

The rest of Employee property mutators resemble the paradigms of setSsn() method. So we forward our attention to TotalSalaryDelta JavaBean.

package com.sap.sdn.samples.pojo.beans.employee; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.math.BigDecimal; import java.util.Collection; import java.util.Iterator; import java.util.Set; import java.util.HashSet; public class TotalSalaryDelta { final private BigDecimal _validVariation; final private BigDecimal _initial; final private Set _registered = new HashSet(); private BigDecimal _delta = new BigDecimal(0); final private PropertyChangeListener _changeListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { _delta = _delta .add( (BigDecimal)evt.getNewValue() ) .subtract( (BigDecimal)evt.getOldValue() ); } }; public TotalSalaryDelta(final Collection employees, double validVariation) { _validVariation = new BigDecimal(validVariation).movePointRight(2); BigDecimal total = new BigDecimal(0); for (final Iterator i = employees.iterator(); i.hasNext(); ) { final IEmployee employee = (IEmployee)i.next(); total = total.add( employee.getSalary() ); employee.addPropertyChangeListener( "salary", _changeListener ); _registered.add( employee ); } _initial = total; } public BigDecimal getInitial() { return _initial; } public BigDecimal getDelta() { return _delta; } public boolean getChangeValid() { return _delta .abs() .movePointRight(2) .divide( _initial, BigDecimal.ROUND_UP ) .compareTo( _validVariation ) < 0; } public void register(final IEmployee employee) { if ( _registered.contains(employee) ) return; _delta = _delta.add( employee.getSalary() ); employee.addPropertyChangeListener( "salary", _changeListener ); } public void unregister(final IEmployee employee) { if ( !_registered.remove(employee) ) return; _delta = _delta.subtract( employee.getSalary() ); employee.removePropertyChangeListener( _changeListener ); } }

From the first look it's quite a strange beast. However, let us analyze it from use case prospective. First it constructor accepts a collection of IEmployee objects and get cumulative value of salary to internal immutable field. This is an initial total salary our externally supplied team has. Next we register a property change listener on IEmployee to track changes of salary property. As far as user is allowed to add/remove employees from team, bean also provides methods to register/deregister employees to track.

Someone may notice that I prefer to declare java.beans.PropertyChangeListener as internal variable rather then implement it by bean itself. From my point of view this approach lead to code exactness - listening for property changes is an implementation details of bean rather then part of it external interface. Keep this rule in mind when designing your own classes.

Finally, TotalSalaryDelta expose 3 read-only properties to access initial value of total salary, total salary delta and validity of change.

Now we are ready to import our beans into WebDynpro model.

Model import

If you are using SP11+ then here are some important hints from my pre-NW04s experience regarding model import. JavaBean Model Import wizard works properly only for beans packed into used public part (actually, it seems that wizard simply relies too heavily on JAR archive & manifest declaration). So, all JavaBeans should be placed into separate DC, added to public part "Java classes / assembly". Second, JavaBean model importer works properly only for JavaBeans these have declaration in manifest file from JavaBeans archive, see WebDynpro forum post Re: WebDynpro Using JavaBean Model ->Please Help by Anilkumar Vippagunta. So do not forget to create manifest file with necessary entries. Also the complete WebDynpro Using JavaBean Model ->Please Help is very enlighten, and I strongly recommend to read this discussion (as a bonus you will get some nifty utility for DB->JavaBeans-for-model-import generation).

Okay, so far so good. So if you are on SP11 add used DC with JavaBeans, if you are "in Paris" simply rebuild project. Now we are ready to launch JavaBeans model importer.

First step of wizard is trivial (defining lookup / output paths). At the second step we have to select 2 our JavaBeans. Enable option to automatically add super classes and interfaces. Right after clicking "Add" we got 5 entries in result list. 3 of them are actually necessary - IEmployee, Employee and TotalSalaryDelta. All the rest are just implementation helper classes and interfaces and hence may be removed. Go ahead to third step.

Here we face one unpleasant fact - wizard treats gender property as a relation. This is due to fact that actual property type (Gender) is not from "standard" types set (note, however, that both java.sql.Date and java.math.BigDecimal are welcome here). Don't panic, simply uncheck gender from list of relations and click finish. Wizard completes with report of successful model generation.

Tweaking model classes

To start, expand generated model in WebDynpro tree. It contains one interface IEmployee, class Employee where all properties are inherited from interface and class TotalSalaryDelta. Open IEmployee interface and add gender property we had to skip at generation phase. Please note that model properties should start with lowercase letter due to WebDynpro name conversion rules.

Continue with customizing other properties. For this we create sub-types in Local Dictionary for built-in data types:

  • FirstName for employee's first name with field/column label, quick info, restriction on min/max length and external length (see "Representation" tab).
  • Last name with same customizations as for as FirstName.
  • Salary with restrictions on scale, decimals, minimal value and representation tweaks.
  • SSN with fixed length 11, texts and external length.

After creating necessary types (re-)open model class editor for IEmployee and change default types of corresponding property (right-click on property, select "Edit", select type from local dictionary).

Using custom dictionary types pays off when you start to configure WebDynpro components view(s). You may by-skip assigning text to labels, column align, column header texts, tooltips, and length of input fields - all the necessary data will be taken from custom type definition. By the way, length of input field is quite interesting in this regard; here is WebDynpro algorithm to choose this value (from higher priority to lower):

  • Length defined in UI control itself (static or via binding).
  • External length of simple type, if defined.
  • Max. Length or Fixed length of simple type, if defined.
  • Client-specific default (approximately 20 characters in browser).

WebDynpro component

Component itself is a trivial Master (table) -> Details (form) editor for team of employees. All the interesting work happens in component controller, so let us discuss its implementation.

public void wdDoInit() { //@@begin wdDoInit() final Collection initial = new ArrayList(5); initial.add( newEmployee("Bob", "Smith", IEmployee.MALE, dt(1962, 10, 8), 74000) ); ... initial.add( newEmployee("Huw", "Thompson", IEmployee.MALE, dt(1956, 5, 18), 90000) ); _totalSalaryDelta = new TotalSalaryDelta(initial, 0.25); _employeeListDelta = new EmployeeListDelta(initial); wdContext.nodeEmployees().bind( initial ); //@@end }

First, wdDoInit start with initialization of fake data. In real-life scenario this data would came from external source or embedding component. After initializing initial team members, we are setting up necessary observers. First one was discussed above - it's an instance of TotalSalaryDelta. The second one is EmployeeListDelta for tracking Insert / Update / Delete changes of employees list:

package com.sap.sdn.samples.pojo.beans.employee; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Iterator; import java.util.Set; import java.util.HashSet; import java.util.Collections; public class EmployeeListDelta { final private Set _original = new HashSet(); final private Set _inserted = new HashSet(); final private Set _updated = new HashSet(); final private Set _deleted = new HashSet(); final private PropertyChangeListener _employeeUpdate = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { final IEmployee empl = (IEmployee)evt.getSource(); if ( _original.contains(empl) ) { _updated.add(empl); empl.removePropertyChangeListener( _employeeUpdate ); } else throw new IllegalStateException("Update of inserted/deleted employee"); } }; public EmployeeListDelta(final Collection original) { _original.addAll( original ); for (final Iterator i = _original.iterator(); i.hasNext(); ) { final IEmployee empl = (IEmployee)i.next(); empl.addPropertyChangeListener( _employeeUpdate ); } } public void trackInsert(final IEmployee employee) { if ( _original.contains(employee) ) throw new IllegalArgumentException ( "Employee "" + employee + "" exists in original list" ); else { if ( !_inserted.add( employee ) ) throw new IllegalArgumentException ( "Attempt to insert employee "" + employee + "" twice" ); } } public void trackRemove(final IEmployee employee) { if ( _original.contains(employee) ) { if ( !_deleted.add( employee ) ) throw new IllegalArgumentException ( "Attempt to delete employee "" + employee + "" twice" ); employee.removePropertyChangeListener( _employeeUpdate ); } else { if ( !_inserted.remove( employee ) ) throw new IllegalArgumentException ( "Employee "" + employee + "" is not maintained" ); } _updated.remove( employee ); } public Set inserted() { return Collections.unmodifiableSet(_inserted); } public Set updated() { return Collections.unmodifiableSet(_updated); } public Set deleted() { return Collections.unmodifiableSet(_deleted); } public boolean isModified() { return !( _inserted.isEmpty() && _updated.isEmpty() && _deleted.isEmpty() ); } }

The next pair of methods actually add / remove employees to list as well as notify observers about list changes:

public void addEmployee( ) { //@@begin addEmployee() final Employee employee = new Employee(); _totalSalaryDelta.register( employee ); _employeeListDelta.trackInsert( employee ); final IPublicEmployeeDetails.IEmployeesNode nEmployees = wdContext.nodeEmployees(); final IPublicEmployeeDetails.IEmployeesElement elEmployee = nEmployees.createEmployeesElement( employee ); final int idx = nEmployees.getLeadSelection(); nEmployees.addElement( idx, elEmployee ); nEmployees.setLeadSelection( idx ); //@@end } public void removeEmployees( boolean isAll ) { //@@begin removeEmployees() final IPublicEmployeeDetails.IEmployeesNode nEmployees = wdContext.nodeEmployees(); final int lead = nEmployees.getLeadSelection(); for (int i = nEmployees.size() - 1; i >= 0; i--) { if ( isAll || i == lead || nEmployees.isMultiSelected(i) ) { final IPublicEmployeeDetails.IEmployeesElement elEmployee = nEmployees.getEmployeesElementAt(i); _totalSalaryDelta.unregister( elEmployee.modelObject() ); _employeeListDelta.trackRemove( elEmployee.modelObject() ); nEmployees.removeElement( elEmployee ); } } //@@end }

The latest interesting method here is save(). First, it consults EmployeeListDelta whether any change what applied at all. If no, it reports warning and returns, otherwise it provides statistica by every specific type of change (insertion, modification, deletion):

public void save( ) { //@@begin save() if ( !_employeeListDelta.isModified() ) { wdComponentAPI.getMessageManager().reportWarning("Employee list was not modified"); return; } showEmployeeDelta( "Inserted employees:", _employeeListDelta.inserted()); showEmployeeDelta( "Updated employees:", _employeeListDelta.updated()); showEmployeeDelta( "Deleted employees:", _employeeListDelta.deleted()); //@@end } private void showEmployeeDelta(final String type, final Collection c) { final IWDMessageManager reporter = wdComponentAPI.getMessageManager(); reporter.reportSuccess(type); for (final Iterator i = c.iterator(); i.hasNext(); ) reporter.reportSuccess("" + i.next() ); }

Conclusion

Models provide great benefits when you are working with WebDynpro context. You may share same model class object between 2 non-mapped nodes and still has data changes synchronized. You may track per-attribute updates of your data as user enters it. You may add validation rules of arbitrary complexity to properties (far beyond basic validation provided by simple types). Also you may achieve productivity boost due to reuse of objects' design - be it either creating context nodes or co-locating read-only accessors for calculated data in one place instead of replicating it in several controllers.

However, there are certain problems with WebDynpro JavaBean models. First, validation constraints require usage of WebDynpro-specific exception classes. Second, to benefit from rich WebDynpro data dictionary types you are sometimes forced to reference classes created by generator (and sometimes such dependency is very undesirable). Hence I put words "almost plain" in title of this post. Sure, it is possible to use WebDynpro JavaBeans model directly over EJB DTO (data transfer objects), but "advance" features described above require introduction of yet another level of indirection in form of WD-specific adapters over original DTO.

Oh, and I have to mention several things with my own sample code:

  • Never program I18N applications as in this sample ๐Ÿ˜‰
  • Code contains one severe bug: it is possible to hit "Add" new employee and right after click "Done" - data will be saved silently with invalid structure. However, component correctly handles cases when user actually types invalid data.
  • When user are living record with invalid data via selecting other row, the error message displayed, but selection itself already changed in table. Currently I'm struggling with this issue (hmmm.., is it a WD bug or WD feature), but have not found any acceptable workaround so far. Sure, I'll post an update to this post when fix this.

Good luck in your experiments,
VS

You may download sample project file here

9 Comments