Don´t save my published property in component frame - delphi

I have a Component inherited of the TFrame... But my published properties don't save the values in .dfm... The error occurs when closing Delphi and open again, reloading the project.
This error is: "Error reading MyComponent1.Obs: property Obs does not exists..."
TMyComponent = class(TFrame)
FObs: string;
procedure SetObs(const Value: string);
published
property Obs: string read FObs write SetObs;
end;
procedure register;
implementation
procedure register;
begin
RegisterComponents('My Components', [TMyComponent]);
end;
procedure TMyComponent .SetObs(const Value: string);
begin
if FObs <> Value then
FObs := Value;
end;

I've managed to reproduce the error and then fix it, but can't understand exactly why it happens so, has to do with visual inheritance which works in rather complex way (hope David or Remy could explain what happens here).
First of all, this error indicates that property actually was saved to dfm file. Point is, it's perfectly normal situation when some properties are absent in dfm, it just means that property must have default value (or value which ancestor had), so IDE never raises error because it didn't find some property in file.
Here the opposite happens: property 'obs' was found in dfm, but wasn't found in class itself. The same happened when I created new project and then added frame unit to project itself. It seems, in that case visual inheritance took place instead of normal inheritance, which is: IDE found, that class TMyComponent is described in 'visual' unit belonging to project, found that line:
TMyComponent = class (TFrame)
and made TMyComponent merely alias for TFrame. That's why error occurred: TFrame really doesn't have 'obs' property.
And here is workaround: Don't add this frame to project itself. Instead, add frame from component palette. This way it works as expected. If IDE can't find files of this component, add folder containing them to search path.

Related

Delphi Bookmark Error: E2003 Undeclared identifier 'TBookmark'

Hey I wanted to use a TBookmark as a varialbe in my Form. I got it running in another Form and it is working there.
But in the new Form I get the Error.. I guess I have to include something in the uses statement but I cant remember what it was. Here is the code TBookmark is underlined in red so thats where the error sits.
procedure TForm4.FormCreate(Sender: TObject);
var test : string;
var selectedRow, rows : TBookmark;
begin
rows := Form1.DBGrid1.DataSource.DataSet.GetBookmark;
Form1.DBGrid1.SelectedRows.CurrentRowSelected := True;
Form1.DBGrid1.DataSource.DataSet.GotoBookmark(rows);
test := Form1.DBGrid1.DataSource.DataSet.FieldByName('name').AsString;
ShowMessage(test);
end;
end.
Your Form4 needs to Use the DB unit, because that's where TBookMark is declared.
Btw, what is in Form1's unit is irrelevant to this. The only relevant thing is that Form4's unit has to Use DB. What happens is that when the compiler tries to compile your Form4 unit, it needs to be able to find the definition of TBookMark, and that is in the standard DB.Pas unit along with lots of other dataset-related stuff. The same is true of any other identifier (or its class) that the compiler encounters in your project's source code.
99% of problems like this can be solved by doing a "Search | Find in Files" through Dephi's source code folders (and your project's folder if it's one of yours) to identify where the "undeclared" or missing item is declared.
Update So, you've got this code, which I'll assume is in your uForm4.Pas unit.
procedure TForm4.FormCreate(Sender: TObject);
var
test : string;
var
selectedRow, rows : TBookmark;
begin
rows := Form1.DBGrid1.DataSource.DataSet.GetBookmark;
Form1.DBGrid1.SelectedRows.CurrentRowSelected := True;
Form1.DBGrid1.DataSource.DataSet.GotoBookmark(rows);
test := Form1.DBGrid1.DataSource.DataSet.FieldByName('name').AsString;
ShowMessage(test);
end;
You want to be able to do something with the Name value that's shown in the current row of
DBGrid1 on Form1. There's nothing particularly wrong with the way you've done it, just that
it's long-winded, error-prone and invites problems like the one you've having with
TBookMark.
The point is that somewhere in your project, maybe in your uForm1.Pas unit, you know,
I don't, there must be a TDataSet-descendant (like TFDQuery, TAdoQuery or TTable) that is
specified in the DataSet property of Form1's DataSource1. For the sake of argument, lets'
say that the dataset component is FDQuery1 on Form1 and you want to get the Name field value
from the current row in DBGrid1.
To get that Name value, you don't actually need the bookmarks your code is using. The way
a TDBGrid works, the currently-selected row in the grid is always the current row in the
dataset component. So you could simply write
procedure TForm4.FormCreate(Sender: TObject);
var
test : string;
begin
test := Form1.FDQuery1.FieldByName('name').AsString;
ShowMessage(test);
end;
because you don't need to go through the rigmarole of Form1.DBGrid1.DataSource.DataSet to get to it.
Now, to explain another little mystery, how come your code would work fine if it was in uForm1.Pas
but you get the Undeclared Identifier: TBookMark error why you try the same code in uForm4.Pas
unit? Well, if you've ever watched the top of a source code file as it's being saved, you'll notice that
Delphi automatically adds, to the Uses list at the top, the units which contain any of the
components you've added to the form since its last save. So adding a TDataSource to the form would add
the DB unit to the Uses list, because that's where TDataSource is declared and so is TBookMark. Which
is why Delphi could compile Form1's code without the error, whereas when you try to mention a TBookMark
to uForm4, you need to add it to the unit's Uses list unless you add a component (like TDataSource)
to Form4 which will cause it to automatically add DB to the Uses list if it isn't already there. Mystery
solved.

