Lotusscript: Mail Notification Class

Back in August 2009, I posted a small class I had created to make it easier for me to create mail notifications in my Lotusscript agents. Since then I have improved on it, and I wanted to post the latest version. One recent feature I added is to send external mail to the internet that looks like it is coming from a different user. This is often needed when you have an agent signed with a developer/admin ID, but you want the mail to look like it is coming from a particular department or user.
This is done by using the (unsupported) method of saving the document in mail.box instead of using doc.Send() to mail it directly. What I found is that you should use mail.box on the same server as the agent is running on, you can not access mail.box on a different server. The code reflect this.

Other new features are support for doclinks, attachments and an option to display a warning message at the end that the mail comes from an un-monitored role account. This warning is actually turn on by default.

Here is an example of how to use the class:

Option Public
Option Declare
Use "Class.MailNotification"

Sub Initialize
 Dim mail As NotesMail
 ' *** Create a mail
 Set mail = New NotesMail()
 ' Set receipient and subject
 mail.MailTo = "texasswede@example.com"
 Call mail.AddMailTo("texasswede2@example.com")
 mail.Subject = "Please Read This"
 mail.Principal = "bogus_user@example.com"
 ' Create body
 Call mail.AppendText("Testing email...")
 Call mail.AppendNewLine(2)
 Call mail.Send()
End Sub

 

And here is the class itself. Enjoy!

Option Public
Option Declare

Class NotesMail
 Public maildoc As NotesDocument
 Public body As NotesRichTextItem
 Private p_subject As String
 Private p_sendto List As String
 Private p_copyto List As String
 Private p_blindcopyto List As String
 Private p_principal As String
 Public NoReply As Integer
 Public mailbox As NotesDatabase

 Public Sub New()
  Dim session As New NotesSession
  Dim mailservername As String
  mailservername = session.Currentdatabase.Server
  ' Using mail.box directly is unsupported, but is the
  ' only way to make the mail look like it is actually
  ' sent from another address, in our case the principal.
  Set mailbox = New NotesDatabase(mailservername,"mail.box")
  If mailbox.Isopen = False Then
   Print "mail.box on " & mailservername & " could not be opened"
   Exit Sub
  End If
  Set me.maildoc = New NotesDocument(mailbox)
  Call me.maildoc.ReplaceItemValue("Form","Memo")
  Set me.body = New NotesRichTextItem(maildoc,"Body")
  me.p_subject = ""
  me.p_principal = ""
  ' Empty lists for addresses
  Erase me.p_sendto
  Erase me.p_copyto
  Erase me.p_blindcopyto
  me.NoReply = False ' Default is to add a disclaimer to the end
 End Sub

 Public Property Set Subject As String
  me.p_subject = FullTrim(Subject)
 End Property

 Public Property Get Subject As String
  Subject = me.p_subject
 End Property

 Public Property Set Principal As String
  me.p_principal = FullTrim(Principal)
 End Property

 Public Property Get Principal As String
  Principal = me.p_principal
 End Property

 '*** Recipient address (mailto) functions ***

 Public Property Set MailTo As String
  me.p_sendto(FullTrim(MailTo)) = FullTrim(MailTo)
 End Property

 Public Property Get MailTo As String ' Get the first address only
  ForAll mto In me.p_sendto
   MailTo = mto 
   Exit ForAll
  End ForAll
 End Property

 Public Property Set SendTo As String ' Alias for MailTo
  MailTo = SendTo
 End Property

 Public Property Get SendTo As String ' Alias for MailTo
  SendTo = MailTo 
 End Property

 Public Sub AddMailTo(address As String) ' Additional address
  me.p_sendto(address) = address
 End Sub

 Public Sub RemoveMailTo(address As String) ' Remove address
  Erase me.p_sendto(address)
 End Sub

 ' *** Functions for CC address ***

 Public Property Set MailCC As String
  me.p_copyto(FullTrim(MailCC)) = FullTrim(MailCC)
 End Property

 Public Property Get MailCC As String ' Get the first address only
  ForAll mcc In me.p_copyto
   MailCC = mcc 
   Exit ForAll
  End ForAll
 End Property

 Public Sub AddMailCC(address As String)
  me.p_copyto(address) = address
 End Sub

 Public Sub RemoveMailCC(address As String)
  Erase me.p_copyto(address)
 End Sub

 ' *** Functions for BCC address ***

 Public Sub AddMailBCC(address As String)
  me.p_blindcopyto(address) = address
 End Sub

 Public Property Set MailBCC As String
  me.p_blindcopyto(FullTrim(MailBCC)) = FullTrim(MailBCC)
 End Property

 Public Property Get MailBCC As String ' Get the first address only
  ForAll bcc In me.p_blindcopyto
   MailBCC = bcc 
   Exit ForAll
  End ForAll
 End Property

 Public Sub RemoveMailBCC(address As String)
  Erase me.p_blindcopyto(address)
 End Sub

 ' *** Functions for email body ***

 Public Sub AppendText(bodytext As String)
  Call me.body.AppendText(bodytext) 
 End Sub

 Public Sub AppendDocLink(doc As NotesDocument, comment As String, linktext As String)
  If FullTrim(linktext) = "" Then
   Call me.body.AppendDocLink(doc, comment) 
  Else
   Call me.body.AppendDocLink(doc, comment, linktext) 
  End If
 End Sub

 Public Sub AppendNewLine(cnt As Integer)
  Call me.body.AddNewline(cnt) 
 End Sub

 Public Sub AddNewLine(cnt As Integer)
  Call me.body.AddNewline(cnt) 
 End Sub

 Public Sub AttachFile(filename As String)
  Call me.body.EmbedObject(1454,"",filename) 
 End Sub

 ' *** Send the mail
 Public Sub Send()
  Dim session As New NotesSession
  Dim richStyle As NotesRichTextStyle
  If me.subject<>"" Then
   maildoc.Subject = me.subject
  End If
  If ListItemCount(me.p_sendto)>0 Then
   maildoc.SendTo = ListToArray(me.p_sendto)
   maildoc.Recipients = ListToArray(me.p_sendto)
  End If
  If ListItemCount(me.p_copyto)>0 Then
   maildoc.CopyTo = ListToArray(me.p_copyto)
  End If
  If ListItemCount(me.p_blindcopyto)>0 Then
   maildoc.BlindCopyTo = ListToArray(me.p_blindcopyto)
  End If
  If me.p_principal<>"" Then
   Call maildoc.ReplaceItemValue("Principal", me.p_principal)
   ' If principal is set, we want to fix so mail looks like
   ' it is coming from that address, need to set these fields
   Call maildoc.ReplaceItemValue("From", me.p_principal)
   Call maildoc.ReplaceItemValue("Sender", me.p_principal)
   Call maildoc.ReplaceItemValue("ReplyTo", me.p_principal)
   Call maildoc.ReplaceItemValue("SMTPOriginator", me.p_principal)
  End If
  ' If NoReply is set (default), append red warning...
  If NoReply = True Then
   Set richStyle = session.CreateRichTextStyle
   richStyle.NotesFont = 4
   richStyle.NotesColor = 2
   richStyle.Bold = True 
   Call me.body.AppendStyle(richStyle)
   Call me.body.AddNewLine(1) 
   Call me.body.AppendText("*** DO NOT REPLY TO THE SENDER OF THIS MESSAGE! ***") 
   Call me.body.AddNewLine(1) 
   Call me.body.AppendText("*** IT IS AN AUTOMATED SYSTEM MAIL ***") 
  End If
  Call maildoc.ReplaceItemValue("PostedDate",Now())
  If me.p_principal<>"" Then
   Call maildoc.Save(True,False) ' Save in mail.box
  Else
   Call maildoc.Send(True)   ' Send mail normally
  End If
 End Sub

 ' *** Private functions called from within the class ***

 ' *** Convert list to array
 Private Function ListToArray(textlist As Variant) As Variant
  Dim i As Integer
  Dim temparray() As String
  ReDim temparray(0) As String
  ForAll t In textlist
   temparray(UBound(temparray)) = t
   ReDim Preserve temparray(UBound(temparray)+1) As String
  End ForAll
  ListToArray = FullTrim(temparray)
 End Function

 ' *** Count items in a list
 Private Function ListItemCount(textlist As Variant) As Integer
  Dim cnt As Integer
  cnt = 0
  ForAll t In textlist
   cnt = cnt + 1
  End ForAll
  ListItemCount = cnt
 End Function

