An elegant, UI-only way to link documents in an IBM Domino database with LotusScript

4. März 2015 Posted by Björn Großewinkelmann

 

Problem Description

In this article I will show you how to elegantly link documents in a Notes database with the use of event handlers. At first glance linking documents together seems to be a very trivial task. It is something we all implemented hundreds of times already, either using built-in Notes response hierarchies or through building document trees of our own design. Doing that programmatically while using backend classes is as trivial as it sounds, but letting the user do it interactively is something else entirely. Because then there are certain additional requirements that should be implemented to guarantee a consistent and hassle-free user experience.

So let us take a look at those requirements for the two standard use cases:

1. The user links an already existing document to the currently open document (through a dialog field or a view selection). The title of the linked document should be displayed in a field and the document can be opened through a hotspot.

Requirements

  • If the user doesn’t save the currently open document the link should be voided i.e. the field should be empty and the hotspot action should not execute.
  • The operation should not access backend classes to avoid replication and safe conflicts.

2. The user creates a new document from an already open document. The new document should be linked to the already opened one and its title should be displayed in a field (and can, after the initial save, opened through a hotspot).

Requirements

  • If the user doesn’t safe the new document AND the current document the link should be voided i.e. the field should be empty and the hotspot action should not execute, even if the new document has been saved.
  • The operation should not access backend classes to avoid replication and safe conflicts.
  • Every change the user performs on the new document should immediately be reflected in the old document.

 

Implementation

During the following demonstration I will call the document that provides the link the parent and the link target the child. I also will not make use of the Parent-Response-Hierarchy but will implement my own hierarchy using document IDs I generate myself programmatically. I am sure you know why that is ;-). Of course you can use a Parent-Response Hierarchy instead and, if you are feeling lucky, you can even use Universal IDs to define the link.

 

Case 1: Linking to an existing document

The first case is more or less trivial. The child already exists and will not be opened during the linking operation hence we actually can (have to actually) access it using backend classes, specifically an instance of NotesDocument or built-in functions e.g. a dialog list field.
We create a dialog list field on the parent form and populate the choices with a column of a lookup view. The column formula should be something like the following:

doc_title + “|” + doc_id

Now make sure “Allow keyword synonyms” is checked in the field properties and you are done.

If you have to actually use backend classes, for example in conjunction with the NotesUIWorkspace Functions PicklistCollelction or PicklistStrings you can use the NotesUIDocument Function FieldSetText to set the value. And yes you can set computed and even hidden fields that way, too. I normally use a dialog list field even if the field is computed and set with FieldSetText. That way I can set the doc_id and after a NotesUIDocument.Refresh the displayed value is automatically translated to the title. For this to work you of course have to check the option “Refresh choices on document refresh” in the field properties.

The hotspot action performs a lookup in the same view (the first sorted column has obviously to be doc_id) and opens the document.

 

Case 2: Linking a new document

This case requires a little more work. So let us split the requirements into two steps to make it clearer.

For the purpose of code clarity and management we create persistent backend classes which serve as a wrapper for NotesUIDocument instances. We accomplish that through creating an instance of our wrapper in the PostOpen event and saving the reference in a variable declared in the Globals section of the form. For more details read my previous articles on the subject:

Event Handling mit LotusScript Teil I - Persistente Event-Handler

 

Step 1: Creating the child

In the backend class for the parent we first declare a private member variable:

Private childUIDoc As NotesUIDocument

 

And the following sub:

Public Sub actionCreateChild()

   Dim ws As New NotesUIWorkspace()
   Dim db As NotesDatabase
   Dim childDoc As NotesDocument

   Set db = ws.CurrentDatabase.Database
   Set childDoc = db.CreateDocument()

   Call childDoc.ReplaceItemValue( "Form", "childDoc" )
   Call childDoc.ComputeWithForm( False, False )

   Set me.childUIDoc = ws.EditDocument( True, childDoc )


End Sub

 

That sub will be called by the action which we use to create the child document. In effect our persistent instance of the parent wrapper class now has a handle on the NotesUIDocument instance of the just created child.
 

Step 2: Handling Child Events in the Parent-Wrapper

