How to write better code in Domino Designer – Part 3

Welcome to the third part of this series of articles about how to write better code for the Notes/Domino platform. In part 1 I wrote about creating your forms in a way so you get an instant idea of how they work (what is hidden, computed-for-display fields, etc) and in part 2 the subject had moved to Lotusscript, more specifically variable names and comments. As already mentioned in that last article (as well as in some of the comments), breaking out code into functions is a great way to make code easier to read as well as dramatically easier to maintain. That is what I will focus on in this article.   Functions In Lotusscript, the functions are declared the same way as in Visual Basic: Function YourFunctionName(argument As DataType) As ReturnDataType YourFunctionName = ReturnValue End Function You can have any number of arguments, from none to many. However, it is often suggested to keep the number of arguments to a minimum. I try to not use more than three arguments, unless there is no way around it. If you need to send a lot of information into a function, use an array or a custom data type instead. Then it is easier to change the arguments later, without changing the signature of the function. I have occasionally seen that cause issues in nested script libraries. Also, with many arguments the function declaration will probably not fit on your screen, unless you wrap the line. I try to always keep code visible in the editor, as it is easy to miss things when you have to scroll sideways to view code. So let's take a look at one of the code samples in my last article, the one where we call names.nsf to get the email address for the current user. Let's make this a function (like Kenneth Axi suggests in his comment), but we will make it a more generic function, returning the email address for any user present in address book, not just current user. Here is the original code: key = session.CommonUsername Set view = db.GetView("People") Set doc = view.GetDocumentByKey(key) emailaddress = doc.GetItemValue("InternetAddress") Below is how I would write the function. We will talk more about error handling later, but you want to make sure to always check return values before you use them. The function takes one argument, the name of the user we want the email address for, and returns the email address, or blank if no user/address was found. The username must be in the same format as the names are being displayed in the view we doing the lookup in. Function getEmailAddress(username As String) As String Dim personname As NotesName Dim nabdb As NotesDatabase Dim nabview As NotesView Dim persondoc As NotesDocument If username = "" Then MsgBox "Username is empty. Exiting.",,"No Value" Exit Function End If '*** Create NotesName object and get CommonName part. '*** We do this so username argument can be in any format. Set personname = New NotesName(username) '*** Use Domino Directory on main server Set nabdb = New NotesDatabase("Domino1/MyDomain","names.nsf") If nabdb Is…

2 Comments

How to write better code in Domino Designer – Part 2

We already talked about how to make your forms and views easier to maintain. But in most Notes/Domino applications, a majority of the code is written in Lotusscript (at least for traditional applications, XPages are outside the scope of this discussion for now). It is not hard to write easy to read Lotusscript code. Just follow some simple rules. As always, there are more than one way to do things, but this is how I do it (most of the time). I found that this works for me.   Variables Always use Option Declare (or Option Explicit) in all your code. This will prevent you from getting errors later when (not if!) you spell a variable wrong. Option Declare forces you to declare all variables before you use them. It also helps you by warning if a function or method is called with a variable of the wrong data type. Most of your bugs can be avoided using Option Declare. Use it! Give your variables meaningful names. There is no need to abbreviate variables and/or give them cryptical names, or  you will just spell them wrong later. Use a naming system that makes sense to you. As I mentioned in my previous post, I avoid Hungarian notation, it just makes the variables hard to read. Since Lotusscript is a strongly typed language, the compiler is taking care of data types. I however do use some indicators for certain variables. For the few global variables I use, I do prefix them with g_, as I then can give local variables a similar name. By giving your variables meaningful names, the code also becomes largely self-documenting. Be consistent in how you capitalize your variables. I normally write all variables in lower case only, but that is my personal preference. That way they are easy to distinguish from built-in Lotusscript functions and properties of objects. Some programmers prefer some version of CamelCase, which is fine. I occasionally use that, especially in Javascript. Be consistent in how you name related variables. If you have a variable named startpos to indicate the beginning position of a string within another string, name the other variable endpos, not posend. Think through all variables you need so you give them good names. The variables posstart and posend are not as easy to read and understand as startpos and endpos. Of course, using CamelCase, posStart and posEnd are much easier to decypher. Use the same/similar naming convention for the Domino objects as IBM uses in the online help. Use ws, uidoc, session, db, doc, etc. If you need more than one object of any type, e.g. NotesDatabase, name them in a consistent way. I always use thisdb for the database the code is executing in, nabdb for names.nsf, archivedb for (you guessed it!) an archive database, etc. Declare the variables at the beginning of the code/subroutine/function. Don't declare them throughout the code, this makes it much harder to find them later, when you perhaps need to see what data type they are. Declare the variables in…

19 Comments

How to write better code in Domino Designer – Part 1