End Class

 

Update: Please don’t use the email addresses in the example code above to actually send email…

14 Comments

Tip of the Week: Open a Hidden View in Notes

Earlier today I talked to someone who needed to delete a large number of ToDo entries in his mail file. I suggested to use either Ytria scanEZ or Martin-Scott NoteMan. As he did not have those tools installed on the machine he was working op, he thought about creating a hidden view and simple delete the documents from there.

When he opened up the design, he found a hidden view that would work, but he did not remember how to open it in the Notes client. I use this all the time, as I have several views I use for support or debug purposes, so I could tell him how to do it. I wanted to share that with everyone else, in case someone else need to do this and don't remember.

imageOpen the database and go to any view. This is important, if you are not in a view, you don't have the correct menu items. Then hold down Ctrl-Shift and select "View" and "Go To…". This will open a dialog box that will display all views, including the hidden ones (in parenthesis). Now you can release the Ctrl-Shift keys and select the view.

That's it.

By the way, I highly recommend the tools mentioned above.
NoteMan is just $395 for the whole suite. Any developer not using it should go get it. I blogged about the tool back in October 2010.
Ytria scanEZ is more, they don't post the price on their site but if I remember correctly it is something like $595. I use several of their tools, and they are really helping me keep a consistent look-and-feel to my applications. I mentioned the tools back in April 2010.
Both companies are usually represented at Lotusphere, so make sure you drop by their booths and get a demo.

 

 

2 Comments

Using Lotusscript lists to increase performance

Earlier this week I had to write an agent to read a CSV file and create Notes documents based on the content. Nothing difficult, this is something most of us do all the time. However, each entry in the CSV file had a producer code. I needed to do a lookup into the database and get the matching producer (also called agency) document and read a particular field from it. This field had to be stored in the new document I was creating, to tie the new entries to an existing producer/agency.

There were 3686 producers in the database, and 47731 entries in the CSV file. In other words, there were several entries per producer.

One solution is to load all agency documents into a list, then perform the lookup against the list using IsElement(). Since the number of producers are much fewer than the number of entries in the CSV file, I figured that I would save some substantial time vs. performing separate lookups for each entry.

I wrote three different version of the importer, as identical as possible.
The first version performed a view lookup for each entry in the CSV file to get the producer. It finished in 520 seconds.
The second version used a NotesViewEntryCollection to loop through all producers and load them into a list, then processed the CSV and used the list to get the producer documents. This one finished in 257 seconds.
The third version is using a combination. I do not pre-load the list with producer documents, instead I use IsElement() on each entry in the CSV file to check if the list already contains an element for the producer. If not, I perform the lookup and store the document into the list, to use next time the same producer is needed. This agent finished in 260 seconds.

The third version is the approach I have been using in the past. The benefit with that approach is that I only load the producers where I actually have data. Suppose only 1000 different producer codes are listed in the CSV file, I will just perform 1000 lookups, instead of 3686.

I use TeamStudio Profiler to time and profile my code, and I noticed something interesting. When the document was loaded into the list in program two and three, the time it took to read the value and store it into the new document was much shorter than when I had just performed the lookup, even if the code was identical. You can see it in the screenshots below. I hope André Guirard or someone else that knows more about the internals of Lotusscript can explain this. See update at the end of this article!

 

Version one – individual lookups for each entry (520 seconds to execute):
Individual Lookups

 

 

Version two – Preloading all producers before import (257 seconds to execute):