Next we want to notify the parent if and when the child gets saved or closed. For that purpose we bind the corresponding events, which are fired by the child NotesUIDocument instance, to event handlers we define in our parent wrapper.

Public Sub actionCreateChild()
    
    Dim ws As New NotesUIWorkspace()
    Dim db As NotesDatabase
    Dim childDoc As NotesDocument
    
    Set db = ws.CurrentDatabase.Database
    Set childDoc = db.CreateDocument()
    
    Call childDoc.ReplaceItemValue( "Form", "childDoc" )
    Call childDoc.ComputeWithForm( False, False )
    
    Set me.childUIDoc = ws.EditDocument( True, childDoc )

    On Event PostSave From me.childUIDoc Call OnPostSave_Child
    On Event QueryClose From me.childUIDoc Call OnQueryClose_Child

End Sub

 

All the PostSave event handler has to do is to set the doc_id and refresh the NotesUIDocument instance of the parent document. This way the current title of the document is always displayed.

Public Sub OnPostSave_Child( Source As NotesUIDocument )
    
    Call me.mUIDoc.FieldSetText( "child_id", _
                                  Source.FieldGetText( "doc_id" ) )
    Call me.mUIDoc.Refresh()
    
End Sub

 

In the QueryClose event handler we first check if the child has been saved. Since we created the document using backend classes we need to check the property IsNewNote on the backend document rather than the property IsNewDoc which is provided by the NotesUIDocument instance (and hence will always be false). If the child has been saved we do nothing, everything already has been taken care of by the PostSave event handler. If it has not been saved we reset the link field to the empty string, thus removing the link.

Public Sub OnQueryClose_Child( Source As NotesUIDocument, _
                               Continue As Variant )
    
    If childUIDoc.Document.IsNewNote Then
          Call me.mUIDoc.FieldSetText( "child_id", "" )
          Call me.mUIDoc.Refresh()
    End If
    
End Sub

 

And that is it. If the child doesn’t get saved the link will be empty. If the parent doesn’t get saved the link won’t change at all. If it was empty before the child was created it will obviously stay that way regardless whether or not the child has been saved.

 

Step 3: Opening the link

To keep the behavior consistent we also have to obtain the child NotesUIDocument instance when the user follows the link. For that we create a new sub for the sole purpose of opening the document.

Public Sub actionOpenChild()

    Dim ws As New NotesUIWorkspace()
    Dim db As NotesDatabase
    Dim view As NotesView
    Dim childDoc As NotesDocument
    Dim childID As String
    
    childID = me.mUIDoc.Document.getItemValue( "child_id" )
    
    If Not childID = ""  Then
          Set db = ws.CurrentDatabase.Database()
          Set    view = db.GetView( "lkpChildDoc" )
          Set childDoc = view.GetDocumentByKey( childID, True )
        
          If Not childDoc Is Nothing Then
                Set me.childUIDoc = ws.EditDocument( True, childDoc )
          End If
    End If
    
End Sub

 

So in theory we are done but what if the child gets opened end edited outside of the parent context? Well if we use the dialog list approach we are still done because when the child gets edited the view entry will change and the link title will be updated the next time you open the parent (into edit mode). But what if you want the parent to reflect changes in the child immediately?
 
There probably is a reason why you made one document the child and the other the parent and not the other way around. You built your hierarchy to reflect some piece of business logic or maybe even real logic ;-).

In consequence it is highly likely that the user is already editing the parent when he opens the child. Hence updating the parent programmatically through the child without having the parent context invites all kinds of trouble. So don’t! Rather write an agent that runs once a night and performs a ComputeWithForm on all parent documents.


 

Extending IBM Domino Web Service Consumers to support SOAP Authentication (Java)

30. Oktober 2014 Posted by Björn Großewinkelmann

 

Today I like to share a simple but elegant and reusable solution to implement Web Service Security (WSS) into Domino Web Service Consumers.

Let's assume you already generated your Web Service Consumer Stub from the WSDL which means you have a class called <WebServicePackage>.<WebServiceName>Stub.

Example:

 

The stub should look something like this:

package com.atradius.connect._2007_08;

 

