|
Is it right to READ to WRITE? |
|
This originally appeared in the February 1996
issue of Visual Developer magazine.
I perhaps should point out that I have come to change my mind about this behavior
being wrong. The effect I describe below is something like setting a font's Color
property as GetFont.Color := XfmColor, which actually makes a lot of sense when
you think about it - you wouldn't want the form loading code to Create a
temporary TFont just so it can be passed to your write method which Assign()s
it to your Font property. [cf page 574 of my Kylix book]
Delphi properties have a simple, powerful interface: they look like variables, but you have full control over how (and whether) your class's users can read and write your class's properties. You can allow direct read access, just as if the property were a variable, or you can specify a read method that will be called whenever the property's value is read. You can allow direct write access, or you can specify a write method that will be called whenever the property's value is set. Right? Wrong.
What actually happensWhile this simple model does apply in most cases, things get weird when the property is a TPersistent like a TBitmap or a TFont. For TPersistents, the write method is called when you set the property at designtime or change it at runtime - but not when a component is created and loaded from its form's .dfm stream. Instead, the runtime library calls the property's read method to get a pointer to the property's private field variable, then uses that pointer to call the property's read-from-stream method. The write method is not called when the component is loaded!
How this can affect youOf course, in most cases this doesn't matter - the property gets loaded, and it has the same value at runtime as it had at designtime. However, there are a couple of ways this can affect you.First, the read method should never return Nil. While it may seem sensible to use a strategy of not actually creating a private field variable until the write method supplies an actual value to copy, Delphi's component loading code is not quite smart enough to notice that it has no TPersistent to tell to load. If your read method returns Nil, you will get GPF's when the component is loaded. (For what it's worth, this behavior is what alerted me to the problem, though I'll confess that it took me a while to find out what was going on.) Second, don't rely on your write method to extract information from the field variable and save it in runtime only fields elsewhere in your component. The write method will be called when you directly set the property at designtime or at runtime, but not when you indirectly set the property at component load time. If you expect your write method to update your component's internal state, your component will not load properly.
What you can do about itNow, I think there's a very good chance that Borland will conclude that read-to-write is a bug, and will change this behavior in future releases of Delphi. That is, you should avoid any solutions to the problems of GPF-on-load or partial-loading that will fail if the write method does get called at component load time.It's easy to prevent GPF-on-load in a 'forward-compatible' way. We now know that if a TPersistent property is stored, Delphi will call its read method when the object is loaded from a stream. So, as in Figure 1, the object's Create constructor must Create an object of the appropriate type, and set the property's private field variable. This is a bit wasteful if the property is not always set or stored, but a few hundred bytes of storage or a few hundred instructions of Create code are just plain insignificant to a sixteen or thirty-two megabyte Pentium. Partial-loading is a bit more complex. Fortunately, Delphi components have a Loaded method which you can override to perform any necessary post-processing. You can overcome partial- loading by taking advantage of the Loaded method and by making a few small changes to your code. The first thing to do is to add a 'fPropertyWritten' boolean flag for every TPersistent property that might be stored. (See Figure 1.) The flag will be set to False when the object is created, and should be set to True only by the write method. Second, you must override your component's Loaded method, and add a line like
if not fPropertyWritten then SetProperty(fProperty)so that Loaded calls your write method if (and only if) the component loading code didn't call it. Third, you generally don't want to Assign a TPersistent to itself, and you certainly don't want to Free it, Create a new instance, then Assign the old (freed) instance to the new instance. It's best to use code like
if fProperty <> NewPropertyValue then begin fProperty.Free; {Assigning 'over' a } fProperty := TPropertyType.Create; {TPersistent can fail:} fProperty.Assign(NewPropertyValue) {Free/Create/Assign is safer} end; {Extract any necessary data from NewPropertyValue} fPropertyWritten := True;This way, you only reset the private field variable if the new value does not equal the existing field. Adding this test ensures that SetProperty(fProperty) will not cause a GPF now, and won't constitute much overhead if read-to-write does go away.
A little perspectiveMy personal suspicion is that read-to-write is the result of a bit of overzealous optimization by the Delphi team. While it seems like it would be pretty hard to make a case for its not being a bug, whenever I run into a bug or bit of poor design in Delphi, I like to ask myself how many apps I've ever used or written are more stable than Delphi, or have a higher ratio of good to bad design decisions. The answer is always "Few, if any".It's also worth bearing in mind that the write method is called at load time for simple types like integers, enums, and strings - and that read-to-write for TPersistents is pretty easy to deal with.
Jon Shemitz has been a Pascal/Delphi consultant and independent developer for the past twelve years. He lives in Santa Cruz, California with his partner and their two children, and is contemplating buying a laptop so he can work in his greenhouse. He can be reached at jon@midnightbeach.com or via http://www.midnightbeach.com/jon. |
||
Created on 1995, last updated July 26, 2002
Contact jon@midnightbeach.com
|