4

I have a business object that I would like to "connect" to my UI better. I've seen some partial solutions for making objects data-aware, but they all involved significant changes to my business object, including an extra layer of abstraction.

I've been looking into the improved RTTI in new versions of Delphi, and it looks very interesting and useful. I'm wondering if I could use it to programmatically inject new write methods for all properties.

The way this would work is that my TEdit descendant would by given a reference to an object property when the form is built. The TEdit would then insert a reference to itself in an attribute for that property (and of course remove itself on destructor or being given another reference). The TEdit would also ensure that the write method for the property is replaced by one that notifies the TEdit of changes after calling the original write method.

Is this feasible? The big show stopper would be that injecting a new write method isn't possible, hence the title for this question.

There are also potential problems with derived properties, but it should be possible to find a solution for that.

boileau
  • 817
  • 6
  • 20
  • Maybe http://docwiki.embarcadero.com/CodeSamples/en/TVirtualMethodInterceptor_(Delphi) and http://blog.barrkel.com/2010/09/virtual-method-interception.html are a place to start? –  Feb 22 '12 at 13:57
  • 1
    Sounds like binding! Can you provide a minimal prototype of your TEdit descendant? – menjaraz Feb 22 '12 at 14:49
  • What do you really want, a `control` aware business object or a `business object` aware control (c.f. data aware control in the VCL)? To my opinion, business objects must be oblivious of UI controls. – menjaraz Feb 22 '12 at 15:04
  • Can you explain why you rejected all other non-RTTI solutions? It seems to me that there are plenty of non-RTTI solutions and it might help us understand the root of your question if we knew why they were not appropriate. – David Heffernan Feb 22 '12 at 15:05
  • @DorinDuminica: Looks very interesting, I will read. – boileau Feb 22 '12 at 15:37
  • @menjaraz: What do you mean by minimal prototype? I haven't gotten any farther in my thoughts than what I've written above. I suppose I'm describing object-aware controls that inject control awareness into an object at runtime. – boileau Feb 22 '12 at 15:39
  • @DavidHeffernan: I haven't rejected them, but I deemed them too be a bad investment of my time right now. My proposed solution may be a much quicker way to do the same thing just as well. Or maybe not. – boileau Feb 22 '12 at 15:43
  • I mean minimal code. If it's about object-aware controls as I expected I am afraid the header must be fixed: `(business) object aware control` instead of **control-aware business object** as it was stated. That may be misleading. – menjaraz Feb 22 '12 at 15:47
  • Please head to this [post](http://stackoverflow.com/a/9330902/744588). It's about an `ORM` using extensive RTTI and planning soon an integration with `Delphi Spring Framework` using too advanced RTTI. Another interesting food for thought. – menjaraz Feb 22 '12 at 16:02
  • Don't you think it would be **a lot** easier to simply give your business object an `OnChange` event and let the control store a method pointer there that the business object will invoke when the corresponding property changes? Business objects that descend from `TComponent` get the "disconnect notification" logic automatically. – Rob Kennedy Feb 22 '12 at 16:07
  • As far as I know, function/procedure/method Injection is possible using (ugly) low level hack. Unfortunately the URL is dead but at your request I can send to you the relevant unit (released under GPL) as an attachement. Hopefully you master french as suggests your username :-) , all comments are in french. – menjaraz Feb 22 '12 at 18:38
  • @DorinDuminica: Your links answers the question in the question title. It seems that it's possible to inject an interceptor method before and/or after all methods in an object that are virtual. To implement what I suggest would require that all properties have a virtual setter. This is feasible, though messy. And every time the interceptor method is called, the original method would have to be checked to see if it was a setter, and in the case, which property it was setting (potentially done through attributes). Anyways, it doesn't look like my approach is the quick win that I had hoped for. – boileau Feb 22 '12 at 20:37
  • @boileau I don't think you have a fast way in this... but another way would be **type** TEdit = **class** (StdCtrls.TEdit) <- override setters, put your code and then add **inherited** or just add **inherited** before your code, depending on what you want to achieve, but you need to add that after **interface** section... –  Feb 23 '12 at 08:26
  • @boileau, patching virtual methods is not enough: there's only one copy of the virtual method tables per class, while you'd most likely need a way to patch things so each INSTANCE of a class does something slightly different. Then you'd need to take care of derived classes, because each gets a fresh and complete copy of the virtual table, so they'd all need to be patched. It'd be much easier to just add a line of code to each setter method that should support binding. – Cosmin Prund Feb 23 '12 at 08:42
  • @CosminPrund, the object aware control would only patch the instance that it is to be bound to. This is by design. – boileau Feb 23 '12 at 10:50
  • @boileau, now I've noticed, it's using the new `TVirtualMethodInterceptor` from Delphi XE. I thought you're considering directly patching the code in memory. – Cosmin Prund Feb 23 '12 at 13:16