public class CoverPortfolioSynchronisationStub

  extends lotus.domino.websvc.client.Stub

  implements com.atradius.connect._2007_08.CoverPortfolioSynchronisation {

 

  public CoverPortfolioSynchronisationStub(

       java.net.URL endpointURL,

       javax.xml.rpc.Service service )

  throws lotus.domino.types.Fault

  {

     super(endpointURL, service);

  }

 

  public void retrievePortfolio(

       java.util.Date dateFrom,

       java.util.Date dateTo,

       com.atradius.connect._2007_08.PoliciesType policies,

       javax.xml.rpc.holders.BigIntegerHolder numberOfCovers,

       javax.xml.rpc.holders.ByteArrayHolder portfolioCovers,

       javax.xml.rpc.holders.StringHolder mimeType,

       javax.xml.rpc.holders.StringHolder contentType)

  throws

  com.atradius.services.exception._2006_12_15.exception.WSException,

  java.rmi.RemoteException

  {

     lotus.domino.websvc.client.Call _call =

       createCall("retrievePortfolio");

 

     java.lang.Object _resp =

       _call.invoke(

            new java.lang.Object[] {dateFrom, dateTo, policies});

 

     numberOfCovers.value =

       (java.math.BigInteger) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "NumberOfCovers",

            java.math.BigInteger.class);

 

     portfolioCovers.value =

       (byte[]) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "PortfolioCovers",

            byte[].class);

 

     mimeType.value =

       (java.lang.String) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "MimeType",

            java.lang.String.class);

 

     contentType.value =

       (java.lang.String) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "ContentType",

            java.lang.String.class);

  }

}

 

Note that the stub extends “lotus.domino.websvc.client.Stub. The easiest way to enable the client to pass SOAP credentials is for us to use our own Stub base class “mypackage.Stub” which in turn inherits “lotus.domino.websvc.client.Stub”.

Extended Stub:

package de.itwu.utils.websvc.client;

 

import java.net.URL;

 

import javax.xml.rpc.Service;

import javax.xml.soap.SOAPElement;

import javax.xml.soap.SOAPException;

 

import lotus.domino.axis.message.PrefixedQName;

import lotus.domino.axis.message.SOAPHeaderElement;

import lotus.domino.types.Fault;

 

public class Stub

  extends lotus.domino.websvc.client.Stub {

 

  public Stub( Service service, String url ) {

     super( service, url );

  }

 

  public Stub( URL endpointURL, Service service ) throws Fault {

     super( endpointURL, service );

  }

 

  protected SOAPHeaderElement getSOAPHeaderSecurity(){

     try{

 

       final PrefixedQName secHeaderQN =

          new PrefixedQName(

                  "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",

                  "Security",

                  "wsse" );

       final PrefixedQName passTypeQN =

          new PrefixedQName(

                  "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",

                  "Type",

                  "wsse" );

      

       final SOAPHeaderElement security = new SOAPHeaderElement( secHeaderQN );

       security.setMustUnderstand( true );

 

       final SOAPElement usernameToken = security.addChildElement( "UsernameToken", "wsse" );

 

       final SOAPElement username = usernameToken.addChildElement( "Username", "wsse" );

       username.addTextNode( this.getUsername() );

 

       final SOAPElement password = usernameToken.addChildElement( "Password", "wsse" );

       password.addAttribute(

            passTypeQN,

            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText" );

       password.addTextNode( this.getPassword() );

 

       return security;

     } catch (SOAPException e) {

       e.printStackTrace();

     }

     return null;

  }

}

 

Our stub implements a new method which constructs and returns the necessary SOAP message header to authenticate to the provider through a username and a clear-text password.
The schema is defined in http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd.

Please note that the above method makes use of getPassword()and getUsername(). Those are built-in getters which retrieve the credentials that have been set through setCredentials().

Now all we have to do is to modify the generated stub. First we change the inheritance, the stub now should inherit from our newly written base.  After that is done we add the following method call to all web service calls:

  _call.addHeader(getSOAPHeaderSecurity());

 

Example:

package com.atradius.connect._2007_08;

 

public class CoverPortfolioSynchronisationStub

  extends de.itwu.utils.websvc.client.Stub

  implements com.atradius.connect._2007_08.CoverPortfolioSynchronisation {

 

  public CoverPortfolioSynchronisationStub(

       java.net.URL endpointURL,

       javax.xml.rpc.Service service )

  throws lotus.domino.types.Fault

  {

    super(endpointURL, service);

  }

 

  public void retrievePortfolio(

       java.util.Date dateFrom,

       java.util.Date dateTo,

       com.atradius.connect._2007_08.PoliciesType policies,

       javax.xml.rpc.holders.BigIntegerHolder numberOfCovers,

       javax.xml.rpc.holders.ByteArrayHolder portfolioCovers,

       javax.xml.rpc.holders.StringHolder mimeType,

       javax.xml.rpc.holders.StringHolder contentType)

  throws

  com.atradius.services.exception._2006_12_15.exception.WSException,

  java.rmi.RemoteException

  {

     lotus.domino.websvc.client.Call _call =

       createCall("retrievePortfolio");

 

     _call.addHeader(getSOAPHeaderSecurity());

 

     java.lang.Object _resp =

       _call.invoke(

            new java.lang.Object[] {dateFrom, dateTo, policies});

 

     numberOfCovers.value =

       (java.math.BigInteger) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "NumberOfCovers",

            java.math.BigInteger.class);

 

     portfolioCovers.value =

       (byte[]) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "PortfolioCovers",

            byte[].class);

 

     mimeType.value =

       (java.lang.String) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "MimeType",

            java.lang.String.class);

 

     contentType.value =

       (java.lang.String) _call.convertOutputParam(

            "http://atradius.com/connect/_2007_08/",

            "ContentType",

            java.lang.String.class);

  }

}

 

