Monday, November 27, 2006

New Web Site

We switched Web and email servers on the weekend. While our old provider gave great service, we outgrew their capabilities. We also wanted to move to a DotNetNuke-based Web site because DNN allows you to create great-looking Web sites with a fraction of the effort required to create one from scratch. We haven't done a DNN version of www.stonefield.com yet, just www.stonefieldquery.com. One of the best features of our new site is a support forums area; check it out if you're a Stonefield Query user.

Thanks to Jeff Zinnert, who did most of the work on the new site, Al Williams of Lucky Dog Systems who did the initial setup and design, and our former provider, Tim Thibault, who helped with the switch-over.

Updated Favorites for IntelliSense

Michael Hawksworth sent a couple of enhancement requests (including code; ya gotta love that!) for the Favorites for IntelliSense (FFI) Builder that deals with handling duplicate or substring names better. I've posted an update on the Technical papers page of www.stonefield.com.

Wednesday, November 22, 2006

New Files on White Papers and Source Code Page

I posted a couple of new files on the White Papers and Source Code page of the Stonefield Web site.

The first is an updated version of the My namespace that ships with the October 2006 CTP version of Sedna. It fixes a couple of bugs in My and includes a sample form demonstrating some of the uses of My.

The second is a compilation of articles originally published in the August and November 2005 issues of FoxTalk describing how to add IntelliSense to runtime applications and how to control IntelliSense so it only shows the properties, events, and methods you're interested in seeing, providing a lot more control over IntelliSense than you normally get (a feature I call "Favorites for IntelliSense").

Rick Strahl Saves the Day

Stonefield Query uses an online activation mechanism so a customer can activate their software without having to contact us for an activation code. When they purchase the software, we assign them a serial number and update a database on our Web site. They install and run Stonefield Query, type in the serial number, and click the Online button. This calls a West Wind Web Connection application on our Web site that returns an activation code. The code on the client site is similar to this:

loHTTP = newobject('wwIPStuff', 'wwIPStuff.vcx')
loHTTP.Connect('www.SomeURL.com')
loHTTP.AddPostKey('SomeKey', 'ValueForKey')
lcPage = 'wc.dll?SomeProcess'
lcHTML = ''
lnLen = 0
try
lnResult = loHTTP.HTTPGetEx(lcPage, @lcHTML, @lnLen)
catch to loException
lnResult = -1
endtry

This works great. However, I got a bug report from one of our support staff the other day. They updated to Internet Explorer 7 and suddenly online activation stopped working. The error message displayed was "invalid handle."

I dreaded having to track this down. First, I had to install IE 7, which I've been putting off doing for a variety of reasons. Second, I'd have to dig into stuff that I'm not really familiar with--the wwIPStuff code and possibly a DLL--to figure out why this is failing. (And why would installing an updated version of an application, albeit one closely tied into Windows, change the behavior of a system DLL anyway?)

However, I remember reading a message on the West Wind Message Board (a great resource even if you don't use any West Wind tools) from a week or so ago stating that the HTTP code in wwIPStuff was deprecated and that wwHTTP should be used instead. I hadn't any reason to revisit any of my code using wwIPStuff because it just worked. So, after installing IE 7, I changed one line of code:

loHTTP = newobject('wwHTTP', 'wwHTTP.prg')

I crossed my fingers and tried the online activation. Problem solved! Since my copy of West Wind Client Tools is a couple of years old (yeah, I know I should update it), clearly pre-dating IE 7, I'm not sure why this worked better than wwIPStuff. In researching the problem later, I found an entry in Rick Strahl's blog that described the problem and the fact that he doesn't know why wwIPStuff fails either. Although I read his blog regularly, clearly I'd missed the significance of this one. Thanks again, Rick, for a great product!

Thursday, October 26, 2006

Southwest Fox 2006, Part 3

I started the last day of the conference by repeating my IntelliSense session, then went to hear Rick Schummer present Fishing with a ProjectHook. Although I've worked with ProjectHooks since VFP 6, Rick showed some really cool uses for them, so I picked up several tips from his session.

The conference wound up with several drawing for prizes and a farewell to everyone. About 20 of us went for lunch in the hotel. I ate too much pizza, so I went to the exercise room to wear some of it off, then had a short nap and watched the Oakland Raiders and Arizona Cardinals (actually, it was the other way around; the game was kind of dull so I nodded off). After a few drinks in the hotel bar with a good sized group of VFPers, Tamar Granor, Craig Berntson, Rob Eisler, and I went to the ASU area for some very tasty Greek food.

Rob and I stayed an extra day to see the Grand Canyon, which neither of us had seen before. Joining us on the tour was Tore Bleken from Norway, who'd been there three times before. This time, he took a helicopter tour in addition to the regular tour and said it was well worth it. Rob and I spent an hour hiking along the South Rim, then did some shopping. It was a long day (they picked us up at 6:30 am and dropped us off at 8:00 pm), over 500 miles of travel, but well worth it -- I definitely want to go back to do some hiking and white water rafting. Tore and I had a very nice dinner at the hotel restaurant (Rob wasn't hungry after a stop at Burger King on the return trip, which I passed on).

Final impressions: what a great conference! The topics were different from the usual fare, especially Christof's sessions, and I made tons of notes of ideas to implement. The speakers all did a great job, and I can't wait until next year's conference.

Southwest Fox 2006, Part 2

I started Saturday morning with Bo Durban's Using GDI+ with VFP session, which focused on the amazing VFPX GDI+ project he and Craig Boyd have been working on. He showed how easy it is to use GDI+ to do any type of drawing on any type of Windows canvas (window, report, printer, etc.), including rectangles, ellipses, pie slices, polygons, and paths of any shape. Very cool session!

Next I presented my Adding IntelliSense to Your Application session. As I blogged about when I presented this in Prague, it's kind of an esoteric topic, since not everyone needs to provide IntelliSense to their users. However, I was pleasantly surprised at how much feedback I got on this session, and how much interest there was in my Favorites for IntelliSense class, which gives you control over how IntelliSense works and what it displays for a particular class.

I missed the next session as I caught up on email and chatted with other developers. However, I'm sorry I missed Mike Feltman's Where Do You Want To Go Today session, which I had originally planned to see; from the comments of those who attended, it was a great session.

After lunch (make your own sandwiches from a buffet), I attended Nancy Folsom's session From Procedure Toward Component. This was a refactoring session similar to the one she did at GLGDW. I really liked the way she presented it, showing one step of a refactoring at a time and going over the code changes at each step. This approach made a somewhat abstract topic very concrete.

The last session of the day was Toni Feltman presenting Using Version Control with VFP. She started the session with a clarification: yes, she is pregnant, not just getting bigger. She then proceeded to show the basics of Visual SourceSafe 6.0, an outdated version she chose on purpose because it's one that many VFP developers already have (it came with Visual Studio 6, as did VFP 6). She showed how it integrates with VFP and discussed the pros and cons of integration. Finally, she showed Visual SourceSafe 2005 and discussed the features of several other popular source control packages.