3 Answers3

1

Your question already puts you ahead of me with programming skill so I'll just add how I might approach this:

If I were to try to write something like that I'd probably start with a TList for each field in your TBusinessObject. That list would be used to indicate what needed to be updated when you needed to push out a change.

So when the TEdit is created it would add itself to a list which was associated with a piece of data in your TBusinessObject. When the TBusinessObject updated that piece of data it would then run through the list of attached objects. It would see the TEdit and, knowing it was a TEdit, would run code to update the .Text. If I attached a TCaption then the code would update the .Caption.

The TEdit, as you indicated, would need to tell the TBusinessObject when it's value was updated. I guess this is the tricky spot - you could create a new TEdit and add in a TList to maintain who it should inform when it changes. If you used the .Tag to indicate a field number in the TBusinessObject then the OnChange (or whatever event) could then call something like TBusinessObject.FieldUpdate[TEdit.Tag, NewValue] which then triggers your business logic. That, in turn, might make the TBusinessObject update other fields, which may have their own TLists to fields to update.

Preventing circular updates would require that you have a way of updating a control without triggering events. For one program I wrote I had two methods to update the control: SetValue and ChangeValue. SetValue disabled any events (OnChange, OnValidate), updated the control's value and then reenabled the events. ChangeValue simply changed the value and allowed any of the control's events to fire as required.

There are probably slicker ways to do this but hopefully this gives you food for thought.

  • That sounds like a workable solution, but my goal with the idea above was to create something that the object is completely unaware of, that inserts everything necessary for the binding into the object at runtime. – boileau Feb 22 '12 at 15:46
1

Possible to change property write methods programmatically using RTTI for creating object-aware controls?

No, it's not possible. RTTI gives you information, it doesn't give the ability to alter types at runtime.

The big show stopper would be that injecting a new write method isn't possible, hence the title for this question

In order for you to change this at runtime there should be something similar to an event handler that you can set. It's an easy concept, but it has some runtime overhead, both in call time (it would be an indirection where a direct call would normally suffice) and in terms of required memory (each property would require an extra TEvent style field). This is easy for you to implement if you need it, but it would be harmful if the compiler automatically generated such code for all classes "just in case".

If you're thinking of somehow patching the code in memory at runtime, that's not going to work and it would be, at best, unreliable.

Cosmin Prund
  • 25,498
  • 2
  • 60
  • 104
0

In this post entitled Inducing The Great Divide, Cobus Kruger talked about business objects.

The solution he cooked is essentially compliant to your requirements:

  1. Make use of advanced RTTI features introduced in recent Delphi version.
  2. Separation of the business logic from presentation logic.

Any PODO (Plain Old Delphi Object) will do as business object !

The magic lays in the TObjectBinding class which ties any TWinControl with any business object.

Excerpt:

TObjectBinding = class
private
  fCtx: TRttiContext;
  fControlType: TRttiType;
  fObjType: TRttiType;

  fPropFieldMapping: TDictionary<TRttiProperty, TRttiField>; // Dictionary of object Properties & corresponding Fields

  fControl: TWinControl; // The control (normally form)
  fObj: TObject; // Object it represents.

  procedure CreateMappings; 

  function FindField(Prop: TRttiProperty; out Field: TRttiField): Boolean;
  function FieldClass(Field: TRttiField): TClass;

  // Modify these to change the rules about what should be matched.
  function IsValidField(Field: TRttiField): Boolean;
  function IsValidProp(Prop: TRttiProperty): Boolean;

  // Modify these to change the mappings of property type to VCL control class.
  procedure AssignField(Prop: TRttiProperty; Field: TRttiField);
  procedure AssignProp(Prop: TRttiProperty; Field: TRttiField);

  // Used from AssignField/AssignProp. Extend these to support a wider range of properties.
  function GetPropText(Prop: TRttiProperty): string;
  procedure SetPropText(Prop: TRttiProperty; const Text: string);
public
  constructor Create(Control: TWinControl; Obj: TObject);
  destructor Destroy; override;
  //
  procedure Load;
  procedure Save;
end;

I hope that this will be a good starting point for you.

Community
  • 1
  • 1
menjaraz
  • 7,551
  • 4
  • 41
  • 81