IT SOLUTIONS
Your full service technology partner! 
-Collapse +Expand
Delphi
Search Delphi Group:

Advanced
-Collapse +Expand Delphi To/From
To/FromCODEGuides
-Collapse +Expand Delphi Store
PRESTWOODSTORE

Prestwood eMagazine

July Edition
Subscribe now! It's Free!
Enter your email:

   ► KBProgrammingDelphi for W...Coding Tasks   Print This    All Groups  
  From the August 2015 Issue of Prestwood eMag
 
Delphi Coding Tasks:
Delphi Best Practices I - Handling TForm.OnCloseQuery
 
Posted 12 years ago on 5/29/2008 and updated 1/24/2010
Take Away:

In this first installment of a New Knowledge Base Series I'll discuss The TForm.OnCloseQuery Event which too frequently goes unhandled.

KB101139

Introduction

Some recent posts on our message boards, some new features in late releases of Delphi, and a couple emails with friends, have encouraged us to start a series of posts to the Prestwood Knowledge Base.

Although we'll call this series "Delphi Best Practices," I want to start by making one thing clear: In the world of programming, there is seldom one "best" way to do anything. When tackling a new problem or requirement, it's common for several approaches to come to mind. Often, good arguments can be made that two or three of them are equally "good."

"Delphi Best Practices," then, will make no pretense as to being the definitive "right way." Instead, we'll focus on some known good ways, point out problems with some of the "old good ways," and touch on a few topics often overlooked by even great Delphi programmers.

The other thing to be said about this series is that, although it specifically deals with Delphi, many of the ideas and principles discussed are applicable to any development system/language. Heck, some of the ideas we'll mention, and a couple of features new to Object Pascal, were borrowed from .NET.

About the code

Example code and snippets will follow the Prestwood Delphi coding conventions. Where these differ significantly from "Borland" style, we'll say a bit about the cases in point. For the benefit of a reasonably wide audience, we'll stick with Delphi version 7, as much as possible. Of course, when we talk about features introduced in versions later than 7, we'll have to use those versions. We'll also avoid 3rd-party tools and components unless they're free, and in fairly common use. One best practice, after all, is to avoid reinventing the wheel.

Old good ways

I think it's a great thing that Delphi has been around for so long, and seen so many improvements. There is, however, a downside. A given developer, starting out with some particular version of Delphi, learns how to do certain things in the ways that are in vogue at that time. Then, as newer versions appear, many developers keep doing those things the same old way, blissfully ignorant that there may be newer, easier, and better ways to handle certain tasks. Good examples were the introductions of TDataModule and TActionList. Many developers have yet to see their value, let alone adopt them.

This is not to say that any particular old good way is wrong. As long as it still works, you can't really say it's wrong. But if there's a better way, why not learn it?

In fairness, it must be recognized that the demands of budgets or deadlines preclude doing a given task in the most elegant manner. Sometimes the best choice is to stick to what's familiar - what you know works - and what you know you can implement quickly.

The Unhandled Event

With this introductory installment, I'll call attention to a simple event that often goes unhandled in many, many of the Delphi applications we see. It's TForm.OnCloseQuery.

The OnCloseQuery event is fired just before a form or dialog closes. Obviously, this event is firing often during the course of any non-trivial Delphi typical application.

What we commonly see, though, is a complete absence of any code to handle this event. We do see is good code attached to the usual "Ok" and "Cancel" buttons - the ways we usually think of users closing our dialogs. But many developers are paying no attention to that little, red �X' button at the right of the caption bar.

Users will use that button! And, when they do, what happens to the changes they may have made in your dialog? If you're not keeping OnCloseQuery in mind, you may not be doing other things that are important on a robust dialog. This can be particularly true of how data validation code is organized.

The event handler for OnCloseQuery provides a single, variable parameter, "CanClose." It defaults to true, meaning that if your event handler does nothing, the form will be allowed to close.

Let's now consider a simple dialog, not connected to a database, and used to gather some "settings" information that will be stored in an INI file.