The inspiration to this series of blog entries partially comes from looking at code posted in the developerWorks forums. Some of it is extremely hard to read and understand, even if you ignore the fact that the forum removes indentation from the code. If you write code that is hard to read, your applications will be hard to maintain. Write the code so it is easy for the poor person who will be maintaining the code in the future. Most probably that person will be you. You might also have to post your code in the developerWorks forum or on StackOverflow for help. If the code is hard to read and understand, you might not get very much help. What I will talk about is what you can do to become a better programmer, and write code easier to maintain. After being a Notes developer since 1996, I have learned a bit about what makes a Notes application easy to read and to maintain. I want to share some of my thoughts on this blog, or in the words of Kevin Spacey at Lotusphere 2011: "sending the elevator down". Hopefully it will help someone. I will not talk to much about basic programming concepts or how to program in Domino Designer. I will assume that the reader already knows that, and is familiar with especially Lotusscript. I will also not talk much about how to create applications with a nice and easy-to-use user interface. That I will save for a later series of articles. Instead I will focus on things that I think will make you a better Notes programmer. I don't take credit for coming up with all the ideas I will talk about, some are from attending sessions at Lotusphere in the past, and some were methods I picked up where I work or worked before. Many of the tips are almost defacto standards among Notes/Domino developers. In this first article, I will start with some tips for when you create forms.   Field Names Use field names that makes sense, and don't use cryptical field names. You may remember right now what kind of data the field is supposed to hold, but in a few months, you have no idea what is stored in it. Some developers use hungarian notation or some similar system with prefixes to indicate what is in a field, but in my experience that makes just for massive confusion later. The only prefixes I use on field names are dsp for any computed-for-display fields and flag for fields that are used to indicate if a document has been processed, is ready to be deleted or to decide if parts of the form should be hidden or not. If you use field names that indicates what kind of data types they contain, be consistent and at least use proper indicators. It is not a good idea to call a field txt_no1 if it contains a number. Anyone that sees that field name will assume it is a text field, and this will cause errors…

18 Comments

Which is faster, ColumnValues() or GetItemValue()?

In a recent discussion thread in one of the forums on IBM developerWorks, the issue of which is faster, doc.GetItemValue() or viewentry.ColumnValues(). I decided to test this, using two Lotusscript agents, created to be as similar as possible, working against the same view of the same database. First, here is Agent #1, using ColumnValues() to get the value we are looking for from the first column of the view. This agent took 66.3 seconds to run, and below you can see exactly how that time was spent:   And this is Agent #2, identical to Agent #1, except two lines, first getting the NotesDocument from the view entry, and then using GetItemValue() to get the value out of the document. This agent took 225.8 seconds to run: In both agents, the call to get the next document in the ViewEntryCollection takes about 60 seconds. As you can see, the 30,000 calls to GetColumnValues() in agent #1 takes pretty much no time -- 1.3 seconds -- while it takes 133 seconds to open the 30,000 documents and read the value of the field from each one in agent #2. Almost exactly 100 times longer! In agent 2, you also have to add 26 seconds to get the NotesDocument object from the ViewEntry object. I hope this settles the discussion.  

3 Comments

Code: Expanded Class for File Functions

