The Perpetual Newbie Journal Entry #2.1
This article first appeared on http://www.undu.com
Every one of us has heard the code reuse mantra. Some of us are a little slower to adopt new practices when old practices are as comfortable as an old tune. But the times, they are a-changin' and it's high time to dip a toe into reuse, even if it's only to reuse a form.
I recently had cause to create a modal form to do some editing of a record from a grid. It made sense to make it a modal form so that the user could see the whole of the information at one time. No problem. I referenced the MainForm's datasource.Autoedit := false and everything worked hunky-dory. The modal form consisted of some edit fields, a post button, a cancel button and three event handlers. Besides the post and cancel handlers, there was a keypress handler that turned enters into tabs. I also turned off the caption-bar exit button. Editing was initiated by a button in the lookout bar on the main form. The concept worked very well in auditing changes made during the editing session in a history field.
So well did the modal form work that I decided to double up use of the form by using it for Adding a new record in the database. All I had to do was to exercise some runtime modifications, changing the caption and tweaking a few other settings prior to opening it up as a modal form. Whereas the Customer ID field was read only in the modal form, it was turned off in the Add form. At least I THOUGHT that was all there was too it.
I encountered a problem because the database in question was a detail database, linked to a master. No problem. I would simply copy the datasource and table to another name (i.e. Customer to CustEdit). Then I could reset the pointers to the copy's datasource. The edit procedure just had to include a gotoCurrent function to harmonize the two copies and finish with a refresh on the original to make sure all of the changes were noted.
The Edit function was easy to modify:
FrmCustomer := TFrmCustomer.Create(nil);
with FrmCustomer do begin
fldCustNum.readonly := true; // NEW Ensure you can't change the key field
caption := 'Edit Customer'; // NEW Make the caption indicate function
TblCustEd.gotoCurrent(FrmMain.TblCustomer); // NEW harmonize the shadow table with the master
if not (sourceCustEd.state in dsEditModes)
then TblCustEd.edit; // Ensure entering edit mode
try ShowModal; // Bring up the form, use it and then lose it
TblCustomer.refresh; // NEW Update the viewing data with the behind the scenes changes
Things with the Add didn't work as soon as I clicked the button during execution. It produced a complaint that the dataset was NOT in edit mode. I put in breakpoints. I switched from using Append to Insert and back again. The program kept breaking on TblCustEdit.append, even when I asserted immediately before hand that the source was in edit mode immediately before. i.e.
FrmCustomer := TFrmCustomer.Create(nil);
with FrmCustomer do begin
fldCustNum.readonly := false;
caption := 'Add A Customer';
try TblCustEd.Append; // BOMB!!!!!!!!!!
except showmessage('Editing problematical');
If I placed and I checked for dsEditModes above the append statement and then marked with a breakpoint, the debugger would NOT execute, it being optimized out or changed in location by the compiler. AND, the code was pointing back at the buttonclick event of the button when you got finished answering dialog boxes. I was one confused puppy.
I was headed to the Internet when I finally saw what the problem was. The newrecord method for TblCustomer included four default field settings. It was phrased in the procedure for example as:
TblCustomer.fieldByName,('Carrier').asString := 'FEDEX';
THIS WAS COPIED VERBATIM to the newRecord field of the copied datasource. I looked at this for hours before noticing that TblCustomer AND TblCustEdit were not one and the same [sheep-faced grimace]! The problem was solved initially by simply correcting the name discrepancy. However, I have come to the conclusion that I am way too apt to make this mistake again. Sooooooooo....
The new approach is to use the calling parameter in the "with" style. I.e.
with dataset do begin
fieldByName('Carrier').asString := 'FEDEX';
fieldByName('GST').asBoolean := true;
fieldByName('PST').asBoolean := true;
fieldByName('Approved').asBoolean := False;
I've decided that I'm going to use that calling parameter more often. It might require a comment or two to be totally clear on quick reading, but I vow to use it to Cover My Butt against stupidity like I've perpetrated against my deadline here today.
Hope this helps prevent a furrowed brow or two in the future.
More on Safe Coding Ideas
by Gary Mugford - firstname.lastname@example.org
The above article generated responses from Ramon Prick and Andy Robinson. Here is my follow up:
Thanks (to Ramon Prick of the Netherlands) for amplifying the concept behind my article. It's the kind of dialog that can often be illuminating and helpful to programmers at all but the expert level. It is true that the idea behind safe-coding is to eliminate errors of omission as much as possible. Field name-changing does go on more often than I would like. Ramon suggested going to some length to avoid mentioning field names by hard-coded names, preferring constants or field-level definitions in data modules.
Yet I've found the FBN references easy to locate and change with any standard grep utility. That the constants suggestion makes ease of replacement a snap is obvious. But that ease does come at a price, storage space. In an app of decent complexity, the number of field names could be sizeable. And these have to be unit-level global constants to have any worth and should be application-wide global constants to be as much value as you desire. Data modules do have the utilization that you suggest, but add another layer to the program code. They aren't all that necessary in a properly managed SDI interface where the child forms use the calling form's datasets where appropriate.
All in all, I find the incidence of changed field names (as adverse added names) occurs infrequently enough, that I choose not to pay the overhead. In group programming efforts or in MDI applications, I can readily see the value in your first suggestion and would recommend its adoption.
In another email, Andy Robinson from South Africa indicated that he had had problems in the past with nested use of dataset parameters. His problem was with unending loops: "...in this kind of structure it would appear that you cannot trust certain statements. eg. I no longer attempt this...
with dataset1 do begin
with dataset2 do begin
This is a different situation then the one I posited that had a single dataset parameter passed through from an automatic method like AfterInsert. While he has not had the time to investigate the problem and pin it down, this kind of heads-up now has me looking very carefully at all nested with statements.
By the way, code template use allows for the most usual problem solution to the looping problem noticed by Andy. I have a WithEOF template that types in:
with | do begin
while not eof do begin
Also, FBNS to expand to fieldbyName('|').asString and FBNI to expand to fieldByName('|').asInteger really work wonders in terms of accuracy and speed of code entry. Ultimately, as a one-man shop, these speed gains merit the continued use of the coding practice that wouldn't work nearly as well in your organization.
Idea Mechanic Inc
Bramalea ON Canada