After a break, we had a speaker and vendor dinner at the conference center. Craig Berntson and Cathy Pountney showed us all up by dressing up (they looked like they were going to a prom). Cathy admitted she'd never seen Monty Python, so Craig tried to convince her by acting out several skits, but was unsuccessful (not surprising if you were there to watch!).

Sunday, October 22, 2006

Southwest Fox 2006, Part 1

After a very early start (5:30 am flight), I arrived here in Phoenix mid-afternoon for the Southwest Fox 2006 conference. Several people told me that Rick Schummer and Craig Boyd were panicking because I hadn't shown up yet, and we were due to deliver the keynote at 7 pm, and hadn't really gone through our presentation yet. Fortunately, we got together about 6:30 and briefly went over what we were planning to show, then spent the rest of the time setting up and fighting with the projector.

I think the keynote went well. Craig started off with a funny story about a hair-raising drive through the canyons of Phoenix (which somehow I've never seen) with me at the wheel. We then showed a Fox Software video from about 1990 showing the then newest version, FoxPro 1.0. Moving on to the recently released CTP of SP2 and Sedna, Craig showed some of the cool reporting enhancements in SP2, I showed My and the new Upsizing Wizard, Rick showed the Data Explorer enhancements he's been working on and discussed NET4COM, and Craig showed some of the things he's created in the Vista Toolkit. We had planned to show some demos of VFPX projects, but since I promised attendees that "shorter keynote = earlier beer", we figured we'd better wind up. However, Ken Levy then got up and announced that I was awarded the 2006 FoxPro Community Lifetime Achievement Award. I was completely surprised and blown away by this!

Friday morning, I had a tough decision to make: attend Rick Borup's Automating the Build session or Christof Wollenhaupt's Security Cookbook session. I knew Rick always creates great white papers, so I decided to see Christof's session because I knew it would be pretty important. Actually, like most of the other attendees, it scared the crap out of me. Christof showed how easy it is to extract the source code from a VFP EXE, even one that had been locked down with Refox. He discussed some of the ways hackers can get access to applications and discussed ways to secure an application. I took about a page of notes from that session!

Next, I presented Installing Applications with Inno Setup. I've been using Inno Setup for several years now and love it. One of the early slides was titled "Why I Hate Windows Installer". Some of the reasons I gave are the large file sizes, incredibly slow performance, problems with installation of ActiveX controls, proprietary database, and the fact that sometimes Windows Installer seems to have a mind of its own when it comes to deciding what files to install. I showed how fast Inno loads, how easy it is to create an Inno setup script (which is just a text file), and the flexibility it gives you in configuring the setup executable. I also showed how you can change the appearance and behavior of the setup using scripting.

I then attended Craig Berntson's Agile Software Development. This session was a nice introduction to the topic, covering things like the principles of agile development, why agile vs. traditional development methods, different types of agile, such as Scrum, and some books worth reading on the topic.

After lunch, I repeated my Inno Setup session, then attended Christof's Crashing VFP and Preventing Crashes session. Once again, Christof showed us internals of VFP that most people aren't aware of. For example, he created a trigger on a table and in the trigger code, did a SUSPEND. This left VFP in data session 0, the so-called "system" data session, and he showed the types of things that appear in that data session. He also showed the types of things that cause VFP to crash, such as dangling object references caused by not nulling references to one object stored in another when the second object is destroyed, and how to avoid those types of problems.

I finished the day with Bo Durban's Creating Custom Report Controls. Bo showed the basics of ReportListeners, then moved on to show some cool uses, including displaying negative numbers with parentheses so the decimals line up and there aren't extra spaces. He then spent time going over new features in SP2, including how easy it is to add you own pages to the Properties dialogs in the Report Builder. I took a lot of notes in this session!

After a few minutes to freshen up, 17 of us went to the F1 Race Factory for some karts racing (other folks went to a local casino). There had been some trash talking before the race, such as Steve Hanlon stating that because he drove a Mini Cooper that he was a force to be reckoned with, Craig challenging Cathy Pountney, who's been around cars and racing her whole life, and even me, who came in third at kart racing in Germany last year behind only Rick Strahl and Jeff Zinnert, neither of whom were here. The race was a blast, but at 10 minutes not nearly long enough. The winner isn't decided by who finishes first but by the fastest lap time, and Cathy was the winner over Rick Schummer by 0.03 seconds. I came in third, largely because I spun out a couple of times while driving a little more aggressively than I should have. This was definitely the most fun activity you can have at a conference!

Wednesday, October 18, 2006

Off to Southwest Fox

I'm headed to Phoenix tomorrow for the third annual Southwest Fox conference. If previous years are any indication, this will be a great conference. All the sessions sound really interesting, but the ones I'm most looking forward to seeing are:

  • Automating the Build by Rick Borup: Rick's sessions are always very detailed and thought-provoking, and this one sounds like a dandy.

  • Creating Custom Report Controls and Customizing the Report Designer by Bo Durban: long title not withstanding :), I haven't heard Bo speak before and this is a topic near and dear to my heart.

  • The Security Cookbook and Crashing VFP and Preventing Crashes by Christof Wollenhaupt: I've known Christof a long time but haven't heard him speak before, and given that his knowledge of the internals of VFP is probably deeper than anyone outside Microsoft, these are must-see sessions.

  • From Procedure to Component by Nancy Folsom: Nancy's session on refactoring at GLGDW earlier this year (which I believe she's also doing a version of here) was great, and this one sounds like it'll be very interesting as well.

Sunday, October 15, 2006

The World's Oldest Rock Band