How to properly publish an event executed from the 'Loaded' procedure?

In a runtime only package, I've defined a TFrame descendant which publishes the OnLoaded event:
type
TMyMethod = procedure() of object;
TMyFrame = class(TFrame)
protected
FOnLoaded : TMyMethod;
procedure Loaded(); override;
published
property OnLoaded : TMyMethod read FOnLoaded write FOnLoaded;
end;
implementation
{$R *.dfm}
procedure TMyFrame.Loaded();
begin
inherited;
if(Assigned(FOnLoaded))
then FOnLoaded();
end;
In a designtime only package, I've registered TMyFrame component as follows:
unit uMyRegistrations;
interface
uses
Classes, uMyFrame;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('MyTestComponents', [
TMyFrame
]);
end;
I've installed the designtime package, I can find TMyFrame in the tool palette and its OnLoaded event is shown in the object inspector.
I've dragged a TMyFrame into a form, then I've assigned the OnLoaded event by doubleclicking from the object inspector.
After assigning the event, I noticed that an access violation error message appears each time I try to open the form's file in Delphi (It let me open the ".pas" file, but I can't switch to visual designer view).
Did I correctly published the OnLoaded event? If so, what else is wrong?
Further Informations:
I'm using Delphi 2007 (don't know if it matters).
The error also appears by doing the same thing with different parent classes (Not only for TFrame descendants).
Updated (somewhat less bogus) answer
You accepted my original answer, but what I wrote was not correct. Rob Kennedy pointed to an article by former Embarcadero developer Allen Bauer on the topic of Assigned.
Allen explains that the Assigned function only tests one pointer of the two pointers in a method pointer. The IDE at design time takes advantage of this by assigning sentinel values to any published method properties (i.e. events). These sentinel values have nil for one of the two pointers in the method pointer (the one that Assigned checks), and an index identifying the property value in the other pointer.
All this means that False is returned when you call Assigned at design time. So long as you check published method pointers with Assigned before calling them, then you will never call them at design time.
So what I originally wrote cannot be true.
So I dug a bit deeper. I used the following very simple code, testing with XE7:
type
TMyControl = class(TGraphicControl)
protected
FSize: Integer;
procedure Loaded; override;
end;
....
procedure TMyControl.Loaded;
begin
inherited;
FSize := InstanceSize;
end;
....
procedure Register;
begin
RegisterComponents('MyTestComponents', [TMyControl]);
end;
This was enough to cause an AV in the IDE at design time whenever the Loaded method was executed.
My conclusion is that the IDE does some rather underhand things when streaming, and your objects are not in a fit state to use when the Loaded method is called. But I don't really have a better understanding than that.
Original (very bogus) answer
You must not execute event handlers at design time, and your code does just that. The reason being that at design time the event handler's code is not available.
The control's code is available, the IDE has loaded it – but the code that implements the event handler is not. That code is not part of the design time package, it is part of the project that is currently open in the IDE. After all, it might not even compile yet!
The Loaded method should defend against this like so:
procedure TMyFrame.Loaded();
begin
inherited;
if not (csDesigning in ComponentState) and Assigned(FOnLoaded) then
FOnLoaded();
end;

Delphi: browsing components inside a property editor