Pre-load into list

 

Version three – Lookup once, store in list for later use (260 seconds to execute):

Lookup once then store in list

   

Update: I just talked to André Guirard   about this. I isolated the slow code to doc.GetItemValue(), and André explained that the slowness I see is because of how Notes documents are handled. The note is not "cracked open" until an item is needed. So when I store the note in the list (example 2 and 3), it is opened once (which is slow), and then the following times I access the same document it is very fast, as it already is open. In the code with individual lookups (example 1), the document has to be "cracked open" over and over again, as the object is recreated for every entry.

 

 

2 Comments

Sony buying out Ericsson

Sony is buying out the Swedish telecom company Ericsson's share in Sony Ericsson, the mobile phone company they created back in 2001. Sony is paying Ericsson 1.05 billion Euros in cash for their 50% share of the company. Reports also say that Sony is planning to change the name of Sony Ericsson in the future.

Press release here.

image

 

 

0 Comments

Ernst & Young moving 150,000 users from Notes to Exchange

Update: As the article (swedish only) now has been published, I am re-publishing this blog entry.

At least that is what is indicated if you look at Neil Langston's profile at LinkedIn…

Supposedly it is a mail-only migration (according to my source), they will keep their Notes/Domino applications. So the end result is just the purchase of new servers and additional licenses being paid to Microsoft. I am sure Microsoft will spin this as a win for them and a loss for IBM, but I don't see IBM losing any money. If Ernst & Young is paying maintenance today, they will most probably continue to do that for a while, thus IBM will see pretty much the same revenue stream (minus a few mail-only servers, perhaps). If Ernst & Young are not on maintenance, IBM is already not getting any revenue, so no different there either.

Neil Langston LinkedIn profile

 

0 Comments

Another Pioneer Lost: Dennis Ritchie 1941-2011

This morning I woke up to the news that Dennis Ritchie died a couple of days ago. Some of my readers may not know who he was, he was never a showman like Steve Jobs, or took part in product launches. None the less, he was one of the big names and early pioneers in IT.

Ritchie was best known as the creator of the C programming language and a key developer of the Unix operating system, and as co-author of the definitive book on C, The C Programming Language, commonly referred to as K&R (in reference to the authors Kernighan and Ritchie).

Ritchie's invention of C and his role in the development of UNIX alongside Ken Thompson has placed him as an important pioneer of modern computing. The C language is still widely used today in application and operating system development, and its influence is seen in most modern programming languages. UNIX has also been influential, establishing concepts and principles that are now precepts of computing.

Source: Wikipedia

I remember when I started learning C, back in 1989. I had already been exposed to first Basic and then Pascal earlier, and until I understand the beauty of C, the whole thing with pointers confused me to no end. But eventually I got pretty decent at writing C code. I also started using Unix (HP-UX) about the same time. A couple of years later, I wrote a couple of small Windows program, again using C. Back in the early days of Windows, C was the only language you could use to write Windows applications.

It is amazing that a person who created some of the most widely used products is not more well known. Dennis Ritchie is at least as important in the history of computing as Steve Jobs. C and Unix may not be as sexy as iPhones, iPods and Macintosh computers, but they are probably more important if you look at the big picture. Apples OS X is based on Unix, and developers use Objective C to create programs and apps for OS X and iOS. Without Dennis Ritchie, the world would probably look very different.

 

Dennis Richie 1941-2011

 Dennis Ritchie
1941 – 2011

 

0 Comments

Samsung Nexus Prime – Verizon exclusive?

Additional details about the upcoming release from Samsung of the Nexus prime, the first phone with Android 4.0 (Ice Cream Sandwich) have been published on some websites.

The new phone, which is expected be be unveiled next week, has a 4.65" display with 1280×720 resolution, a 5MP camera, 1 GB of system RAM and 32 GB of built-in user memory. The dual-core processor is running 1.2 GHz or 1.5 GHz (according to what source one listen to). Reports claims that it is an LTE (Long Term Evolution) phone, and that it will be available only through Verizon, at least initially.

The Nexus Prime will probably be called Droid Prime when it is sold by Verizon. AT&T is also working on it's LTE network, but the two carriers use different frquencies, so they are not compatible with each other. The question is how the Nexus Prime will work overseas, where LTE networks are also being built right now, using different frequencies. Some reports say that Nexus Prime will support HSPA or HSPA+, but what about GSM/3G support, for fallback internationally? And will there be quad-band GSM support? Hopefully those questions will be answered next week.

Samsung Nexus Prime

 

 

0 Comments

Steve Jobs – Gone but not Forgotten

I have never owned a Macintosh, iPod, iPhone or iPad. I have however used different products from Apple over the years. I have even used the NeXT.

Back in 1983 (if I remember correctly), I spent a week in a German school (my mom had this idea about sending me to Germany to stay with relatives or friends for a few weeks every summer to improve my German), and in their computer room they had Apple IIe that I got to play around with a little. I was programming on a similar computer (the Swedish ABC 80) at my school, and I found them fairly similar. About the same time, I started hearing about Apple Lisa, and a year or so later I saw my first Macintosh at a computer trade show in Stockholm.

But it was not until a few years later (I think in 1987) I got to actually use a Macintosh. My godmother's husband owned a printing business, and he — like so many others in the graphics industry — used Macintosh. As I "knew computers", I was called in to figure out a few things and teach him. I think it had to do with sending files through a modem or something, on his Macintosh II. At this time I was using another Swedish computer at school, the CP/M-86 based Compis, and the graphics environment on the Macintosh was very impressive.

Then in 1988 I started working at Microsoft, and I was assigned a Macintosh SE (in addition to an IBM PS/2 Model 60). Now I got to use it a bit more extensive, but when I started looking at buying my own computer a year later, the higher price and fewer choices when it came to software made me choose the DOS/Windows platform.

