Adding controls at runtime

Posted by jmgawlik on 16-Jan-2009 11:49

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.

All Replies

Posted by rbf on 16-Jan-2009 12:06

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.

Posted by Admin on 16-Jan-2009 12:20

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 .

Posted by jmgawlik on 16-Jan-2009 12:38

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.

Posted by jmgawlik on 16-Jan-2009 13:05

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?

Posted by jmls on 16-Jan-2009 13:11

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.

Posted by rbf on 16-Jan-2009 13:12

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

Posted by Thomas Mercer-Hursh on 16-Jan-2009 13:24

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.

Posted by Admin on 16-Jan-2009 13:46

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).

Posted by jmls on 16-Jan-2009 13:51

oh : I was reading the manual for dynamic-new *note the sentence "If expression specifies a .NET object" *

[ , parameter ][ NO-ERROR ]

..snip..

..snip..

Posted by jmgawlik on 16-Jan-2009 14:00

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.

Posted by Admin on 16-Jan-2009 14:09

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.

Posted by jmls on 16-Jan-2009 14:24

When I try your code, (or any code) I get

wonder why there is a difference ...

null

edited for neatness ...

Posted by Matt Baker on 16-Jan-2009 14:30

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

Posted by Admin on 16-Jan-2009 14:33

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?

Posted by jmls on 16-Jan-2009 14:34

thanks for that, Matthew. However, the question about the documentation remains: is it wrong to mention .net objects in the DYNAMIC-NEW statement ?

Posted by rbf on 19-Jan-2009 03:55

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]

Posted by Admin on 19-Jan-2009 05:49

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.

Posted by Thomas Mercer-Hursh on 19-Jan-2009 10:42

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?

Posted by Admin on 19-Jan-2009 10:50

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.

Posted by Thomas Mercer-Hursh on 19-Jan-2009 11:06

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?

Posted by Admin on 19-Jan-2009 11:26

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.

Posted by jmgawlik on 19-Jan-2009 11:37

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.

Posted by Thomas Mercer-Hursh on 19-Jan-2009 11:43

Progress.Lang.Object is the only object datatype supported as a temp-table column.

Well, that's dumb!

Posted by Admin on 19-Jan-2009 11:54

Posted by jmgawlik on 28-Jan-2009 10:31

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

Posted by jmgawlik on 28-Jan-2009 12:58

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.

Posted by rbf on 29-Jan-2009 06:15

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

Posted by jmgawlik on 29-Jan-2009 11:33

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.

Posted by Admin on 29-Jan-2009 11:50

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...

Posted by rbf on 29-Jan-2009 15:19

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).

Posted by Admin on 29-Jan-2009 15:25

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...

This thread is closed