Done. Now all SOAP requests will have the specified wss header. Just don’t forget to actually set the credentials in your code ;-).

 

CoverPortfolioSynchronisation stub = new AtradiusConnectSynchronisationServiceLocator().getCoverPortfolioSynchronisation();

       stub.setEndpoint( url);

       stub.setCredentials( "user", "password" );

       stub.retrievePortfolio(

          startDate,

          new Date(),

          policies,

       numberOfCovers,

       portfolioCovers,

       mimeType,

       contentType);

 

The complete wss specification can be found at:

http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf

http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf

 

The Law of Unintended Consequences

1. Oktober 2014 Posted by Björn Großewinkelmann

 

Ok, so we all know that the road to becoming a Notes developer is a rocky one but there really are some things that take the cake in that regard. Today I like to talk about standard functions and that they never behave quite like you might expect them to.

Since I work primarily with LotusScript let us take a look at a very useful und often used function in the NotesDocument class, namely ComputeWithForm.

First let’s read the manual (8.5.3):


ComputeWithForm method

Validates a document by executing the default value, translation, and validation formulas, if any are defined in the document form.

Defined in

NotesDocument

Syntax

flag = notesDocument.ComputeWithForm( doDataTypes, raiseError )

Parameters

doDataTypes

Boolean. The method ignores this parameter. Specify either True or False.

raiseError

Boolean. If True, an error is raised if the validation fails. If False, no error is raised; instead, the method returns False if validation fails.

Return value

  • True indicates that there are no errors on the document.

  • False indicates that there are errors on the document.

[*snip*]



Source: http://www-01.ibm.com/support/knowledgecenter/SSVRGU_8.5.3/com.ibm.designer.domino.main.doc/H_COMPUTEWITHFORM_METHOD.html?lang=en

 

Let us just ignore interesting tidbits like “The method ignores this parameter. Specify either True or False.” and move on to the really strange stuff. So what does this method claim to do?

1) It executes validation formulas

If you work like me you will never use that feature since I always put field validations in my backend class for the following reasons:

  1. I always know where to find them, trust me that is not a small issue when it comes to complex business logic and interdependent rules.
  2. It is far easier to write such complex rules in LS than in Formula Language.
  3. You can debug LS!
  4. You can evaluate all of the rules and give the user a summary report instead of raising an error after the first evaluated rule fails to validate.

 

2) It executes Input Translation Formulas

Yep it does and you have to be aware of that. But if you use Input Translation at all you probably know what you are doing since otherwise you couldn’t.

 

3) It “executes” default values.