In 1992 I got to play with a NeXT at the place I worked, and it was way cool. I was not able to spend much time on it, but I could see that this was a totally different version of Unix compared with the mostly text-based systems I had used before. As we all know, NeXT was purchased by Apple and became the foundation of OS X.

In 1993 I started my career as a journalist and technical writer. All the desk editors used Macintosh (and Quark Xpress), and a few times I got to actually edit my own articles on them. Once, I believe some time in 1995 or 1996 I even filled in as a desk editor for a day, creating a page or two. This was the last time I actually used a Macintosh.

I have since played with iPhones (my son got one, while I am still on Blackberry), iPad and iPod. All great products, easyto use and powerful. But for me, as a technical person, I am willing to give up some usability and ease of use for a more open and flexible environment. That is why I have a Cowon A2 as my MP3 player. When I got it, it was technically superior to anything Apple had, with a 4 inch widescreen display, built-in speakers and microphone and video in and out. It also supports AVI, Divx/Xvid, Flac and many other formats that iPod did not support without converting. I could also copy files to it directly by connecting it through USB, without having to use a special program like iTunes. But it is bigger, bulkier and heavier than an iPad. The user interface is not nearly as developed. It simply lack the sexiness everyone associate with Apple products.

Steve Jobs was a visionary. Using the words of the Swedish criminal writer Leif GW Persson: "He could see around corners." Steve Jobs could see the future, and managed to deliver products that shaped the future. New products are measured againts the Apple products, and even if they technically may be better, they fall short in sales. This is not just due to the marketing, but also because the Apple products are such household names. Today many people say "iPod" instead of the more generic "MP3 player", and "iPad" is on the way to head that way as well.

The software industry has lost one of it's pioneers and biggest leaders. Steve Jobs family has lost a husband and father, and Apple has lost a visionary.

image

 

0 Comments

Create and update Calendar reminders from Notes document

At work I was asked yesterday if I could give the users a button to set reminders for meetings/actions directly from a document in one of our Notes applications. So I created a simple solution where I added two action buttons and a field to the form from which the calendar entry (reminder) would be created.

I wanted to share this code. It is nothing complicated, and the main functionality consists of some code Palmi Lord posted in LDD last year.

The new field on the form is called 'CalendarUNID' and will contain the Universal ID of the calendar entry. If the field is blank, I will display the 'Add to Calendar' button, if it contains a value I will display the button 'Update Calendar Entry' instead. This field is also used by the update function to get to the original calendar entry. Note that the reminder is created in the current user's calendar, and can only be successfully updated by the same user.
The date field used is named 'InspedtionDate', and I set the time to 8am. In the reminder I am also putting the address of the insured (the application is used by an insurance company) in the location field, and the insured/account name and policy number in the subject field.

 

'Add to Calendar' button

Hide-when formula:

CalendarUNID!="" | InspectionDate=""

Lotusscript Code:

Sub Click(Source As Button)   Dim ws As New NotesUIWorkspace   Dim uidoc As NotesUIDocument   Dim calentry As NotesDocument   Dim appdate As NotesDateTime   Dim location As String   Dim subject As String   Dim calunid As String    Set uidoc = ws.CurrentDocument   Set appdate = New NotesDateTime(uidoc.FieldGetText("InspectionDate") _ & " 08:00:00 AM")   ' Get physical location of insured   location = uidoc.FieldGetText("LocAddress") & ", "   location = location & uidoc.FieldGetText("LocCity") & ", "   location = location & uidoc.FieldGetText("LocState") & " " _ & uidoc.FieldGetText("LocZIP")   ' Build subject line for reminder   subject = "Inspection Due ("   subject = subject & uidoc.FieldGetText("AccountName") & " - "   subject = subject & uidoc.FieldGetText("PolicyNumber") & ")"   calunid = createReminder( appdate, location, subject)   Call uidoc.FieldSetText("CalendarUNID", calunid)   Call uidoc.Save()   Call uidoc.RefreshEnd SubFunction createReminder( dateTime As notesDateTime, _ location As String, subjectStr As String ) As String   Dim sess As New NotesSession   Dim userMailDb As New NotesDatabase( "", "" )   Call userMailDb.OpenMail   Dim reminderDoc As New NotesDocument( userMailDb )   Dim DTItem As NotesItem     With reminderDoc      .Form = "Appointment"      .ReplaceItemValue "$Alarm", 1      .ReplaceItemValue "$AlarmDescription", subjectStr      .ReplaceItemValue "$AlarmMemoOptions", ""      .ReplaceItemValue "$AlarmOffset", 0      .ReplaceItemValue "$AlarmUnit", "M"      .Subject = subjectStr      .Alarms = "1"      .CalendarDateTime = dateTime.lsLocalTime      .StartDate = dateTime.lsLocaltime      .StartTime = dateTime.lsLocaltime      .StartDateTime = dateTime.lsLocaltime        .EndDate = dateTime.lsLocaltime      .EndTime = dateTime.lsLocaltime      .EndDateTime = dateTime.lsLocaltime        .AppointmentType = "4"       .Location = location      .Category = "Service Plan Activity"      .Save True, False       .ComputeWithForm True, False      .Save True, False      .PutInFolder( "$Alarms" )   End With   createReminder = reminderDoc.UniversalIDEnd Function

'Update Calendar Entry' button:

Hide-when formula: CalendarUNID=""

Lotusscript Code:

Sub Click(Source As Button)   Dim ws As New NotesUIWorkspace   Dim uidoc As NotesUIDocument   Dim session As New NotesSession   Dim calentry As NotesDocument   Dim appdate As NotesDateTime   Dim location As String   Dim subject As String   Dim calunid As String   Set uidoc = ws.CurrentDocument   Set appdate = New NotesDateTime(uidoc.FieldGetText("InspectionDate") _ & " 08:00:00 AM")   ' Get physical location of insured   location = uidoc.FieldGetText("LocAddress") & ", "   location = location & uidoc.FieldGetText("LocCity") & ", "   location = location & uidoc.FieldGetText("LocState") & " " _ & uidoc.FieldGetText("LocZIP")   ' Build subject line for reminder   subject = "Inspection Due ("   subject = subject & uidoc.FieldGetText("AccountName") & " - "   subject = subject & uidoc.FieldGetText("PolicyNumber") & ")"   calunid = uidoc.FieldGetText("CalendarUNID")   Call updateReminder(calunid, appdate, location, subject)   Call uidoc.Save()   Call uidoc.RefreshEnd SubSub updateReminder(unid As String, dateTime As notesDateTime, _ location As String, subjectStr As String )   Dim sess As New NotesSession   Dim userMailDb As New NotesDatabase( "", "" )   Call userMailDb.OpenMail   Dim reminderDoc As NotesDocument   Dim DTItem As NotesItem    Set reminderDoc = userMailDB.GetDocumentByUNID(unid)   If reminderDoc Is Nothing Then      Msgbox "Failed to locate calendar entry."       Exit Sub    End If   With reminderDoc       .ReplaceItemValue "$AlarmDescription", subjectStr     &n
bsp;.Subject = subjectStr      .CalendarDateTime = dateTime.lsLocalTime      .StartDate = dateTime.lsLocaltime       .StartTime = dateTime.lsLocaltime      .StartDateTime = dateTime.lsLocaltime         .EndDate = dateTime.lsLocaltime      .EndTime = dateTime.lsLocaltime      .EndDateTime = dateTime.lsLocaltime        .Location = location      .Save True, False      .ComputeWithForm True, False      .Save True, False   End WithEnd Sub

 

 

 

0 Comments

Dynamic tables in classic Notes

Using Xpages, you can do many neat things in Notes. But I am still doing all my development in classic Notes, despite being on Notes/Domino 8.5.2. The reason is simple. Memory. There is just not enough memory to run the standard client in our Citrix environment, so all users are using the basic client. And Xpages is not working in the basic client.

So how do I solve issues like multiple documents tied to a main document, being displayed inline in the main document? Well, I solved this a long time ago by using a rich text field and building the document list on the fly. I am sure many other Notes developers use this technique. I know it was discussed in a session at Lotusphere several years ago, perhaps as long ago as 7-8 years ago. So this is of course not anything I came up with myself.

In this post I just want to share how I am using this, and perhaps this can help someone.

The database (download it here) is very simple. It contains three (3) forms and two (2) views. In addition I have some icon as image resources, but that is just to make things pretty.

 

Forms

The ‘MainDoc’ form is the main document, being displayed in the view ‘By Last Change’. It contains a few fields:
‘Title’ – a text field where the user can enter a title/description
‘Data’ – a rich text field where the rendered table rows will be displayed
‘ParentUNID’ – a hidden computed-when-composed field where the UniversalID of the document is stored
‘EntryCount’ – a hidden text field used to count the number of entries (to display in the views)
‘LastEntry’ and ‘LastEntryDate’ – Hidden text fields to keep track of the last person who edded an entry
The form also contains some regular action buttons (‘Save’, ‘Edit’, ‘Close’), as well as ‘Add entry…’, an action button with two lines of Formula language:
@Command([FileSave]);
@Command([Compose];”Entry”)

image

The QueryOpen event, which executes before the form is opened in the front-end and displayed to the user, contains some Lotusscript code. This event let us modify the document in the backend, which is where rich text fields can be updated. The code – which I will post at the end of this article – is very simple, it is just over 50 lines of code.

 

The ‘Entry’ form is where the entries are created. It is set to let fields inherit values from the document it is created from, and contains a couple of fields, some action buttons and some Lotusscript code as follows:
‘ParentUNID’ – editable, default value is “ParentUNID”, to inherit the value from MainDoc
‘Created’ and ‘Creator’ – computed-when-composed to store date/time and user name of creator
‘ItemDate’, ‘Issue’ and ‘Action’ – user data fields.

The ‘Entry’ form contains the following Lotusscript code:

(Declarations)
Dim callinguidoc As NotesUIDocument
Dim callingdoc As NotesDocument

Sub Initialize
   Dim ws As New NotesUIWorkspace
   Set callinguidoc = ws.CurrentDocument
   If Not callinguidoc Is Nothing Then
      Set callingdoc = callinguidoc.Document
   End If
End Sub

Sub Terminate
   Dim ws As New NotesUIWorkspace
   If Not callingdoc Is Nothing Then
      Call ws.EditDocument(False,callingdoc)
   End If
   If Not callinguidoc Is Nothing Then
      Call callinguidoc.Close(True)
   End If
End Sub

The code simply store the calling document (MainDoc) in a global variable when being opened/created, and then force a save and reopen of it when closing. This will trigger the QueryOpen event in the MainDoc document to rebuild the rich text content.

The action button ‘Delete’ contains some code to flag the current document for later deletion. I normally do not allow users to delete document in production databases, instead I use a field to indicate that the document should be ignored in views and lookups. Later I run a scheduled agent to actually delete the flagged documents.

Sub Click(Source As Button)
   Dim ws As New NotesUIWorkspace
   Dim uidoc As NotesUIDocument
   Dim session As New NotesSession
   Dim doc As NotesDocument
   Dim unid As String
   Dim result As Integer

   result = Msgbox("Are you sure you want to delete this document?",_ 
   4+32,"Delete Document")
   If result = 7 Then
      Exit Sub
   End If
   Set uidoc = ws.CurrentDocument
   unid = uidoc.Document.UniversalID
   Call uidoc.Close(True)
   Set doc = session.CurrentDatabase.GetDocumentByUNID(unid)
   doc.flagDelete = "Yes"
   Call doc.Save(True, False)
End Sub

The final and last form is a repeat of the Entry form, but formatted the way you want each entry/row to be displayed in the rich text field. I call the form ‘RecTemplate’:

image

 

Views

