Posts Tagged: ‘Java Script’

XPages: Access resources and their content

15. Juli 2012 Posted by Sven Hasselbach

To access XPages resources during runtime and to get their content as a string, you can use the following SSJS code snippet:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:label id="label1">
        <xp:this.value><![CDATA[#{javascript:
            var url = "/WEB-INF/faces-config.xml";
            var data = facesContext.getExternalContext().
                getResourceAsStream( url );
            var txt = "";
            while( data.available() ){
                txt += @Char(data.read());
            }
            txt}]]>
        </xp:this.value>
    </xp:label>

</xp:view>

[This displays the current faces-config.xml]

By using the correct URL path it is for example possible to access the code of SSJS libraries or even the source code of java classes in the database. To access the source code of a XPage, just use the name of the XPage itself. Same procedure for SSJS libraries, just add a “.jss” at the end of the library’s name. For accessing a java class, the “dots” in the package names have to be replaced with “slashes”.

To access f.e. the java file for class ch.hasselba.jsf.debug.ResourceUtil the url has to look like this:

var url = "ch/hasselba/jsf/debug/ResourceUtil.java";

Quick-n-Dirty: HTML5 UIComponents without effort

17. Mai 2012 Posted by Sven Hasselbach

Obviously it was Chris Toohey who first has discovered the way of manipulation UIComponents with the tagName attribute: http://www.dominoguru.com/pages/xpage_xptext_tagName_options.html

 

The xp:text – element can easily manipulated to add HTML5 functionality to a XPages. By overriding the property tagName, the component can be accessed like every other UI component in the component tree.

Normally the designer only allows a small list of choices for the tagName property:

But this can be easily overwritten by manually editing the tagName attribute in the source and allows to change the generated HTML element to whatever you want.

