Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
TIWCustomControl's FPageContext field
#1
I would like to suggest a possible change in the way IntraWeb custom controls decide to call the UnRegisterAsyncEvents method.

At this time, in IW 15.2.69, the Dispose method of TIWCustomControls only checks to see if IsDesignMode is true. I would like to add another condition to that:

  if Assigned(OwnerForm) and not (OwnerForm as TIWForm).PageContext.Destroying then

This will be faster, since the UnRegisterAsyncEvents will no longer have to do through all controls and all events to unregister them (which is not required, since we are in the Destroy of our main form anyway).

It is also a bit safer, since my code uses an approach with templates where numerous controls are added dynamically to the form, with a temporary PageContext. Since this PageContext often no longer exists when the page is destroyed, I sometimes get access violations (if the custom control's PageContext, which no longer exists, has a Destroying field pointing to some place in memory which just happend to be False).

I've been using the modified IWControl.pas for a while now, without any further problems.

Thanks for your consideration.

Groetjes, Bob Swart
Reply
#2
Hi Dr. Bob,

thanks for your input. I'll review that change and let you know.

Cheeers,
Reply
#3
Hi Dr. Bob,

After reviewing your suggestions, I'll need to enter into a little more detail here, going through the creation/life/destruction of an IWForm instance.

Let me start with a bit of context: In older IW versions, there was a session-wide list of callbacks, owned and controlled by the IWApplication instance. This has proven to be unreliable. Mostly because unregistration of async callback methods didn't occur in a fail-proof manner, because:

a- In some cases, 3rd party controls just ingnored this requirement and never unregistered their callbacks when the control was destroyed. If for some reason the callback was called at a later time, an AV would definitely happen, because the control was gone, but the callback was still there registered. Please note that this is an unrecoverable condition, once the callback could never be unregistered and the solution was to restart a new session, which is pretty bad.

b- In other cases, any exception during the form destructor (who never experienced a DB connection failure when trying to make sure that all your data has been saved?) would mean that the controls wouldn't have oportunity to unregister their events and we go back to scenario (a) above.

Since IW 15.1 IIRC, we changed that. The PageContext instance - that was temporary - became permanent and it is an integral part of any IWForm now. The form creates the PageContext before anything else and destroys it after everything else. The PageContext (meaning the IWForm instance) owns its own callback list. The IWApplication finds the active form, dispatches to it the callback to be executed and the IWForm finds one of its child controls that should respond to it. When the form is gone, the page context and the callback list are both gone too. If the form destruction fails, doesn't matter, the callback list is either destroyed or in a limbo state and will never be used again. It won't affect the session anymore. This solves both problems (a) anb (b) above.

Now, let's see some parts of IW code:

This is the TIWForm constructor:

Code:
constructor TIWForm.CreateNew(AOwner: TComponent; const ADummy: Integer = 0);
begin
  // make sure TIWApplication is always the form owner
  if not (aOwner is TIWApplication) then begin
    aOwner := GetWebApplication;
  end;
  FPageContext := CreatePageContext;

  inherited CreateNew(AOwner, ADummy);
end;

The form's PageContext instance is created here and is kept alive for the whole life-time of the form. 

And this is the form Destructor:

Code:
destructor TIWForm.Destroy;
begin
  if Assigned(FPageContext) then begin
    TIWPageContextAccess(FPageContext).FDestroying := True;
  end;
  inherited;
  FreeAndNil(FPageContext);    // Callback unregistration depends on PageContext
end;

As you can see, the PageContext object is left to be destroyed last, after the inherited destructor has been called. This guarantees that all owned controls will be destroyed while the PageContext object is still alive. The comment you see in the code above is original part of the code. 

Please notice that the PageContext Destroying flag is set before calling the inherited destructor, so all the controls can know, inspecting their own FPageContext reference, that their owner form (and PageContext) is being destroyed.

There is no other place where page contexts objects are created or destroyed, not even when using templates (the template processor uses the owner form PageContext instance to render anything). There are temp component contexts which are a different class of objects but alll component contexts also point to the same IWForm's PageContext which is unique.

Now, back to the TIWCustomControl destruction/callback unregistration:

Code:
rocedure TIWCustomControl.Dispose(ADispose: Boolean);
begin
  ...

  if ADispose then begin
    if not IsDesignMode then begin
      UnRegisterAsyncEvents;
    end;

This is part of the TIWCustomControl.Dispose method, the same part that you mentioned in your post.

Now, this is a piece of the UnregisterAsyncEvents method of TIWCustomControl:

Code:
procedure TIWCustomControl.UnRegisterAsyncEvents;
  ...
begin
  // PageContext owns callback registration. No need to unregister if
  // PageContext is about to be destroyed
  if not Assigned(FPageContext) or FPageContext.Destroying then begin
    Exit;
  end;

  ...

As you can see, each control checks for the PageContext destruction before unregistering the async callbacks.

So, according to the code above, under normal circumstances, there is no way for a control *owned by a TIWForm* to reference a non-existing PageContext.

The other possibility would be controls not owned by a TIWForm (not even indirectly). Please notice that even controls owned by a TIWFrame instance that are ultimately parented to an TIWForm are covered by the premisse that they will be destroyed before the PageContext instance that they used to register the callbacks (and that is referenced by their FPageContext field).

This leaves certain bizarre scenarios like: The user creates an IWFrame at runtime, sets an IWForm as a parent of the IWFrame and later makes another IWForm instance it's parent or, worse, makes its parent nil. To be honest I'm not even sure that something like that works (i.e. has the desired effect) in a simple VCL desktop application, let alone an IntraWeb application.
These kinds of scenarios I'd just say that are simply not supported. IMO, there is no reason to do some crazy stuff like that with an IWFrame (like trying to reuse an existing IWFrame with different IWForm instances).

In short, the only way to have an AV there is if the IWControl outlives the IWForm that it once has been rendered on. If some IW users have a very unusual test case that does that I would first try to convince they not to do this. Or they can convice me that it is an acceptable use case and we revisit this topic again  Smile

I know that some IntraWeb users - especially the most experienced ones - do things that I can't even imagine Big Grin If you have another usage scenario that breaks this logic I'd honstely like to hear.
Reply
#4
PS: My reluctance to use a call to OwnerForm in the IWControl destructor is because OwnerForm (which calls the IWBaseForm.GetOwnerForm method) is a damn slow piece of code. Not because that we are sloppy with it, but because there is no simple way to retrieve the owner form of a control in 100% of the use cases (e.g. a frame owned by another frame, owned by other frame, owned by no one and parented to an IWForm).

BTW, my recommendation is that you store the result of the OnwerForm call in a local variable, so you call it only once per control.
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)