Yesterday I blogged about a simple class to parse file names, and that inspired me to improve it and add some functionality, which will actually come in handy for a project at work shortly. The class is pretty self-explanatory, there is really nothing complicated in the code. When the class is initialized, if a path to a directory (i.e. ending with \) is passed to the constructor the directory is created if it does not exist. If the directory exist, there are functions to copy or move both single files or all files in the directory. Directories can also be deleted using the RemoveDir method. In addition, there are properties to get the path, file name, extension and file size (in bytes) of the file (if the class was initialized with a file name). Here is an agent with some examples of how to call the class: Option Public Option Declare Use "Class.FileFunctions" Sub Initialize Dim file As FileObject Dim cnt As Integer Dim success As Boolean '*** Create new file object Set file = New FileObject("D:\Downloads\Downloads\MERP\Assassins of Dol Amroth.pdf") '*** Copy the file to another (new) directory Call file.CopyTo("D:\Downloads\MERP1\", file.FileName) '*** Move the file to a new location and replace space with + in file name Call file.MoveTo("D:\Downloads\MERP2\", Replace(file.FileName," ","+")) '*** Create a new directory if it does not exist Set file = New FileObject("D:\Downloads\MERP3\Test\") '*** Copy all files in specified directory to another directory Set file = New FileObject("D:\Downloads\Downloads\MERP\") cnt = file.CopyAllTo("D:\Downloads\MERP\Backup\") MsgBox "Copied " & cnt & " files." '*** Move all files in the previously specified directory to another location cnt = file.MoveAllTo("D:\Downloads\Middle-Earth Role Playing Game\") MsgBox "Moved " & cnt & " files." '*** Remove D:\Downloads\Downloads\MERP\ Call file.RemoveDir("") '*** Remove D:\Downloads\MERP3\ and Test directory that we created earlier success = file.RemoveDir("D:\Downloads\MERP3\Test\") If success = True Then success = file.RemoveDir("D:\Downloads\MERP3\") If success = False Then MsgBox "Failed to delete D:\Downloads\MERP3\" End If Else MsgBox "Failed to delete D:\Downloads\MERP3\Test\" End If End Sub   Below is the class itself, I put it in a script library called Class.FileFunctions. %REM Copyright (c) Karl-Henry Martinsson 2012. Some code copyright Andre Guirard (see below). You are free to use and modify my code, as long as you keep all copyright info intact. If you improve the code, please consider sharing it back to the community. %END REM Option Public Option Declare Type FileDetails path As String filename As String extension As String filesize As Long End Type Class FileObject Private file As FileDetails Public silent As Boolean Public Sub New(filepathname As String) silent = False FullPathName = filepathname If file.FileName = "" Then If file.Path "" Then On Error 76 GoTo parentDoesNotExist 'No filename but path, then we create that directory (if missing) If Dir$(file.Path,16)="" Then createDirectory: Call MakeDir(file.Path) End If End If file.FileSize = 0 Else file.FileSize = FileLen(filepathname) End If Exit Sub parentDoesNotExist: Resume createDirectory End Sub Public Property Set FileName As String file.FileName = FileName file.Extension = StrRightBack(FileName,".") End Property Public Property Get FileName As String FileName…

0 Comments

Show and Tell – Dynamic Web Calendar

In this post in the developerWorks forum, Andy Herlihy is asking about how to create a small calendar that can be embedded into an existing intranet website. As Carl Tyler points out, Andy could simply use Dojo. However, this is a good example to show some jQuery combined with the power of IBM Lotus Domino.  Lets' break it down into a few smaller steps. The first one is to create a Domino web agent that will take the current date and build a calendar for that month. There will be no links or javascript code in the HTML generated, just plain HTML code, with some id attributes to make it easy to address different cells later. The next step is to create a very simple webpage, with some Javascript to load jQuery and set up the calendar to detect clicks. The clicks will cause another call to the Domino server, this time to a view using RestrictToCategories to only get the entries for the specified date. The entries are returned as HTML and through jQuery they are displayed in a div on the webpage. We also want to add a little bit of CSS to make the calendar pretty. The CSS also lives on the HTML page. Finally we create the view, it's associated view template form and a form to create some entries that we can use for testing.   Let's look at the code. First the Domino web agent. It should be pretty self explanatory: %REM Agent WebCalendar Created Nov 26, 2012 by Karl-Henry Martinsson/Deep-South Description: Returns HTML for a calendar for current month %END REM Option Public Option Declare Sub Initialize '*** Used to get URL params Dim session As New NotesSession Dim webform As NotesDocument Dim urlstring As String Dim urlarr As Variant Dim urlvaluename As Variant Dim urldata List As String Dim webservername As String '*** Agent specific Dim startDate As NotesDateTime Dim endDate As NotesDateTime Dim i As Integer Dim j As Integer Dim startweekday As Integer Dim thisMonth As Integer Dim currDate As Double Dim cellclass As String Set webform = session.DocumentContext '*** Calculate path for this database, for image/icon file references later webservername = webform.GetItemValue("Server_Name")(0) '*** Remove leading "OpenAgent" urlstring = StrRight(webform.Query_String_Decoded(0),"&") If urlstring <> "" Then '*** Get all params passed to agent urlarr = Split(urlstring,"&") For i = LBound(urlarr) To UBound(urlarr) urlvaluename = Split(urlarr(i),"=") urldata(urlvaluename(0)) = urlvaluename(1) Next If IsElement(urldata("date")) = False Then urldata("date") = Format$(Now(),"mm/dd/yyyy") End If Else urldata("date") = Format$(Now(),"mm/dd/yyyy") End If '*** Get first and last date of current month Set startDate = New NotesDateTime(Format$(urldata("date"),"mm/01/yyyy")) Set endDate = New NotesDateTime(startdate.DateOnly) Call endDate.AdjustMonth(1) Call endDate.AdjustDay(-1) currDate = CDbl(CDat(startDate.DateOnly)) startweekday = Weekday(Cdat(startDate.Dateonly)) '*** HTML header Print "Content-type: text/html" ' & CRLF Print |<table class="calendar">| '*** Create calendar header with weekdays Print |<tr><td colspan=7 class="labelMonthYear">| + Format$(CDat(startDate.DateOnly),"mmmm yyyy") + |</td></tr>| Print |<tr class="calendarHeader">| Print |<td>S</td><td>M</td><td>T</td><td>W</td><td>T</td><td>F</td><td>S</td>| Print |</tr>| '*** Build rows for the weeks For i = 1 To 6 Print |<!-- Start row | & i & | -->| Print |<tr class="calendarRow" id="calendarRow| & i…

1 Comment

How to detect changes in fields on a form

A question I have seen a few times in the developerWorks forums is how to detect what values (often names) have been added in a multi-value field. This is not very difficult to do in Lotusscript, and there are a few different ways to do it. I prefer to use lists, but you can of course use arrays as well. The basic idea is to declare a global variable to hold the initial value of the field, populate it when the document is either opened or put into edit mode, and then checked when the document is saved. Globals: Dim oldValue List As String PostModeChange: Dim oldarray as Variant '*** Create array, assume ; as item separator in field oldarray = Split(uidoc.FieldGetText("myMultiValueField"),";") '*** Build a list of values ForAll o in oldarray oldValue(o) = o End ForAll QuerySave: Dim newarray as Variant Dim newValue List As String '*** Create array, assume ; as item separator in field newarray = Split(uidoc.FieldGetText("myMultiValueField"),";") '*** Compare with values in list ForAll n in newarray '*** Check if value is not in global list, then it is new If IsElement(oldValue(n)) = False Then newValue(n) = n ' Add new value to list End If End ForAll '*** Loop through all new values and print them ForAll nv in newValue Print nv + " is new." End Forall The same technique can be used to detect what fields have changed since the document was opened. Just store the values of all fields (except any starting with $) in a list, then check the values in the fields against that list when saving the document. Globals: Dim oldValue List As String QueryOpen: '*** Build a list of values with field name as listtag ForAll i in doc.Items If Left$(i.Name,1)"$") Then oldValue(i.Name)=i.Text End If End ForAll QuerySave: Dim modified List As String Dim tmpValue As String '*** Compare current fields with values in list ForAll o in oldValue '*** Check if value is the same or not tmpValue = uidoc.FieldGetText(Listtag(o)) ' Get current field value If tmpValue o Then modified(ListTag(o))=tmpValue ' Add new value to list of modified fields End If End ForAll '*** Loop through all modified fields and display new value ForAll m in modified Print Listtag(m) + " was changed from " + oldValue(Listtag(m)) + " to " + m End Forall All very easy when you know how to do it. And again, it shows the power of lists.

