Andre Guirard | 22 August 2008 10:00:46 AM ET | Caribou Coffee, Minnetonka, MN, US | Comments (0) | Permanent Link
A while back, I posted a utility to search among all your databases for design elements with specified attributes or search terms in the code. This is a newer, better version.
I've been using this often myself, to find example code from some application I developed years ago or investigate applications developed by others, so I've been improving the tool as I went along. I've always had a list of the more things I needed to do before I let anyone have it, and now I'm done with that list, so it's time to release again, quickly, before I think of something else.
The main differences you'll notice from the previous version are:
- more design elements to select from, including new ones from version 8.
- an option to search for words in the DXL of the design notes, in case what you're looking for might be in a weird place we wouldn't recognize as code or rich text, like the name of a column or part of a CSS.
- the ability to display the context in which your search strings were found. This is enabled with a checkbox in the search parameters dialog -- lower left. The search terms are displayed boldface, with about 25 characters on either side, to help you decide whether it's an occurrence you're interested in.
- if you display context, the name of the containing entity (action, form event, subroutine...) is included. This doesn't work if you select the "All DXL" search.
And here's the application, which you may download and use:
Andre Guirard | 11 August 2008 05:15:00 AM ET | Man-Cave, Plymouth, MN, USA | Comments (8) | Permanent Link
The NotesTimer class isn't needed very often, but when you do need it, there are a couple of things to watch out for.
First, bear in mind that the current NotesUIDocument or view isn't necessarily the one in which your timer is running -- the user can leave a window open and open or switch to another window, while your time keeps ticking away in the original one. Use a global variable to store the NotesUIXxx object, and assign it from the Postopen event of the design element containing the timer object. Never use NotesUIWorkspace.CurrentWhatever method unless it's to compare with the global object to see whether this window is still the current one.
By the way, just because the current UI window isn't the window containing your timer, don't assume that the timer window isn't visible to the user. In a frameset (or composite application), another frame could easily have focus but your problem report alert window (or whatever) still be visible.
Second, bad things (e.g. a crash) can happen if another timer event occurs while you haven't finished processing the previous one. Even if you think whatever you're doing is going to be pretty quick, if it involves access to a remote resource or to the UI, you should assume it's going to take a long time, and disable the timer until you exit the timer subroutine. If there's a modal dialog open, UI operations can get hung up for as long as the dialog remains open, so there's really no telling how long it'll take just to update that field value.
Andre Guirard | 7 August 2008 05:09:00 AM ET | Home, Plymouth, MN, USA | Comments (1) | Permanent Link
Users of other object-oriented languages may be accustomed to registering "listeners" (for the Java crowd) or "callbacks" (for C/C++). These are functions or classes that you can pass in to other functions or classes; when certain parts of the process are reached, your routine will be called. For an example of what I'm talking about, see the NotesTimer class; you use the On Event statement to register your own routine to be called when the timer triggers.
Problem is, there's no facility in LotusScript for creating your own event types, so you can't extend this system to your own classes. There's also not a way in LotusScript to get a handle to a subroutine, so you can't pass your subroutine to another subroutine as an argument.
What you can do, however, is define a class of your own with certain methods that the called code expects to find, and put your own code in those methods. Then, create an object of that class, and pass the object to something you're making a call to. When it gets to the appropriate point in its code, whatever you called invokes a method of the object you passed it (assuming that the method actually exists and takes the arguments it's expected to).
For a real-life example, consider the code I posted yesterday to sort fields in a table. This works great, except that it makes certain assumptions about how field names are formatted. It works in cases where the fieldnames in the table are of the form name?#, where ? is some dividing character, like underscore, and # is the row number starting with 1. Of course, Murphy's law says that when you have a restriction like that, the very first developer who tries to make use of it has an incompatible form -- they start numbering their rows at zero, or the row number doesn't come at the end, or (as occurred) both.
We could do little things to make this more flexible -- have them pass us a macro formula, or a LotusScript expression we would pass to Evaluate to calculate the field name, for instance -- but but that's a little slow, and not very powerful if you wanted to do more than just calculate a simple value. Worst of all, it doesn't demonstrate the technique I wanted to demonstrate.
So, with the new code (attached), if you want to use the table sorter, and you have fieldnames of a form other than name_#, you (the caller) have to supply a subroutine that calculates the name. You do this by creating a new class -- call it what you like -- with a method named Mangle, defined as follows:
Function Mangle(fname$, Byval row%) As String
' insert your custom code here
End Function
End Class
For example, in the specific case, the developer has columns as follows: cat#, lot#, cst#, vb#_0, vb#_1, vb#_2, vb#_3, vb#_4, vb#_5, vb#_6, vb#_7, vb#_8, vb#_9, Desc#, h#_0, h#_1, gp#, PercentIncrease#, pr#_0 where # is the zero-based row number.
So his callback class could be:
Function Mangle(fname$, Byval row%) As String
Mangle = Replace(fname, "#", Cstr(row-1))
End Function
End Class
and he could call the sorting function as follows:
Call SortATable(uidoc, "cat#, lot#, cst#, vb#_0, vb#_1, vb#_2, vb#_3, vb#_4, vb#_5, vb#_6, vb#_7, vb#_8, vb#_9, Desc#, h#_0, h#_1, gp#, PercentIncrease#, pr#_0", "cat#", 10, myMangle, True)
At the receiving end, the myMangle argument is declared as Variant, because of course we have no idea what its actual datatype might be. Now, the code that does the sorting takes the myMangle argument, stores it, and when it needs to calculate the name of a field, calls its Mangle method:
Set record = New sortingRecord(Ubound(m_keys), Ubound(m_fields))
For k = 0 To Ubound(m_keys)
fname = m_callback.Mangle(m_keys(k), i)
record.keys(k) = doc.GetItemValue(fname)(0)
Next
One problem with this approach is that if you mess up and pass in a class that doesn't have the right methods and arguments, you don't know about it until you run the code and it fails. Java handles this more gracefully, by letting you define an "interface" -- a class definition without any of the methods -- that you can use as a model for your callback class. The compiler confirms that your class has in fact implemented all the methods that are expected by the code you're calling. I can't think of any way to do this in LotusScript -- maybe one of you folks can.
Andre Guirard | 5 August 2008 10:30:00 PM ET | Home, Plymouth, MN, USA | Comments (5) | Permanent Link
There was an oft-repeated question in the Notes 6&7 forum this morning, which inspired me to write the attached utility. This is for where you have a table on a form with a field in each cell, and you want to be able to keep the table sorted by one or more columns.
The source file is attached, which you can import into a script library or the globals section of the form or whatever.
To invoke the sorting function, make a call such as:
The arguments, in order, are:
- A NotesUIDocument (for the document you're editing), or a NotesDocument (if you want to sort data in a back-end document).
- A comma-delimited list of the fieldnames in the table, minus the numerical suffix designating the row.
- A comma-delimited list of which of these you want to sort by.
- The number of rows in the table.
- The character used to separate the column name from the row number to create the fieldname.
- True for ascending, False descending.
Attachment:
Andre Guirard | 4 August 2008 12:29:51 PM ET | Home, Plymouth, MN, USA | Comments (4) | Permanent Link
On the art of setting the meeting alert time preference to the correct value.
As a home office worker, most of my meetings are conference calls. I don't need the default 10 minute alert, since it takes less than a minute to dial in. Except, of course, that sometimes I need to wrap up what I'm doing, and that I don't always notice in advance when my meetings are going to be, and that I'm not at my computer every minute. If I have the alert set to one minute, and go make myself a coffee, the meeting alert could pop up and the meeting could start before I get back. So 1 minute is the wrong amount of time for the meeting default alert.
Even people in an office have a similar situation. They come back into their office with their coffee and see there's a meeting in 7 minutes (because it popped up 3 minutes ago). They need one minute to walk to the meeting room. So they set the snooze to 5 or 6 minutes and they answer emails until the alert goes again, and they can get to their meeting right on time.
But obsessive Andre is always counting clicks. Why should they have to look at the seconds on the clock to decide whether it's 5 or 6 minutes (because 8:53:xx plus 6 minutes might be 8:59:59, and then they would be late)? Why should they have to adjust the time if they don't see the notice for three minutes; doesn't Notes know that it's silly to alert someone three minutes after a meeting starts? Why shouldn't the default behavior if they just click "snooze" be something reasonable?
Alas. Afraid not. I'll just have to do the best I can with what's here -- for now at least -- which for me means setting the default alert time to 11 minutes, so that if I see the alert right away and click snooze, I'll still have time to dial in when it pops up again. But I think I'll drop a note to the UI planners suggesting the different interface pictured here.
While I'm at it, I'll mention again that the dialog shouldn't get dismissed if someone's typing when it comes up, forcing them to open their calendar to see what the notice was about. (Hint: it was about an event three days away. They won't see anything on today's meetings. They will shrug; must've been some other kind of message. They won't realize what Notes was trying to tell them until they get in to work after a very frosty breakfast the day of their wedding anniversary.)
Andre Guirard | 7 July 2008 08:42:52 AM ET | Home, Plymouth, MN, USA | Comments (5) | Permanent Link
I don't know why that last post wasn't showing up, but try again now...
Andre Guirard | 25 June 2008 09:14:11 AM ET | Home, Plymouth, MN, USA | Comments (6) | Permanent Link
I know that in Domino developer training, you were taught to create a dozen temporary variables for the arguments to your @Functions.
months := 0;
days := 30;
hours := 0;
minutes := 0;
seconds := 0;
tmp := @Adjust(SomeDate; years; months; days; hours; minutes; seconds);
If you still do this, don't. Because:
- It's unnecessary extra typing, using up time that you could be spending on slack.
- It hurts performance, especially in "hot" code areas such as before a SELECT statement.
- It makes the formula harder to read because you can't tell what the value of an argument is without looking at two different places in the code -- and if the temporary variable assignments are far from the @Function, a reader must scan all the intervening code to be sure they haven't changed meanwhile.
- No, you can't make it a rule to always define the temporary variables you need just before you use them, because the syntax doesn't always allow that (maybe the @Function is in the fifth "elseif" clause of an @If). So there will be intervening code, or else you force yourself to use @Do everywhere just so you can do this dumb thing.
- Person A writing the formula will be lazy and reuse an argument variable name further down in the formula so that they don't have to make up a new name and type the assignment. Person B (or person A later on, having forgotten) will change the value of the argument variable because they want to change it in the first function call, not realizing that they also affect the second function call.
- Because macro language lets you use an undeclared name anytime, it won't warn you if the spelling of your variable name is different in the two places.
- It prevents you from noticing otherwise obvious errors in the code as you're looking at it for other reasons, because you don't know the argument values as your eyes scan past them.
Here's another problem:
fieldname := "History";
value := "";
uniqueKey := "";
REM {Clear the history entries from the shared profile document.};
@SetProfileField( profilename ; fieldname ; uniqueKey; value )
This works great! Later, someone decides that the proper value for a cleared history is "
value := "
This likely results in a post to the Domino forums that "@SetProfileField doesn't work." Figuring out why it doesn't work is left as an exercise for the reader. It might take you longer when you run across it unexpectedly 'in the field' because I've already hinted where to look. How long might it take someone without the hint, and who also doesn't know formula language as well as you, to catch on?
If you must flag your arguments with a name, take advantage of the fact that := returns the assigned value as its value. E.g.:
You could still fool yourself as in the above example, but at least you don't have the possibility of spelling them differently in the two places, or of changing their values between where they are set and used, and it doesn't matter if you use the same variable name in two places. I'm not saying you should do it this way; I'm just saying this is a little less dumb than the way you were taught.
Andre Guirard | 25 June 2008 06:09:00 AM ET | Home, Plymouth, MN, USA | Comments (14) | Permanent Link
When a document is created, it's assigned a unique ID based on the creation time. However, the clock ticks over about every .01s, so if you create two documents in a very short time, it gives the second one a future creation time. Normally you don't notice this because this time is just .01s ahead, but if you create many, many documents in a short time, each has to have a unique creation time, so it pushes the time farther and farther forward, until it can be off by hours -- enough that users who create documents in the morning may find they're shown as being created early that afternoon, for instance.
If you modify an existing document, you aren't assigning a new ID, so I believe Notes doesn't find it necessary to advance the clock to prevent two documents having the same modified time. But if you create a document, and then modify a document (even if they aren't the same document), we want the creation time and the modified time to be in the right order. So if the creation time has been pushed ahead, this will also affect the modified time of documents you edit.
Mass document creation most often happens because of an agent that deletes all the documents overnight, and creates all new ones from some outside data source. Another effect of such an agent is to create many, many deletion stubs -- many times more than you have documents. There are many reasons you don't want to do this; weirding out your creation/modification times is one of the more minor ones. It's much better to synchronize documents with an outside data source by locating the corresponding old documents and only updating them if you need to.
The algorithm for doing such an update is shown in this download. It contains an agent named "5. Replicate Pirates," with a reusable replication engine that reads one record at a time from two LC LSX result sets and compares the keys and data values to decide whether or not there's a difference between the records, synchronizing the data one-way. This is possibly slower than just deleting and re-creating, but possibly not, since those deletion stubs tend to hurt performance.
Andre Guirard | 24 June 2008 11:16:38 AM ET | Home, Plymouth, MN, USA | Comments (7) | Permanent Link
I went to a fantasy writing convention this weekend, and was introduced to something kind of cool. You might know how some popular TV shows -- especially SF and fantasy shows like Buffy, Star Trek, and Firefly -- inspire people to create fan websites, write fan fiction (their own stories with the same characters and setting), and so on. A few people have decided to take this one step further -- they're doing the same thing, only they dispensed with the TV show. They got together a production staff (small, since they're not actually filming anything), cast the show with a mix of real, imaginary, and posthumous actors, and they pretend to have produced the episodes, release little snippets of gossip about the actors, create blogs and LiveJournal pages for the actors and (oddly) for the characters who are purportedly fictional (so that you can send them email and see what they're doing in their off hours), mocked up script pages with the actors' handwritten notes on them, sell show merchandise....
They have everything but the actual episodes (which are posted on the website as novellas at exact times the episodes were to have aired, so that the fans will know what they're supposed to have seen). I suppose this saves a lot of money on production -- and they still have hundreds of fans speculating about how the episodes are going to come out, writing fan fiction; one of them, knowing from his LJ that one of the fictional characters would be in her neck of the woods, emailed him and set up a date. (She got stood up -- couldn't be helped, as that character was having a really bad week -- and now he's posting to his LJ from his hospital bed, lying to his friends about how badly hurt he is so they won't worry about him). Someone sent him brownies.
Anyway, the result is Shadow Unit. Check it out; it's fun; and the episodes, written by the "high-standards" fantasy authors Emma Bull, Elizabeth Bear and Will Shetterly -- really are quite good; certainly better than what you'd expect to see on TV.
P.S. Yes, the fans all know that the show doesn't really exist -- unless possibly a few of them are seriously schizophrenic. It's all part of the game.
P.P.S. I don't think they've figured out a way to make money from this yet -- the merchandise sales don't do much more than cover their costs, if that. But they're having a blast doing it. In case you think it's a really cool idea and want to see it continue, they do take donations -- or you could buy a tote bag.
P.P.P.S. Season 1 is over, so it's not quite as much fun at the moment. But they plan to start up again along with all the regular series in the fall, so make a note on your calendars, and in the meantime catch the reruns to see what you missed.
Andre Guirard | 22 June 2008 09:05:32 PM ET | | Comments (1) | Permanent Link

