Tuesday, April 25, 2006

Don't RETURN Inside WITH

At GLGDW, I mentioned during Marcia Akins' excellent Best Practices for Class Design session that one of the leading causes of C5 errors is using RETURN inside WITH structures. Given the number of people that came up to me after the session, this clearly isn't well-known, so here's the scoop.


I'm not sure exactly what happens when you use a WITH structure, but clearly VFP stores a reference to the object specified in the WITH statement somewhere. Obviously, the reference must be removed at some point or else the object couldn't release, but I suspect there's a memory leak under these conditions, and that when enough of these memory leaks happen, you end up with a C5 error. The insidious thing about memory leaks is that the C5 error can occur far away in both code and time from the original source, so they're next to impossible to track down for mere mortals like me who don't do C code debugging.


I first started looking into this about 18 months ago. We had fairly regular reports of C5 errors from people going into or out of the Report Designer from within Stonefield Query. The problem is that it wasn't reproducible -- I could never get it to happen when I tried (I did have it happen a couple of times when I didn't want it to, such as during demos!). I went through the code related to the Report Designer with a fine-tooth comb and couldn't see anything that could cause this. Then I remembered some weird behavior from years earlier: if I used RETURN within a WITH statement, under some conditions, the object specified wouldn't release. That problem was fixed in a later version of VFP, so I'd forgotten about it, but it occurred to me that I'd used that a lot in my code even though it really isn't a good practice. So, I spent a day or two refactoring every single instance of RETURN inside WITH so the RETURN statement was after the ENDWITH. Since I couldn't reproduce the C5 errors on demand, it was hard to know whether this worked or not, but we haven't had a single C5 error reported since we released that version of Stonefield Query. And in thinking about what may be going on underneath the covers, it makes sense to me that this was likely the culprit.


So, a heads-up for everyone: if you're getting C5 errors and are pulling your hair out trying to track them down, look at all your WITH structures and move any RETURN statements below the ENDWITH. Not only is it good programming practice, it may kill those maddening problems.


Back from GLGDW

I got back from Milwaukee late last night. What a conference! Many people were saying it was the best conference they ever attended. Here are my thoughts:


  • I loved the format: one track so everyone was in one room and lots of audience participation. It took a little getting used to, and definitely was hard on time management, but it was great hearing audience members telling everyone about their experiences with X or what utilities they use to solve a problem. It felt more like a big workshop rather than a conference.

  • Whil's daughter Aleix, who helped with everything from registration to book sales, was amazing. At 13, she has the poise and self-confidence of someone much older. For example, she came to dinner Saturday night with 6 of us without her Dad--who, although he gave another excuse, was scared of the Thai restaurant we went to (gd&rfwh)--and it didn't faze her in the slightest listening to our typical 40-something conversation.

  • Nearly every speaker told me that even they learned a lot. In fact, most speakers attended every session, something you rarely see at other conferences.

  • The only negative was the wireless connection provided by the hotel. It only worked in the lobby, not the conference room, nor in my hotel room (although some people said it worked OK in their rooms).