It'll have the usual "OK" and "Cancel" buttons, plus four TEdits, for First Name, Last Name, Email Address, and the path to a folder for "personal" data. That's pretty simplistic, but it'll serve well to illustrate this, and a couple of articles to follow.

When the form is displayed, we'll populate the TEdits for which we already have data. The rest (possibly all of them) will be blank.

We want to be sure that the user enters data in all fields. In addition, we want to be sure the personal data folder exists. (For the sake of simplicity in this illustration, we'll skip the business of creating the folder if it doesn't exist, and simply assume that it must before this dialog can be completed.

We'll also want to know, at all times, whether the form's data has changed, because the user might click that evil �X' button at any time. For this, a private, Boolean member of our form can be handy. We'll declare it as named "FDirty." Immediately after populating the TEdits with existing data, and before the user has a chance to do anything else, we'll set FDirty to false;

The TEdit control has a useful event, OnChange, that fires whenever the text in the TEdit changes. We want to trap that event for each TEdit on our form, and do one simple thing: set FDirty to True;

Tip: You can set the OnChange handler for all of your TEdit controls to point to the same handler. There's no sense writing four identical versions of an OnChange event handler when just one will do.

Now, because any change, anywhere on our form, will ensure that FDirty is true, we can use this information in our OnCloseQuery handler to decide whether to pester the end user when she attempts to close the form. If FDirty is true, we should prompt the user, asking her if she wants to save her changes.

Which brings us to the fun part: If the user says "Yes" (wants to save changes) we need to do that, but we should also be sure that the entries on our form are valid. Before we get to that, let's look at a typical OnCloseQuery handler:

procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
mr: word;
begin
CanClose := not FDirty;

// Only if our form is dirty, do we have to do anything special

if not CanClose then
    begin
      mr := MessageDlg('Save your changes to the data folder settings?',
        mtConfirmation, [mbYes, mbNo, mbCancel], 0);
      case mr of
        mrNo: CanClose := True; // The user doesn't want to save changes
        mrYes:
          begin
            if AllEntriesAreValid then
              begin
                SaveSettings;
                CanClose := True;
              end
            else
              CanClose := False;
          end;
        mrCancel: CanClose := False;
      end; // case mr
    end;
end;

In the code above, you can see that, as long as our form isn't dirty (no changes were made), we have nothing to do. The statement, "CanClose := not FDirty;" will have set the handler's CanClose parameter to true, allowing the form to close.

Only if the form is dirty, do we need to obtain, and act upon the user's choice. Incidentally, I almost never write my MessageDlg calls by hand. GExperts, an indispensible - and free - add-in for Delphi, has a very nice dialog for designing and testing MessageDlg calls.

For the sake of simplicity, assume that the function, "AllEntriesAreValid" is already in place to validate data entry. If it returns true, a procedure named SaveSettings is invoked, CanClose is set to true, and our form is allowed to close. If it returns false, the form is not allowed to close, and the user is encouraged to correct things.

Naturally, we'll want to code AllEntriesAreValid such that the user is informed of which entries are invalid, and it would be very polite of us to set focus to the offending control.

Commenting Code

Note the comment that follows the closing "end" of our case block. This is a good coding habit. Whenever a case or begin block gets very long, it's helpful to comment exactly what is ending. This is especially true if your block includes nested blocks. Those, too, should have their "end" commented. It makes maintenance much easier on the next guy that visits your code.

The "Early Out"

There is one more subtle point in the organization of the code, above. The first two statements comprise an "early out," that is, they assure that minimal code has to be executed before the handler exits. Delphi applications are noted for their blazing performance. By keeping our eyes open for early out opportunities, we help maintain top performance. Granted, in the tiny block of code, above, nobody will ever notice. But you will encounter many cases in which providing early outs will improve performance.

Next: Desiging Good Form Validation Code

All of which brings us to the next topic in this series: designing good form validation. But I think you can see already, that by understanding OnCloseQuery, and keeping it in mind, we can make much more informed choices about how we'll validate our forms.

More Info

Article:  Delphi Best Practices 2: Reusable Main Forms
Link:  GExperts

Comments

1 Comments.
Share a thought or comment...
Latest Comment
Comment 4 of 4

mtiede,

I've taken your suggestion to heart - and heartily agree with you. Wink

Here's an example of the same handler coded for the affirmative, per your suggestion. (I've removed the validation checking code just for simplicity.)

procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var  mr: word;
begin
  if FDirty then
    begin
      mr := MessageDlg('Save your changes to the data folder settings?',
        mtConfirmation, [mbYes, mbNo, mbCancel], 0);
      case mr of
        mrNo: CanClose := True; // The user doesn't want to save changes
        mrYes:
          begin
            SaveSettings;
            CanClose := True;
          end;
        mrCancel: CanClose := False;
      end; // case mr
    end // if FDirty
  else
    CanClose := True;
    // Note: This else clause could be eliminated if our first line were
    //       "CanClose := not FDirty;"
end;

I like it much better.  Thank you for pointing out the error of my ways.

Incidentally, mtiede's observations point out another "best practice."  In the heat of coding under a deadline, we sometimes take the path that's most obvious to us at the moment.  It may work, but that doesn't mean it's the best way to express what the code is doing. Taking the extra moment to think out the advantages of coding for the affirmative case is well worth it.

Wes

Posted 10 years ago

Comment 3 of 4

mtiede,

I'm viewing the article in IE8, and the code is, indeed, indented. Years ago I was handed some code to maintain, and the author hadn't indented anything.  I really dislike that, but DelForX took care of the problem handily.

As for end comments: Not all versions of Delphi support begin/end matching.  But even for the versions that do, if the begin isn't visible on the screen, having a comment after the end blocks can still be a help.

Like you, I don't like long, deeply nested blocks, and avoid them as much as possible. I love Delphi's local functions/procedures for simplifying complex blocks of code.

Sometimes, though, the clearest expression of an algorighm calls for a lengthy block. Of course, what constitutes "clearest" is a matter of personal interpretation.

With respect to the negative/positive.   I take your point and, next time I go over this code, I'll keep it in mind.

Thanks for your comments.

Wes

Posted 10 years ago

Comment 2 of 4

Oops.  I'm too used to Delphi Prism.  Delphi Prism does the matching "begin-end" type highlighting.  Apparently Delphi 2010 doesn't do that.  Unless there is an option to turn that on.  But I just checked it on mine, and it doesn't do that.

So I withdraw my comment on the "end comment".  Embarassed

Posted 10 years ago

First Comment
Comment 1 of 4

Is there a way to show your code indented?  I assume you intended to have indenting.

I actually disagree with the end comment.  Nowadays, if you have a question as to what the "end" is ending, you can click on that "end" and it will be highlighted with its corresponding "begin" or other element also highlighted.

If the nesting gets too deep to see the corresponding element, the code probably needs refactoring.  THAT could really help the next guy.

Another niggle is about the comment before the "if" being in the affirmative and the code being in the negative.  When not avoid the comment and make the code in the affirmative?  The affirmative is usually easier to wrap one's head around than the negative expression.

Why not "if FDirty" rather than "if not CanClose"?

Posted 10 years ago
 
Write a Comment...
...
Sign in...

If you are a member, Sign In. Or, you can Create a Free account now.


Anonymous Post (text-only, no HTML):

Enter your name and security key.

Your Name:
Security key = P125A1
Enter key:
Article Contributed By Wes Peterson:

Wes Peterson is a Senior Programmer Analyst with Prestwood IT Solutions where he develops custom Windows software and custom websites using .Net and Delphi. When Wes is not coding for clients, he participates in this online community. Prior to his 10-year love-affair with Delphi, he worked with several other tools and databases. Currently he specializes in VS.Net using C# and VB.Net. To Wes, the .NET revolution is as exciting as the birth of Delphi.

Visit Profile

 KB Article #101139 Counter
23633
Since 5/29/2008
Sales Website: www.prestwood.com Or visit our legacy sales site: 
legacy.prestwood.com


©1995-2020 Prestwood IT Solutions.   [Security & Privacy]