« Snow Lessons | Main| Update to my Tips post from yesterday... »

Couple of code tips...

QuickImage   
Category
Bookmark : del.icio.us  Technorati  Digg This  Add To Furl  Add To YahooMyWeb  Add To Reddit  Add To NewsVine 


Often when I am looking for content to add to the blog I look at the work am doing at the present time. This week has been a good source for a couple of small things that I want to share with you. The first is a reminder or tip when writing code. The second is a technique that I came up with, and I think it is kewl enough to share.

Tip 1

OK, pop quiz time: What is wrong with the code below?


     
Dim s As New NotesSession
     
Dim thisdb As NotesDatabase, imgdb As NotesDatabase
     
Dim thisview As NotesView, imgview As NotesView
     
On Error Goto errHandler
               
     
Set thisdb = s.CurrentDatabase
               
     
Set thisview = thisdb.getView("IMPORT.with.Graphics.PV")
     
If thisview Is Nothing Then Error 1000, "Unable to find view: IMPORT.with.Graphics.PV"
               
     
Set imgdb = DBHandle("IMAGES.NSF")
     
If imgdb Is Nothing Then Error 1000, "Unable to find IMAGES.NSF database"
               
     
Set imgview = imgdb.getView("Images.by.ID.PV")
     
If imgview Is Nothing Then Error 1000, "Unable to find view: Images.by.ID.PV"

Assume the the DBHandle function figures out the directory that the current database is in, builds a path to the new database provided (IMAGES.NSF), and then returns a NotesDatabase object pointing to the new database using NotesSession.getDatabase (this is a handy function to have in multi-db applications).


Spotted it yet?



Tip 1: Answer

The problem is the line that tests to make sure I got a proper database back from the DBHandle function:


       
If imgdb Is Nothing Then Error 1000, "Unable to find IMAGES.NSF database"

The imgdb variable won't be "Nothing" - it contains a database object regardless of whether one was actually found or not. The test that I SHOULD be making is if the db is open, like this:


       
If Not(imgdb.isOpen) Then Error 1000, "Unable to find IMAGES.NSF database"

Or:


       
If imgdb.isOpen = False Then Error 1000, "Unable to find IMAGES.NSF database"

the NotesSession.getDatabase method returns an open NotesDatabase object if the database was found; otherwise it returns a closed NotesDatabase object. Small thing, but it caught me, and I thought it wouldn't hurt to point this out to you as well.


Or I am highlighting my own momentary lapse in reason :)


Tip 2: Yet another way to display a categorized view

I am working on an application for a client that has both a Notes and Web client interface. We have a "look" that we use for the Web, so we are bound to a certain style and way of displaying views in general. I have a view that is categorized. The view uses some inline HTML to make it display the way we want. We also have prev/next buttons, etc, and we have a "View Jump" that allows the user to enter the beginning of some entry in the first column and it will jump the view to that point. This works great for views that display documents in, say, numeric order - but if I have a column with any number of categories, it is hard to know what categories are available to jump to. We don't use the traditional categorized views with the twisties and all that because they don't work with the HTML we're using for making the view pretty. So, I came up with an idea: I decided to display the categories at the top of the view, and use the "StartKey" parameter on the URL to let the user jump to the right category.


The view now looks something like this:

A picture named M2

Now that's not all of it - I blocked out stuff so my client won't be mad for showing this view, and I didn't show the second column of categories. But in this case the user has clicked on "Regulatory Standards", which caused the view to jump to that beginning. You can see the categories of the first column listed there.


This works really well, and was really simple to implement. first, I placed some passthrough HTML that had the open and close table tags. Then I placed some computed text between these tags:


look := @DbColumn("" : ""; ""; "Published.By.Type.PV"; 1);

list := @If(@IsError(look); ""; @Unique(look));

list1 := @If(@Elements(list) > 1; @Subset(list; @Round(@Elements(list)/2)); list);

list2 := @If(@Elements(list) > 1; @Subset(list; @Elements(list1) - @Elements(list)); "");