3 Comments

Export from Notes to Excel – 3 different ways

While reading the developerWorks forums, I have noted that some tasks seem to be common, and users often have issues with it. One very common is to export data from Notes/Domino to Excel. My guess is that some manager/executive want to get the data to manipulate and analyze it in Excel, which they know better, and where reporting can be done easier. First of all, for anyone that want to do real integration with Excel (or Word, or any other office program), I would suggest to look at the presentations given by John Head at Lotusphere and other conferences in the past: Lotusphere 2011 - JMP208 IamLUG 2009 - Integration and Coexistance Lotusphere 2006 - BP309 Lotusphere 2005 - JMP108 But if you just need to export raw data to Excel, there are a couple easy ways to do that. You don't need to automate Excel, something the programmers posting in the forums seem to frequently do. That just makes things more complicated, and if you run the code in a server agent, Microsoft Office have to be installed on the server itself. Different versions of Excel also cause different issues. The different methods I use to export Notes data to Excel are as follows: Create a CSV file, which can be opened in Excel Create a HTML table in a file with .xls extension, which can be opened in Excel Create a HTML table and return it through a browser, with a content type set to open in Excel. Depending on what the requirement is, I usually choose one of those method.   Method 1 - CSV file (local) The easiest way to get data from Notes to Excel (or any other spreadsheet program) is to simply save the data as  comma-separated values (CSV). Use regular Lotusscript file operations and write the values to the file, encased in double quotes and separated by a comma. You need to build a string containing a complete line, as the Print statement adds a line break to the end automatically. A CSV file could look like this: FirstName,LastName,Department,StartYear "Karl-Henry","Martinsson","IT","2002" "John","Smith","Accounting","2009"   The first row is the header, with the field names listed. This helps on the receiving side, but is optional. Dates and numbers don't need quotes around them, as long as they don't contain commas. So make a habit to format any numbers without commas.   Method 2 - HTML file (local) This method is very similar to the CSV method. The difference is that you create a table in HTML, matching the cells in Excel you want to create/populate. But instead of creating a file with the extension .htm or .html, you give it the extension .xls. Excel is intelligent enough to identify it as HTML and map the content correctly. The biggest advantages of using a HTML file instead of CSV is that you get more control over the formatting. By using colspan and rowspan, you can merge cells together, for example in a header. You can also use markup to make cells bold or italic, or even set the…

14 Comments

Reminder – Great icon set for free!

Two years ago I wrote about the FamFamFam Silk icons, a free set of 1000 icons that I use. I still use them in all my applications, and have even modified a few of them to suit my purposes. They come as PNG files, but I have converted them to GIF format so I can use them in for example Notes action buttons. If you downloaded my generic database template, which I posted about two years ago as well , you have the GIF version of all the icons as image resources. So if you haven't taken a look at those icons, you should. Few things help your application or website to look professional as a set of consistent looking icons. And yes, it works in Xpages. You just have to add a little piece of CSS to make it look good, as Per Henrik Lausten told me when I posted about that in StackOverflow. The magic code is:  .lotusBtn img {   margin-right: 10px; } This will give you 10 pixels between the icon and the label. It might be enough with 5 or 6 pixels, depending on what the desired look of the page is.  

7 Comments

Replace images on web page using jQuery