It does. Which is the reason why we use it so frequently so we can set all the items we need on a document that was created through backend classes or agents. One ComputeWithForm and all the default values are there. Immensely useful but Notes wouldn’t be Notes if it hadn’t some surprises in stock for us there.

 

4) It returns a Boolean to indicate whether or not the validation succeeded

Well, yes it does …kind of… maybe … if nothing else did go wrong you have no idea about….

 


based on © Andy Dean - Fotolia.com

“Default” is a just a word…

So let us unpack the more problematic und not very well documented “features” of this function and let’s start with the default values it is supposed to execute.

First if you think that “default values” refer to… well default values you are obviously new at this but don’t despair you’ll get there eventually. So no, “executing default values” does NOT ONLY mean that an item is created on the document which has the values you can specify in the “Default”-section of any of the editable fields on a given form. In fact it means a lot more than that.

ComputeWithForm also executes formulas in Computed- and ComputedWhenComposed-Fields. Which is great! But: if no “default” value is available the created item will be of type text (1280) and the value will be set to the empty string! So, arguably yes it does execute default values… if you use that term rather loosely.

… “Error” on the other hand…

If you read the description in the manual and my remarks above you might think that ComputeWithForm essentially creates all not yet existing items then executes the input translations to finalize their values before it checks those values against the validation rules. At this point the function returns either ‘true’ or ‘false’ (or raises an error if you set raiseError = ‘true’) to indicate whether or not all rules validated successfully.

But then I guess at this point you already know better than that.

Suppose you have a DBLookup in one of your computed fields and that lookup fails for whatever reason.
Right, it is an error but is it a validation error?
Even if there is no input validation specified for that field?
And what if you use [FailSilent]?
Still an error?
Surely not and that can’t possibly be treated as a validation error right!?
Well, wrong on all counts!

The type of error does not matter at all. Every single error in a formula on your document will cause ComputeWithForm to fail and to fail miserably! Because every single error, even the ones that aren’t (because of [FailSilent]), will be treated as fatal which means that ComputeWithForm will stop evaluating the document which in turn means that not all the default values will be set. One failed lookup and not another item will be evaluated!

Yes the manual states: False indicates that there are errors on the document.

But I bet you didn’t think they literally meant every possible error. And on top of that even the ones that are already handled!

Well now you know!

So if you for example use a lookup in a computed field that might fail you have to identify why that could possibly be and then check for those conditions with @If. Then ComputeWithForm will finally work as you expected it to all along...

... kind of. At least, if you know what it does and doesn't do you can work around it and might even find it useful... maybe. Of course ComputeWithForm is not the only example of ... interesting ... behavior, it is probably not even the worst one. But from what you can gather on the net a lot of people believe that it works in a lot of different ways that seem to be mutual exclusive and people are (as always) not shy about sharing incomplete information. Which, in a sense, makes this function the worst of its kind after all because people get frustrated.

But maybe I am wrong, maybe I should see stuff like this more relaxed and treat the documentation more like loose guidelines.... What do you think? And while we're at it: What do you believe to be the most atrociously under-documented or reality-bending, may-work-as-intended-on-the-fourth-Tuesday-in-February-on-a-parallel-plane-of-existence-if-it’s-not-a-leap-year-function in the Notes/Domino environment? Leave a comment and share the pain. Trust me, it's good therapy.

 

Event-handling with LotusScript Part II – Inheritance

18. Juni 2014 Posted by Björn Großewinkelmann

So, let's try it in English this time!

First of all, sorry for the delay - I know I've promised to get this post out to you in a weeks time when I wrote the first blog post in this series "Event Handling mit LotusScript Teil I - Persistente Event-Handler" and now it's been FOUR weeks...Sch?chtern  But to make this up to you I will publish the first blog post in English as well - let's say within the next four weeks Winken.

But now, let's get started so I can show you how event-handlers can also profit from inheritance in LotusScript!

Once you have a class that implements custom event-handlers you should know how its instances behave in case the class is part of an inheritance hierarchy. There are a couple of things you have to consider when designing inheritance hierarchies in general and inheritance with event-handlers in particular.

Flow Control

