I am trying to re-create an application that I did in 10.1 GUI where I would populate a frame at runtime with labels and textboxes, using create statements, based on records in a temp-table.
I know that you can add controls at runtime a couple of ways:
define private variable TextBox1 as System.Windows.Forms.TextBox no-undo.
TextBox1 = new TextBox().
this-object:Controls:Add(TextBox1).
or
this-object:Controls:Add(new System.Windows.Forms.TextBox()).
Since I don't know how many controls I will have to add I can't really define the TextBox by name. The second method doesn't allow for naming it at instantiation (if I understand this correctly) so setting the properties becomes pretty difficult. I am thinking that the only way would be to find the controls individually by index in the Controls collection and assign the properties there (e.g. Name).
I admit it's been a really long time since I've done much with .Net controls so I'm sort of stuck with the best solution. Any ideas would be appreciated.
You can use a temp-table. It is possible to define a field of type Progress.Lang.Object in a temp-table. You can assign the object reference to that field and CAST the value to whatever you like when you need to manipulate the object.
Looks like you already have a preference to access them by name.
I'd go with the first approach, set the Name property and add it to the Controls collection.
Then you can access them later using THIS-OBJECT:Controls .
Thanks to both of you. I just need to try and try a couple of things out to get it straight in my head. Gave me great ideas though.
Okay, I'm sort of playing around with this. Here's what I did to test out what's in my head.
I have a temp-table definition:
define temp-table tt-controls
field fieldName as character
field formControl as Progress.Lang.Object
.
for the sake of simplicity, I have a single method where I create and use the record...
create tt-controls.
assign
tt-controls.fieldName = "myTextBox"
tt-controls.formControl = new Progress.Lang.Object().
...since I already have the buffer...
this-object:Controls:Add(cast(tt-controls.formControl, System.Windows.Forms.TextBox)).
When I run the app I get:
Invalid cast from Progress.Lang.Object to System.Windows.Forms.TextBox....
Am I missing something obvious here?
the tt record can only store a progress.lang.object, so you need to create the control first, then cast it to an object before storing it in the tt record.
have a look at the dynamic-new command to create the appropriate control (text box / label / etc).
Once you created the control, cast, and store. You probably also should store the object type in the tt record so that you can recast it back to the original object type.
I don't have a machine handy so not tested but you would do it the other way around:
define temp-table tt-controls
field fieldName as character
field formControl as Progress.Lang.Object.
create tt-controls.
assign
tt-controls.fieldName = "myTextBox"
tt-controls.formControl = this-object:Controls:Add(System.Windows.Forms.TextBox).
So you are assigning the object handle to the formControl field.
You need to cast it for subsequent manipulation:
CAST(tt-controls.formControl,System.Windows.Forms.TextBox):Text.
or even better (untested):
DYNAMIC-CAST(tt-controls.formControl,tt-Controls.objectType):Text.
where you store the Object Type in the tt as well.
Oops of course you need to NEW the TextBox first but you get the idea.
Message was edited by:
Peter van Dam
There is an example of this kind of usage in my old Collection Classes (http://www.oehive.org/CollectionClasses) ... ah 10.1A so long ago, but Peter is bang on here. The basic idea is that a Progress.Lang.Object is the ultimate super, so it isn't much, really. You can't turn it into something it isn't with a cast. But, since it is a super, you can store anything in it and cast it back into its original type.
Possibly, you could use something other than Progress.Lang.Object, as long as it was super to anything you were going to store there.
have a look at the dynamic-new command to create the
appropriate control (text box / label / etc).
DYNAMIC-NEW won't work with .NET Classes. It just woks with ABL Classes (or ABL Classes inherriting form .NET Classes).
oh : I was reading the manual for dynamic-new *note the sentence "If expression specifies a .NET object" *
[ , parameter ][ NO-ERROR ]
..snip..
..snip..
I actually just realized this myself.
The idea of this application is to fill a dataset on the server with records that will coincide with parameters for a report. Ship it down to the client, then create the appropriate text and label fields for the UI.
I'm afraid I'm still sort of confused on what I need here.
Well, then it must be a bug:
System.TypeLaodException: The type System.Windows.Forms.Control in Assembly Progress.NetUI, Version=1.03229.34577, Culture=neutral, PublicKeyToken=null could not be loaded.
When I try your code, (or any code) I get
wonder why there is a difference ...
null
edited for neatness ...
Dynamic new .NET style:
define variable t as System.Type no-undo.
/* The type helper avoids having to spell out the fully qualified type name */
t = Progress.Util.TypeHelper:GetType("System.Windows.Forms.Control").
define variable obj as Progress.Lang.Object no-undo.
/* all objects inherit from Progress.Lang.Object, but to create a .NET object we can use the .NET reflection API */
obj = System.Activator:CreateInstance(t).
/* show the class name */
MESSAGE obj:ToString()
VIEW-AS ALERT-BOX.
Added some comments to the code
Well .NET reflection is a powerful utility.
But why does the DYNAMIC-NEW docu mention .NET classes, when there's actually a problem with it? A bug?
thanks for that, Matthew. However, the question about the documentation remains: is it wrong to mention .net objects in the DYNAMIC-NEW statement ?
John,
I have attached a basic code sample for you. The most important lines are:
FOR EACH tt-control:
i# = i# + 1.
/* Create the controls and store the object reference */
CASE tt-control.fieldType:
WHEN "TextBox" THEN
tt-control.formControl = NEW System.Windows.Forms.TextBox().
WHEN "ComboBox" THEN
tt-control.formControl = NEW System.Windows.Forms.ComboBox().
END CASE.
/* Now manipulate the objects. Need to CAST */
THIS-OBJECT:Controls:Add(CAST(tt-control.formControl,System.Windows.Forms.Control)).
CAST(tt-control.formControl,System.Windows.Forms.Control):top = i# * 20.
END.
Notes:
- You need the CASE statement because apparently you cannot DYNAMIC-NEW .Net controls
- You need to cast, but you can cast to the least common denominator so you can still keep things generic.
Hope this gets you started.
[View:~/cfs-file.ashx/__key/communityserver-discussions-components-files/19/dynform.cls:550:0]
I'd new to a variable of type System.Windows.Forms.Control
It's very likely that Top is not the only property you are manipulating (I'd bet for Left and Width as well). This reduces the usage of CAST to zero in this sample. CAST is a function call, so there's very likely a minimal runtime overhead - beside the better readability of the source code.
Why not new directly to tt-control.formControl? Wouldn't one have issues on the second and succeeding passes through the loop unless one cleaned up oControl before the next NEW?
Why not new directly to tt-control.formControl?
Because it's not strong typed? To access the basic properties like Top, Width, Left, Text etc. you need a reference that's at least typed to System.Windows.Forms.Control.
Wouldn't one have issues on the second and
succeeding passes through the loop unless one
cleaned up oControl before the next NEW?
Which issues are you expecting?
The temp-table record and oControl both hold a reference to the same instance (oControl just until the next NEW). When you leave the method, oControl is out of scope, so that won't count for the refenrece counter anymore and the GC is fine.
Because it's not strong typed?
Well, but if you are newing everything to oControl which is of type System.Windows.Forms.Control, then why not define tt-control.formControl as being of type System.Windows.Forms.Control? The only reason to make it of type Progress.Lang.Object is if one is going to be storing arbitrary objects there.
As for issues, perhaps it is just a matter of style, but I dislike the idea of NEWing into a variable that already contains another object, even if it works without any issues.
Are you going to get a new reference on the second NEW?
Well, but if you are newing everything to oControl
which is of type System.Windows.Forms.Control, then
why not define tt-control.formControl as being of
type System.Windows.Forms.Control? The only reason
to make it of type Progress.Lang.Object is if one is
going to be storing arbitrary objects there.
Did you try that? It won't compile. Progress.Lang.Object is the only object datatype supported as a temp-table column.
Well, this really did answer my original question about how to get started. I really appreciate everyone's input. These discussions usually help find better and/or faster and cleaner ways of doing things for me, so don't stop on my account.
Thanks again.
Progress.Lang.Object is the only object datatype supported as a temp-table column.
Well, that's dumb!
I realize that it has been a while since you all helped me but I really just got back to this project. I have another question.
I have filled my dataset with my report parameters. With each parameter record I have two fields of type Progress.Lang.Object (one for a label one for a textbox). On the client, I do:
for each tt-param
by tt-param.param-order:
...
tt-param.textbox-cntrl = new System.Windows.Forms.TextBox().
this-object:Controls:Add(cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox)).
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Top = ...
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Left = ...
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Text = tt-param.pinitial.
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Name = tt-param.Field-name.
... etc
end.
This works great. I get everything on the screen as expected. Later, however, when I try to read the updated values from the field I still get the initial value.
For example:
Message cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Text.
Finally, my question. The way I'm doing this, am I actually creating a separate instance of the TextBox object? The confusing part to me is that I am using the object reference in the temp-table to set the initial properties, why would it not be the same reference later? Should I need to capture the hwnd at creation time and use it later instead?
Perhaps, I am overlooking the obvious but any thoughts would be appreciated.
Message was edited by:
John Gawlik
Okay, I just tried something that I hadn't thought of at first. When I fill the dataset on the server I do:
dataset dsReportParams:fill().
for each tt-param:
tt-param.textbox-cntrl = new System.Windows.Forms.TextBox().
end.
Then I just set the rest of the properties when I get to the form level on the client. Finally, I can then use the reference in the temp-table to access the properties.
I just did it a little out of order.
Thank you anyway.
This does not make sense to me at all. Are you saying you are creating the TextBox controls on the server? As in AppServer?
That probably is not the case since it would never work.
So can you explain what you are doing. It should work fine when you create the TextBox controls on the client as described above. You are not creating separate instances of the TextBox object.
Message was edited by:
Peter van Dam
Bummer, I think you're right. What I forgot was that I was never actually calling my AppServer connection because I don't have a server with 10.2 yet.
So here is what I did:
I have the client app, which consists of a .Net form and a service adapter ojbect class. For most of my code I just call .p files (on the AppServer) as my interfaces, which in turn call my entity objects (.cls) which fill datasets and the like and ship the whole thing back. Normally, this works well but since I don't have 10.2 on the server I just left my interface and entity on the local machine. I think I just stepped on my own foot.
As far as what I got to work (again, locally):
1. I use my form class object to call my service adapter object (SAReport) to get my parameter records from the db. These records are just a basic description of the individual parameters needed to run a report.
2. In my reporting entity I fill my dataset. Then I simply 'for each' through the temp-table and:
for each tt-param:
assign
tt-param.textbox-cntrl = new System.Windows.Forms.TextBox()
tt-param.label-cntrl = new System.Windows.Forms.Label().
end.
...then I just end the method and return the full dataset back to the client.
3. Back in the client form I have a method to build my dynamic interface. I 'for each' though the tt-param temp-table again and for each new record I add the controls to the form without having to "NEW" the object.
this-object:Controls:Add(cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox)).
At this point, I can set my properties too...
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Name = tt-param.Field-name.
cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Text = tt-param.initial.
This gets everything up on the screen with all of the expected settings.
The when I am ready to run my report (which I really do on the appserver) I can use the temp-table:
for each tt-param:
runStatement = runStatement + "input " + cast(tt-param.textbox-cntrl, System.Windows.Forms.TextBox):Text.
end.
Now obviously I am creating a string that I use on the AppServer. That part doesn't really matter here but that's what I am doing in a nutshell.
So I really don't know if this will ultimately work on an AppServer with 10.2 so I may still actually be stuck. I thought I was on to something though.
You might need two temp-tables (one for the AppServer, one for the client).
A temp-table (even when it's empty) with an Progress.Lang.Object column will never make the jump from the AppServer to the client. Any attemp to do so will result in a runtime error in the AppServer logfile...
You might need two temp-tables (one for the
AppServer, one for the client).
A temp-table (even when it's empty) with an
Progress.Lang.Object column will never make the jump
from the AppServer to the client. Any attemp to do so
will result in a runtime error in the AppServer
logfile...
In addition to that, a reference to a TexBox is really a pointer to a piece of memory, so it does not make sense accross the AppServer boundaray. If this were the WIDGET-HANDLE of a normal GUI control your approach would not work either for that reason.
So you must NEW the controls on your client and extract the TEXT property to a CHARACTER field before sending the request to the server. And since you cannot pass object references to the server, you cannot use the same temp-table for this. Hence you will need two temp-tables.
The good news is that this will work with your 10.1C AppServer, since you are not using anything from 10.2A there (certainly no .NET).
In addition to that, a reference to a TexBox is
really a pointer to a piece of memory, so it does not
make sense accross the AppServer boundaray. If this
were the WIDGET-HANDLE of a normal GUI control your
approach would not work either for that reason.
But a WIDGET-HANDLE column does not prevent a temp-table from being passed from the AppServer to the client. Just another case where classes are treated worse than widgets in the ABL...