Here is a small example for a HTML5 progessbar which can be accessed like every other UIComponent:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:text escape="false" id="pBar" disableTheme="true">
      <xp:this.value><![CDATA[#{javascript:""}]]></xp:this.value>
      <xp:this.tagName>
         <![CDATA[#{javascript:"progress"}]]>
      </xp:this.tagName>
      <xp:this.attrs>
         <xp:attr name="value">
            <xp:this.value><![CDATA[#{javascript:
               if( !sessionScope.containsKey("pBar") )
                  sessionScope.put("pBar", 0 );

                var value = sessionScope.get("pBar");

                if( value > 100 ) value = 0;

                sessionScope.put("pBar", (value+10) );

                value
               }]]></xp:this.value>
         </xp:attr>
         <xp:attr value="100" name="max"></xp:attr>
      </xp:this.attrs>
   </xp:text>

   <xp:button value="Label" id="button1">
      <xp:eventHandler event="onclick" submit="true"
            refreshMode="partial" refreshId="pBar">
      </xp:eventHandler>
   </xp:button>

</xp:view>

The generated HTML Tag looks like this and can for example be refreshed with a partial refresh etc.

<progress id="view:_id1:pBar" value="40" max="100"></progress>

XSnippets: Cancel a partial refresh via SSJS

3. Mai 2012 Posted by Sven Hasselbach

With the assistance of Philippe Riand I was able to shorten the original idea of canceling a partial refresh to a single SSJS function.  By setting the HTTP header “X-XspRefreshId” to “@none” it is possible to get the same result as in the posting before, but there is no “Dojo hack” required.

function cancelPartialRefresh(){
   var response = facesContext.getExternalContext()
      .getResponse();
   response.setHeader("X-XspRefreshId", "@none");
   response.reset();
   response.commitResponse();
   facesContext.responseComplete();
}

To use this function you just have to call it.  Here is the same example like in the previous posting:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <xp:button value="Label" id="button1">
      <xp:eventHandler event="onclick" submit="true"
         refreshMode="partial" refreshId="label1">
      </xp:eventHandler>
   </xp:button>
   <xp:br/><xp:br/>
   <xp:label id="label1">
      <xp:this.value>
      <![CDATA[#{javascript:
      function cancelPartialRefresh(){
         var response = facesContext.getExternalContext()
            .getResponse();
         response.setHeader("X-XspRefreshId", "@none");
         response.reset();
         response.commitResponse();
         facesContext.responseComplete();
      }

      var ajax = new com.ibm.xsp.ajax.AjaxUtil();
      if( ajax.isAjaxPartialRefresh(facesContext) == true){
         cancelPartialRefresh();
         return; // stop execution of SSJS code
      }
      java.lang.System.currentTimeMillis();}]]>
      </xp:this.value>
   </xp:label>
</xp:view>

I have added the code to the XSnippet Contest. Here is the link to the XSnippet: http://openntf.org/XSnippets.nsf/snippet.xsp?id=cancel-partial-refresh

If you want to read more information about the HTTP header  you can read an earlier posting (Sorry, on german only).

Cancel a partial refresh via SSJS

1. Mai 2012 Posted by Sven Hasselbach

After reading Tim Tripcony’s blog post , I thought about a way how to cancel a partial refresh via server side javascript. To bring this to life, there are just three things to do:

  1. Abort the processing of the request on the server
  2. Give feedback to the client that request is canceled
  3. Stop Dojo to process the XHR request

To stop the processing of a request on the server and to send an empty response to the client, this SSJS code can be used:

var response = facesContext.getExternalContext().getResponse();
response.reset();
response.commitResponse();

The client will receive an empty HTTP response body:

A HTTP Header has to be added to the response to inform Dojo that the request was canceled. In this example it is “X-Partial-Refresh-Cancel“:

response.setHeader("X-Partial-Refresh-Cancel", "1");

The header is now added to the HTTP response and sent back to the client:

The XHR response must be hijacked before it is processed by Dojo. To do this, a new XHR function has to be injected between the existing one. Here is a basic code snippet:

var xhrLoad = null; // placeholder for the original function

if( !dojo._xhr )
   dojo._xhr = dojo.xhr;

dojo.xhr = function(){
   var args = arguments[1];
   xhrLoad = args["load"]; // "save" the original load function
                           // and overwrite with a new one
   args["load"] = function( a, ioArgs ){
      // execute custom code
      // call the original load function:
      xhrLoad( a, ioArgs );
   }

   // do XHR request
   dojo._xhr( arguments[0], arguments[1], arguments[2] );
}

The load function of a Dojo XHR request has a parameter ioArgs. This parameter gives a handle to an object that allows to access the HTTP response headers, so the response can be identified as canceled:

var canceled = ioArgs.xhr &&
   ioArgs.xhr.getResponseHeader("X-Partial-Refresh-Cancel");

if( canceled ){
   XSP.allowSubmit();
   return;
}

If the HTTP header is set, the processing of the request can be stopped. After stopping the request, the XSP object is not allowed to fire the next event. To allow this again, the function XSP.allowSubmit() must be called.

Here is a demonstration XPage with a button and a label. The partial refresh will never be processed, instead a “Canceled” message will occur.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
   <xp:button value="Label" id="button1">
      <xp:eventHandler event="onclick" submit="true"
         refreshMode="partial" refreshId="label1">
      </xp:eventHandler>
   </xp:button>
   <xp:br/><xp:br/>
   <xp:label id="label1">
      <xp:this.value>
      <![CDATA[#{javascript:
      var response = facesContext.getExternalContext().getResponse();
      var ajax = new com.ibm.xsp.ajax.AjaxUtil();
      if( ajax.isAjaxPartialRefresh(facesContext) == true){
         response.setHeader("X-Partial-Refresh-Cancel", "1");
         response.reset();
         response.commitResponse();
         return;
      }
      java.lang.System.currentTimeMillis();}]]>
      </xp:this.value>
   </xp:label>
   <xp:br/>
   <xp:br/>
   <xp:scriptBlock id="scriptBlockXHRHandler">
   <xp:this.value><![CDATA[
      var xhrLoad = null;
      dojo.addOnLoad( function(){
         if( !dojo._xhr )
            dojo._xhr = dojo.xhr;

        dojo.xhr = function(){
           try{
              var args = arguments[1];
              xhrLoad = args["load"];
              args["load"] = function( a, ioArgs ){
                 var canceled = ioArgs.xhr &&
                    ioArgs.xhr.getResponseHeader("X-Partial-Refresh-Cancel");
                 if( canceled ){
                    alert("Canceled!");
                    XSP.allowSubmit();
                    return;
                 }
                 xhrLoad( a, ioArgs );
               }
            }catch(e){}
            dojo._xhr( arguments[0], arguments[1], arguments[2] );
         }
      });]]>
      </xp:this.value>
   </xp:scriptBlock>
</xp:view>

XSnippets: Fire querySave / postSave – Events

22. April 2012 Posted by Sven Hasselbach

The second XSnippet I have added to the XSnippet Contest is a help to fire the querySave- and postSave-events from SSJS: Save Datasource & Fire querySave/postSave events

If you only do a simple document1.save() , the events of a datasource won’t be executed. To fix this, you have to use the save() method of com.ibm.xsp.model.domino.DominoDocumentData instead:

var dsName = "document1.DATASOURCE"; // change this to the name of
                                     // the datasource you want
                                     // to save
var app = facesContext.getApplication();
var ds = app.getVariableResolver()
           .resolveVariable(facesContext, dsName);
ds.save( facesContext, true);

The variable dsName contains the name of the datasource to save, followed by “.DATASOURCE“. To use it f.e. with the current document, you have to change the variable to “currentDocument.DATASOURCE“.

The difference here is the type of object that is used: If you save a document datasource, the save method of an object of type NotesXspDocument is called. The com.ibm.xsp.model.domino.DominoDocumentData object has another method for saving. The first object type is like a backend NotesDocument, the second object is like the NotesUIDocument class.

Quick-n-Dirty: SSJS Code vor BeforPageLoad ausführen

11. März 2012 Posted by Sven Hasselbach

Um SSJS Code vor dem “BeforePageLoad“-Event auszuführen, kann der Code einfach in eine DataContext-Variable gepackt werden. Die Berechnung der DataContext-Variable sollte auf “ComputeOnPageLoad” gesetzt sein, dann wird der Code auch nur einmal ausgeführt. Als Rückgabewert sollte null verwendet werden, dann verschwindet Sie aus dem Speicher und die Variable ist auch nicht via SSJS referenzierbar.

Natürlich bleiben die im SSJS-Code definierten Variablen Ereignisübergreifend erhalten. Hier ein Beispiel, indem die Variable testVar vor dem “BeforePageLoad“-Event befüllt und während des Events ausgewertet wird:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:this.beforePageLoad>
      <![CDATA[#{javascript:
         print("Before PageLoad: " + testVar )}]]>
   </xp:this.beforePageLoad>

   <xp:this.dataContexts>
      <xp:dataContext var="dummy">
         <xp:this.value>
            <![CDATA[${javascript:print("Data Context!");
               testVar = "Data Context!"
               return null}]]>
         </xp:this.value>
      </xp:dataContext>
   </xp:this.dataContexts>
   