As briefly mentioned in the first part of this series: you can in fact bind multiple event-handlers to the same event. In case of Query-Events the triggering command (Open, Save, Send, Close) will only be executed if none of the bound handlers set ‘Continue’ to False. Since ‘Continue’ is declared as a Variant (which of course is a reference type) it is shared between all methods that handle the event. So if you set ‘Continue’ to True it will be true for all handlers from there on until another handler sets it to false again. Since you can’t be (reasonably) sure in which order your bound event-handlers are executed you really shouldn’t build handlers that depend on each other or use the ‘Continue’ to communicate. Fortunately this is not how inherited event-handlers work.

Inheritance

If your class overwrites an event-handler which it inherited from some base class only the child event-handler will be triggered when the event fires. The event-handler of the base class will be ignored completely. It does not matter where you bind the handler, in the base class or the child. Both times only the child event-handler will be bound.

Let’s take a look at an example (Code 1), the following code defines a controller base class:

 

Public Class UIDocControllerBase

 

           Private p_uidoc As NotesUIDocument

 

           Public Property Get UIDoc() As NotesUIDocument

                      Set UIDoc = p_uidoc

           End Property

 

           Public Sub New( pUIDoc As NotesUIDocument )

                      Set p_uidoc = pUIDoc

           End Sub

     

           Public Sub BindEvents()

                      On Event PostOpen From UIDoc Call PostOpen

                      On Event QueryClose From UIDoc Call QueryClose

                      On Event QuerySave From UIDoc Call QuerySave

           End Sub

     

           Public Sub PostOpen( Source As NotesUIDocument )

                      Print "BASE: Post Open"

           End Sub

     

           Public Sub QueryClose( Source As NotesUIDocument, Continue As Variant )

                      Print "BASE: Query Close"

           End Sub

     

           Public Sub QuerySave( Source As NotesUIDocument, Continue As Variant )

                      Print "BASE: Query Save"

           End Sub

     

           Public Sub Delete()

                      Print "Delete: UIDocControllerBase"

           End Sub

     

End Class

 

The class implements handlers for the PostOpen, QueryClose and QuerySave events. In addition it implements the BindEvents-Method which takes care of the event binding.

The following code (Code 2) defines a class CompanyUIDocController which inherits from the UIDocControllerBase class. It implements its own handlers for the QueryClose and PostOpen events, it does however not overwrite its parent’s QuerySave event-handler nor does it overwrite the BindEvents-Method.

 

Public Class CompanyUIDocController As UIDocControllerBase

     

           Public Sub New( pUIDoc As NotesUIDocument )

           End Sub

 

           Public Sub QueryClose( Source As NotesUIDocument, Continue As Variant )

                      Call UIDocControllerBase..QueryClose( Source, Continue )

                      Print "CompanyUIDocController: QueryClose"

           End Sub

     

           Public Sub PostOpen( Source As NotesUIDocument )

                      Print "CompanyUIDocController: PostOpen"

           End Sub

                       

           Public Sub Delete()

                      Print "Delete: CompanyUIDocController"

           End Sub

           

End Class

 

If you now substitute the class definition from the first blog entry of this series with the two classes of Code 1 and Code 2 and then Open, Save and finally Close the corresponding document the output will be as follows:

Fig. 1: Example output

 

Analysis

First the BindEvents-Method is called and binds the events of the NotesUIDocument to all not overridden event-handlers. That means that the PostOpen and QueryClose events are bound to the CompanyUIDocController class while the QuerySave event is bound to the base class. When we trigger the events those and only those event-handler are being executed. The QueryClose event-handler in the base class is only executed because we are explicitly calling it inside the QueryClose event-handler of CompanyUIDocController by using the “..”-notation in the function call.

     Call UIDocControllerBase..QueryClose( Source, Continue )

A call like that will always refer to the base-instance instead of the “youngest”-instance like

     Call me.QueryClose( Source, Continue )

would. And before you get any ideas both of the above calls are illegal in the event binding definition!


The only two methods which automatically call their equivalents in the base instance(s) are New and Delete. If you wish to cascade any other function calls you have to do it explicitly.
 