Last week I encountered a situation where I wanted to replace images on a Domino generated webpage. I am sure you all know what doc links look like by default when rendered by the Domino HTTP task:   By adding a few lines of jQuery to the form, you can manipulate the image source, as well as the CSS class and other attributes. This allows you to modify a page served by the Domino HTTP task almost as much as you like. Below is the code I used to modify what you see above into something that I found a little bit nicer.     $("img").each( function() {         var t = $(this);         imgicon = t.attr("src");         if (imgicon == "/icons/doclink.gif") {            t.attr('src', '/applications/losscontrol.nsf/icon_picture.gif');            t.addClass('photoicon');         }         if (imgicon.indexOf("Attachments") > 1 ) {            t.attr('src', '/applications/losscontrol.nsf/icon_attach.gif');            t.addClass('attachmenticon');            t.attr('height','16');            t.attr('width','16');         }     });     $("a").each( function() {         var t = $(this);         url = t.attr("href");         if (url.indexOf("$FILE")>0) {            t.removeAttr("style");            t.addClass('attachmentLink');            t.outerHTML = t.outerHTML + "<br>";         }     });     var plink = $('#photolinks').html();     plink = plink.replace(/<br>/i,'');     plink = plink.replace(/<\/a><font\b[^>]*>/gim,'<span class="photoLink">');     plink = plink.replace(/<\/font>/gim,'</span></a>');     $("#photolinks").html(plink);     var alink = $('#attachmentlinks').html();     alink = alink.replace(/<\/a>/ig,'</a><br>');     $("#attachmentlinks").html(alink);   What I am doing is to loop through all img tags on the page, and identify the ones that are doc links (using the file name for the icon). I replace the src attribute of those links with a different icon I added as an image resource to the database. I then set the class name for the link, so I can manipulate the look using CSS. I also look for any src attribute containing the field name "Attachments", which is where the attachments (if present) are located. I change the icon from the one generated by Domino to another image resource in the database. The next section of the code will loop through all anchor tags and check if the link includes "$FILE", indicating it is an attachment. If that is the case, I remove the Domino generated style attribute, set a class name and append a line break to the end of each link. I then perform some string replacements to remove the font tags that Domino generate automatically when rendering rich text fields. I replace the font tags with a span (containing a class name) so I can style the look of the link later, and also move the </a> tag to after the link text. The last thing I do is to add a line break after each attachment link. Here is the result:   Hope this can help anyone. And if you wonder, I am using the fieldset tag to create the box around each set of icons.

3 Comments

Moving blog posts from Connections to WordPress

As I switched from IBM Connection to WordPress for my blog, I started thinking about my existing content. Was there a way to move them all over without having to manually copy and paste and recreate all 268 entries? Well, there is, and this is how I did it, using just a  few tools. First I used Wget to retrieve my old blog. This put all the posts on one folder (entries), and all images in another (resource). It was then a simple task to write a Lotusscript agent that processed each file in that folder and read the content, parsed out the title, date originally posted and HTML for the blog post itself. I put that data into separate Notes documents, after performing some cleanup and string replacement. I had already moved all images to a filer on my primary web server, so I performed a replace of the image URLs in the HTML, to have any images pointing to their new location. I also had to fix some special characters and replace them with the corresponding HTML entities. Now when I had all the data, I just wrote another agent to export the data out again, to create a CSV file. I then installed a CSV importer in my WordPress blog and used to to import the file I just created. After a few tweaks I performed a successful import. Later I realized I had missed a few special characters, so I had to fix those entries, but we are talking about 4 or 5, out of 268 entries. If there is an interest, I might clean up the code a little and create a nicer UI (right now many of the values like path and URL are hard-coded) and then release the code if anyone else is planning to go through the same exercise. Below is the existing code to read the blog entries into a simple Notes database. Option Public Option Declare Dim entrydir As String Dim resourcedir As String Sub Initialize Dim filename As String Dim cnt List As Integer Dim blogentry List As String Dim tst As Variant entrydir = "D:\BleedYellowBlog\www.bleedyellow.com\blogs\texasswede\entry\" resourcedir = "D:\BleedYellowBlog\www.bleedyellow.com\blogs\texasswede\resource\" cnt("Total") = 0 filename = Dir$(entrydir + "*.*") Do While fileName <> "" blogentry(filename) = entrydir + filename cnt("Total") = cnt("Total") + 1 fileName = Dir$() Loop cnt("Processed") = 0 ForAll be In blogentry cnt("Processed") = cnt("Processed") + 1 Print "Processing " & cnt("Processed") & " of " & cnt("Total") Call ProcessBlogEntry(ListTag(be),be) End ForAll End Sub Function FixHTML(html As String) As String Dim tmp As String tmp = Replace(html,_ "https://www.bleedyellow.com/blogs/texasswede/resource/",_ "http://www.texasswede.com/blogfiles/resource/") tmp = Replace(tmp,_ "http://www.bleedyellow.com/blogs/texasswede/resource/",_ "http://www.texasswede.com/blogfiles/resource/") tmp = Replace(tmp,"/BLOGS_UPLOADED_IMAGES/","/uploaded_images/") tmp = Replace(tmp,"´",|"&acute;"|) tmp = Replace(tmp,"’","&acute;") tmp = Replace(tmp,"“",|&quot;|) tmp = Replace(tmp,"”",|&quot;|) tmp = Replace(tmp,"…",|"..."|) tmp = Replace(tmp,"<wbr>",||) tmp = Replace(tmp,"> < ",|>&anp;nbsp;< |) FixHTML = tmp End Function Function ProcessBlogEntry(filename As String, localpath As String) As Boolean Dim session As New NotesSession Dim db As NotesDatabase  Dim blogentry As NotesDocument Dim rtitem As NotesRichTextItem  Dim siteurl As String Dim html List As String…

1 Comment

Nostalgia