</xp:view>

Bug: ${} in Output Script-Blöcken

8. Februar 2012 Posted by Sven Hasselbach

Bei der Verwendung eines Output Scripts muss darauf geachtet werden, dass kein Code verwendet wird, der eine Zeichenfolge beinhaltet, die eine “Compute On Load“-ähnliche Syntax hat: Ein Bug sorgt dafür, das bei der Verwendung von ${} (mit oder ohne Inhalt) einiges durcheinander gerät, und der komplette SSJS-Code falsch verarbeitet wird.

So gibt folgender Code wie zu erwarten eine Messagebox mit der Id des Labels aus…

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:label value="Label" id="label1"></xp:label>
   
   <xp:scriptBlock id="scriptBlock1">
      <xp:this.value>
         <![CDATA[$
            var id = '#{id:label1}';
            alert( id );
         ]]>
      </xp:this.value>
   </xp:scriptBlock>
   
</xp:view>

… wird aber an irgendeiner Stelle im Script Block die genannte Kombination verwendet, gerät alles in Schieflage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:label value="Label" id="label1"></xp:label>
   
   <xp:scriptBlock id="scriptBlock1">
      <xp:this.value>
         <![CDATA[
            var id = '#{id:label1}';
            var bug = '${id:label1}';
             alert( "id: " + id + "\nbug: " + bug);
         ]]>
      </xp:this.value>
   </xp:scriptBlock>
   