In conclusion, if you want to implement some functionality which is the same for all of your derived classes that code should go in the base class event-handler. On the other hand  any functionality that deals with issues specific to your current class should be located within the derived class. An example would be a base class that implements a function which forces the user to write a comment during the QuerySave event. More specific functionality like updating a parent document which is only needed on a specific form would go in the event-hander of the derived class. Just remember that you have to call the base class handler explicitly if you need the base functinality too.

If you have any more questions concerning event handling in LotusScript, please leave us a comment so we can expand this mini-series a little further. 

 

Event Handling mit LotusScript Teil I – Persistente Event-Handler

2. Juni 2014 Posted by Björn Großewinkelmann

In dieser Beitragsreihe möchte ich euch eine Einführung in das Event Handling mit objektorientiertem LotusScript geben. In dem heutigen Artikel geht es um das Erstellen persistenter Event-Handler. In der nächsten Woche sprechen wir dann über Event-Handler in Vererbungshierarchien.

In beinahe allen Projekten, die mit objektorientiertem LotusScript umgesetzt werden, erstellt sich der Entwickler eine eigene Wrapperklasse für NotesUIDocument, oft eine pro Maske. Im Falle von größeren Projekten kommt dann noch zusätzlich Vererbung ins Spiel. Sobald ein Projekt eine gewisse Größe überschritten hat, steht man oft vor dem Problem wie man den Code nicht nur effizient sondern auch wartbar organisiert. Dem Event Handling kommt hierbei eine besondere Bedeutung zuteil, da man schnell versucht ist, die in der Maske eingebetteten Routinen zu benutzen. Dies trägt im besten Fall zwar nur zur allgemeinen Unübersichtlichkeit bei; im schlimmsten Fall führt es aber dazu, dass man Code dupliziert, was am Ende den Wartungsaufwand potenziert.

Unter diesen Gesichtspunkten macht es immer Sinn, sich eigene Event Handler zu schreiben, um sich insbesondere Vererbungshierarchien zu Nutze zu machen.

Im folgenden Beispiel benutze ich einen sehr generischen Wrapper, welcher das Event Handling für UI-Dokumente der Maske „Company“ übernimmt. Auf Besonderheiten wie Error-Traps werde ich der Übersichtlichkeit halber in diesem Beitrag verzichten.

 

Public Class CompanyUIDocController

     

      Private p_uidoc As NotesUIDocument

     

      Public Property Get UIDoc As NotesUIDocument

                         Set UIDoc = p_uidoc

      End Property

     

      Public Sub New( pUIDoc As NotesUIDocument )

                         Set p_uidoc = pUIDoc

      End Sub

     

End Class

 

Die Klasse besteht zunächst einmal nur aus ihrem Konstruktor, welcher als Parameter das NotesUIDocument erwartet und dieses über die Property UIDoc nach außen zugänglich macht. Nun möchten wir diese Klasse zum Handler für die Events machen, die von der NotesUIDocument-Instanz gefeuert werden. Dazu müssen wir zunächst einmal dafür sorgen, dass uns eine persistente Instanz der Klasse zur Verfügung steht. Wir brauchen also eine Instanz, welche beim Erstellen der unterliegenden NotesUIDocument-Instanz, sprich dem Öffnen des Dokumentes, einmal instanziert und erst recyclet wird, nachdem das Dokument geschlossen wurde. Dafür benutzen wir die globalen Definitionen der entsprechenden Maske.

Zunächst wird wie üblich die benutze Skript-Bibliothek eingebunden (siehe Abb. 1). Danach deklarieren wir eine private Variable p_this vom Typ unserer Controller-Klasse (siehe Abb. 2).

 

Abb. 1: Options Abb. 2: Declarations


















 

Diese Variable dient als Backend für eine Property desselben Typs. Die notwendige Objekt-Referenz für p_this  (siehe Abb. 3) wird mit dem Sub InitThis (siehe Abb. 4) erzeugt.

 

 
Abb. 3: This  
 
Abb. 4: InitThis  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

In dem in der Maske eingebetteten QueryOpen-Handler schließlich wird unsere CompanyUIDocController-Klasse, durch Aufruf von InitThis, instanziiert und die Referenz in p_this gespeichert (siehe Abb. 5).

 