results1 := "<TR VALIGN=top><TD WIDTH=50%><a href=\"/" + search_view + "?OpenView&StartKey="+@URLEncode("Domino"; list1)+"&Count=50\" target=_self>" + list1 + "</TD>";


results2 := "<TD WIDTH=50%><a href=\"/" + search_view + "?OpenView&StartKey="+@URLEncode("Domino"; list2)+"&Count=50\" target=_self>" + list2 + "</TD></TR>";


results2mod := @If(@Elements(results1) > @Elements(results2); results2 : "<TD WIDTH=50%>&nbsp</TD></TR>"; results2);


results := results1 + results2mod;

@Implode(results; @NewLine)


OK, here's a breakdown. First I do a DBColumn to get the categories, then I make sure it isn't an error - if it isn't I Unique it to get a unique category list. This next bit of code splits the list into two lists evenly:


list1 := @If(@Elements(list) > 1; @Subset(list; @Round(@Elements(list)/2)); list);

list2 := @If(@Elements(list) > 1; @Subset(list; @Elements(list1) - @Elements(list)); "");


If there is only one element, then it is in list1 and list2 is an empty string. If there is more than one in the list I subset it by half of the number of elements in the list, rounding up. So, if there are 7 elements in the list, then list1 has 4. List2 is simply what's left from list1; so, if list had 7 elements, then list1 has 4 and list2 has 3.


Then I build my two "columns" for my table. list1 becomes results1 when wrapped with the right HTML; list2 becomes results2:


results1 := "<TR VALIGN=top><TD WIDTH=50%><a href=\"/" + search_view + "?OpenView&StartKey="+@URLEncode("Domino"; list1)+"&Count=50\" target=_self>" + list1 + "</TD>";


results2 := "<TD WIDTH=50%><a href=\"/" + search_view + "?OpenView&StartKey="+@URLEncode("Domino"; list2)+"&Count=50\" target=_self>" + list2 + "</TD></TR>";


Notice that I am using @URLEncode to encode the categories for the query string - @URLEncode is a hidden function available in Notes/Domino R5.x, but it is not hidden in Notes/Domino 6 - and it works against lists.


Now, remember that list2 is shorter than list1 in our example? This means that results2 is shorter as well, and is now missing the second column HTML and the close row tag. So, we have to test for that and add it if necessary:


results2mod := @If(@Elements(results1) > @Elements(results2); results2 : "<TD WIDTH=50%>&nbsp</TD></TR>"; results2);


Once we've added on a dummy cell if needed, we add the two lists together. Remember, adding lists is a pair-wise operation - that's what makes all this work. After we add the two results together, we implode the whole thing on a new line, and we're done!


results := results1 + results2mod;

@Implode(results; @NewLine)


Not much code at all, and very useful. Now, this may become unwieldy if your categories get too large; in that case you can build other interfaces to accomplish the same thing (like a combobox, or A-Z quick pick, etc.). But for a quick-n-dirty way to display a "categorized" view that is highly modified with HTML, this works fine.


Conclusion

Hope you find these useful. Let me know if you have anything to add!


Rock

**I want to die peacefully in my sleep like my grandfather. Not screaming in terror like his passengers.~Michael Aulfrey

Comments

1 - Rock,
That's a very nice, compact navigation layout. I like! That's always been a problem... how do you convey tons of information without showing tons of information. You could further reduce the screen realestate used with this thing I'm doing for my archive categories... though it might not look AS nice.

http://www.datatribesoftwerks.com/members/datatribe/DatatribeBlog.nsf/archive/20040308-6A5C25?OpenDocument&count=-1

2 - Great tips, Rocky. I have actually experienced the first one with an agent to create replicas from our field servers to the corporate SAN.

Your second tip has me a bit confused only because I can't see an application of it with anything I have here at present. Guess I need to find something to break... err, play with and try this out.

Thanks,
Chris

3 - Ohhh... I get it !!! Very nice. Now to implement some of that in my apps :)

Dan

4 - Rock,