</xp:view>

Die Variable id ist leer, und die Variable bug wird nicht verändert:

Es spielt keine Rolle an, welcher Stelle im Script Block die fehlerhafte Variante vorkommt, auch der Inhalt zwischen den eckigen Klammern ist unbedeutent: Ein auskommentierter Code über mehrer Zeilen hat die gleiche Auswirkung!

Varianten wie z.B.

//var bug = '${
// Kein Text!
//}';

oder

var bug = ${X}

werfen keine Fehler, sondern generieren im besten Fall “nur” fehlerhaften CSJS-Code.

Erst wenn keine Anführungszeichen verwendet werden und die EL-Syntax fehlerhaft ist, tritt ein Laufzeitfehler auf.

var bug = ${/EL}

 

Der Bug existiert in 8.5.2 als auch in 8.5.3. Andere Versionen können ebenfalls betroffen sein.

DataContext-Variablen

1. Februar 2012 Posted by Sven Hasselbach

Will man auf die DataContext-Variablen einer XPage zugreifen, gibt es die Möglichkeit, die Methode getDataContexts() zu verwenden, die für jede UIComponent existiert. Die Methode liefert eine java.util.List zurück, die sich z.B. in einen Array verwandeln lässt, um auf die einzelnen Mitglieder zu zugreifen. Die Mitglieder wiederum sind vom Typ com.ibm.xsp.model.DataContext, mit getVars() lässt sich auf den Inhalt der jeweiligen Variable zugreifen.

Hier ein Beispiel:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:this.dataContexts>
      <xp:dataContext var="dcVar" value="1" />
   </xp:this.dataContexts>

   <xp:text escape="true" id="computedField1">
      <xp:this.value>
         <![CDATA[#{javascript:
         var lc:java.util.List = view.getDataContexts();
         var dc:com.ibm.xsp.model.DataContext = lc.toArray()[0];
         dc.getVars().toString()
         }]]>
      </xp:this.value>
   </xp:text>
</xp:view>

[Fett: Die DataContext-Variable // Rot: Die UIComponent, an der die Variable "hängt"]

In diesem Beispiel ist die Variable direkt der UIViewRoot-Komponente zugeordnet worden. Ist die Variable an einer anderen UIComponent (z.B. einem Custom Control), muss auf den DataContext dieser Komponente zugegriffen werden. Ist also eine DataContext-Variable im Custom Control definiert, nicht jedoch in der eigentlichen XPage, so würde ein view.getDataContexts() nichts zurück liefern.

Um auf die Variablen eines Custom Controls zuzugreifen, muss also entweder der komplette Komponentenbaum durchlaufen werde, oder das Custom Control muss mit id definiert sein, um referenziert werden zu können.

Hier das Custom Control ccDCVar:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

   <xp:this.dataContexts>
      <xp:dataContext var="dcVarCC" value="2" />
   </xp:this.dataContexts>

</xp:view>

Die XPage, die auf das Custom Control zugreift, kann dann wie folgt aufgebaut sein:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core"
         xmlns:xc="http://www.ibm.com/xsp/custom">

   <xc:ccDCVar id="cc1"></xc:ccDCVar>

   <xp:text escape="true" id="computedField1">
      <xp:this.value>
         <![CDATA[#{javascript:
         var lc:java.util.List = getComponent("cc1").getDataContexts();
         var dc:com.ibm.xsp.model.DataContext = lc.toArray()[0];
         dc.getVars().toString()
         }]]>
      </xp:this.value>
   </xp:text>
