Monday, March 8, 2010

XPages: Making validation behave properly

Update 13.01.23 Added Java code neeeded to check what component triggered submit in response to this question on Stack Overflow.

Java version:
@SuppressWarnings( "unchecked" )
public static String getParameter( String name ) {
	Map parameters = resolveVariable( "param", Map.class );
	return parameters.get( name );
}

@SuppressWarnings( "unchecked" )
public static UIComponent getChildComponent( UIComponent component, String id ) {
	if( id.equals( component.getId() ) ) {
		return component;
	}

	Iterator children = component.getFacetsAndChildren();
	while( children.hasNext() ) {
		UIComponent child = children.next();
		UIComponent found = getChildComponent( child, id );
		if( found != null ) {
			return found;
		}
	}

	return null;
}

public static  T resolveVariable( String name, Class typeClass ) {
	Object variable = ExtLibUtil.resolveVariable( FacesContext.getCurrentInstance(), name );
	return variable == null ? null : typeClass.cast( variable );
}

public static UIComponent getComponent( String id ) {
	return getChildComponent( (UIViewRootEx) FacesContext.getCurrentInstance().getViewRoot(), id );
}

public static boolean wasSubmittedByComponentId( String componentId ) {
	if( componentId == null ) {
		return false;
	}

	String eventHandlerClientId = getParameter( "$$xspsubmitid" );
	if( eventHandlerClientId == null ) {
		return false;
	}

	// Extract the component id for the event handler
	String eventHandlerComponentId = eventHandlerClientId.replaceFirst( "^.*\\:(.*)$", "$1" );
	UIComponent eventHandler = getComponent( eventHandlerComponentId );
	if( eventHandler == null ) {
		return false;
	}

	// Fetch the component the event handler belongs to
	UIComponent submissionComponent = eventHandler.getParent();
	if( submissionComponent == null ) {
		return false;
	}

	return ( componentId.equals( submissionComponent.getId() ) );
}
Update 06.10.10 Slimmed the code

Validation in XPages is sometimes extremely hard to work with. Especially when you have partial updates on the page.

While looking for a way to make it easier to work with partial updates on a page with validation, I stumbled onto this blogpost (JSF). It describes calculating the required property to determine when the validation should execute.

While we can't apply the technique discussed in the above blogpost, we have another tool at our hands, $$xspsubmitid. This field contains the id of the event handler that triggered the update.

I wrote a function that lets you test if a specific component triggered an update.
// Used to check which if a component triggered an update
function submittedBy( componentId ){
 try {
  var eventHandlerClientId = param.get( '$$xspsubmitid' );
  var eventHandlerId = @RightBack( eventHandlerClientId, ':' );
  var eventHandler = getComponent( eventHandlerId );  
  if( !eventHandler ){ return false; }
  
  var parentComponent = eventHandler.getParent();
  if( !parentComponent ){ return false; }
  
  return ( parentComponent.getId() === componentId );  
 } catch( e ){ /*Debug.logException( e );*/ }
}
If you only want the validation to run when the user clicks a specific button, write this in all the required-attributes:
return submittedBy( 'id-of-save-button' )

id-of-save-button is the id of the component that triggers a save.

The above code results in the validation only executing when the document is saved. No more broken partial updates.

The downside to this technique is that you have to compute all required-attributes, but I personally think that's a small price to pay to have the XPage behave as expected.

Update:
Julian Buss posted a very valid question. What about the other validators? I did a little test, and here's what I came up with.

For the other kinds of validators, you should be able to use an if-statement to conditionally execute the validator. I took a look at the generated java code for the validators, and from what I can tell, you can put JavaScript statements inside all of them.

E.g. for constraint validators:
if( submittedBy( 'id-of-save-button' ) ){ return /\d+/; }

For big expression statements, it's probably better to do something like this at the top of the script:
if( !submittedBy( 'id-of-save-button' ) ){ return true; }

Share and enjoy!

26 comments:

Julian Buss said...

Tommy, that's a very creative workaround for this kind of problem.

But what about other validators like Constraint or Expression-Validators?

They don't have an "enabled" flag. Unless you have a solution for that, too, I can only say that Lotus is very aware of that problem, and they are thinking how to solve this.

Tommy Valand said...

I haven't worked much with XPages "forms", so this may be a stupid question.. Do the contraint/expression validation run when the field isn't required?

What (I think) I'd like IBM to do is to have validation off by default. If you want an update to trigger a validation, set that property. More or less the opposite of what's standard in JSF/XPages today.

Tommy Valand said...

Solution added.. Thanks for pointing out the other validators. :)

Anonymous said...

Hi Tommy,

I used your function submittedBy('id_save_button') and it worked well in dijit.Dialog box. The server side validalition fired properly. However, dijit.byId('myDialogId').isValid() is always return false and no error message display in the Display Error control. Ideally, I want the dialog not to close and show error messages in control if validation failed. Is your solution not workable on dijit Dialog box? Please help and give me some hints to make it work on dijit dialog box. Big thanks in advance.

Tommy Valand said...

submittedBy is a server side tool that you can use to specify when to validate. dijit.dialog is an object that lives in the browser. Therefore dijit.dialog doesn't affect the submittedBy function.