In addition to the forms, there are two views. ‘By Last Change’ is just a normal view to display the documents based on the ‘MainDoc’ form. The other one is the one used by the code. It is a hidden view called ‘(LookupEntry)’. The first column is sorted (to allow lookups using col.GetAllDocumentsByKey() and contains the ParentUNID field. I have the column categorized as well, but that is really not needed. I also added two additional columns to make it easier for you as developer to look at the content. The view selection is as follows:
SELECT Form=”Entry” & flag_Remove=””

 

Lotusscript for QueryOpen on ‘MainDoc’ form

Finally the code that makes it all happen. The code simply perform a lookup in the view (LookupEntry) to get all entries associated with the current document and updates a few fields (mainly for view display purposes). The code then loop through the entries and for each entry creates a template document and render it into the rich text field. That is basically it.

Sub Queryopen(Source As Notesuidocument, Mode As Integer, Isnewdoc As Variant, _
    Continue As Variant)
    Dim session As New NotesSession
    Dim db As NotesDatabase
    Dim view As NotesView
    Dim col As NotesViewEntryCollection
    Dim entry As NotesViewEntry
    Dim entrydoc As NotesDocument
    Dim thisdoc As NotesDocument
    Dim datafield As NotesRichTextItem
    Dim templatedata As NotesRichTextItem
    Dim entrydata As NotesRichTextItem
    Dim doclink As NotesRichTextItem
    Dim template As NotesDocument
    Dim parentunid As String

    If source.IsNewDoc Then
        Exit Sub  ' Exit if new document, we do not have a ParentUNID or entries yet
    End If
    Set thisdoc = source.Document
    Set datafield = New NotesRichTextItem(thisdoc,"Data")
    parentunid = thisdoc.GetItemValue("ParentUNID")(0)
    Set db = session.CurrentDatabase
    Set view = db.GetView("(LookupEntry)")
    Set col = view.GetAllEntriesByKey(parentunid,True)
    Call thisdoc.ReplaceItemvalue("EntryCount",Cstr(col.Count))
    Set entry = col.GetFirstEntry
    If Not entry Is Nothing Then
       Call thisdoc.ReplaceItemvalue("LastEntryBy", _
       entry.Document.GetItemValue("Creator")(0))
       Call thisdoc.ReplaceItemvalue("LastEntryDate", _
       Format$(Cdat(entry.Document.GetItemValue("ItemDate")(0)),"mm/dd/yyyy"))
       Call thisdoc.Save(True,True)
   Else
       Call thisdoc.ReplaceItemvalue("LastEntryBy","")
       Call thisdoc.ReplaceItemvalue("LastEntryDate","")
       Call thisdoc.Save(True,True)
   End If 
   Do While Not entry Is Nothing
       Set entrydoc = entry.Document
       Set template = New NotesDocument(db)
       Call template.ReplaceItemValue("Form","RowTemplate")
       Call template.ReplaceItemValue("ItemDate", _
       Format$(Cdat(entrydoc.GetItemValue("ItemDate")(0)),"mm/dd/yyyy"))
       Call template.ReplaceItemValue("Creator", _
       entrydoc.GetItemValue("Creator")(0))
       Call template.ReplaceItemValue("Issue", _
       entrydoc.GetItemValue("Issue")(0))
       ' *** Copy Rich text Field "Issue"
       Set entrydata = entrydoc.GetFirstItem("Issue")
       Set templatedata = New NotesRichTextItem(template,"Issue")
       Call templatedata.AppendRTItem(entrydata)
       ' *** Copy Rich text Field "Action"
       Set entrydata = entrydoc.GetFirstItem("Action")
       Set templatedata = New NotesRichTextItem(template,"Action")
       Call templatedata.AppendRTItem(entrydata)
       ' *** Add doclink to entry
       Set doclink = New NotesRichTextItem(template,"DocLink")
       Call doclink.AppendDocLink(entrydoc,"Open Entry")
       Call template.ComputeWithForm(True,False)
       ' *** Refresh form
       Call template.RenderToRTItem(datafield)
       Set entry = col.GetNextEntry(entry)
   Loop
   Call thisdoc.ComputeWithForm(True,False)
End Sub

And this is what it looks like after adding three entries:

image

It is of course easy to change the sort order, field to use for sorting, etc.

 

Update: 2011-08-17 – Fixed download link.

 

7 Comments

Lotusscript frustrations…

This morning I ran into a problem that frustrated me to no end, until I finally figured it out. The answer was of course in the online help…

I had a class that processed people associated with a file. Each person is assigned (in creation order) a number, starting with 1. The class load the person documents when the class is initiated, and store the info in an array.

What happend is that I got the "Subscript out of range" error on a particular file. What I found was that it had 10 person documents, numbered 2 through 11. This is of course 10 persons, so the array was dimmed as 1 to 10. When I hit the 10th person, it had the number 11 and my code blew up when I tried to put the info into the 11th (non-existing) array element.

OK, this should be easy to fix. Just check the person number field and if the value is larger that the size of the array, redim the array using Redim Preserve. Nope, this line now gave me the same error, just in a different location, on the line where I do Redim Preserve person(1 to personcount) as NotesDocument.

The solution was easy. You can not change the lower bound of the array, so the correct code would be Redim Preserve person(personcount) as NotesDocument instead. Then it worked.

Just a small tip I wanted to share.

 

 

0 Comments

Vacation Memories: Royal Castle in Stockholm

During my vacation I took a tour of the Royal Castle in Stockholm. Despite living in the Stockholm area for 28 years, and then going back top visit pretty much every year, I had still never taken a tour of the actual castle. This year I remedied this, and while I walked through the rooms I took some pictures. I am posting some of them below.

Due to the dark interior, and in some cases the bright sunshine outside, I used High Dynamic Range (HDR) to bring out details and contrast. Enjoy!

DSC_0312_HDR copy

DSC_0304

DSC_0305

DSC_0307

DSC_0385_HDR

DSC_0388_HDR copy

DSC_0286

DSC_0403_HDR

Vaktparad

 

0 Comments

Vacation Memories: Denmark

I have been on vacation in my native home country Sweden, but now I am back in the USA and Texas, after two and a half weeks of meeting family and friends, eating some great food as well as experiencing and seeing a number of new things.

Karl-HeinzOne of the most emotional things on this trip was one of those new experiences. As I written about before, I am named after my uncle Karl-Heinz, who was killed in the last days of WWII. He is buried in Copenhagen, as he died while being treated for his wounds at a German military hospital there. I have never visited his grave, so when we decided to take a tour of Sk? to view places like Ale´s Stones, Kivik and then take the ?esunds Bridge over to Denmark, I wanted to take the opportunity to visit my namesake.

Karl-Heinz Groeling was born on December 20, 1919, soon after my grandfather returned from WWI. My mother was born in 1926, and two more girls (Anneliese and Katja) in the next few years. My uncle was, like so many other boys, interested in airplanes, and he learned to fly gliders and sailplanes in the 1930´s. After the Treaty of Versailles after WWI, Germany was not allowed any armed aircrafts. The workaround was to train future pilots in gliders. In 1939 he joined the Luftwaffe (German Air Force) as a glider pilot. He took part in the invasion of Crete in 1941, and was later reassigned to a flak (anti-aircraft artillery) regiment. On February 23, 1945 he was wounded (shot through the left lung) and evacuated to a hospital ship. On March 10 he arrived at the hospital in Copenhagen, but during the night between March 15 and 16 his condition deteriorated and he died in the morning at 03:10.

 

DSCN0056Thanks to the internet, I quickly found Volksbund Deutsche Kriegsgr?rf??rge who has an online database of German war graves. I found his grave and located the cemetery. I was surprised and happy to find that it was within walking distance from my hotel, just about a mile and a half away. So the morning after arriving in Copenhagen I walked to Vestre Kirkeg?, a beautiful cemetery with nice landscaping and art decorations, and found the German section (picture right), where 4,636 German soldiers and 4,019 civilian refugees are buried.

After a brief search, I located the grave. Due to limited space, most graves contained more than one person. Most contained two, but I found some containing up to five. As I mentioned earlier, it was strangely emotional to stand there, in front of that simple stone cross. He was only 25 years old when he died, and had just started his life. If he had managed to survive two more months, the war would have been over.

While I was on the way back to Stockholm after the trip to Denmark, the massacre on Ut??/span> in Norway happened, and 77 more people, most of them as young or younger than my uncle lost their lives. More lives wasted for political reasons, by another mad man.

DSCN0067

 

0 Comments

Cheese Cake – Swedish style

In Sweden we have a dish called ostkaka, which translates to "cheese cake". It should not be confused with the American cheese cake, A better name might be "curd cake".

This dessert was for a long time one of the things from Sweden I missed living in Texas. However, a few years ago I remembered that we made it in school (in Sweden home economics/cooking is a mandatory subject in Junior High School), and I found a recipe that worked for me. Over the last year I have been making this dish a number of times, and I have experimented a little to see what worked out.

Here is one variant of the recipe that should be easy to follow. Good luck!

 

Ingredients:
6-7 large eggs
1/2 cup granulated sugar (or Splenda for a low carb/sugarfree version)
1/2 cup flour
4 lbs cottage cheese (small curd) – should be the regular version, not low fat
Just over 1 1/2 cup heavy whipping cream
2 oz chopped almonds (fine)
2 tsp almond essence

Topping:
Whipped cream
Jam (Cloudberry, Blueberry or Queens Berry, can be found at IKEA) or frozen (defrosted) fruit like strawberries, blackberries or blueberries.

 

Instructions:
Beat eggs and sugar/Splenda with a mixer. Add almond essence and flour.
Mix down half the cottage cheese. Add the chopped almonds and mix.
Add the rest of the cottage cheese.
Whip the cream, should be nice and solid. Fold it into the mix. Do not beat it or stir too vigorously.
Pour in a buttered and oven safe dish.
Bake in 350 degrees for about an hour (will not hurt to go over the time, I sometimes had to bake it for 75-85 minutes).
It should be all solid but soft. The top surface should be nice and dark brown.

Serve warm (but not hot) with berries/jam and whipped cream.
 

image

 
Note: This is not my cheesecake, I will upload a picture when I get home tonight.
 

 

 

0 Comments

The Lord of The Rings (Extended Edition) and Star Wars coming on Blu-ray

A couple of interesting Blu-ray are scheduled for this fall.

The Lord of The Rings Extended Edition will be released on June 28. The three movies are probably my favourites. However, it seems like there are really not many extras compared to the DVD Extended Collectors Edition I already own on DVD, so the only reason to purchase this set would be for the picture quality.

Star Wars: The Complete Saga is scheduled to be released on September 16. The box will include all six movies (Episode I-VI). There are a lot of complaints already, as the number of extras and bonus material is considered less than satisfactory by many fans.

I have said for a long time that when The Lord of The Rings is released on Blu-ray, that's when I will update my system at home from DVD to Blu-ray. I might do it this fall anyway, but I am not as eager as before. Perhaps I should wait for The Hobbit to be released, I am sure we will see a new box released by then…

 

 

0 Comments

Memorial Day

Today the United States is celebrating Memorial Day, a day when the fallen soldiers are remembered/honored. In November, Veterans Day is celebrated, honoring all men and women who served in the US military.

I am originally from Sweden, a neutral country who have not been in a formal war since 1809. Despite this, Swedish soldiers have fought and died in several wars since then, though. In 1940, many Swedes volunteered to help Finland in the Winter War against the invading Soviet Union.
In the early 1960’s, Sweden sent ground troops as well as jet fighters to Congo during the Katanga crisis.

Sweden has also lost troops in Afghanistan, where about 500 Swedish soldiers currently are part of ISAF. Five Swedish soldiers and one dual-citizen Swedish-Norwegian who was serving with the Norwegian forces have been killed this far. They are pictured below.

image

So my Memorial Day is not just to honor the men and women of the United States armed forces who died over the years, but all soldiers who served their countries to the best of their ability and paid the ultimate price.

In my close family, I have my uncle Karl-Heinz, whom I am named after. He served in the German Air Force (Luftwaffe) during WWII, first as a glider pilot, later in an anti-aircraft artillery unit.

image

Unteroffizier (NCO) Karl-Heinz Groeling
1919-1945

 

 Note: I wrote this entry over the weekend, but due to the BleedYellow blogs having technical issues, I was not able to post it until this morning.

 

0 Comments

Icon Collection for Notes Applications

As of Notes 8.5.2 the Notes client now support prettyapplication icons, using thePNGformat. A handful of icons have been released on OpenNTF.org by Mary Beth Raven, but I have been looking for more.
 
I found a nice set of icons yesterday, and I thought they might look good as application icons.Imailed the page owner where I found them to find out moreThe icons were purchased from IconExperience, a full set of the X-Collection (XP look) is $289 and the V-Collection (Vista/Windows 7 look) is $379.I consider that a very good price.
 
In the mean time,I decided to just try some of the iconson a couple of my existing databases. I think it really makes a huge difference. See below for a sample.I believe I am covered under Fair Use when publishing this screenshot since this only are 8 of the 2,400 icons in the collection.
 
I think the important thing is to getthe look ofthe icons consistent, not mix different looks. I happen to know that The Lotusphere Widow plan to design some icons in the near future,that will match the icons posted on OpenNTF.So keep your eyes open…
 
PNG Icons in Lotus Notes 8.5.2
To clarify, the upper row in each sectionare the applications, and the row below are the templates.
In previous versions of Domino Designer I used a solid red background colorbehind the icons to indicate templates, that would be a nice enhancement in un upcoming version of Domino designer, to have the chicklets in a different color for templates.
 
 

 

0 Comments

Product Review: Gillette Thermal Face Scrub

This is a non-technology related post, but I wanted to share my experiences with this particular product.

I have always been shaving with a razor and gel, that is how my dad did it and I followed his lead. For the last 20 years, I have been using Gillette products. They simply worked the best for me. YMMV
As a true geek, I have always been using their latest model of razors, and I am currently using the Fusion Proglide Power. A couple of weeks ago, I had to buy new blades (they are getting pretty outrageously expensive, though!), and in the packet I got s small travel size sample of their Thermal Face Scrub.

I have always been very skeptical to that kind of products. Soap and water have worked for me the last 40 years”…” A year or two ago I got a face scrub to use once a twice per week, and even if I have to admit it felt nice, I dis not see a need to get another one when I ran out.
So I tried this sample from Gillette, and it was very interesting. I rinsed my face as usual, then applied a small amount of the product. The skin got nice and warm. I then rinsed it off, according to the instructions, applied the shaving gel I use and shaved.

The result? Well, I almost hate it when the commercials or advertising is correct. But it was a noticeable difference. I shaved at 6.30am, and by 6pm, my face was still as smooth as in the morning.
I had used brand new blades, which might have made a slight difference, so I tested it a few days later by using brand new blades on one side of the face, and some well used (about 1 month) blades on the other side. The difference was hardly noticeable, with of course the side where I used the old blades felt slightly rougher. Still an improvement compared to not using the product.

I do not know if long-term use of this product will have any effect on the skin, but I usually don´t shave during the weekend, allowing the skin to rest. I really like the product and purchased a full size tube a few days later, saving the travel size one for travel.

Disclaimer: Sample received free, purchased full-size product.

 

0 Comments

CrashPlan online backup

I have been thinking about using one of the many online/cloud services for backup. I have plenty of photos (200+ GB) and also other important documents I don’t want to lose. Today I have them mirrored on an external USB drive, but in case something happens to my place, like fire or burglary, that drive will most probably also be gone. So an online service would make sense.
There are a number of contenders out there. Carbonite and Mozy are perhaps the most high profile ones, because of their advertising. Mozy just switched from unlimited storage to plans where you pay more if you store more.
Carbonite still offers online storage for $59/year, but they don’t support backups of large files (4GB+), don’t include video files by default, and don’t support external drives. There are also bandwidth restrictions. Up to 35 GB you get full speed, then it drops to 512kbit/s up to 200 GB. After that the bandwidth is throttled down to 100 kbit/s. An online calculator showed that 250 GB would take me 83 days to upload.And I actually have closer to 400 GB that I want to backup. Both services also lack a Linux client, the clients are only available for Windows and MacOS.
However, I stumbled on a new service yesterday, called CrashPlan.Not only does it cost about the same as Carbonite, at $5/month or $49.99/year, they also claim not to have bandwidth restrictions (throttling). In addition they have clients for Linux and Solaris, as well as apps for Android and iOS.
But the really cool features are some that Carbonite and Mozy does not have, and that to me are very useful. You can backup not only to the online storage on the CrashPlan servers, but also to external USB drives, network drives or even a friend across town or in another country. You can create different backup sets, and have them being backed up to different places.
I installed the client at home, and created a few backup sets. My photos are backed up to my external 1.5 TB Seagate drive, as well as to the CrashPlan servers. My documents and images (like icons and graphics I use for my Notes/Domino applications) are backed up to the online storage only. My MP3 files are backed up only to the external drive.
I also plan to setup CrashPlan on my sister’s computer in Sweden and backup my photos there. The files are encrypted on the external drive and at the friend/family member, so they can not see the filenames or the content.
I think this combination of backups in multiple places is brilliant. I am currently using the 15 days free trial, but I intend to purchase theservice in the next day or two, if it lives up to the promises.
Update: CrashPlan is now $59.99/year or $5.99/month for the cheapest unlimited plan. Details here. Also, something both me and other noticed is that the upload does take time, about the same as Carbonite. So they have some kind of bandwidth limitation, but it seem to be constant, or possibly they just have so much traffic that their bandwidth is not enough.

 

0 Comments

End of content

No more pages to load