</xp:view>

 [Fett: Zugriff auf die UIComponent mittels getComponent()  // Rot: Id des Custom Controls]

Quick-n-Dirty: Locale setzen

5. Januar 2012 Posted by Sven Hasselbach

Um die Locale-Einstellung einer XPage programmatisch zu beeinflussen, kann die Methode setLocaleString bzw. setLocale des context-Objektes verwendet werden.  Damit die Änderungen übernommen wird, muss die Einstellung im BeforePageLoad gesetzt werden.

So ändert ein…

context.setLocaleString("zh-tw")

… bzw. ein …

context.setLocale(java.util.Locale.TAIWAN)

…die Spracheinstellungen der generierten XPage. Sowohl das lang-Attribute der HTML-Seite als die Dojo-Konfiguration wird dabei gesetzt:

<!DOCTYPE html>
<html lang="zh-tw">
<script type="text/javascript"
src="/xsp/.ibmxspres/dojoroot-1.6.1/dojo/dojo.js"
djConfig="locale: 'zh-tw', parseOnLoad: true"></script>

Durch diese Änderung wird z.B. der Datepicker-Dialog des DojoToolkit auf Taiwanisch gerendert.

Eine Liste der möglichen Einstellungen findet sich in der Beschreibung des java.util.Locale-Objektes.

Domino Datasources On-the-Fly (3): ValueBinding & MethodBinding

29. Dezember 2011 Posted by Sven Hasselbach

Das ist der dritte Teil der Serie “Domino Datasources On-the-Fly”. Der zweite Teil findet sich hier.

Will man eine Wertezuweisung berechnen lassen, muss ein ValueBinding erstellt werden. Damit lässt sich dann z.B. der Formname eines DominoDocumentData-Objektes berechnen (zugegebenermaßen ein kleinwenig sinnlos, da man dies im Code sowieso erledigen könnte…).

Hierfür existiert die Methode createValueBinding() der Application-Klasse, mit der ein neues ValueBinding-Objekt instanziert werden kann:

var app = facesContext.getApplication();
var vb = app.createValueBinding("#{javascript:@Text(@Now())}");

Dieses ValueBinding muss dem jeweiligen Datasource-Objekt über die Methode setValueBinding() zugewiesen werden. Als erster Parameter muss der Name der zu setzenden Eigenschaft angegeben werden, dann folgt das jeweilige ValueBinding-Objekt.

var data = new com.ibm.xsp.model.domino.DominoDocumentData();
data.setVar("document1");
data.setValueBinding("formName", vb);
view.addData(data);

Die Fett hervorgehobene Eigenschaft entspricht dem Parameter im XPages Source:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:this.data>
        <xp:dominoDocument var="document1"
           formName="#{javascript:@Text(@Now())}" />
    </xp:this.data>
</xp:view>

Allerdings ist diese Zuweisung nicht bei allen Eigenschaften möglich! Bei noch auszuführenden Methoden wie z.B. dem QuerySaveDocument-Event muss stattdessen mit einem MethodBinding-Objekt gearbeitet werden. Wieder findet die Instanzierung über das Application-Objekt statt:

var code = "";
code += "#{javascript:document1.replaceItemValue(\"Test\"";
code += ", @Text(@Now()));}";
var mb = app.createMethodBinding( code, null);

Eine Methode setMethodBinding() existiert jedoch nicht; das MethodBinding-Objekt muss daher über spezielle Methoden dem jeweiligen DataSource-Objektes zugewiesen werden. Die Namensgebung der Methoden ist hierbei jedoch trivial, es muss nur ein “set” vor die jeweilige Methode gestellt werden:

var data = new com.ibm.xsp.model.domino.DominoDocumentData();
data.setVar("document1");
data.setQuerySaveDocument(mb);
view.addData(data);

Hier noch der Source einer vollständigen Beispiel-Implementierung:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

<xp:button value="Create Datasource" id="button1">
   <xp:eventHandler event="onclick" submit="true"
       refreshMode="complete">
       <xp:this.action>
           <![CDATA[#{javascript:
              var app = facesContext.getApplication();
           var data = new com.ibm.xsp.model.domino.DominoDocumentData();

           data.setVar("document1");

           var value = "#{javascript:@Text(@Now())}";
           var vb = app.createValueBinding(value);
           data.setValueBinding("formName", vb);

           var code = "";
           code += "#{javascript:document1.replaceItemValue(\"Test\"";
           code += ", @Text(@Now()));}";
           var mb = app.createMethodBinding( code, null);
           data.setQuerySaveDocument(mb); 

           view.addData(data);
        }]]>
       </xp:this.action>
   </xp:eventHandler>