Here's a breakdown of the sessions:


  • Best Practices for Development Environment Setup: the first session, this was sort of a panel presentation on Friday night. I say "sort of" because there wasn't a table for panelists, but most of the speakers sat in the front row. Rick Schummer ran the show and did a great job of encouraging audience participation. In fact, that session set the tone for the rest of the conference. The topic was about the best ways to set up the VFP IDE, such as modifying the VFP system menus to provide quick access to commonly used utilities.

  • Best Practices for Error Handling and Reporting: Rick did his usual great job presenting his session on error handling, pointing out things such as the priority of error handling in a mixed environment of TRY structures, Error methods, and a global error handler.

  • Best Practices for Class Design: given that I've been using VFP for more than 10 years, I didn't expect to learn much from this session, but I was surprised. Marcia Akins challenged everyone with her ideas of the "must, could, and should" rules of class design and provided lots of examples to nail down the points. I'll definitely used some of the things she taught to make my class designs better in the future. Best of all, she gave away treats to people with the best suggestions or answers!

  • Best Practices for User Interfaces: Tamar Granor's session showed us good and bad examples of user interfaces (not just computer UI) and broke it down by section, including dialogs, menus, user accessibility, and so on.

  • Best Practices for Data Access (local): I'm ashamed to say that I missed Andy Kramek's session because I really wanted to go over my session one more time. Fortunately, Andy writes great white papers so I know I can get the content from there.

  • Best Practices for Data Access (remote): Andy's second session, held Saturday night after one of the best Thai meals I've ever had, was one of two sessions (Nancy Folsom's was the other) where some of the best practices were somewhat controversial. For example, Andy suggested that DSNs are preferred over DSN-less connections and that stored procedures are to used as a last resort. Not everyone agrees with these, but given Andy's extensive experience in this area, it's best to at least reconsider your views on these areas. He even gave a short break so those so inclined could get a beer from the nearby bar!

  • Best Practices for Refactoring: Nancy Folsom's 8:30 am session was one of my favorites. She went through reasons for refactoring, general concepts (for example, test before and after the change so you can confirm functionality wasn't affected and don't change too much in one swoop), and then detailed techniques, showing examples of code before, during, and after refactoring. Some of the "bad smells" (reasons to suspect you need to refactor) were controversial, such as the presence of comments (even her slide has a "wha??" after that point!), but thought-provoking. She only finished about half her material due to audience participation, but I think that's a good thing, as it was great to explore ideas with others.

  • Best Practices for Reporting and Output: Barbara Peisch showed how she provides a generic reporting dialog to her users and showed the advantage of using a common output routine for all reports.

  • Best Practices for Project Management: Cathy Pountney discussed a wide range of issues when working on VFP projects, such as team building and management, scheduling, requirements analysis (she hates the term "gathering", which implies that requirements are just lying around waiting to be scooped up).

  • Best Practices for Vertical Application Development: my session did not start well. For some reason, my laptop wouldn't talk to the projector. I've never had problems with projectors before, so this was unexpected and very unpleasant. After fiddling with settings and a restart, we decided (15 minutes after the session was supposed to start) to rearrange the schedule and do the Best Practices for Debugging panel session while I moved everything I needed over to Rick Schummer's laptop. As a result, I totally missed that session (which I'm bummed about), including not giving the best practices I'd planned to present, so I'll blog about those in the next couple of days. I finally started at 4:45, more than an hour late, but after getting over the jitters due to what happened, using someone else's system, and not being able to show everything I planned because I couldn't install everything, I think the session went well. I discussed application activation and licensing, maintenance models, version update mechanisms, support policies, and, after a short break to allow those who were getting hungry to leave (it was past 6 pm at this point), error reporting. It was great getting feedback from the audience about how they do some of those things--I definitely liked the audience participation, even if it made it impossible to cover everything (as Nancy and some others found) or finish on time (in my case--my session was about 1:45 rather than the planned 1:30, but there was nothing after my session so it worked out).

  • Best Practices for Designing Middle Tier Components: Craig Berntson discussed the importance of separating business logic, data access, and user interface into different components, even if they're in the same physical layer, and presented some simple classes that explained the concepts very well.

  • Best Practices for Deployment: Rick Borup's session was probably the closest one to the "best practices" theme, as he discussed the five stages of deployment. I came away with several good ideas about how to make deployment work better.


The general consensus was that there should be another GLGDW next year. Whil didn't promise anything, so anyone interested in attending an inexpensive conference where you'll learn things that simply aren't presented at other conferences should email Whil and tell him they'll be there next year. Thanks, Whil, for going to all the hard work of putting on yet another great conference, and here's my vote for GLGDW 2007.

Checking out Qumana