I highly doubt the isValid method of dijit.dialog works correctly with XPages. It's a client side method, that's designed to be used in conjunction with things like dijit.Form.

Rasmus said...

Hi!

Having had success consulting you with XPages-related challenges earlier, and reading this post, I'm kindly asking you to help me with an event-related problem in XPages.

I'm trying to save a Notes Document from the web (in an xpage), but I'm having trouble getting it to work in Internet Explorer 7. Could you please have a look?

Further details are provided at http://devxpages.blogspot.com/2010/05/trouble-saving-notes-document-from-web.html

Karthikeyan Alagirisamy said...

Hi Tommy,

Thanks a lot for sharing this information :). I was about to panic..

If possible please let me know a work around for the following issue please.

After clicking a submit button, pressing F5 on key board and clicking ok on the forth comming popup, creates a duplicate document..

I am searching for a fix to this... cos i end up letting users creat dulplicate records

Tommy Valand said...

I've posted a simple workaround for this problem. Let me know how it goes. :)

Unknown said...

Hi Tommy. Would you mind being a little more specific as to how you implement this code? I have put this function in my javascript library. I have a required field (phone) that I only want to validate when the approveBtn or the denyBtn are clicked. How/where do I call the function? Thanks.

Tommy Valand said...

Compute required to:
return ( submittedBy( 'approveBtn' ) || submittedBy( 'denyBtn' ) );

The above statement becomes true when a refresh is initiated by the two buttons you mentioned. true -> the field is required.

Anonymous said...

Hello Tommy,
Very nice trick. small question here : what if you use several controls with the same server side id in your page? (case of a custom control inserted twice for example). it doesnt seem to work in that case.

Or am i wrong?

Kudos for your work.

Luc.

Tommy Valand said...

If you have a save button repeated, I would think you could use the server id to check if a save button was clicked, but using the code in this blogpost, you can't determine which row in the repeat control triggered the event..?

Anonymous said...

Hi, Is it possible to use this function in conjunction with a check on the value of another field? For eg. Make a field required if another field, may be check box, is checked?

Tommy Valand said...

Sure.. The required propery is computable.

Return true when the field should be required, false when it shouldn't.

Anonymous said...

Thank You Tommy.
Thomas.

Steve Zavocki said...

Hi Tommy, thanks for you work!

I have it working sort of. How can I customize the error message. The "required field error message" text doesn't show up, just the JSF "Validation Error - value is required.".

Also do you know why I have to push the button twice to get it to fire. The event saves a data source on a tab table using partial refresh of a panel.

Tommy Valand said...

You can specify the message in Properties -> Validation -> Required field error message

Regarding having to click twice. This is most common when a partial refresh has just run. E.g. when a field has an onchange event that refreshes part of the page.

I haven't tried this, but it might fix the click twice issue. For the button, in client side code onclick: XSP.allowSubmit()

There is a blocking mechanism built into XPages to prevent firing several partial updates at once. This is probably to prevent issues on the server. XSP.allowSubmit() should disable the "lock".

Steve Zavocki said...

Tommy,

The XSP.allowSubmit() didn't seem to do anything. The only thing that makes the refresh behave normally is to turn on partial execution mode. If I do this, the document is created, but user entered fields aren't saved to the document. I tried using getComponent.getValue() every which way and could not get it to work.

If I shut partial execution mode off then it all works, except having to push the button twice. If you have any tips on getting a handle to the front end (user entered) data I would love to hear them.

Tommy Valand said...

Partial execution mode means that only the area that's partially refreshed is processed server side. Any field outside this region will not be processed.

Since your problem might be of public interest (others might run into the same issue), I suggest you post the question on http://stackoverflow.com/ and tag it XPages. If you try to have the question as detailed as possible, I will try to answer it. If I'm not able to give you help, I bet someone else is. A lot of great XPages developers reads StackOverflow.

Steve Zavocki said...

Tommy,

I will do that. Currently I have broken the whole thing and can't figure out what I did to at least get back to partially working.

XPages needs some kind of versioning, or roll back. I imagine that 3rd party tools already exist to do this.

I really appreciate your help.
Steve

Anonymous said...

Thanks for your work :)

Anonymous said...

hi,
i use your function.it is working but it is workin just a first click a button. then function doesnt call again.i want to call function clicking button every time. have can i do that?

Anonymous said...

Hello Tommy,

thank you for the post. You can actually implement the check in the field's "disableValidators" property (can be found in the "All Properties" tab under the section "data").

This will turn off / on all validators of the field and you don't have to implement the check in each individual validator.

Tommy Valand said...

At the time when this blogpost was written, I don't believe this attribute was available.

In my current code, I tend to have disableValidators on most event handlers except for those on "submit buttons".

Unknown said...

Hello Tommy...

Just fantastic, work really well and so simple to use with the disableValidators property !

Thank you very much, Always useful after 5 years (not so far of 6 years !).

Great trick !

cntrydad said...

It took me quite a while to figure out that this only works when you set the property "disableClientSideValidation" to "true" on each field. Once I got past that and figured out to put the submittedBy function in a server-side JavaScript library it worked great.

Thanks,