</xp:button>
<xp:button value="Use Datasource" id="button2">
   <xp:eventHandler event="onclick" submit="true"
     refreshMode="complete">
      <xp:this.action>
         <xp:actionGroup>
            <xp:executeScript>
               <xp:this.script>
                  <![CDATA[#{javascript:
                     document1.setValue("Feld", "ABC");
                  }]]>
               </xp:this.script>
              </xp:executeScript>
           <xp:saveDocument var="document1" />
         </xp:actionGroup>
      </xp:this.action>
   </xp:eventHandler>
</xp:button>
</xp:view>

Domino Datasources On-the-Fly (2): DominoDataView

27. Dezember 2011 Posted by Sven Hasselbach

Das ist der zweite Teil der Serie “Domino Datasources On-the-Fly”. Der erste Teil findet sich hier.

Views lassen sich ebenfalls programmatisch erstellen, allerdings muss mindestens der Name der Ansicht angegeben sein (und ausserdem muss die Ansicht existieren):

var data = new com.ibm.xsp.model.domino.DominoViewData();
data.setVar("view1");
data.setViewName("All");
view.addData(data);

Hier eine Beispiel-Implementierung in eine XPage (in der Datenbank muss die Ansicht “All” vorhanden sein) :

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

<xp:button value="Create Datasource" id="button1">
   <xp:eventHandler event="onclick" submit="true"
      refreshMode="partial" refreshId="label1">
      <xp:this.action>
         <![CDATA[#{javascript:
            var data = new com.ibm.xsp.model.domino.DominoViewData();
            data.setVar("view1");
            data.setViewName("All");
            view.addData(data);
         }]]>
      </xp:this.action>
   </xp:eventHandler>
</xp:button>
<xp:label id="label1">
   <xp:this.value><![CDATA[#{javascript:
      try{
         view1.getName();
      }catch(e){e}
    }]]>
    </xp:this.value>
   </xp:label>
</xp:view>

Folgende Eigenschaften können bei einem DominoViewData-Objekt gesetzt werden (Stand 8.5.3):

  • setParentId(String)
  • setSearchList(String)
  • setDatabaseName(String)
  • setVar(String)
  • setSearch(String)
  • setExpandLevel(int)
  • setSearchExactMatch(boolean)
  • setViewName(String)
  • setSortOrder(String)
  • setRequestParamPrefix(String)
  • setSortColumn(String)
  • setKeys(String)
  • setScope(String)
  • setStartKeys(String)
  • setSearchVariants(boolean)
  • setCategoryFilter(String)
  • setSearchMaxDocs(int)
  • setDataCache(String)
  • setKeysExactMatch(String)
  • setIgnoreRequestParams(boolean)
  • setSearchFuzzy(boolean)

Eine vollständige Liste findet sich hier.

Im nächsten Teil werden die Eigenschaften des DominoDataDocument-Objekts vorgestellt.

Quick-n-Dirty: Hijacking TypeAhead in CSJS

9. Dezember 2011 Posted by Sven Hasselbach

Matthias Nicklisch hat eine interessante Frage im XPages Forum gestellt, nachdem er festgestellt hat, dass im Designer zwar ein OnStart- / OnComplete-Event für die TypeAhead-Funktion angeboten wird, der Code aber als Deprecated angezeigt wird – und auf der XPage auch nicht funktioniert: Wie kann ein OnStart- / OnComplete-Event trotzdem verwendet werden?

