The problem is as follows:

  • There is an Interface with two methods Load and Save and isChanged property
  • There are many TFrame frames that support this interface.
  • Frames have different components with data entry capability, such as TEdit , TCheckBox , TComboBox , TMemo , and possibly others.
  • Load - fills the fields with values
  • Save - writes (only if isChanged = true )
  • isChanged - set if any input field has been changed
  • When writing a request to save the changes, with a positive answer Save , negative, if something changed - Load .

In general, all components have an onChange event in which you can set isChanged , BUT ... You need to recursively bypass all the components and subcomponents of the frame ( TPageControl , TScrollBox , etc.) to initialize the event (or add, if it is already set), and boot all disable them, then assign again. Somehow it's all long, slow and not beautiful.

In general, a fresh look at the problem is needed ... Maybe it is possible to somehow globally intercept input from all components in the frame in order to include the isChanged flag? Is it possible to create an event in the frame that will react to a change in the input fields of one of its components? Or how to approach this issue better?

    2 answers 2

    And what's the problem once write recursive bust?

    TControlObject = class(TAggregatedObject) strict private FOwner: TWinControl; FEvents: TDictionary<TObject, TNotifyEvent>; FIsChanged: Boolean; strict private procedure DoChange(ASender: TObject); procedure InitControl(AControl: TControl); procedure DeInitControl(AControl: TControl); procedure EnumControl(AControl: TWinControl; const AAction: TNotifyEvent); public constructor Create(AOwner: TWinControl); destructor Destroy; override; procedure Load(); procedure Save(); function IsChanged(); end; constructor TControlObject.Create(AOwner: TWinControl); begin inherited Create(AOwner); FOwner := AOwner; FEvents := TDictionary<TObject, TNotifyEvent>.Create; EnumControl(FOwner, InitControl); end; destructor TControlObject.Destroy; override; var LObj: TObject; begin for LObj in FEvents.Keys do DeInitControl(LObj); FEvents.Free; inherited Destroy; end; procedure TControlObject.DoChange(ASender: TObject); var LEvent: TNotifyEvent; begin FIsChange := True; LEvent := FEvents[ASender]; if Assigned(LEvent) then LEvent(ASender); end; procedure TControlObject.InitControl(AControl: TControl); var LEvent: TNotifyEvent; LFound: Boolean; begin LFound := True; if AControl is TEdit then begin LEvent := TEdit(AControl).OnChange; TEdit(AControl).OnChange := DoChange; end else if AControl is TComboBox then begin LEvent := TComboBox(AControl).OnChange; TComboBox(AControl).OnChange := DoChange; end else if AControl is ... then begin ....... end else LFound := False; if LFound then FEvents.Add(AControl, LEvent); end; procedure TControlObject.DeInitControl(AControl: TControl); var LEvent: TNotifyEvent; LFound: Boolean; begin LEvent := FEvents[AControl]; if AControl is TEdit then TEdit(AControl).OnChange := LEvent; else if AControl is TComboBox then TComboBox(AControl).OnChange := LEvent; else if AControl is ... then ....... end; procedure TControlObject.EnumControl(AControl: TWinControl; const AAction: TNotifyEvent); var Li: Integer; LControl: TControl; begin for Li := 0 to AControl.ControlCount - 1 do begin LControl := AControl.Controls[Li]; AAction(LControl); if LControl is TWinControl then EnumControl(LControl, AAction); end; end; procedure TControlObject.Load(); begin EnumControl(FOwner, DoLoad); end; procedure TControlObject.Save(); begin EnumControl(FOwner, DoSave); end; function TControlObject.IsChanged(); begin Result := FIsChanged; end; ............... TMyFrame = class(TFrame, IControl) strict private FControl: TControlObject; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; public property Control: TControlObject read FControl implements IControl; end; constructor TMyFrame.Create(AOwner: TComponent); begin inherited Create(AOwner); FControl := TControlObject.Create(Self); end; destructor TMyFrame.Destroy; begin FControl.Free; inherited Destroy; end; 
    • 2
      If an event is assigned to a component and reassigned dynamically, it will not be very good, since the container will store the “old” references to event handlers. And if controls are generally created at runtime, you will need to intercept CM_CONTROLLISTCHANGE additionally or call InitControl manually. There is not always OnChange, sometimes the desired event may not be TNotifyEvent ... - kami
    • I will now advise you to dynamically change the window procedure via SetWindowLongPtr (), but you will say that someone else can call SetWindowLongPtr and you will be right. There is no reception against scrap. Each determines the permissible risks and takes them. Nobody calls DataSource.DataSet.Free or StringGrid.Rows.Free? Although they can. - Anton Shchyrov
    • The event does not have to be called OnChange. In the InitControl method, you can set any event. If the event is not TNotifyEvent, then the author will have to come up with something. In any case, the class is sharpened only for a specific set of components. I have a similar mechanism works fine on a real project. - Anton Shchyrov
    • I didn’t mean to say that the approach you suggested is bad. Sorry if it turned out harsh. It is good enough, but in a limited number of cases, what should be understood by those who will implement this in themselves. - kami
    • The problem is not to write once, but that it will constantly spin and be assigned / re-assigned ... But in general, the example is beautiful, thank you. And for the dynamic assignment of events by "someone" should be responsible "he is", if such a need suddenly arises - Isaev

    One of the possible options (I think it has been described more than once): We create a separate module in which we overlap the "regular" TEdit , TMemo , etc.:

     unit SubControls; type TEdit = class(System.StdCtrls.TEdit) protected procedure Change; override; end; procedure TEdit.Change; var intf: IMyInterface; begin inherited; // здесь будет вызвано штатное событие OnChange, мы его не трогаем // а вот тут реализовываем свою логику if Owner.GetInterface(IMyInterface, intf) then if not intf.Loading then // не знаю, есть ли это свойство, что-то типа "мы сейчас грузимся" intf.IsChanged:=True; end; 

    In the same way we modify the other components used. The resulting module is included in the uses in the interface section after all the regular ones (one of the last). Thus, the controls lying on the frame / form will implement the functionality from our module.

    • An interesting approach, tomorrow I will check whether it works as it looks, it looks quite logical. The only drawback is that it will be necessary to constantly expand this module when new input components are detected in frames, this is not very critical, but not pleasant. - Isaev
    • one
      @Isaev But this approach is able to work with such components for which OnChange is not in principle provided. For example - TXXXButton, able to remain in the pressed position, etc. - kami
    • In principle, it works quite well, even all the components have already been rewritten for other purposes and more than half are implemented. But I liked the second method more, in components where onChange is not provided, it seems there is no need yet. - Isaev