After a few uses, I've decided the Blogger editor kind of sucks. It's slow (especially if you write longer entries as I've tended to so far), klunky, and seems to have a mind of its own regarding formatting. The fatal thing for me, though, was losing an entire entry just as I was finishing it up last week. So, I'm using a free blog editor called Qumana after reading Craig Bailey's recommendation.

So far, so good. It's very responsive (as you'd expect for a desktop app rather than a browser-based one like the Blogger editor), has a built-in spell checker with the little red squigglies like Microsoft Word, works offline (like I'm doing right now), and supports the same features I like about the Blogger editor (being able to switch between editing in text or HTML, formatting toolbar, easy hyperlinking, etc.). A few things I haven't tried yet are support for ads, tags, categories, and trackbacks, and the DropPad, which allows you to add text or images from any source by dragging and dropping. The only thing I've found that I don't like is the lack of local help; it's available on the Qumana web site. Since I don't currently have a connection (I'm typing this on my flight to Milwaukee), that doesn't work.


Wednesday, April 19, 2006

Forget TXTWIDTH - use GdipMeasureString

For years, we've used code like the following to determine the width of a string:
lnWidth = txtwidth(lcText, lcFontName, lnFontSize, ;
lcFontStyle)
lnWidth = lnWidth * fontmetric(6, lcFontName, ;
lnFontSize, lcFontStyle)
This code works OK in many situations, but not in one in particular: when defining how wide to make an object in a report.

The value calculated above is in pixels, so you must convert the value to FRUs (the units used in reports, which are 1/10000th of an inch); you need to multiply by 104.166 (10000 FRUs per inch / 96 pixels per inch). Instead of doing all that work, you could use the GetFRUTextWidth method of the FFC _FRXCursor helper object:
loFRXCursor = newobject('FRXCursor', ;
home() + 'FFC\_FRXCursor.vcx')
lnWidth = loFRXCursor.GetFRUTextWidth(lcText, ;
lcFontName, lnFontSize, lcFontStyle)
The problem is this doesn't actually give you the correct value. The reason is because reports use GDI+ for rendering and GDI+ renders objects a little larger than you'd expect it to.

To see this problem, do the following:
use home() + 'samples\data\customer'
loFRXCursor = newobject('FRXCursor', ;
home() + 'FFC\_FRXCursor.vcx')
select max(loFRXCursor.GetFRUTextWidth(trim(company), ;
'Arial', 10)) from customer into array laWidth
wait window laWidth[1]
I get 22500. Now create a report, add a field, enter "company" as the expression, and make it 2.25 inches wide (22500 FRUs / 10000 FRUs per inch). Preview the report. The telltale ellipsis at the end of some values indicates the field wasn't sized wide enough.

This drove me crazy for years. I figured out an empirical "fudge" factor to add to the calculated width; 19 pixels (1979.154 FRU) seemed to work most of the time, but occasionally I'd find that wasn't enough for some values.

Fortunately, since reports use GDI+, we can use a GDI+ function to accurately calculate the width. GdipMeasureString determines several things about the specified string, including the width. Even better, VFP 9 comes with a GDI+ wrapper object so you don't have to understand the GDI+ API to call GdipMeasureString.

To show an example of using the GDI+ wrapper classes, take a look at this function:
function GetWidth(tcText, tcFontName, tnFontSize)
local loGDI, ;
loFont, ;
lnChars, ;
lnLines, ;
loSize
loGDI = newobject('GPGraphics', ;
home() + 'FFC\_GDIPlus.vcx')
loFont = newobject('GPFont', ;
home() + 'FFC\_GDIPlus.vcx', '', tcFontName, ;
tnFontSize, 0, 3)
loGDI.CreateFromHWnd(_screen.HWnd)
lnChars = 0
lnLines = 0
loSize = loGDI.MeasureStringA(tcText, loFont, , , ;
@lnChars, @lnLines)
lnWidth = loSize.W
release loGDI, loFont, loSize
return lnWidth
Now try the following:
select max(GetWidth(trim(company), ;
'Arial', 10)) from customer into array laWidth
wait window ceiling(laWidth[1] * 104.166)
This gives 23838. Change the width of the field in the report to 2.384 inches and preview it again. This time the values fit correctly.

The only problem now is that this code can take a long time to execute if there are a lot of records because for each call, a couple of GDI+ wrapper objects are created and some GDI+ setup is done. I created a wrapper class for GdipMeasureString called SFGDIMeasureString that works a lot more efficiently.

Let's look at this class in sections. Here's the start: it defines some constants, the class, and its properties:
* These #DEFINEs are taken from
* home() + 'ffc\gdiplus.h'

#define GDIPLUS_FontStyle_Regular 0
#define GDIPLUS_FontStyle_Bold 1
#define GDIPLUS_FontStyle_Italic 2
#define GDIPLUS_FontStyle_BoldItalic 3
#define GDIPLUS_FontStyle_Underline 4
#define GDIPLUS_FontStyle_Strikeout 8
#define GDIPLUS_STATUS_OK 0
#define GDIPLUS_Unit_Point 3

define class SFGDIMeasureString as Custom
oGDI = .NULL.
&& a reference to a GPGraphics object
oFormat = .NULL.
&& a reference to a GPStringFormat object
oFont = .NULL.
&& a reference to a GPFont object
oSize = .NULL.
&& a reference to a GPSize object
nChars = 0
&& the number of characters fitted in the
&& bounding box
nLines = 0
&& the number of lines in the bounding box
nWidth = 0
&& the width of the bounding box
nHeight = 0
&& the height of the bounding box
nStatus = 0
&& the status code from GDI+ functions
The Init method instantiates some helper objects and declares the GdipMeasureString function. Destroy nukes the member objects:
function Init
This.oGDI = newobject('GPGraphics', ;
home() + 'ffc\_gdiplus.vcx')
This.oFormat = newobject('GPStringFormat', ;
home() + 'ffc\_gdiplus.vcx')
This.oFont = newobject('GPFont', ;
home() + 'ffc\_gdiplus.vcx')
This.oSize = newobject('GPSize', ;
home() + 'ffc\_gdiplus.vcx')
declare integer GdipMeasureString ;
in gdiplus.dll ;
integer nGraphics, string cUnicode, ;
integer nLength, integer nFont, ;
string cLayoutRect, integer nStringFormat, ;
string @cRectOut, integer @nChars, ;
integer @nLines
endfunc

function Destroy
store .NULL. to This.oGDI, This.oFormat, ;
This.oFont, This.oSize
endfunc
MeasureString determines the dimensions of the bounding box for the specified string:
function MeasureString(tcString, tcFontName, ;
tnFontSize, tcStyle)
local lcStyle, ;
lnStyle, ;
lnChars, ;
lnLines, ;
lcBoundingBox, ;
lnGDIHandle, ;
lnFontHandle, ;
lnFormatHandle, ;
lcRectF, ;
lnStatus, ;
llReturn
with This

* Ensure the parameters are passed correctly.

do case
case vartype(tcString) <> 'C' or ;
empty(tcString)
error 11
return .F.
case pcount() > 1 and ;
(vartype(tcFontName) <> 'C' or ;
empty(tcFontName) or ;
vartype(tnFontSize) <> 'N' or ;
not between(tnFontSize, 1, 128))
error 11
return .F.
case pcount() = 4 and ;
(vartype(tcStyle) <> 'C' or ;
empty(tcStyle))
error 11
return .F.
endcase

* Set up the font object if the font and size
* were specified.

if pcount() > 1
lcStyle = iif(vartype(tcStyle) = 'C', ;
tcStyle, '')
.SetFont(tcFontName, tnFontSize, lcStyle)
endif pcount() > 1

* Initialize output variables used in
* GdipMeasureString.

lnChars = 0
lnLines = 0
lcBoundingBox = replicate(chr(0), 16)

* Get the GDI+ handles we need.

lnGDIHandle = .oGDI.GetHandle()
if lnGDIHandle = 0
.oGDI.CreateFromHWnd(_screen.HWnd)
lnGDIHandle = .oGDI.GetHandle()
endif lnGDIHandle = 0
lnFontHandle = .oFont.GetHandle()
lnFormatHandle = .oFormat.GetHandle()

* Get the size of the layout box.

lcRectF = replicate(chr(0), 8) + ;
.oSize.GdipSizeF

* Call the GdipMeasureString function to get
* the dimensions of the bounding box for the
* specified string.

.nStatus = GdipMeasureString(lnGDIHandle, ;
strconv(tcString, 5), len(tcString), ;
lnFontHandle, lcRectF, lnFormatHandle, ;
@lcBoundingBox, @lnChars, @lnLines)
if .nStatus = GDIPLUS_STATUS_OK
.nChars = lnChars
.nLines = lnLines
.nWidth = ctobin(substr(lcBoundingBox, ;
9, 4), 'N')
.nHeight = ctobin(substr(lcBoundingBox, ;
13, 4), 'N')
llReturn = .T.
else
llReturn = .F.
endif .nStatus = GDIPLUS_STATUS_OK
endwith
return llReturn
endfunc
GetWidth is a utility method that returns the width of the specified string:
function GetWidth(tcString, tcFontName, ;
tnFontSize, tcStyle)
local llReturn, ;
lnReturn
with This
do case
case pcount() < 2
llReturn = .MeasureString(tcString)
case pcount() < 4
llReturn = .MeasureString(tcString, ;
tcFontName, tnFontSize)
otherwise
llReturn = .MeasureString(tcString, ;
tcFontName, tnFontSize, tcStyle)
endcase
if llReturn
lnReturn = .nWidth
endif llReturn
endwith
return lnReturn
endfunc
SetSize sets the dimensions of the layout box for the string:
function SetSize(tnWidth, tnHeight)
if vartype(tnWidth) = 'N' and ;
tnWidth >= 0 and ;
vartype(tnHeight) = 'N' and tnHeight >=0
This.oSize.Create(tnWidth, tnHeight)
else
error 11
endif vartype(tnWidth) = 'N' ...
endfunc
SetFont sets the font name, size, and style to use:
function SetFont(tcFontName, tnFontSize, tcStyle)
local lcStyle
do case
case pcount() <>= 2 and ;
(vartype(tcFontName) <> 'C' or ;
empty(tcFontName) or ;
vartype(tnFontSize) <> 'N' or ;
not between(tnFontSize, 1, 128))
error 11
return .F.
case pcount() = 3 and ;
vartype(tcStyle) <> 'C'
error 11
return .F.
endcase
lcStyle = iif(vartype(tcStyle) = 'C', tcStyle, '')
lnStyle = iif('B' $ lcStyle, ;
GDIPLUS_FontStyle_Bold, 0) + ;
iif('I' $ lcStyle, ;
GDIPLUS_FontStyle_Italic, 0) + ;
iif('U' $ lcStyle, ;
GDIPLUS_FontStyle_Underline, 0) + ;
iif('-' $ lcStyle, ;
GDIPLUS_FontStyle_Strikeout, 0)
This.oFont.Create(tcFontName, tnFontSize, ;
lnStyle, GDIPLUS_Unit_Point)
endfunc
Let's try the previous example using this class:
loGDI = newobject('SFGDIMeasureString', ;
'SFGDIMeasureString.prg')
select max(loGDI.GetWidth(trim(company), 'Arial', 10)) ;
from customer into array laWidth
wait window laWidth[1] * 10000/96
This is a lot faster than the GetWidth function presented earlier. The following would run even faster because the font object doesn't have to be initialized on each call:
loGDI = newobject('SFGDIMeasureString', ;
'SFGDIMeasureString.prg')
loGDI.SetFont('Arial', 10)
select max(loGDI.GetWidth(trim(company))) ;
from customer into array laWidth
wait window laWidth[1] * 10000/96
The cool thing about this class is that it can do a lot more than just calculate the width of a string. It can also determine the height or the number of lines a string will take at a certain width (think setting MEMOWIDTH to a certain width and then using MEMLINES(), but faster, more accurate, and supporting fonts).

For example, I have a generic message dialog class I use to display warnings, errors, and other types of messages to the user. I don't use MESSAGEBOX() for this because my class support multiple buttons with custom captions. The problem is that the buttons appear below an editbox used to display the message. So, how much room do I have to allocate for the height of the editbox? If I don't specify enough, the user has to scroll to see the message. If I specify too much, short messages look goofy because there's a lot of blank space before the buttons. Now, I can make the editbox an arbitrary size and use SFGDIMeasureString to determine the necessary height for the editbox for a given message, adjusting the positions of the buttons dynamically. To do so, I call the SetSize method to tell SFGDIMeasureString the width of the editbox (I pass a very large value, like 10000, for the height, so it isn't a factor), then call MeasureString, and use the value of the nHeight property for the height of the editbox.

I'm finding a lot more uses for this class. I hope you find it useful too.

Off to GLGDW

Friday morning I head for Milwaukee (my flight leaves bright and early at 6 am) to join 100 attendees and 10 speakers at the Great Lakes Great Database Workshop. This was always the best VFP conference and while this year's has a very different format, it promises to live up to its "great" monikor.

I'm especially interested in hearing Rick Schummer's session on "Best Practices for Error Handling and Reporting" and Rick Borup's session on "Best Practices for Deployment", two topics that are near and dear to my development heart.

I'm a little nervous about my "Best Practices for Vertical Application Development" because it's quite a departure from my usual code-rich sessions. This one is almost all "here's how I do things" with lots of explanation and demos with a real application, but almost no code shown.

Wednesday, April 05, 2006

Error Handler Gets Harder

One of the best things added to VFP 8 is structured error handling using TRY ... CATCH ... ENDTRY structures. It can also make error handling harder than it used to be.

First, some background. I use a Chain of Responsibility error handling mechanism I discuss in my error handling white paper. All of my base classes have code in their Error methods that implement this mechanism. If an error occurs in an object and that object's Error method doesn't handle it, the error is bubbled up the class hierarchy, then the containership hierarchy. If no object handles the error, the global error handler (stored in an object named oError) handles it. This mechanism has served me well for more than a decade.

One issue with this design, though, is dealing with anticipated errors. It's kind of a pain to put code into the Error method of an object just to deal with things you know could go wrong, such as opening a table. That means having to use code like:

lparameters tnError, tcMethod, tnLine
local lcReturn
do case

* Handle a specific error.

case tnError = SomeValue
* deal with it

* Use the normal error handling mechanism for all other types of errors. Note
* that we add a period to the method name passed. This tells our parent class
* to return the error resolution string back to us, which is required for RETRY
* to work.

otherwise
lcReturn = dodefault(tnError, '.' + tcMethod, tnLine)
* deal with the various return values
endcase
The reason I had to do it this way is because once you have code in the Error method of an object, local error handling like the following is ignored; llError never becomes .T. because the Error method is fired instead.

on error llError = .T.
llError = .F.
* some code that may fail
if llError
* deal with it
endif llError
TRY to the rescue. All of the ugly error-specific code in Error can now be removed and true local error handling implemented:

try
* some code that may fail
catch to loException
* deal with it
endtry
However, now we have a big problem: under some circumstances, we can no longer deal with the error.

Here's the scenario: the global error handler presents a dialog to the user, informing them of the problem and asking if they want to continue working in the application or quit. If they choose to continue, we need a way to prevent execution from returning back to the method that caused the error, since that would almost certainly cause more errors. So, the error handler object has a cReturnTo property that specifies where to RETURN TO when the user chooses that option. cReturnTo is normally set to the name of the method containing the READ EVENTS statement for the application, and the following code does the trick:

lcProgram = This.cReturnTo
do case
case inlist(_vfp.StartMode, 2, 3, 5)
* use COMRETURNERROR
case not llReturnTo
case not empty(lcProgram)
return to &lcProgram
otherwise
return to master
endcase
Normally, this works great. If an unexpected error occurs, the user has the choice of continuing to use the application or quitting. If they choose to continue the application, they usually don't lose any work (such as a new report they were working on), which they would if they chose to quit.

Here's where the problem comes in: if the code in a TRY block calls a method of an object and that object has code in its Error method (which all of my base classes do), the error is handled by the Error method rather than the TRY block. So, we're sort of back to the "ON ERROR gets ignored" scenario, except we're worse off. To see why, imagine code like this:

try
SomeObject.SomeMethod()
catch to loException
* deal with it
endtry

define class SomeObject as SFCustom of SFCtrls.vcx
function SomeMethod
* some line of code causing an error
endfunc
enddefine
Because SomeObject is a subclass of SFCustom, ultimately the error is going to bubble up to the global error object. When the user chooses to continue with the application, the RETURN TO &lcProgram statement executes, and BOOM! The user gets hit with a "RETURN/RETRY statement not allowed in TRY/CATCH" error, and because we're inside an error handler, the built-in VFP error handler kicks in and we can say the application has crashed.

Aha, but we could use the SYS(2410) function to determine whether we're inside a TRY structure and then handle that, right? Wrong -- that function tells you what mechanism is used to handle an error, not whether there's a TRY structure somewhere in the call stack. So, how do we determine whether to allow the user to choose to continue if we don't know whether we're in a TRY or not?

My workaround for this works but feels like a kludge, so I'd appreciate any suggestions to handle this better. I added an lInsideTry property to my global error handler object and if that property is true, the error handler tells the user that "Due to the nature of the error, the application must shut down" and hides the Continue button. An additional CASE statement in the code I showed above quits the application if lInsideTry is .T. lInsideTry is normally .F., but any TRY structure that calls the method of an object now looks like this:

oError.lInsideTry = .T.
try
SomeObject.SomeMethod()
catch to loException
endtry
oError.lInsideTry = .F.
Fortunately, there are only about a half-dozen places in Stonefield Query where I had to resort to this ugly code.

In conclusion, while some new features make things easier, in some cases, mixing new and old features can sometimes have unexpected consequences or even make things harder.