Monday, November 30, 2009

Using messages to notify events

Lately, i've been using frames extensively in my projects and recently i faced a little problem while working with them: how to notify the parent control (often a TForm) changes in the state of the frame controls/data?

I tried some approaches:

1) Add an event handler to the TDatasource.OnDataChange event.
This has the disadvantage of not being a generic solution since not always the frame has a TDataSource. Also this event is fired in changes to all fields of the respective TDataset, and often only a few fields need to be monitored, leading to some overhead. Yet is not possible to set the event at design time due to bug 14947.

2)Create handlers for events of child controls.
Works by setting in the parent form handlers for events, e.g. OnChange, of child controls of the frame. It has the drawback of needing more than one event handler in the case where is necessary to monitor more than one control/field.
Another problem is that such event handlers can be cleared by the IDE due to bug 14835. Not to say that sometimes the changes in the frame are not propagated to frame instances. In this case the workaround i found to sync the frames was to remove and add the frame again, loosing all properties set in the frame parent control.

So, i decided to implement a specific event for the frame. The first approach i thought was adding a TNotifyEvent property to the frame but this has almost the same drawbacks of approach 2. At this point the idea of using messages came in my mind.

AFAIK messages are used mostly in LCL and seldom in user projects so i had doubts if would work. Here is what i did:

1) Declare a custom message id constant
I declared the following constant in a shared unit:

LM_CHILDDATACHANGED = LM_USER + 1;
2) Create a method in the frame to send the message
I created a method SendDataChangeMsg with the following code:

var
Form: TCustomForm;
begin
Form := GetFirstParentForm(Self);
if Form <> nil then
Form.Perform(LM_CHILDDATACHANGED, 0, 0);
end;

The code is pretty straight. It get the parent form and then send the message through Perform method

3) Hook the events of the controls that should be monitored
Mostly OnChange events. Just called SendDataChangeMsg inside them

4) Add a property ValidData
This property checks if the data in the monitored controls are valid

5) Intercept the message at the frame parent (TForm)
In the TForm i put the frame, i created a method to intercept the message declared as follow:

procedure ChildDataChanged(var Msg: TLMessage); message
LM_CHILDDATACHANGED;


I'm using that to allow saving or not the data so i put a code to enable/disable the save button:

SaveButton.Enabled := Frame.ValidData;
That's all. It's working fine for me. In the end i got a pretty clear notify system. Now i can remove/add the frame as necessary without the need to reconnect the event handlers every time.



Sunday, November 15, 2009

Qt and Lazarus: a nice surprise (again)

Long, long time ago i wrote how the Lazarus Qt interface impressed me by its functionality. Yesterday, after updating my Lazarus working copy to do some debugging in Linux, i rebuilt the IDE as usual. But the IDE looked a little different, most notably the source editor font. I've got sometime to figure that the IDE was compiled using the Qt interface. I use the gtk2 interface but somehow i left the LCL build configured to Qt, so the unexpected result.

Again, the Qt interface surprised me. The IDE ran fine with good looking and very responsive except by bugs 15101 and 15103. Unfortunately, i use a lot the features that these bugs affect and i will stay with gtk2 for now. But as soon as these bugs are fixed i will give a try to Qt again.

Sunday, November 01, 2009

Using TDBEdit to handle date fields

If you try to link a date field with a TDBEdit, when an user inserts an invalid date, the control reacts with a non user friendly exception message. So, for long time, i used a TMaskEdit to edit date values and then update manually the dataset.
After some debate about TMaskEdit in the mail list i tried to workaround this problem.

The first think i did was to set the EditMask property to !99/99/9999;1;_ and hardcode the ShortDateFormat with a compatible format, e.g. 'dd/mm/yyyy'.

To validate the value inserted i created a handler to be linked to TDBEdit OnExit event(the color setting is optional):

var
D: TDateTime;
DateEdit: TDBEdit;
begin
DateEdit := Sender as TDBEdit;
if not TryStrToDate(DateEdit.Text, D) then
begin
ShowMessage(DateEdit.Text + ' is not a valid date');
DateEdit.Clear;
DateEdit.Color := clRed;
//or set a default value
//DateEdit.Text := '10/10/1900';
end
else
DateEdit.Color := clWindow;
end;

So you clear the TDBEdit and get a empty string that is valid value right? Not so fast. The message dialog will trigger an KillFocus message updating the Dataset with the old (and invalid) value. Calling ShowMessage after Clear does not help because the the control will try to update the dataset with " / / ", that is also not a valid value.

The solution i found was to handle the OnSetText event of the date field to skip all invalid values:

var
D: TDateTime;
begin
if not TryStrToDate(aText, D) then
Sender.Value := Null
else
Sender.AsDateTime := D;
end;

With this approach is possible to allow date edition with masks safely.

However, setting events for each TDBEdit and date field is annoying, so i created an TDBEdit descendant to handle this issue. It can be found in the LuiControls package (svn version).

In time:
  • Rx package has a TDBDateEdit component, but it does not allow editing with masks and don't manage invalid dates.
  • It's required a recent (post 0.9.28) svn version because of a bug in TMaskEdit in the earlier versions