Maybe I'm a little dense, but I've looked at the code and it's cool but I cannot figure out why you had to build two lists then explode them together at the end? The sample you gave only had one column, why the two lists???

5 - Hey Rock.

I was looking at your code up top for the DBHandle function and had these thoughts:

1. It looks like the the function assumes the Images Db is in the same filepath as the the db where the call is made from. What happens if it is not in the same file path?

2. I still am haunted by a colleagues "Thou shalt not hard code anything, espacially Database names, into subroutines". This hangover has me using application profile documents where I store the replica ID of the databases needed in lookups.

Because of #'s 1 and 2 above, my approach would be:

dim viewProfiles as NotesView
set viewProfiles=thisdb.GetView("luvaAppProfiles")
dim docAppProfile as NotesDocument
set docAppProfile=viewProfiles.GetFirstDocument
if not (docAppProfile is nothing) then
Dim imgdb As New NotesDatabase( "", "" )
If imgdb.OpenByReplicaID( thisdb.server, docAppProfile.ImagesRepID(0) ) then

'whatever
End If
End if

Then I do not have to worry about the file path.

3. The error message

If imgdb.isOpen = False Then Error 1000, "Unable to find IMAGES.NSF database"

may be a little misleading, because the database may have been found, but could not be opened for one reason or the other (such as no access).

Hope Mouse is doing well.

Peace!

Chris

6 - John, you're right - throwing numbers that let you do more discreet cleanup, responses, etc. is a great use of error trapping - but in this case it is small, so no need to get that granular. In fact what I usually do is have a case statement that shows a "cleaner" error message without the error number if it is one of my errors (i.e. 1000), otherwise I throw all the info to help troubleshoot. I have also used case statements for doing other things - such as in Surely Template, I check for a specific C API error that tells me that the code doesn't have access to the template. No need to stop all processing, we just log it and skip that db.

As far as nesting goes, I basically use it if it makes sense. Sometimes the nesting can get so deep that it is basically messy, and hard to follow - especially if the code is long.

But that's the beauty of code, isn't it? We can all express ourselves and how we "think" through our code - which is way kewl

Rock

7 - Hmm, I see that in several article the images are replaced with ' A picture named M2'. Like on this page. Why? Are they comming back? Are they archived?

8 - The IsOpen test is so important... I've lost count of how many bits of code I've seen over the years that don't test that and fail showily as a result.

9 - Hey Dan - the thing that makes my code work as-is is the fact that I am using the Error statement to my advantage. I wrote up a blog entry about this technique awhile back:

http://www.lotusgeek.com/SapphireOak/LotusGeekBlog.nsf/d6plinks/ROLR-5MXR69

Where I show how you can use error trapping to your advantage. I do this in all my code now, so that if I don't get a handle to something I need, I throw my own error condition. It makes for cleaner code and more descriptive error messages, that help me troubleshoot when something does go wrong. Check it out.

Rock

10 - Rock, you're missing my point. You posed a "find what's wrong with this code" challenge. But the challenge relied on knowing the return value of a function that you didn't include. It would make a lot of sense to create a dbHandle function that returned Nothing if the database wasn't open, precisely to simplify the code of the calling routine. But as readers, we don't have any clue what dbHandle returns until you actually post that function.

That's why I said you were cheating. You expected your audience to spot the problem, while hiding the most relevant piece of information to the problem. Might as well as you what I have in my pocket right now.

11 - John - the two lists represent the left column and the right column. You implode them because if you don't, you get little semicolons in your display because they are a list, not a string.

Chris - I'll address each of you points individually:

1) You're right - DBHandle does assume that. A cardinal rule of any db system I build is that all related dbs must reside in the same directory. It makes finding related dbs much easier.