Last week, my wife Peggy, business partner Mickey Kupchyk, his wife Sharon, and I went to see the Rolling Stones. Yes, the Rolling Stones came to Regina. In case you're not familiar with where I live, it's a city of under 200,000 in a province with a population under 1 million. (Also, as Mick Jagger himself stated near the beginning of the concert, Regina is a city that rhymes with fun.) So, how did a venue that small rate the Stones? Well, it turns out the promoter decided to hit a number of smaller sites on this tour, including Montana, which is similar in population. However, the response was anything but small; the concert sold out all 45,000 seats (it's in our CFL football stadium) in 17 minutes! So, they added a second show, the only city on the tour to have one, and it sold out in an hour. As a result, Regina outsold New York City for Stones tickets.

By sheer dumb luck, we managed to score second row, center-stage tickets for less than a third of the going price. We arrived at the stadium just as the warm-up band started, and discovered that the line to get in was at least half a kilometer long. Forty-five minutes later, we found our seats and laughingly figured they were almost too close to the stage: since it was about six feet high, we'd have to stand to see the whole stage. No biggie, of course, since I figured we wouldn't be sitting during the concert any way.

While we waited for the concert to start, we overheard the conversations of Rolling Stones nerds. (Until that night, I couldn't imagine putting "Rolling Stones" and "nerds" in the same sentence). One guy said this was his 24th concert on this tour, and 119th altogether. The couple beside him were from Norway, and they've seen the Stones hundreds of times in at least 30 countries. Someone claimed the Stones were his whole live, much more important to him that his family and friends. And here we were, Stones virgins!

Then the lights went down and out they came. Wow, this close up, you can really see the wrinkles! They started with Jumping Jack Flash, one of my favorites, and it just got better from there. In addition to the four usual members (Mick, Keith, Ron, and Charlie, if I can be so presumptuous to use their first names), they had a horn section for some songs, a great bassist (replacing Bill Wyman, who retired several years ago), keyboardist, another guitarist, and several singers.

The concert was amazing. They played all my favorites, including Honky Tonk Women and Brown Sugar, and Peggy's favorite, You Can't Always Get What You Want. Mick has an incredible amount of energy, bouncing and running all over the stage, which is doubly amazing considering he's 63. Keith looked as zoned-out at times as I expected, and often had his trademark cigarette hanging from his lips. At one point, the center of the stage, which was situated in the south end zone of the stadium, broken away and moved 55 yards to the middle of the field. Of course, that meant we had pretty much no view of them for a couple of songs, but those people in the north end zone sure appreciated it.

After more than two hours, they finally left the stage amidst shooting flames and a lot of fireworks. It was easily the best concert I've been to, and I've been to a lot. If you ever get a chance to see the Stones live, don't pass it up!

Monday, October 02, 2006

MVP Renewed

Thank you Microsoft for renewing my MVP status for another year. I've been an MVP since 1996 and feel honored every time I'm renewed.

Friday, September 29, 2006

Newest Stonefield Members

I'm pleased to announce the birth of Gavin Lee Zinnert, first child of Jeff and Amanda Zinnert, born last night at 11:18 pm. The baby was a healthy 6 lb. 7 oz. and 20". Jeff is a National Account Manager for Stonefield Software and has worked for us for a couple of years. You may have met him at DevCon, Southwest Fox, or German DevCon.

Also, technical support guru Tonia Batten has left us (her husband got a job in another city). We're sad to see Tonia go; her professionalism and keen attention to detail will be missed. However, I'm happy to announce the addition of two new members to our technical support team: Trevor Mansuy and Chris Wolf. Both have quickly come up to speed on Stonefield Query and Trevor has already taken over webinars. Because he is the third Chris and the second Wolf at Stonefield, which is interesting for a company of 18 people (we also have two unrelated Andersons and, including spouses, two Peggys), Chris Wolf has agreed to change his name to Quentin Tarantino, whom he vaguely resembles.

FoxTalk Rips Off Itself

I got another mailing from FoxTalk yesterday. Thinking it was their weekly load of spam mail soliciting a subscription, I almost didn't open it, but it was in a bigger envelope, so I decided to chance it. Of course, inside was the usual subscription offer, but it also included the September 2006 issue of FoxTalk. There on the front cover was an article by me titled "Generate Web Page Components in VFP". I immediately saw red, not just because that's the magazine's theme color. I wrote that article a couple of years ago and it was published in the May 2004 issue. Essentially, editor Jonathan Rabson repackaged it to fit their new, goofy editorial guidelines (and introduced a typo -- my Web site is www.stonefieldquery.com, not stonefieldquire.com) and published it again. So now, in addition to the fact that most articles have nothing to do with VFP and are written by folks who aren't VFP developers at all let alone experts and that the Kit Box column is a shadow of the excellence written by Andy and Marcia each month, we have to put up with recycled articles!

It is truly sad to see the incredibly fast decline in a once great magazine (and as a charter subscriber who still has the very first issue, I thought it was great long before I started writing for it).

Tuesday, September 19, 2006

Sightseeing in Prague

On Wednesday, Craig, Alan, Uwe, Venelina, Igor, and I took the train to Karlstejn, a picturesque little town about 40 km south of Prague with a castle high up overlooking the town. It was a bit of a hike up to the castle, but well worth it. We took a guided tour of the castle, including a chapel that included many rare paintings.

Here's the castle from the town:

Here's a view of one of the turrets from another part of the castle:

We took the train back to Prague and had a great dinner hosted by Gaby and Hans Lochmann at the TV Tower. The TV Tower is a Communist-era structure with a restaurant at an observation deck. It recently was decorated with what I think look like naked alien babies. Here's a close-up:

Craig and I spent Thursday and Friday sightseeing in Prague. I must say I'm very impressed. Prague is a lovely city: very tourist-friendly, the people are nice, the majority of the historical buildings are within walking distance of each other, and it's inexpensive (Igor, Craig, and I had a fabulous dinner Thursday night which cost, including beer, about $8 per person). It felt like we walked at least 20 or 30 km each day. We saw a lot of beautiful Gothic churches and climbed five towers: the clock tower at Old City Hall, a scaled-down version of the Eiffel Tower, the Powder Tower, and the towers of both St. Nicholas church (the one in New Town; there are two churches named St. Nicholas in Prague) and St. Vitus Cathedral (at the Prague Castle). Here's the Old City Hall (the observation deck is above the clock):

Here's the Powder Tower (the observation deck is just below the steeple):

Here's the Eiffel Tower clone:

As you can tell from these pictures, we spent as much time going vertical as we did horizontal.

Here's the beautiful Municipal Hall, used for concerts:

We met up with Igor late Thursday afternoon and went bar hopping to try out the various local brew. Craig doesn't drink beer (or a lot of anything else), so Igor and I felt challenged to corrupt him. He finally agreed to drink one beer, and here's the result:

Igor led us to Two Cats, where I had a great traditional Czech dinner of roast pork stuffed with cabbage, bread dumplings, red cabbage, and more beer.

After sightseeing Friday, Craig and I were both wiped out, plus I had to get up at 4 am for my flight home, so we ate at the Bavaria Restaurant at our hotel, and once again had a fabulous meal (peppercorn steak in my case, cooked perfectly).

What a great trip! Thanks to Igor for inviting me to speak and being a wonderful host.

Thursday, September 14, 2006

Prague DevCon, Day 2

I decided to skip Craig's COM+ sessions: I'd seen him present this topic before, he has excellent session notes, and I wanted to practice the two sessions I was doing that afternoon one last time. After doing so, and catching up on some email, I attended Alan's Introduction to LINQ session. LINQ is a lot further along that when I last looked at it, and it's very cool. I especially like the way you can query against anything IEnumerable, including arrays and collections, and that you can call functions and do just about anything in the query itself, just like you can with VFP.

After lunch (submarine sandwiches again), I presented my Mining for Gold in XSource session. In this session, I show how to reuse some of the components that come with VFP, such as a progress bar, object-oriented shortcut menu, and even a powerful but easy-to-use builder framework. I saw a lot of smiles when I showed how to create a useful builder in less than five minutes.

My second session, Cool Uses for ReportListeners, followed a short break. This is a fun session to present, because I show how to hyperlink reports, useful for things like drilldown reports, how to create a report preview window with a "live" surface (one that supports click and other events), and doing text search within a preview window. I got a lot more feedback from these two sessions than I did yesterday's, so hopefully they went over well with the audience.

That evening, the speakers took a bus, the metro, and a tram to a restaurant in downtown Prague (the TOP Hotel, where the conference is located, is quite a ways out of central Prague) owned by Derek, who is the majority owner in the company Igor works for. It's a brand-new restaurant (only open for two weeks) and is very nice. The restaurant was closed to the public for the night, so Derek, his son Adam, Liz, and the other staff devoted their time to just us. Figuring we were a group of geeks, Derek prepared a flowchart of the evening, including what would be served at each course and the drinks we were expected to consume. The food was fabulous -- I especially liked the toast with pieces of garlic you rub on them, as everyone who watched me pound back about 10 of them can attest. Of course, I wasn't the only one enjoying the food; Alan had several helpings of the thinly-sliced roast beef, including one after dessert! Adam, who's only 13 and had school the next day, was a waiter extraordinare. He was incredibly funny and a talented singer (he favored us with a couple of songs, including dancing). We left the restaurant after midnight and must have been a little noisy while waiting for our cabs, because someone dumped a bucket of what I hope was water from an upstairs window (fortunately, they just missed us).

Prague DevCon was a great conference. The sessions were top-notch, the food excellent, the conference staff wonderful, and Igor was an extremely gracious host. I can't wait until next year's conference!

Wednesday, September 13, 2006

More Tips for Speakers

Rick's tips for speakers has an excellent list of things all speakers should do before presenting a session. It prompted me to post one of my own: controlling your fonts.

One thing that's always distracting to attendees is when a speaker has to constantly increase the font size of every window they open. This is really unnecessary because it's very easy to do it in advance. Obviously, for sample forms you create, you should use a large font, such as Arial 14 or 16 bold, but what about Windows dialogs and VFP windows?

You can easily control Windows dialogs with a special theme you set up in advance. I have one I call "Demo" because that's what it's for. To create such a theme, right-click on the Windows desktop and choose Properties. Select the Appearance page and click Advanced. Change Size to 29 and Font Size to 14 for the following elements: Active Title Bar, Caption Buttons, Inactive Title Bar, Message Box, Change Size to 21 and Font Size to 12 for Menu, Selected Items, and Tooltip. Choose OK, then select the Theme page, click Save As, and save the new theme in the default folder (My Documents). You can then switch back to your normal theme. Before you start a presentation, switch to your saved presentation theme and all Windows dialogs will be readable to everyone in the room. After the presentation, switch back to your normal theme.

The settings for VFP windows are stored in the Registry, so before you begin changing things, save the current settings by running RegEdit, navigating to HKEY_CURRENT_USER\Software\Microsoft\VisualFoxPro\9.0, and choosing Export from the File menu to save to a file such as VFP9Normal.REG. Launch VFP, choose Options from the Tools menu, and select the IDE page. For each of the window types (including Program Files, Memo Fields, and Browse), change the font size to 16 bold and be sure to turn on Override Individual Settings so all windows will use the desired font size. Close the Options dialog and exit VFP. Run RegEdit again, navigate to HKEY_CURRENT_USER\Software\Microsoft\VisualFoxPro\9.0, and choose Export from the File menu, saving the settings to a file such as VFP9Demo.REG. You can then restore your normal settings by double-clicking VFP9Normal.REG. Before you start a presentation, double-click VFP9Demo.REG and all code, BROWSE, and Memo windows will be readable to everyone in the room. After the presentation, double-click VFP9Normal.REG.

With these two simple changes, you can completely eliminate the need to apologize and change the font size for every window in your presentation (or worse, make your attendees shout out "Bump up the font!").

Tuesday, September 12, 2006

Prague DevCon, Day 1

I decided to skipped Alan's keynote because I'd seen it at DevCon in Phoenix two weeks ago and wanted to go over my sessions one last time before presenting them (in keeping with Rick's suggestion to practice, practice, practice). I then wanted to go to Craig's session on Windows Communication Foundation and Windows Presentation Foundation, but jet lag got the best of me (although it was 11 am, it was 3 am by my internal clock) so I had an hour nap instead.

Lunch was a submarine sandwich and banana, and then I presented my Extending VFP with VFP session. One thing that's hard for me to get used to is the lack of feedback by European audiences. I mean no disrespect -- it's a cultural thing and I was warned by other speakers last year at the German DevCon -- but it's still hard to tell whether my ideas are sinking in or not. I agree with Dave Crozier's assessment; it was a pretty intense session, so if I do it again, I'll try to lighten up the material a bit.

I attended Craig's Object Thinking session and enjoyed it. As Dave pointed out, some of the concepts are a little controversial, especially the somewhat anti-data ones to a VFP audience. However, it's always good to challenge your viewpoints.

I then presented my Adding IntelliSense to Your Applications session. This is a somewhat esoteric session, in that it's not something every VFP developer would use, but in speaking with several attendees afterward, I know that a least a few will make use of the information.

A group of about 20 of us went to dinner at a restaurant somewhat removed from the hotel (it took about 20 minutes of walking through winding alleys, parks, streets). The food and beer were very good, although the service was very slow -- it was nearly two hours after ordering before our food finally arrived. Igor was kind enough to escort Alan, Craig, and I back to the hotel, as we never would have found it on our own.

I finished up the day catching up on email, talking to my wife, and reading a great book, The Cabinet of Curiosities by Douglas Preston and Lincoln Child (I've read several of their books and am a big fan).

Monday, September 11, 2006

Prague DevCon, Days -1 and 0

Yesterday was a very long day (actually two days). I'm speaking at DevCon in Prague this week, so I got up Saturday at 4 am Mountain Time, which is noon Central European Time (the time zone for Prague), and didn't go to bed until 10 pm Sunday. Except for about a one-hour nap on the flight, I was up for 34 hours.

The flight was uneventful (always a good thing). Igor Vit picked up Craig Berntson and I at the airport because even though we came from different cities on different airlines, we arrived at exactly the same time. After freshening up at the hotel, we had a very traditional Czech lunch of roast pork, bread dumplings, and sauerkraut.

We then met up with several other FoxPro folk (including wOOdy, his wife, Uwe, Venelina, JoKi, and Alan Griver) and took a four-hour walking tour of Prague with a very knowledgeable tour guide, Martina. Prague is beautiful. I loved the architecture of the historical buildings. There were lots of statues and fountains, interesting bridges, and, because it was a gorgeous day, tons of people everywhere. Martina explained the historical significance of the pedestrian-only Charles Bridge and several of the many statues on it. We took the Fundicular (a cross between a cable car and gondola ride) up the hillside and walked through a picturesque park to the Prague Castle. The castle is one of the largest in the world. It isn't a single building but a collection of many, including a Gothic cathedral similar in design to the one in Cologne or the Notre Dame in Paris.

Here are a couple of photos of the areas we visited:

Here's a group photo in front of the fountain in the second courtyard of the castle:

After the tour, we joined up with Hans and Gaby Lochmann and two of the local Microsoft people for dinner at a Brazilian restaurant. If you haven't been to one before, I highly recommend it. We started with a "salad" bar which, although there was no actual salad, had lots of delicious cold foods, including sushi. The waiters then brought us an interesting combination of fried bananas and fries (only Hans and I ate the bananas, so we had all we wanted). Next, the waiters brought an unending series of huge skewers of meat (pork, beef, lamb, sausage) and carved off pieces of any you wanted. You were supposed to flip a card on the table in front of you from green to red when you had enough, but the waiters ignored them and kept bringing tempting morsels anyway. Between all the food, great Czech beer, and pear schnapps in a cool tri-glass, I was completely stuffed. Great conversations with old and new friends, great food, and great beer made for a great evening.

After returning to my hotel room, I called home on my PC using Skype (thanks to Rick Schummer for introducing it to me). I love Skype: it's free to download and you can place calls to another PC on the Internet for free. In my case, I talked to my wife and son on her cell phone using a feature called Skype Out, which isn't free but is very cheap: my 10-minute call from the Czech Republic to Canada cost $0.24.

Time for bed!

Thursday, September 07, 2006

Really Busy

As you can likely tell by the scarcity of my posts lately, I'm busier than usual right now. I had planned to blog about a few things, such as the VFPX GDI+ classes and a new tool called UP2D8, but just haven't had the time. I'll get to them in future posts but first a quick catch-up.

First there was DevCon in Phoenix a couple of weeks ago. I arrived late Saturday night: one of my flights was delayed by three hours so I had a really long layover in Denver, then it took 1/2 hour for my luggage to come down, then SuperShuttle jerked me around for 1/2 hour before taking me to the hotel. The JW Marriott in Phoenix was great. The rooms weren't quite as large or luxurious as the ones in Las Vegas, but the hotel itself was much nicer. It even had a "lazy river" in the pool area.

Sunday Tamar Granor, Rick Schummer, Jeff Zinnert and I went to Sedona, about 1.5 hours north of Phoenix (or about 1 hour with Jeff driving). Rick has been there many times so was a great tour guide for us. The red rock mountains and mesas are so beautiful. My favorite part was the Slide Rock area, where you can slide in a natural water slide (very slippery and pretty cold, but lots of fun) or just jump off a cliff into a somewhat shallow (6 ft. or so) pool.

The conference itself was great as usual: excellent food, good sessions, fun hanging out with friends. The best meal of the trip: the last night at Bahama Breeze in the Desert Ridge Mall, walking distance from the hotel. The only disappointing thing was the attendance; based on a quick head-count at the keynote, I estimated about 120. However, Rick counted attendees in both rooms (yes, only two tracks!) several times and consistently found only 60 - 70 people.

Next, one of our technical support people resigned. Her husband found a job in Saskatoon (about 250 km north of Regina) so she found a new job there as well. So, my partner Mickey and I scrambled to find a replacement. We were already in the process of finding another support person, so we sped things up and did a bunch of interviews the day after I returned from Phoenix. We hired a couple of new guys and they started yesterday. They'll spend the next week with Tonia, getting up to speed on Stonefield Query and how to support it before she leaves us. She'll be greatly missed -- her work ethic, professionalism, and great customer skills were a joy -- so the new guys have big shoes to fill. I'm sure they'll do fine, though.

I had planned to take this week off: my mother-in-law died several weeks ago and my sister-in-law, brother-in-law, and our nephew came from Winnipeg to help my wife Peggy and I clean up her house so we can sell it this fall. What a huge job it's been -- it's not a very large house but she lived there for more than 40 years, so it has 40 years worth of stuff in it. Since they're only here until the end of this week and we're having a garage sale Friday and Saturday, we have some time pressure to get as much done as possible. Unfortunately, with all the things going on at work, I wasn't able to completely take the week off.

Saturday, I leave for Prague to speak at the DevCon there. (Which means I won't be here for the second day of garage sale. Dang. However will I console myself?) Igor Vit has been trying to get me to speak there for four years now, but we could never get our schedules to match up. Fortunately, it worked out this year, so I'm really looking forward to going. I have a few days of sightseeing planned so I'll blog about it when I return.

Thursday, August 17, 2006

What is it With Airlines?

I seem to have ongoing bad luck with Air Canada. It isn't always their fault: I've been stranded overnight at Pearson Airport several times in the past couple of years because of bad weather. That I can live with. It's the attitude of the airline and their employees that pisses me off.

The latest issue: we were on vacation, staying with friends in CT, when my wife Peggy found out her mother was quickly declining (she fought breast cancer for more than six years). I called Air Canada to change her flight to come home on the same flight just one day early, and they wanted $1,000, plus the change fee, of course, to do that.

Change fees I understand (well, sort of; $100 seems like a lot for a simple 2-minute conversation and database change) but the whole "we'll refund your current ticket and then sell you a new ticket at last-minute prices" thing doesn't make sense to me. I paid for a service: flying my family from point A to point B. If there are empty seats on the plane, why can't they simply change the flight from one date to another? I've also run into this when, at the last moment, a staff member got sick and couldn't attend a conference so we wanted to send someone else instead. Again, just a database change, right? Nope, it's the "refund old, buy new" policy again.

Maybe one day airlines will see their customers as people they appreciate doing business with rather than as bags of money.

Monday, August 14, 2006

Gotta Get to Phoenix

In case you missed Andy Kramek's blog on this, let me reiterate his point: Southwest Fox 2006 is a must-see conference. The 2004 and 2005 editions were the best conferences in North America, and I'm sure 2006 will easily exceed everyone's expectations. It's organized around a weekend to minimize missing billable hours and feels more like a reunion of friends than a big formal conference. Come hang out with the speakers and other attendees at meals or over drinks and get all your VFP questions answered.


Friday, July 14, 2006

XZip: Free Zipping Utility

In a message on the Universal Thread, Frank Cazabon pointed out a free zipping utility named XZip. I downloaded and installed it today and already like it. It's a 138K DLL COM object, so it must be registered when you deploy it.

It's really easy to use: simply instantiate XStandard.Zip and call the Pack method to add a file or folder to a ZIP file (the file is created if it doesn't exist) or Unpack to extract a file or folder. There are also methods to delete or move files or folders, and one that returns a collection of items in a ZIP file.

Here's some sample code:

#define tFolder 1
#define tFile 2

loZIP = createobject('XStandard.Zip')
loZIP.Pack('SomeFile.txt', 'MyZip.zip')
loItems = loZIP.Contents('MyZip.zip')
for each loItem in loItems
? 'Name:', loItem.Name
? 'Date:', loItem.Date
? 'Path:', loItem.Path
? 'Size:', loItem.Size
? 'Type: ' + iif(loItem.Type = tFolder, 'Folder', 'File')
next

Vacation Time

Today's my last day at work for three weeks. I haven't taken a three-week vacation in four years; it's always been a week here or ten days there. I'm really looking forward to this; I'm mentally pretty tired right now and need a break, plus we have tons of fun planned.

We (my wife Peggy, son Nick, and I) fly to New York City and stay with friends in Connecticut for a couple of days. We then all drive to a beach house in NC, with an overnight stay in Washington, DC, which we've never visited before. After a week swimming, golfing, and eating and drinking too much, we drive back to CT, visiting with Tamar Granor and her husband on the way. We plan to do some sightseeing in NYC for a few days, then head home and take a few days to decompress from our vacation.

Another interesting aspect of this vacation is it's the first one since the last three-weeker that I'm not taking my laptop. I made a deal with my friend Tracey from Dallas, who will also be at the beach house with her family (three families altogether) that I wouldn't bring mine if she doesn't bring hers. She even more rabidly addicted to email than I am, so it'll be harder on her than it will on me. We'll see if she actually arrives sans computer or not, given the temptation of high-speed Internet access in the house.

What started as three friends (Betsy, Tracey, and I) who met in graduate school in Dallas in 1980 has expanded to three families who vacation together almost every year. The great thing is that you could take any two people out of the group, including kids, and they'd have a great time together. That's pretty rare in my experience. Like the saying goes, old friends are the best friends.

Friday, July 07, 2006

SQLXMLBulkLoad Rocks!

For reasons that will become obvious in September (sly grin), I've been working a lot lately on uploading data to SQL Server 2005. There are a variety of mechanisms you can use to do this, but all of them have their shortcomings under certain conditions. For example, using a series of INSERT statements is slow while bulk insert doesn't work with memo fields. While looking for something else in SQL Server Books Online, I happened across a topic on bulk XML loading. After playing with it for a while, it seemed pretty easy to do, so I did some testing on a relatively large file (365,741 records) containing memo fields (which means I couldn't use bulk insert). Using other mechanisms, it took more than two hours to load the data into a SQL Server table. Using bulk XML load, it took 11 minutes. That's a 90% improvement in speed. I love making things faster!

Using bulk XML load is easy: you instantiate SQLXMLBulkLoad.SQLXMLBulkload.4.0, set its ConnectionString property to an OLE DB connection string, set some other properties appropriately (for example, KeepNulls to .T. to insert null rather than default values for missing column values), and call Execute, passing it the name of a schema file and the name of the XML data file. Execute throws an error if there's something wrong with either file, and import errors are logged to a file whose name is in the ErrorLogFile property.

There's one slight wrinkle in working with VFP data: while the CURSORTOXML() function can create a schema file, it needs to be tweaked slightly to work with bulk XML load. Also, DateTime fields have to be flagged with the sql:datatype="dateTime" attribute (the latter was documented, the former took some trial and error to resolve).

Here's a generic program that will load the data from an open VFP cursor into an existing SQL Server table. You can download it from http://www.stonefield.com/articles/other/bulkxmlload.zip.

*==============================================================================
* Function: BulkXMLLoad
* Purpose: Performs a SQL Server bulk XML load
* Author: Doug Hennig
* Last revision: 07/06/2006
* Parameters: tcAlias - the alias of the cursor to export
* tcTable - the name of the table to import into
* tcDatabase - the database the table belongs to
* tcServer - the SQL Server name
* tcUserName - the user name for the connection (optional:
* if it isn't specified, Windows Integrated Security is
* used)
* tcPassword - the password for the connection (optional:
* if it isn't specified, Windows Integrated Security is
* used)
* Returns: an empty string if the bulk load succeeded or the text of
* an error message if it failed
* Environment in: the alias specified in tcAlias must be open
* the specified table and database must exist
* the specified server must be accessible
* there must be enough disk space for the XML files
* Environment out: if an empty string is returned, the data was imported into
* the specified table
*==============================================================================


lparameters tcAlias, ;
tcTable, ;
tcDatabase, ;
tcServer, ;
tcUserName, ;
tcPassword
local lnSelect, ;
lcSchema, ;
lcData, ;
lcReturn, ;
loException as Exception, ;
lcXSD, ;
loBulkLoad


* Create the XML data and schema files.


lnSelect = select()
select (tcAlias)
lcSchema = forceext(tcTable, 'xsd')
lcData = forceext(tcTable, 'xml')
try
cursortoxml(alias(), lcData, 1, 512 + 8, 0, lcSchema)
lcReturn = ''
catch to loException
lcReturn = loException.Message
endtry


* Convert the XSD into a format acceptable by SQL Server. Add the SQL
* namespace, convert the start and end tags to ,
* use the sql:datatype attribute for DateTime fields, and specify the table
* imported into with the sql:relation attribute.


if empty(lcReturn)
lcXSD = filetostr(lcSchema)
lcXSD = strtran(lcXSD, ':xml-msdata">', ;
':xml-msdata" xmlns:sql="urn:schemas-microsoft-com:mapping-schema">')
lcXSD = strtran(lcXSD, 'IsDataSet="true">', ;
'IsDataSet="true" sql:is-constant="1">')
lcXSD = strtran(lcXSD, '<xsd:choice maxOccurs="unbounded">', ;
'<xsd:sequence>')
lcXSD = strtran(lcXSD, '</xsd:choice>', ;
'</xsd:sequence>')
lcXSD = strtran(lcXSD, 'type="xsd:dateTime"', ;
'type="xsd:dateTime" sql:datatype="dateTime"')
lcXSD = strtran(lcXSD, 'minOccurs="0"', ;
'sql:relation="' + lower(tcTable) + '" minOccurs="0"')
strtofile(lcXSD, lcSchema)


* Instantiate the SQLXMLBulkLoad object, set its ConnectionString and other
* properties, and call Execute to perform the bulk import.


try
loBulkLoad = createobject('SQLXMLBulkLoad.SQLXMLBulkload.4.0')
lcConnString = 'Provider=SQLOLEDB.1;Initial Catalog=' + tcDatabase + ;
';Data Source=' + tcServer + ';Persist Security Info=False;'
if empty(tcUserName)
lcConnString = lcConnString + 'Integrated Security=SSPI'
else
lcConnString = lcConnString + 'User ID=' + tcUserName + ;
';Password=' + tcPassword
endif empty(tcUserName)
loBulkLoad.ConnectionString = lcConnString
*** Can set the ErrorLogFile property to the name of a file to write import
*** errors to
loBulkLoad.KeepNulls = .T.
loBulkLoad.Execute(lcSchema, lcData)
lcReturn = ''
catch to loException
lcReturn = loException.Message
endtry


* Clean up.


erase (lcSchema)
erase (lcData)
endif empty(lcReturn)
select (lnSelect)
return lcReturn

Wednesday, June 14, 2006

Updated SFTreeView

I wrote a two-part article in the July and August issues of FoxTalk called "The Mother of all TreeViews" that presented a class library providing all the features you (or at least I) would ever need in a TreeView, including automatically dealing with the twips coordinate system used by the control, handling drag and drop, only loading parent nodes at startup, saving and restoring expanded and selected nodes, and so forth.

Andrew Nickless emailed me today about a bug: navigating the TreeView using the up and down arrows didn't cause the sample form that accompanied the article to refresh and show the properties for the selected node. Fortunately, it was easy to fix:

1. In TreeNodeClick, change the IF statement as shown:

*** if isnull(.oTree.SelectedItem) or toNode.Key <> .oTree.SelectedItem.Key
if isnull(.oTree.SelectedItem) or not toNode.Key == .cCurrentNodeKey

2. Add a new cCurrentNodeKey property

3. Add this line to SelectNode right after the assignment to cCurrentNodeID:

.cCurrentNodeKey  = loNode.Key

However, while I was looking into this, I decided to delve into another weird behavior that had bugged me for a while: sometimes clicking on the + to expand a parent node showed the placeholder "Loading..." node rather the actual children. The reason is that the Expand method of the TreeView didn't fire, and that method was responsible for removing the placeholder node and adding the child nodes. The weird thing is that it wasn't consistent; I could only make it happen about 25% of the time, if that. And given that I haven't seen that happen in other applications using a TreeView, it was clearly something I'd done in my class.

Long story short, it turned out (by trial and error, sadly) that code I had in the MouseMove method seemed to be the culprit. I say "seemed" because after removing it, I couldn't reproduce the behavior, but given that it didn't always happen, and of course it never happened when tracing the code, it's darned hard to confirm that it's gone for good. MouseMove called TreeMouseMove (thanks to Steve Black for drilling "events call methods" into my brain), which didn't do anything; I put it there in case I wanted to handle that in a subclass. Turns out I never have, so removing that behavior was no big deal.

Monday, June 05, 2006

DevCon Discount

Advisor Media is offering a $100 discount for Microsoft Visual FoxPro DevCon. Simply write "Doug discount" in the comment field of the registration form to qualify.

This will be my 16th DevCon as an attendee (I only missed the first one in Toledo in 1989), 13th as an exhibitor (1993 was the first), and my 10th as a speaker (1997 was the first). As far as I know, I'll be the only person to have attended 16 consecutive DevCons. Anyone know of someone who's attended them all?

For posterity, here are the ones I've attended:

1990 Toledo
1991 Toledo
1992 Phoenix
1993 Orlando
1995 San Diego
1996 Scottsdale
1997 San Diego
1998 Orlando
1999 Palm Springs
2000 Miami
2001 San Diego
2002 Ft. Lauderdale
2003 Palm Springs
2004 Las Vegas
2005 Las Vegas
2006 Phoenix

Thursday, May 25, 2006

RUN and GetShortPathName

Stonefield Query has a function in its developer interface (the Configuration Utility) to generate an InnoSetup script and compile it into a setup executable. The idea is to make it as easy as possible for someone to deploy a custom Stonefield Query application without having to be an installer expert. Generating the script is easy because InnoSetup scripts are just text files. Compiling the script is also easy: use the RUN command to call the InnoSetup compiler, passing it the name of the script file to compile.

I get the location of the compiler from the Windows Registry, at HKEY_CLASSES_ROOT\'InnoSetupScriptFile\Shell\Compile\Command, which on my system gives "D:\Program Files\Inno Setup 5\Compil32.exe" /cc "%1". So, it's a simple matter to read this value into a variable (for example, lcInnoCompiler) and then:

lcInnoCompiler = strtran(lcInnoCompiler, '%1', lcScriptFile)
run /n1 &lcInnoCompiler

This works great on my system and lots of customers' systems. However, one of our sales guys (Jeff Zinnert) reported that he got a "RUN command failed; file does not exist" error when he tried it and so did a customer. We checked that the InnoCompiler was installed correctly, in the place the Registry said it was, and that the script file existed, but to no avail.

While pondering this, I came across a message on the Universal Thread that was completely unrelated but mentioned an issue with "short" (ie. the old DOS 8.3) paths. That reminded me of a similar issue I'd run into several years ago but had forgotten about. A function I'd written years ago calls the Windows API GetShortPathName function to convert a "long" path into a short one:

lparameters tcPath
local lcPath, ;
lnLength, ;
lcBuffer, ;
lnResult
declare integer GetShortPathName in Win32API ;
string @lpszLongPath, string @lpszShortPath, integer cchBuffer
lcPath = tcPath
lnLength = 260
lcBuffer = space(lnLength)
lnResult = GetShortPathName(@lcPath, @lcBuffer, lnLength)
return iif(lnResult = 0, '', left(lcBuffer, lnResult))

I used this function to convert the paths in the lcInnoCompiler variable to short paths and Jeff and the customer no longer get this error.

Once again, the UT saves my butt even though the answer wasn't directly there.

Wednesday, May 17, 2006

Varchar, SET ANSI, and the UT

I've been working on updating Stonefield Query for GoldMine to use the version 3.0 Stonefield Query Developer's Edition as its engine. While doing some testing, I found that one of the queries was taking significantly longer in the new version than the old version: 30 seconds rather than 3 seconds. My first thought is that it's a VFP 9 issue, since the old version uses VFP 8. I remembered from messages on the Universal Thread regarding query performance in VFP 9 that Rushmore, the key to VFP's magical speed in performing queries, is disabled if the code page of a cursor doesn't match the current code page (ie. CPDBF() <> CPCURRENT()). However, that wasn't the case here. In fact, if I ran the old version under VFP 9, it gave the same performance as VFP 8, so that wasn't the problem.

To dig into this further, I searched the Univeral Thread for messages regarding performance, and saw one where Sergey Berezniker (the king of the Universal Thread) mentioned that expressions using ALLTRIM() aren't optimized because Rushmore requires fixed length keys, but that SET ANSI ON would take care of that. Again, that wasn't the case here; the query didn't use ALLTRIM(). However, that got me to thinking: I wonder if the fields involving in the JOIN clause were Varchar fields. Sure enough, they were. Why the difference between the old and new versions? I added the following to the new version so Varchar and Varbinary fields are properly supported:

cursorsetprop('MapBinary', .T., 0)
cursorsetprop('MapVarchar', .T., 0)

This means that in the new version, the fields retrieved from the database were Varchar rather than Character as they were in the old version. Because Varchar fields could have different lengths (in fact, they were the same length for the fields involved in this join), Rushmore won't optimize them unless ANSI is set on. So, a quick SET ANSI ON added to the code, and the query is now as fast in the new version.

So, two lessons: sometimes a little change in one part of an application can cause a big change in another part, and search the Universal Thread (or other online sources) before spending hours trying to track down a problem. This one only took me about 15 minutes to fix. Thanks, Sergey!

Wednesday, May 10, 2006

FoxUnit is Cool!

FoxUnit has been available for at least of couple of years, and I've always meant to work with it, but it was one of those things I just didn't get around to. However, after attending Nancy Folsom's session on refactoring at the recent GLGDW, in which she discusses the importance of testing both before and after refactoring to ensure the functionality remains the same, I figured it was time to get to it.

What is FoxUnit? As defined on the FoxUnit Web site, 'FoxUnit is an open-source unit testing framework for Microsoft Visual FoxPro®. It is based on unit testing frameworks as described in Kent Beck's book "Test Driven Development by Example" but takes a more pragmatic approach to unit testing for Visual FoxPro than a more purist xUnit implementation would.'

The idea is to create tests for the various pieces of your application. You then run some or all of the tests prior to releasing a new build (or performing refactoring or checking in the latest update or whenever you want) to ensure everything works correctly. Note that unit testing isn't a replacement for system or acceptance testing, but is one more tool in your professional developer's toolkit.

What got my current interest in FoxUnit started is the need to refactor some code. One method is particular is huge (several hundred lines long) and has been getting more convoluted with time. Before I add some new functionality, I want to refactor it so it's easier to comprehend, easier to maintain, and easier to test. However, I'm scared that during refactoring, I'll drop some functionality or introduce bugs. Hence the need for suite of tests I can run before and after each refactoring task. (Nancy stressed doing refactoring in small steps rather than one huge job. In addition to being easier to do, it's easier to test and less likely to cause broken functionality).

So, I downloaded and installed FoxUnit. My introduction was a little rough -- because I didn't follow instructions and SET PATH to the folder when I installed it, a few things didn't work right. In my opinion, a app should be able to find its own pieces without having to use a crutch like SET PATH, so I made a few minor tweaks (like having the program figure out what directory it's running in and using that path in a few places rather than assuming the files can be found without one). However, once I got past that, it was pretty easy to work with.

Tests are stored in PRGs which are managed by the FoxUnit UI. Each PRG contains a class subclassed from FXUTestCase, the base class FoxUnit test class. Each test is a method of the subclass (although there can be non-test methods too, of course). Although you could write all of the tests for your entire application into a single PRG, that wouldn't be very granular. I prefer one PRG for a single "thing" (module, class, or whatever) I want to test, and then one or more test methods within the PRG to test the functionality of that "thing".

The idea is to write small tests that each test one aspect of one method. For example, if a method accepts a couple of parameters and does different things based on the parameters passed, there should be tests for each parameter being passed or not, different types of bad values for the parameters, different types of good values that result in different behavior, etc. As a result, you'll have a lot of tests for even the simplest application, but each test is small, easy to understand, easy to maintain, and does just one thing. And since the FoxUnit UI manages all of the tests for you, and gives you options to run a single test, all tests in one PRG/class, or all tests, the management of these tests isn't too bad.

Tests are easy to write. Since it's just code in a VFP class, you can add custom properties if necessary. For example, rather than instantiating the object to be tested in every test method, you could create a property of the test class such as oObjectToTest, and in the Setup method (called just before any test is run), instantiate the object into that property: Here's an example, along with a test to ensure the object actually instantiated properly:

define class MyTest as FxuTestCase of FxuTestCase.prg
oObjectToTest = .NULL.

function Setup
This.oObjectToTest = newobject('MyClass', 'MyLibrary.vcx')
endfunc

function TestObjectWasInstantiated
This.AssertNotNull('The object was not instantiated.", This.oObjectToTest)
endfunc
enddefine

Note the use of one of the test methods, AssertNotNull. There are several similar methods available, including AssertEquals and AssertTrue. These methods test that some condition is the way it's expected to be, and if it isn't, the test fails and displays the failure message specified in the assert call.

In addition to instantiating the object, Setup can be used to perform other tasks needed for every test, such as setting up the environment or the object under test. A similar method, TearDown, can be used to perform common tasks after a test has been run, such as restoring the environment.

When you run a test, it either passes, in which case it's shown in green in the FoxUnit UI, or fails, which is shown in red. Tests that haven't been run are shown in gray. Thus it's easy to visually see the results of test runs.

FoxUnit is one of those things that seems like a good idea until you try it, and once you do, you realize it's a great idea. I'm kicking myself for not trying it out a couple of years ago when I first saw Drew Speedie demonstrate it at DevTeach in Montreal. But now that I've worked with it for a while, I'm a firm believer. So, if you haven't started using FoxUnit, do yourself a favor: take out an hour, download it, and create some simple tests. You'll become a believer too.

Friday, May 05, 2006

Debugging Tips

As I mentioned in my post about GLGDW, I missed the Best Practices for Debugging session. Here are a couple of tips I was going to mention. Sorry if someone else mentioned them; I was too busy setting up Rick's system to do my presentation to listen.

1. Write debugging-friendly code.

I used to write code like this:

llReturn = SomeFunction() and SomeOtherFunction() and YetAnotherFunction()
I don't do that for two reasons now: it's harder to understand than:
llReturn = SomeFunction()
llReturn = llReturn and SomeOtherFunction()
llReturn = llReturn and YetAnotherFunction()

But more importantly, it's harder to debug. If you step through the code, execution goes into SomeFunction. If you decide you don't need to trace inside that function and want to jump to the next one, you'd think Step Out would do it. Unfortunately, that steps out all the way back to the next line following the llReturn statement. So, the only ways to trace YetAnotherFunction are to go all the way through SomeFunction and SomeOtherFunction or specifically add a SET STEP ON (or set a breakpoint) for YetAnotherFunction.

Note that I don't typically write code like:

llReturn = SomeFunction()
if llReturn
llReturn = SomeOtherFunction()
if llReturn
llReturn = YetAnotherFunction()
endif
endif

That seems (to me) harder to read than the first example.

Here's another example of debugging-unfriendly code I used to write:

SomeVariable = left(SomeFunction(), 5) + substr(SomeOtherFunction(), 2, 1)

The reason that's not friendly is that you can't see what SomeFunction and SomeOtherFunction return unless you actually trace their code. Instead, I now use code like:

SomeVariable1 = SomeFunction()
SomeVariable2 = SomeOtherFunction()
SomeVariable = left(SomeVariable1, 5) + substr(SomeVariable2, 2, 1)

That way, I can trace this code and see what the functions return without having to trace the functions. If something doesn't look right, I can always Set Next Statement back to one of the statements calling a function and Step Into the function to see what went wrong.

2. Instrument Your Application

I, Rod Paddock, Lisa Slater Nicholls, and others have written articles on instrumenting your applications. The idea is to sprinkle calls to a logging object throughout your application. If logging is turned off, nothing happens. If logging is turned on (for example, oLogger.lPerformLogging is .T.), some message is written to some location (a text file, a table, the Windows Event log, etc.) indicating what the application is doing.

I've found this extremely valuable in tracking down problems. While error logs are great, they only give you a snapshot of how things were at the time the error occurred. Sometimes, you need to know how you got there. Also, sometimes the problem the user is experiencing doesn't cause an error but incorrect behavior. By perusing a diagnostic log, you can see all of the steps (the ones you've instrumented, anyway) that lead to the behavior. Yes, you can use SET COVERAGE in your application, but that generates a ton of information, likely a lot more than you need unless you have a really ugly problem that you have no clue about the cause.

Lisa's article is the most recent one I've read, so it's a good starting place. Rod's article is in the September 1997 issue of FoxTalk and mine is in the October 2003 issue of FoxTalk (both articles require you to be subscribers).

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
&&amp;amp; 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.