Couple of code tips...
Category Technical
Bookmark :
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:
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%> </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%> </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







Blog Roll









Comments
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
Posted by Jerry Carter At 12:12:53 PM On 03/11/2004 | - Website - |
Your second tip has me a bit confused
Thanks,
Chris
Posted by Chris LeRoy At 12:16:23 PM On 03/11/2004 | - Website - |
Dan
Posted by Dan At 10:19:04 AM On 03/12/2004 | - Website - |
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???
Posted by John Wargo At 09:02:46 AM On 03/24/2004 | - Website - |
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
Posted by Christopher Byrne At 04:07:38 PM On 03/31/2004 | - Website - |
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
Posted by Rock At 01:54:51 PM On 03/15/2004 | - Website - |
Posted by rvamerongen At 06:19:06 PM On 04/08/2006 | - Website - |
Posted by Ben Poole At 03:30:11 AM On 03/13/2004 | - Website - |
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
Posted by Rock At 09:41:28 AM On 03/12/2004 | - Website - |
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.
Posted by Nathan T. Freeman At 01:54:42 PM On 03/15/2004 | - Website - |
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
Posted by Rock At 02:22:32 PM On 04/03/2004 | - Website - |
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
Posted by Dan At 09:36:37 AM On 03/12/2004 | - Website - |
Why not then just store a CFD field (again DOTS), and parse out @DBName and save the overhead of a subroutine?
Just a thought...
Posted by Christopher Byrne At 11:29:24 AM On 04/05/2004 | - Website - |
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
Posted by Rock At 01:42:05 PM On 03/12/2004 | - Website - |
Posted by Nathan T. Freeman At 10:22:33 AM On 03/12/2004 | - Website - |
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
Posted by Christopher Byrne At 11:20:31 AM On 04/05/2004 | - Website - |
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.
Posted by John Marshall At 12:27:09 PM On 03/13/2004 | - Website - |