Abb. 5: der eingebettete QueryOpen-Event Handler

 

An diesem Punkt angekommen werden wir mit der ersten Einschränkung dieses Ansatzes konfrontiert. Die notwendige Instanz von CompanyUIDocController kann logischerweise nicht vor dem Feuern des QueryOpen-Events erzeugt werden. Dementsprechend kann unser zukünftiger Event-Handler das QueryOpen-Event nicht automatisch verarbeiten sondern der ensprechende Handler muss manuell aufgerufen werden.

Zunächst einmal haben wir an dieser Stelle aber unser erstes Teilziel erreicht. Wir verfügen nun über eine persistente Instanz der Klasse CompanyUIDocController. Diese Instanz wird solange referenziert sein wie die unterliegende NotesUIDocument-Instanz existiert, also das aktuelle Dokument vom Typ „Company“ geöffnet ist. Testen können wir das ganze durch Überschreiben des Destruktors der Klasse CompanyUIDocController.

 

Public Class CompanyUIDocController

     

      Private p_uidoc As NotesUIDocument

     

      Public Property Get UIDoc As NotesUIDocument

                         Set UIDoc = p_uidoc

      End Property

     

      Public Sub New( pUIDoc As NotesUIDocument )

                         Set p_uidoc = pUIDoc

      End Sub

           

      Public Sub Delete()

            Print "Deleting instance of CompanyUIDocController"

      End Sub

     

End Class

 

Delete wird erst beim Schließen des Dokumentes aufgerufen, da erst dann die in p_this gespeicherte Instanz dereferenziert und damit das Objekt invalidiert und vom Garbage Collector recycelt wird.

Nachdem wir also nun diesen ganzen Aufwand betrieben haben, um eine persistente Instanz unserer Controller-Klasse zu erhalten, sollten wir auch etwas damit anfangen. Zunächst einmal in aller Kürze die Grundlagen:

 On Event <Event> From <Instance> Call <Handler>

Mit demOn Event“-Statement wird angezeigt, dass das Event <Event>, welches von dem Objekt <Instance> gefeuert wird, durch die Methode <Handler> verarbeitet werden soll. Die Signatur der Handler-Methode ist dabei fix und durch das zu verarbeitende Event vorgegeben, dementsprechend wird unter <Handler> nur der Methodenname angegeben, keine Parameter.

Nun erweitern wir unsere Controller-Klasse um einen Handler für das QuerySave-Event und eine Methode BindEvents, welche das Binden des Events an den entsprechenden Handler für uns erledigt:

 

Public Class CompanyUIDocController

     

      Private p_uidoc As NotesUIDocument

     

      Public Property Get UIDoc As NotesUIDocument

                          Set UIDoc = p_uidoc

      End Property

     

      Public Sub New( pUIDoc As NotesUIDocument )

                          Set p_uidoc = pUIDoc

      End Sub

 

      Public Sub BindEvents()

                          On Event QuerySave From UIDoc Call QuerySave

      End Sub

     

      Private Sub QuerySave( Source As NotesUIDocument, Continue As Variant )

                          Print "CompanyUIDocController: Query Save"

      End Sub

     

End Class

 

Abschließend ergänzen wir den in der Maske eingebetteten QueryOpen-Event Handler um den Aufruf von BindEvents, damit die Events an unsere persistente Instanz gebunden werden.

 

Abb. 6: der eingebettete QueryOpen-Event Handler mit Aufruf von BindEvents()

 

Fertig!

Von nun an wird das QuerySave-Event bei jedem Öffnen eines Dokumentes in der Maske Firma automatisch immer an eine, für diese NotesUIDocument-Instanz spezifische, This-Instanz gebunden und in dieser verarbeitet. Der gesamte Code zum Event Handling liegt nun in der Skript Bibliothek und die entsprechenden Routinen in der Maske müssen nie wieder angefasst werden. Sollen auch andere Events verarbeitet werden genügt es lediglich die entsprechenden Handler in der CompanyUIDocController-Klasse zu definieren und in der Methode BindEvents zu binden.

In der nächsten Woche erzähle ich euch dann, wie Event Handler von Vererbungshierarchien profitieren können. Man darf also gespannt sein!