You are bound to the architecture your application was designed around. Don't try to fight against it. Let the data aware controls do what they are good at, data synchronization. If your controls are already bound to their data sources using the dfm there shouldn't be a problem.
What you do need to refactor is any event handlers you have attached to your controls. I suggest you take a look at the Supervising Controller pattern. I've found example implementations for:
While there are a few examples of UI architectural patterns in Delphi those that are geared toward desktop applications tend to be about the Passive View rather than Supervising Controller. So here is my take on it.
You'll want to start with defining at least one interface for each form in your application. I say at least one because some forms are complex and may need to be broken into multiple interfaces.
IProductView = interface
end;
Then have your form implement it.
TProductForm = class(TForm, IProductView)
...
end;
Next you'll need a presenter/controller. This will handle everything except data synchronization.
TProductPresenter = class
private
FView: IProductView;
public
constructor Create(AView:IProductView);
end;
Create an private field in your form class and create/free the presenter when the form is created/freed. Whether you use the form's constructor/destructor or the onCreate/onDestroy events doesn't matter much.
TProductForm = class(TForm, IProductView)
private
FPresenter: TProductPresenter;
public
constructor Create;
...
end;
implementation
TProductForm.Create
begin
FPresenter := TProductPresenter.Create(self);
end;
Now when you need the form or one of its controls to respond to an event delegate responsibility to the presenter. Lets assume you need to check that the product name uses proper capitalization.
TProductForm.NameDBEditChange(Sender: TObject);
begin
FPresenter.ValidateName;
end;
Rather than pass the control or its text property as an argument you expose the data as a property on the interface...
IProductView = interface
function GetName:string;
procedure SetName(Value: string);
property Name: string read GetName write SetName;
...and implement GetName
and SetName
on the form.
TProductForm.GetName: string;
begin
Result := NameDBEdit.Text;
end;
TProductForm.SetName(Value: string);
begin
NameDBEdit.Text := Value;
end;
Its important to expose the data in the simplest form possible. You don't want the presenter depending on the product name being stored in a TDBEdit. The presenter should only see what you explicitly allow it to see through the interface. The main benefit of this is you can modify the form as much as you want(or replace it entirely) and as long as it adheres to the interface no changes will need to be made to the presenter.
Now that all your business logic has been moved to your presenter it will resemble a god class. Your next step will be to refactor that logic into appropriate classes broken up by responsibility. When you reach this point you're in a much better position to attempt an architectural redesign (if your still considering it).
"Wow! That looks like a lot of work!" you might say. You'd be right (but then you knew it would be a lot of work before you got started). It doesn't have to be done all at once. None of these steps is changing the behavior of the logic just where it takes place.
Advantages
- UI is now easy to modify
- Business logic can more easily be tested in isolation
- Can be implemented incrementally
Disadvantages
- It is more work at first though this is eventually offset by more maintainable code later on.
- Not suitable for all applications. For small projects the additional infrastructure may not be worth the effort.
Other references