Search code examples
delphidependency-injectioninversion-of-controlspring4d

How to correctly inject a property to form?


I will up the question at second time. Do not blame me please.

Situation:

I have a form

TfrmMain = class(TForm)
private
   [Inject('IniFileSettings')]
   FSettings: ISettings;
public
end;

I have container initialization procedure:

procedure BuildContainer(const container: TContainer);
begin
  container.RegisterType<TIniSettings>.Implements<ISettings>('IniFileSettings');

  container.RegisterType<TfrmMain, TfrmMain>.DelegateTo(
    function: TfrmMain
    begin
      Application.CreateForm(TfrmMain, Result);
    end);

  container.Build;
end;

So I initialize both TfrmMain as well as TIniSettings via container.

in .DPR I have:

begin
  BuildContainer(GlobalContainer);
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TfrmMain, frmMain);
  Application.Run;
end.

Also I have a helper for TApplication:

procedure TApplicationHelper.CreateForm(InstanceClass: TComponentClass; var Reference);
var
  locator: IServiceLocator;
begin
  locator := TServiceLocatorAdapter.Create(GlobalContainer);
  if locator.HasService(InstanceClass.ClassInfo) then
    TObject(Reference) := GlobalContainer.Resolve(InstanceClass.ClassInfo).AsObject
  else
    inherited CreateForm(InstanceClass, Reference);
end;

Problem: when I try to

procedure TfrmMain.FormCreate(Sender: TObject);
begin
   s := FSettings.ReadString('Connection', 'Server', 'localhost');
end;

I get AV exception because FSettings currently is NIL.

What is correct way to get FSettings object from the container?

UPDATE:

FSettings := GlobalContainer.Resolve<ISettings>;

This row works perfectly... As in last time I have problem to use [Inject] attribute. Even with solution from Stefan I can make the method working:

How to initialize main application form in Spring4D GlobalContainer?


Solution

  • First the reason why the container does not have a HasService anymore is because that method has been removed. You can access it as follows:

    if container.Kernel.Registry.HasService(...) then  // yeah yeah, I know LoD is crying right now ;)
    

    I would avoid mixing using ServiceLocator and GlobalContainer. While they should point to the same instance it might not be the case because actually someone could point one of them to another instance. If you really want to use ServiceLocator in this case then also resolve from ServiceLocator. But keep in mind that there is nothing on it that the container does not know (even if you have to call some different parts of the kernel.

    But that is not the problem you are facing here with your settings injection. The problem you have is the timing. The FormCreate method (I just guess it is attached to the OnCreate event). So the container instantiates the TfrmMain, the event gets called and then returns to the container code which afterwards does all the injections. So calling something that has not been injected via constructor in some code being called during construction is a temporal coupling.

    There are different approaches to this problem:

    • moving your access to the FSettings to some event that gets triggered later (like OnShow or OnActivate)
    • don't use field injection it can be nice but that couples your code to the container because "traditional" code cannot do that. Use property injection and a setter that executes the code.

    When you consider constructor injection as for dependencies that are mandatory and property injection for those that are optional I would say go for the constructor injection. But knowing that you working with a TComponent descendant I probably would use the property injection in that case although that dependency is not optional.