When a property is a simple component of any class, the IDE's property editor is able to drop down a list of all compatible components in all the project's forms.
I want to do some equivalent task, but with some filtering based on acceptable component classes for the property; these classes common ancestor is only TComponent and they have custom interfaces.
Currently I have a working property editor that uses a paValueList attribute and some filtering in the GetValues procedure, based on checking the supported interfaces, but it is limited to the current form :-(.
How to browse all the forms like the IDE does?
I want to do some equivalent task, but with some filtering based on acceptable component classes for the property; these classes common ancestor is only TComponent and they have custom interfaces.
If you are filtering for only 1 interface, you should change the property in question to accept that interface type instead of a TComponent, and then the default property editor for interface properties (TInterfaceProperty) will filter the components automatically for you:
property MyProperty: IMyInterface read ... write ...;
Currently I have a working property editor that uses a paValueList attribute and some filtering in the GetValues procedure, based on checking the supported interfaces, but it is limited to the current form :-(.
How to browse all the forms like the IDE does?
To manually filter the components in a custom property editor, you need to do the same thing that the default component property editor (TComponentProperty) does to obtain the compatible components, and then you can filter them further as needed.
Internally, TComponentProperty.GetValues() simply calls Designer.GetComponentNames(), passing it the PTypeData of the property type that is being edited:
procedure TComponentProperty.GetValues(Proc: TGetStrProc);
begin
Designer.GetComponentNames(GetTypeData(GetPropType), Proc);
end;
So, if your property accepts a TComponent (since that is the only common ancestor of your intended components):
property MyProperty: TComponent read ... write ...;
Then GetPropType() in this case would return TypeInfo(TComponent).
GetComponentNames() (whose implementation is in the IDE and not available in the VCL source code) enumerates the components of the Root (Form, DataModule, or Frame) that owns the component being edited, as well as all linked Root objects that are accessible in other units specified in the edited Root's uses clause. This is documented behavior:
DesignIntf.IDesigner60.GetComponentNames
Executes a callback for every component that can be assigned a property of a specified type.
Use GetComponentNames to call the procedure specified by the Proc parameter for every component that can be assigned a property that matches the TypeData parameter. For each component, Proc is called with its S parameter set to the name of the component. This parameter can be used to obtain a reference to the component by calling the GetComponent method.
Note: GetComponentNames calls Proc for components in units that are in the uses clause of the current root object's unit (Delphi) or included by that unit (C++), as well as the entity that is the value of Root.
So, in your GetValues() implementation, call Designer.GetComponentNames() specifying the PTypeData for TComponent and let the IDE enumerate all available units and provide you with a list of each component's Name. Then you can loop through that list calling Designer.GetComponent() to get the actual TComponent objects and query them for your desired interface(s):
procedure TMyComponentProperty.GetValues(Proc: TGetStrProc);
var
Names: TStringList;
I: Integer;
begin
Names := TStringList.Create;
try
Designer.GetComponentNames(GetTypeData(TypInfo(TComponent)), Names.Append);
for I := 0 to Names.Count-1 do
begin
if Supports(Designer.GetComponent(Names[I]), IMyInterface) then
Proc(Names[I]);
end;
finally
Names.Free;
end;
end;
In fact, this is very similar to what the default TInterfaceProperty.GetValues() implementation does:
procedure TInterfaceProperty.ReceiveComponentNames(const S: string);
var
Temp: TComponent;
Intf: IInterface;
begin
Temp := Designer.GetComponent(S);
if Assigned(FGetValuesStrProc) and
Assigned(Temp) and
Supports(TObject(Temp), GetTypeData(GetPropType)^.Guid, Intf) then
FGetValuesStrProc(S);
end;
procedure TInterfaceProperty.GetValues(Proc: TGetStrProc);
begin
FGetValuesStrProc := Proc;
try
Designer.GetComponentNames(GetTypeData(TypeInfo(TComponent)), ReceiveComponentNames);
finally
FGetValuesStrProc := nil;
end;
end;
The only difference is that TInterfaceProperty does not waste memory collecting the names into a temp TStringList. It filters them in real-time as they are being enumerated.
Remy's solution works perfectly for my needs.
Nevertheless I've "simplified" a bit the filtering procedure:
procedure TMyComponentProperty.ReceiveComponentNames(const S: string);
var
Temp: TComponent;
Intf: IInterface;
begin
if Assigned(FGetValuesStrProc) then
begin
Temp := Designer.GetComponent(S);
if Assigned(Temp) then
if Temp.GetInterface(IMyInterface, IntF) then
FGetValuesStrProc(S);
// May add other interfaces checks here
end;
end;

Can you remove a published property from a delphi component and not cause errors on the forms where the component is used?

I have a component that was developed by my company, and would like to remove one of the published properties that is no longer needed and clashes with the way the component works now.
Is there a way to remove the property and not cause property not found errors at runtime or design time when forms that use the component are loaded?
i.e. Is there a way to make Delphi silently drop a component property?
Yes. Just remove the property, then override DefineProperties and process it there. That will satisfy the streaming system by loading the value which you can just throw away. Be sure to do nothing when writing the stream.
Depending on the property, the easiest would be to leave the property, but mark it as deprecated and just have the Read/Write bits point to a field that's never used.
Alternatively, you can override DefineProperties and call Filer.DefineProperty('PropertyName', ReadProc); where PropertyName is the property you've removed, and ReadProc is a function that calls various TReader Read* functions. This has the advantage that the properties aren't in your interface anymore.
For example, say you've removed this property:
property Center: TPoint read FPoint write SetPoint;
Here's what you would add to your component:
TMyComponent = class...
private
procedure SkipReadPoint(Reader: TReader);
protected
procedure DefineProperties(Filer: TFiler); override;
end;
procedure TMyComponent.DefineProperties(Filer: TFiler);
begin
inherited;
Filer.DefineProperty('Center', SkipReadPoint, nil, False);
end;
procedure TMyComponent.SkipReadPoint(Reader: TReader);
begin
Reader.ReadListBegin;
Reader.ReadInteger;
Reader.ReadInteger;
Reader.ReadListEnd;
end;
Skipping a simple type like Boolean or Integer is easier, since you can just call ReadBoolean or ReadInteger without bothering with the ReadList functions.
In our case we had a lot of similar properties across a bunch of classes (for CLX compatibility) so we had global "dummy" functions like this:
procedure DummyReadBool(Self: Pointer; Reader: TReader);
begin
Reader.ReadBoolean;
end;
const
SkipReadBool: TMethod = (Code: #DummyReadBool; Data: nil);
and then the DefineProperty call looks like this:
Filer.DefineProperty('PropName', TReaderProc(SkipReadBool), nil, False);
That way each class doesn't have to have duplicate Skip* functions.
You can't "silently" remove it, if it's stored in the .DFM or referenced in code.
If it wasn't referenced in code, and it's stored in text .DFM files, the JVCL has a utility called DFM Cleaner that will go through the .DFMs and remove the properties for you. Then you can open the forms and recompile your applications safely. It's part of the JVCL and is placed in the $(JVCL)\DevTools folder during install.

RTTI on objects in Delphi

I'm trying to parse objects to XML in Delphi, so I read about calling the object's ClassInfo method to get its RTTI info.
The thing is, this apparently only works for TPersistent objects. Otherwise, I have to specifically add a compiler directive {$M+} to the source code for the compiler to generate RTTI info.
So I happily added the directive, only to find that, even if it did return something from the ClassInfo call (it used to return nil), now I cannot retrieve the class' properties, fields or methods from it. It's like it created the object empty.
Any idea what am I missing here? Thanks!
Did you put those properties and methods into the published section?
Besides that, 'classical' RTTI ($TYPEINFO ON) will only get you information on properties, not on methods. You need 'extended' RTTI ($METHODINFO ON) for those.
Good starting point for extended RTTI: David Glassborow on extended RTTI
(who would believe that just this minute I finished writing some code that uses extended RTTI and decided to browse the Stack Overflow a little:))
RTTI will only show you published properties,etc. - not just public ones.
Try your code with a TObject and see what happens - if that isn't working, post your code because not everyone is psychic.
Have you considered using the TXMLDocument component? It will look at your XML and then create a nice unit of Delphi classes that represents your XML file -- makes it really, really easy to read and write XML files.
As for the RttiType problem returning only nil, this probably occurs for one reason: in your test, you did not instantiate the class at any time. The compiler, because it never has a reference to this class (because it is not an instance at all), simply removes it from the information as a form of optimization. See the two examples below. The behavior is different when you have the class instantiated at some point in your code or not.
Suppose the following class:
type
TTest = class
public
procedure Test;
end;
and the following code below:
var
LContext: TRttiContext;
LType: TRttiType;
LTest: TTest;
begin
LContext := TRttiContext.Create;
for LType in LContext.GetTypes do
begin
if LType.IsInstance then
begin
WriteLn(LType.Name);
end;
end;
end;
so far, TTest class information is not available for use by RTTI. However, when we create at some point, within the application, then a reference is created for it within the compile, which makes this information available:
var
LContext: TRttiContext;
LType: TRttiType;
LTest: TTest;
begin
LTest := TTest.Create; //Here i´m using TTest.
//Could be in another part of the program
LContext := TRttiContext.Create;
for LType in LContext.GetTypes do
begin
if LType.IsInstance then
begin
WriteLn(LType.Name);
end;
end;
end;
At that point, if you use LContext.FindType ('TTest'), there will not be a nil return, because the compiler kept reference to the class. This explains the behavior you were having in your tests.

Resources