I am sure most of you who started programming around the same time that I did (in the first few years of the 1980's) at one point carried something like this in your wallets:  

1 Comment

Things to think about when programming in Notes

Inspired by some of the posts in the DeveloperWorks forums and on StackOverflow, I thought I would post some more basic concepts and how I handle them. I am not saying my way is the best way, this is just what works for me. I am sure there will be more posts in the future"..." I will also mention a few other things I noticed while reading the code posted in the forums.   Retrieve something that doesn´t exist The question is how to identify what dates there are no documents created for. This is where lists are very useful. Richard Schwartz answered this question and posted some good code. Rich suggests to create a list of dates, with each list item having an initial values of false, and then loop through the documents. As each document is processed, the value of the corresponding list item is changed from false to true. You can then go through the list and see which dates still have a value of false, those dates are missing documents. My version of the same code is to actually delete the list item you have a match for, instead if setting it to true. In the end you have a list of just the items of dates without a corresponding document.   Write readable code This could be a blog entry all by itself. But I notice that much of the code in the DeveloperWorkds forums is hard to read"´". Partially because any tabs or multiple spaces used for indenting the code is stripped out, but also because the posters don´t write easy-to-read code. Variable names are often not descriptive: Dim db1 As NotesDatabase Dim db2 As NotesDatabase vs Dim thisdb As NotesDatabase Dim nabdb As NotesDatabase Which one is easier to understand? In my opinion (and I am sure you agree) the second variant. Also function names and other variables should be named so you understand what they do and what kind of data they contain. Comments are mostly non-existing. It is not that hard to add some comments to the code that explain what the code is doing. But don´t explain every line of actual code (it should be self-explanatory, if variables are named correctly), explain what a particular section of code is intended to do. Here is a section of code from an agent I wrote earlier this week: '*** Read PhotoUNID field in LossControl document'*** and build a list of the UNID values in the fieldphotoUNID = lcdoc.GetItemValue("PhotoUNID")(0)If photoUNID<>"" Then '*** Create array of values and put into photolist tmparray = FullTrim(Split(photoUNID,";")) ForAll t in tmparray If t <> "" Then photolist(t) = t End If End ForAll End If The comments above will help the next person to look at the code to quickly understand what it is intended to do.   More on variables Use Option Declare/Option Explicit. This will find many errors, especially for more inexperienced programmers, where variables are misspelled or missing, something that is a very common…

0 Comments

Are inexperienced developers the death of Notes?

Lately I have been more active in the IBM DeveloperWorks forums, as well as on StackOverflow, trying to help people with development problems. As I am just myself starting with Xpages, I been staying in the forums for "classic" Notes development. I have noticed a trend, based on the postings. It seems like there is a substantial number of new developers who are not very familiar with Notes/Domino development. They sometimes think Domino works like a relational database. There are then several who are posting about very simple things, that can easily be found in the online help, or by looking at the properties for an element. Like how to extend the last column in a view to use all available space. There was even one user asking about how to duplicate a specific @Formula in Lotusscript, when the help file got a cross reference to the class and method to use… There are others who does not seem to even understand the basics, either when it comes to programming in general or specifically of Notes/Domino. Some of them don't understand data types. They declare a variable as integer, then make a calculation that results in a value of say 3.5, and is then wondering why the result is 4. Others don't understand the difference between strings and variables, they are surprised when @SetField("myField"; "myField + 1") does not give them the expected result (the value in the field ‘myField’ increased by one). On StackOverflow it is possible to see what other areas the user posted in. Some of the users seems to have a background in Java, SQL, .NET or other platforms. My guess is that they been thrown into a Notes projekt after their company took on a new development project, with the hope that they could learn it quickly. I think this could be dangerous, from some of the code I have seen, the lack of experience and understanding of the Notes/Domino platform will cause sub-standard or slow code, which of course will make executives think that Notes is a bad development platform. After all, if the expensive consulting company (or the off-shore based development house with all developers being at least Ph.D.) can't write fast and good code, the platform must be at fault, right? Another thing I noticed over the last year or so is that in the Notes-related groups on LinkedIn, there has been a number of requests for the answers to the IBM certification tests. They have originated from both some big consulting companies and from within IBM. None of them were from the US (or Europe, if I remember correctly), but from countries more traditionally associated with outsourced or "off-shore" development. My guess is that the companies want their developers to be certified on paper, as they can either charge higher rates, or pass themselves off as being “experts” on the platform. A number of the questions in the DeveloperWorks forums were posted under names that often are associated with the same…

0 Comments

Regular Expressions in Notes (Lotusscript)

Today I needed to use regular expressions (a.k.a. regexp) in a Lotus Notes application. I just wanted to check if the user entered a claim number (in the format "nnXXXXXnnnnn", e.g. 12RICTX12345) in a field. A quick online search found a blog entry with some code using the VBScript object available in Windows, and I adapted it for my application. Just in case someone need this, I am posting the code below. I am not taking credit for the code, I found it on Giles Hinton´s blog and just adapted it a little bit. I also found information about using LS2J and Java to handle regular expression in Notes, which should be platform independent, not restricted to just Windows. Since all our users are on Windows (either directly or through Citrix), I could use the quick method below. But I would probably use the script library posted on OpenNTF for more serious code.   Dim ws As New NotesUIWorkspace Dim uidoc As NotesUIDocument Dim regex As Variant Dim pattern As String Dim result As String Dim match As Boolean '*** Define pattern and get text value to check for match pattern = |b([0-9]{2}[a-zA-Z]{5}[0-9]{5})b| Set uidoc = ws.CurrentDocument subject = uidoc.FieldGetText("ShortDescription") '*** Create RegExp object Set regex = CreateObject("VBScript.Regexp") regex.Global = True regex.IgnoreCase = True regex.Pattern = pattern '*** Test for match of pattern in text match = regex.Test(subject) If match = True Then Msgbox "Claim number was found in the field." End If

0 Comments

Free Tool: Analyze ACL in Notes Application/Database

Yesterday my network admin asked me if I could write a simple tool that could provide him with with a spreadsheet of what users had access to a certain database, and through what groups and roles. A couple of hours later I had created an agent that analyze the ACL and identify the users who can access it. The result is presented as a CSV file. I am sharing the code below. It is pretty straight forward. As you can see, I am using lists to hold the data for easy export later to CSV. Run the code with the Lotusscript debugger turned on, and put a breakpoint before the CSV export starts, and you can see how the data is stored in the lists. The function ExpandGroups() is called recursively to drill down, if the group contains additional groups. This function also use a lookup into a custom view, (LookupPeople), that we have in our corporate NAB, I am sure you can modify this code with something that works for you. Enjoy! As always, use the code on your own risk, no warranties, etc. %REM Agent Export ACL Info to CSV Created Nov 14, 2011 by Karl-Henry Martinsson/Deep-South Description: Read ACl for specified database and create a CSV file with info about each user's access (roles, groups, delete access, access level). %END REM Option Public Option Declare Dim nab As NotesDatabase Type RowData role As String group As String username As String deletedoc As String level As String levelno As Integer End Type Class GroupData Public roles List As String Public Sub New() End Sub End Class Class PersonData Public accesslevel As Integer Public roles List As String Public deletedoc As boolean Public accessthrough List As String Public Sub New() me.deletedoc = False End Sub Public Sub SetAccessLevel(level As Integer) If me.Accesslevel<level Then me.AccessLevel = level End If End Sub Public Function GetAccessLevelText() Select Case me.AccessLevel Case 0 : GetAccessLevelText = "No Access" Case 1 : GetAccessLevelText = "Depositor" Case 2 : GetAccessLevelText = "Reader" Case 3 : GetAccessLevelText = "Author" Case 4 : GetAccessLevelText = "Editor" Case 5 : GetAccessLevelText = "Designer" Case 6 : GetAccessLevelText = "Manager" End Select End Function End Class Class RoleData Public groups List As String Public Sub New() End Sub End Class Sub Initialize Dim ws As New NotesUIWorkspace Dim session As New NotesSession Dim db As NotesDatabase Dim pview As NotesView Dim pdoc As NotesDocument Dim acl As NotesACL Dim entry As NotesACLEntry Dim person List As PersonData Dim group List As GroupData Dim role List As RoleData Dim users As Variant Dim row List As RowData Dim cnt As Long Dim groupname As String Dim filename As String Dim rowstr As String Dim dbname As String Dim servername As String servername = InputBox$("Enter server for database:","Select Server") If servername = "" Then Exit Sub End If dbname = InputBox$("Enter full path of database:","Select Database") If dbname = "" Then Exit Sub End If set nab = New NotesDatabase(servername,"names.nsf") Set db…

3 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") 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…

7 Comments

Import CSV from Excel into Notes documents

The other day there was a post on LinkedIn regarding importing Excel data into Notes documents. Someone suggested to save into Access format, and then export from there intosome 1-2-3formatthat Notes can read. I suggested to save the Excel spreadsheet as a CSV file, and then import it. So I decided to write a small generic importer. I built a class called "csvFile", which I put in a script library called "Class.ImportCSV". Below is the code for the actual import agent. It creates a new csvFile object, which load all the CSV data into an array in memory. Each array element is in turn a class, containing a list of entries. This is because you can not create arrays of arrays or lists, they have to be in another object/class. If you know the row number and column label (the first row in the CSV file will be considered the column labels), you can address the value like this: csvfile.row(r).entry("ColumnLabel"). Option Public Option Declare Use "Class.ImportCSV" Sub Initialize ' *** Import CSV file and create matching documents in Notes ' *** By Karl-Henry Martinsson, April 8, 2010 Dim session As New NotesSession Dim db As NotesDatabase Dim doc As NotesDocument Dim csvfile As csvFile Dim rowcnt As Long Dim r As Long Set db = session.CurrentDatabase Set csvfile = New csvFile("c:\Book1.csv") rowcnt = Ubound(csvfile.row) + 1 ' *** Loop through the rows and create a new document for each For r = Lbound(csvfile.row) To Ubound(csvfile.row) If (r+1 Mod 10) = 0 Then ' Update status bar every 10 documents Print "Importing " & r+1 & " of " & rowcnt End If Set doc = New NotesDocument(db) Call doc.ReplaceItemValue("Form", "MyFormName") ' *** Loop though entries for the row and populate corresponding fields in doc Forall e In csvfile.row(r).entry Call doc.ReplaceItemValue(Listtag(e), e) End Forall Call doc.Save(True,False) Next End Sub Here is the script library. Simply create a new script library, call it "Class.ImportCSV" and paste the code into it's Declaration section: ' *** Created by Karl-Henry Martinsson on 2010-04-08 ' *** Email: TexasSwede@gmail.com ' *** Blog: http://blog.texasswede.com ' *** ---------------------------------------------------------- ' *** You are free to modify and edit this code, but please keep ' *** all comments intact, and publish any changes you make so ' *** the Lotus community can benefit. You are allowed to use ' *** this code in commercial/closed source products, but are ' *** encouraged to share your modifications. ' *** Disclaimer: Use this code at your own risk. No warranties ' *** what so ever. Don't run code you don't know what it does. ' *** ---------------------------------------------------------- Class RowData Public entry List As String End Class Class csvFile Public row() As RowData ' Storing the rows in the imported CSV file Public column List As String ' List containing column labels Private fileno As Integer ' File number Public Sub new(filename As String) Dim temprow As String Dim temparr As Variant Dim fixedarr() As String Dim i As Integer Dim flagQuoted As Integer fileno…

5 Comments

Free Application Template/Framework for Notes

Many of my Notes application have a similar/the same look, based on a generic application template I created a while back. The standard look makes the users feel at home when I deploy a new application, and the template makes life easier for me. If I create a new application based on that template, I already have the frameset, navigator and a few design element done before I even start the development. Below is a screenshot of what it looks like in the Notes 8.5.2 client:   I want to share the template with the Lotus community, as a small way of giving back. I may add more things to it later, I will post any modifications here as well. Click here to download the file. Update: Download link fixed now.Thanks Hynek for pointing it out!  

2 Comments

Agent Logging – Budget Style

At my work, we had several issues/needs related to agents in different Domino databases. I tried to come up with a quick and lean solution, that gave us the functionality we needed, nothing more and nothing less. Yes, I am aware of Julian Robichaux's excellent OpenLog. But we had some needs that I did not feel were addressed in that project. What I wanted was a database where the agents could be documented, and also where the log data was stored. So I created a Agent Log database where one document is stored per agent. The form let me document the actions of the agent, as well as what server it is running on, in what database, when it is triggered, etc. When the document is opened, I also read the last time the agent was run and if the agent is enabled or not. This is a sample document for an agent. It is scheduled to run at 11am every day:   This is what the actual database looks like. I plan to release the database on OpenNTF.org when I get the time.   Nothing complicated this far. The next step was to create a small script library, doing the actual logging. I created a script library called Class.AgentLog with the following code: Public Const THREAD_TICKS=6 Public Const THREAD_TICKS_PER_SEC=7 Class AgentLog Private session As NotesSession Private logdb As NotesDatabase Private agentdoc As NotesDocument ' Main doc in Agent Log db Private logdoc As NotesDocument Private running As Integer Private tstart As Long ' Ticks at start Private tend As Long ' Ticks at end Private tps As Long ' Ticks per second (system dependant) Public Sub New( agentname As String ) Dim view As NotesView Dim key As String running = True tstart = GetThreadInfo( THREAD_TICKS ) tps = GetThreadInfo( THREAD_TICKS_PER_SEC ) Set session = New NotesSession Set logdb = New NotesDatabase( session.CurrentDatabase.Server, "IT/AgentLog.nsf" ) Set view = logdb.GetView( "(LookupAgent)" ) If agentname = "" Then agentname = session.CurrentAgent.Name End If key = session.CurrentDatabase.FilePath & "~" & agentname Set agentdoc = view.GetDocumentByKey( key ) If agentdoc Is Nothing Then Print "AgentLog: Failed to locate agent document for " & agentname & "." Exit Sub End If Set logdoc = New NotesDocument( logdb ) logdoc.Form = "LogItem" Call logdoc.MakeResponse( agentdoc ) Call logdoc.ReplaceItemValue("AgentName", agentname ) Call logdoc.ReplaceItemValue("StartTime", Now() ) If session.IsOnServer = True Then Call logdoc.ReplaceItemValue("RunBy", session.CurrentDatabase.Server ) Else Call logdoc.ReplaceItemValue("RunBy", session.CommonUserName ) End If End Sub Public Sub Finish() running = False End Sub Public Sub Terminate() Dim seconds As Integer If logdoc Is Nothing Then Exit Sub End If tend = GetThreadInfo( THREAD_TICKS ) seconds = (tend - tstart) / tps Call logdoc.ReplaceItemValue("EndTime", Now() ) Call logdoc.ReplaceItemValue("Seconds", seconds ) If running = True Then ' Check if not terminated gracefully Call logdoc.ReplaceItemValue("Terminated", "Yes" ) End If Call logdoc.Save(True,True) End Sub End Class All code goes in the (Declarations) section of the script library. There are a few things to notice. I actually make each log item a child…

0 Comments

End of content

No more pages to load