2) I am the exact opposite of you, Chris. I never use the replica ID of a db in code, ever (OK, 99% of my apps - Surely Template does use replica IDs, for a different reason). Why? Because I don't have as much control over what version of the db is gotten, because it makes it harder to keep up with copies of the db (i.e. if someone makes a copy of the system, and doesn't update where the replica IDs are stored, the system breaks), and transferring a replica ID around is much more prone to errors. And in the "real" world I do store names of Dbs in profile documents that are managed through a centralized config db for the application. There is a profile doc where users enter a DB "title" and the db filename. When this doc is saved it pushes these changes to a profile doc in each db in the system (since I know that all my dbs in my system are in the same dir). This is a clean way to manage this, and since I come from a product background I learned a long time ago that replica IDs aren't worth the hassle.

As far as the error msg, yeah you're right, but you know what? End users don't care. I want to give a succinct, user friendly error message that the end user can report to the admin, who can then in turn figure out what really happened.

BTW, all of this discussion proves my belief that code is truly more of an art form than a science

Rock

12 - Rocky,

Very nice tip.

Correct me if I'm wrong but in this section of your code:

Set imgdb = DBHandle("IMAGES.NSF")
If Not(imgdb.isOpen) Then Error 1000, "Unable to find IMAGES.NSF database"

Else?

Set imgview = imgdb.getView("Images.by.ID.PV")
If imgview Is Nothing Then Error 1000, "Unable to find view: Images.by.ID.PV"


Should'nt there be an Else between the two conditions? One wouldn't want the second condition to evaluate if the first one is false, right? (Cos we don't have a handle to the imgDB)

Dan

13 - Oops and I forgot...

Why not then just store a CFD field (again DOTS), and parse out @DBName and save the overhead of a subroutine?

Just a thought...

14 - Nope, Nathan, I have it return exactly what the getDatabase function returns, to keep the test appropriate. It returns a NotesDatabase object - and it is either open or not, just like getDatabase itself. Here's the code for the DBHandle function...

Function DBHandle(destdb As String) As NotesDatabase
Dim s As New NotesSession
Dim db As NotesDatabase
Dim dbopen As NotesDatabase
Dim dbpath$, dbname$, NewDBPath$
Dim count As Integer

Set db = s.CurrentDatabase
dbpath = db.FilePath ' Path & filename of current database
dbname = db.FileName ' Filename of current database, excluding the path
count = Instr(1, dbpath, dbname, 1) ' Find out starting position of current database filename

'----Extract everything in dbpath up to the filename, and concatenate the new file name with it
NewDBPath=Left(dbpath, count - 1) & destdb

'----get a handle on the desired db, pass it back out of the function
Set DBHandle = s.getdatabase(db.Server, NewDBPath)
End Function

Have fun!

Rock

15 - You cheated, Rock. You didn't show the dbhandle function. *IT* might return Nothing if the data isn't open. After all, it would make a lot more sense to do your open check inside that function.

16 - Yes, code is definitely an art and if it ever became a science, we would all be out of work.

There are definite advanatages and disadvantages to both approaches and I do agree that ReplicaID management can be a hassle, it just addresses the issue of when databases are not necessarily kept in the same directory, especially between servers in a replicated environment (which we cannot always control after we hand an application over to a customer ).

The error message makes more sense now in your context. I suppose I would write a more "admin" specific message also and write it to the agent log.

The answer, as always is DOTS: depends on the situation.

How is Mouse doing?

Chris

17 - I have used the error throwing trick many times before but IMHO you cannot beat a nice bit of nesting.

Although I can't see the point (in your example in Tip 1 and related blog entry) in throwing errors if the error number stays the same. At least with different error numbers you can detect them easily and maybe write a recovery or clean up routine if required.

Sorry just pickin as usual.

Meet Rocky

Rock - February 2010
Rocky Oliver
If you see me at a conference, please stop me and say hi!

Calendar

Search

Categories

Proudly Employed By

Wofkflow Studios

LOTUS GEEK gear

Social Networking


Add to Technorati Favorites

View Rocky Oliver's profile on LinkedIn

Rocky  Oliver

LotusGeek Blog Roll

Why display a blog roll when Planet Lotus does it so much better?

Dilbert

Buy my book!

Blog Buttons

Atheist - Unitarian - Humanist

Poker Players Alliance