Meine Idee dazu ist, den darunter liegenden dojo.xhr-Request zu hijacken, und auf diese Weise die Events zu erhalten. Dadurch lässt sich der Code bequem auf die jeweilige XPage einbetten, ohne das eine Manipulation der original Javascript-Dateien erfolgen muss.

Der folgender Code muß in einem CSJS-Scriptblock eingebettet werden. Dann erhält man für die TypeAhead-Funktion die Events, um zum Beispiel ein kleines “Loading”-Icon einzublenden, wenn die Daten vom Domino Server geladen werden.

var typeAheadLoad;

dojo.addOnLoad( function(){
   /*** hijacking xhr request ***/
   if( !dojo._xhr )
      dojo._xhr = dojo.xhr;

   dojo.xhr = function(){
      try{
         var args = arguments[1];
         if( args['content'] ){
            var content = args['content'];
               if( content['$$ajaxmode'] ){
                  if( content['$$ajaxmode'] == "typeahead" ){
                
                     /*** hook in load function ***/
                     typeAheadLoad = args["load"];

                     /*** overwrite error function ***/
                     args["error"] = function(){
                        alert('Error raised!')
                     };
                    
                     /*** hijack load function ***/
                     args["load"] = function(arguments){
                 
                        /*** On Start ***/
                        alert("On Start!");
                    
                        /*** call original function ***/
                        typeAheadLoad(arguments);
                    
                        /*** On Complete ***/
                        alert("On Complete!")
                     };
                 }
             }
         }
      }catch(e){}
      dojo._xhr( arguments[0], arguments[1], arguments[2] );
   }
});

Performance-Tuning (6): Parallele Partial Refreshs

7. Dezember 2011 Posted by Sven Hasselbach

Multiple Partial Refreshs sind eine schöne Sache, um mehrere Elemente einer XPage zu aktualisieren. Doch da die AJAX-Requests generell asynchron verarbeitet werden, stellt sich die Frage, in wieweit es erforderlich ist, sie sequentiell wie in dem verlinkten Beispiel abzuarbeiten: Denn je länger die Kette der Partial Refreshs ist, desto mehr Performance gewinnt man, wenn man stattdessen mit parallenen Aufrufen arbeitet.

Das verlinkte Beispiel sieht in der parallelen Variante wie folgt aus:

XSP.partialRefreshGet(id1);
XSP.allowSubmit();
XSP.partialRefreshGet(id2);
XSP.allowSubmit();
XSP.partialRefreshGet(id3);

Das Ergebnis unterscheidet sich nicht von von der sequentiellen Variante, ausser der Tatsache, das die Performance im Client deutlich höher ist. Zwischen den einzelnen Partial Refreshs muss nur die Funktion XSP.allowSubmit() aufgerufen werden, um die interne “Refresh-Sperre” zurück zu setzen (Eine kurze Erläuterung hierzu findet sich in Matthias Blog).

Die Events der Parial Refreshs (OnComplete, OnError, OnStart) können natürlich wie sonst auch verwendet werden.

Wichtig ist nur, dass man eine “Feinabstimmung” vornimmt, denn es läßt sich aufgrund der Asynchronität nicht voraussagen, in welcher Reihenfolge die Partial Refreshs verarbeitet werden: Sind z.B. die Elemente im DOM-Baum voneinander abhängig und ein Element wird durch ein Partial Refresh ausgeblendet, kann das natürlich zu ungewollten Fehlern führen – und ein sequentielles Aufrufen erforderlich machen. Doch wo es möglich ist, sollte aus Performancegründen der Einsatz von parallel ausgeführten Partial Refreshs erfolgen.

Quick-n-Dirty: Das xp:hidden-Element

6. Dezember 2011 Posted by Sven Hasselbach

Durch die Verwendung des <xp:hidden>-Elements lässt sich ein verstecktes Feld auf der XPage anlegen.

Hier ein Beispiel mit einem statischen Wert:

<xp:inputHidden id="inputHidden1" value="abc" />

Die XPages-Engine rendert daraus diesen HTML-Code:

<input type="hidden" id="view:_id1:inputHidden1"
   name="view:_id1:inputHidden1" value="abc">

Soweit so gut, doch wenn man den Wert dynamisch zuweisen will, rendert die XPages-Engine nicht mehr ein referenzierbares Feld,…

<xp:inputHidden id="inputHidden1">
   <xp:this.value>
      <![CDATA[#{javascript:"abc"}]]>
   </xp:this.value>
</xp:inputHidden>

… sondern einen <span>-Tag, der natürlich auch den Wert nicht enthält:

<span id:"view:_id1:inputHidden1"></span>

Will man trotzdem den Wert des Feldes berechnen, gibt es zwei Möglichkeiten:

1. Die Berechnung wird auf Compute on page load geändert:

<xp:inputHidden id="inputHidden1">
   <xp:this.value>
      <![CDATA[${javascript:"abc"}]]>
   </xp:this.value>
</xp:inputHidden>

2. Dem Feld wird eine Scope-Variable oder einem Dokumentenfeld via EL zugewiesen

<xp:inputHidden id="inputHidden1" value="#{viewScope.hiddenField}">
<xp:inputHidden id="inputHidden1" value="#{document1.hiddenField}">

Dann wird wie in der statischen Variante ein verstecktes Feld generiert, was sich sowohl mit CSJS als auch mit SSJS verarbeiten lässt.

Der neue HTTP Header ‘X-XspRefreshId’

5. Dezember 2011 Posted by Sven Hasselbach

Mit Domino 8.5.3 ist der neue HTTP Header ‘X-XspRefreshId’ eingeführt worden, mit dem sich die refreshId eines Partial Refreshs vom Server aus verändern lässt. Dadurch ist es möglich, ein Element zu refreshen, dass Ergebnis dieser Operation jedoch auf ein anderes Element im Client anzuwenden.

Hier ein kleines Beispiel anhand einer XPage, die vor dem Partial Refresh wie folgt aussieht:

Screenshot: Vor dem Partial Refresh

Der Code der XPage ist ebenfalls simpel, ausser das bei einem Partial Refresh der XPage ein Header an den Request angefügt wird. Dazu wird das Event afterRestoreView genutzt.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:this.afterRestoreView><![CDATA[#{javascript:
        var exCon = facesContext.getExternalContext();
        var writer = facesContext.getResponseWriter();
        var response = exCon.getResponse();
        response.setHeader("X-XspRefreshId",  getClientId('label2') ); }]]>
    </xp:this.afterRestoreView>

    <xp:button value="Label" id="button1">
        <xp:eventHandler event="onclick" submit="true"
            refreshMode="partial" refreshId="label1">
        </xp:eventHandler>
    </xp:button>
    <xp:br></xp:br>
    <xp:br></xp:br>
    <xp:label value="Label1" id="label1"></xp:label>
    <xp:br></xp:br>
    <xp:br></xp:br>
    <xp:label value="Label2" id="label2"></xp:label>
</xp:view>

Der Button löst einen Partial Refresh auf das Label label1 aus, der dazugehörige Request, der an den Server gesendet wird liefert auch den zu erwartenden HTML-Code zurück:

<span id="view:_id1:label1" class="xspTextLabel">Label1</span>

Doch nun kommt der zusätzliche Header ins Spiel. Er bewirkt, dass nicht das label1 ersetzt wird, sondern das Element label2:

Durch den in der HTTP Header in der Antwort des Servers wurde das XSP-Objekt dazu veranlasst, den HTML-Code im DOM-Baum an einer anderen Stelle zu ersetzen.

In diesem kleinen Beispiel tritt ein kleiner Seiteneffekt auf: Das Label mit der id label2 verschwindet komplett aus dem DOM-Baum. Betätigt man den Button ein zweites Mal, funktioniert der Refresh nicht mehr, und folgende Fehlermeldung erscheint im Browser: