Dynamically change Property Type?

Suppose I have 2 WebContainers that can be used on WebPage1: DesktopWebContainer and MobileWebContainer. Depending on a check done in WebPage1.Shown, one or the other will be embedded within WebPage1. All controls in the 2 WebContainers are the same, with the same names. They’re just arranged differently on the screen for obvious reasons.

Now, I have WebPage1 Methods and Handlers that refer to controls in the embedded container. I want to use a Property for this: WebContainer1. That way I can have Methods and Handlers access the various controls in the embedded WebContainer like this: WebContainer1.TextField1.Text, WebContainer1.TextField2.Text, etc.

WebPage1.Shown will assign the appropriate WebContainer to Property WebContainer1.

My problem is that if Property WebContainer1 is of type WebContainer (the super of the 2 possible WebContainers that can be embedded), then the compiler won’t find WebContainer1.TextField1.Text or WebContainer1.TextField2.Text because they are not members of Class WebContainer. They are members of Subclasses DesktopWebContainer and MobileWebContainer.

Of course I can have 2 different WebPage1 Properties: DesktopWebContainer1 and MobileWebContainer1, and assign them the appropriate WebContainers in WebPage1.Shown. But then my Methods and Handlers will have to be completely re-written to access objects in the active WebContainer. That is, I would need a different dot prefix for one versus the other, to access, say, TextField1.Text: DesktopWebContainer1.TextField1.Text vs MobileWebContainer1.TextField1.Text.

Is there a way to programmatically change a Property’s Type? If so, I could solve this by keeping my single WebContainer1 property and change its Type to either DesktopWebContainer or MobileWebContainer, as needed.

Or perhaps there’s a more obvious way to solve this problem without having to rewrite my Methods and Handlers.

There are (at least) 2 ways to do this.

  1. Casting.
if WebContainer1 IsA DesktopWebContainer then
   DeskTopWebContainer(WebContainer1).Property1 = newValue
end
  1. Create an Interface that each container implements.

Thanks, as usual, Tim. From the little I can tell from your example and doing some reading on IsA and Casting, it looks like I would have to do that conversion on each of many lines of code where I currently have things like

If WebContainer1.TextField1.Text ... Then ... End If

I’m looking for a way to not have to change any of those lines of code, and have Property WebContainer1 refer to the proper WebContainer in virtue of changing its reference long before any such Methods are executed. Am I missing something very basic here regarding Casting when I say all this?

I just printed out some Xojo doc stuff on Interfaces and will read up on that too, responding to your suggestion.

Yes.

WebContainer1 does not have a TextField1. So WebContainer1.Property1 cannot compile. When you assign a DesktopWebContainer instance to it, the compiler still thinks it is a WebContainer1 – you defined it so. The cast DeskTopWebContainer(WebContainer1) tells the compiler: treat WebContainer1 as a DeskTopWebContainer – I, the developer, know that at runtime there will be a DeskTopWebContainer assigned.

So yes, you need to cast for each WebContainer version (DeskTopWebContainer, MobileWebContainer, etc.).

I’m going to have to find more examples of Casting to read online. This isn’t sinking in.

I tried many variations of the following, to no avail:

If Session.WebContainerState = 1 Then Dim c As New DesktopWebContainer Dim l As Integer = (Self.Width - c.Width)/2 Dim t As Integer = (Self.Height - c.Height)/2 c.LockHorizontal = True c.LockVertical = True c.ScrollbarsVisible = 1 c.EmbedWithin( Self, l, t, c.Width, c.Height ) WebContainer1 = c Else Dim c As New MobileWebContainer c.LockHorizontal = False c.LockVertical = False c.ScrollbarsVisible = 1 c.EmbedWithin( Self, 0, 0, c.Width, c.Height ) MobileWebContainer(WebContainer1) = c End If

Assuming it’s important, the Property WebContainer1 has Type DesktopWebContainer, so the first part of the If-Then here works fine. It’s obviously the last line (before End If) that’s the problem. I also tried this for the last line, among other things:

   DesktopWebContainer(WebContainer1) = c

You’ve got it reversed. Cast first, then set the values.

Dim c As New DesktopWebContainer
c = DesktopWebContainer(WebContainer1)
c.LockHorizontal = True
...

However, if you have a lot of code to change, an Interface may be the better approach. That’s why I mentioned both, not knowing the volume of code you’d be dealing with. For just a few lines here and there, cast. For a lot of code that needs to treat 2 different things as though they were the same, use an Interface.

Hmmm…in this code:

Dim c As New DesktopWebContainer c = DesktopWebContainer(WebContainer1) c.LockHorizontal = True ...

WebContainer1 is a Property. Is that what you intended here?

Yes. WebContainer1 is a property that can hold either a DesktopWebContainer or a MobilWebContainer. I assumed it was set previously and this was the code you were talking about that needed to access it. Or is this the code where you set it initially? Setting it is a simple assignment, no casting required. Using it later, you have to cast it to the appropriate type before you can use it.

Webcontainer1 = c

The trouble is that Property WebContainer1 is Type DesktopWebContainer. This was the original code I was trying to change to handle 2 different WebContainers:

If Session.WebContainerState = 1 Then Dim c As New DesktopWebContainer Dim l As Integer = (Self.Width - c.Width)/2 Dim t As Integer = (Self.Height - c.Height)/2 c.LockHorizontal = True c.LockVertical = True c.ScrollbarsVisible = 1 c.EmbedWithin( Self, l, t, c.Width, c.Height ) WebContainer1 = c Else Dim c As New MobileWebContainer c.LockHorizontal = False c.LockVertical = False c.ScrollbarsVisible = 1 c.EmbedWithin( Self, 0, 0, c.Width, c.Height ) WebContainer1 = c End If

Note I was indeed setting the Property WebContainer1 at the end of each section of the If-Then statement. The first part of the if-Then statement above has always worked fine, of course. The Else section is the problem. WebContainer = c bombs out during compilation as a mismatch, of course. So I was trying to figure out how to change (in the second If-Then section) Property WebContainer1 to Type MobileWebContainer, so I could then not have to change code in Methods and Handlers that prefix dot notation references to controls using “WebContainer.”.

If I try this

Else Dim c As New MobileWebContainer ... ... MobileWebContainer(WebContainer1) = c End If

it bombs out with a message that “this is a module not a variable…”

If I try

Else Dim c As New MobileWebContainer ... ... c.EmbedWithin( Self, 0, 0, c.Width, c.Height ) WebContainer1 = MobileWebContainer(c) End If

I get the mismatch error again.

And if I try

Else Dim c As New MobileWebContainer ... ... c.EmbedWithin( Self, 0, 0, c.Width, c.Height ) WebContainer1 = DesktopWebContainer(c) End If

it compiles, but when the Else section of the If-Then statement is executed, I get an Illegal Cast Exception runtime error.

I really feel like I’m so close to solving this when I finally fully understand the proper use of Casting here.

Sometimes a picture is worth a thousand words, so have a look at this demo project

WebContainer1 can’t be a DesktopWebContainer, because that is incompatible with a MobileWebContainer. WebContainer1 must be a super of both, such as WebContainer.

As has been pointed out the issue is that WebContainer doesn’t have those parts your existing code interacts with. The two subclasses happen to have those same parts, but those parts are only exposed/accessible/known-about when the variable is typed as one of those subclasses.

What you want is a property, WebContainer1, that can 1) store either of the subclasses and 2) exposes those common parts.

For WebContainer1 to store either subclass it needs to be a type that both subclasses share, that is, a super class or an interface. The current super class, WebContainer, doesn’t work because it doesn’t have those parts you want, so you will need to make a new super class or an interface to satisfy these requirements.

An issue with both approaches, super class or interface, is that the shared parts you want to expose are already named. Duplicating those names in a common type will clash. Example, here’s the current setup

Class DesktopWebContainer inherits WebContainer Control TextField1 As TextField End Class

To make an interface that will read the same in your existing code you’ll want to name it’s parts the same

Interface MyInterface Function TextField1() As TextField End Interface
but then applying this to your class causes problems

Class DesktopWebContainer inherits WebContainer implements MyInterface Control TextField1 As TextField Function TextField1() As TextField //duplicate method return TextField1 //more than 1 item with name End Function End Class

To fix this I’d rename the controls and make them private. Now the interface works.

Class DesktopWebContainer inherits WebContainer implements MyInterface Private Control innerTextField1 As TextField Function TextField1() As TextField return innerTextField1 End Function End Class

In summary, create an interface exposing all those parts you need. Rename the corresponding controls on both subclasses then add the interface to them and fill it in, just returning the renamed controls. Lastly, type WebContainer1 as the interface and you should be good to go.

To do this via a superclass may be a bit easier, or not. Create a class with properties for the common parts, named what they should be.

Class MyContainerSuper inherits WebContainer Property TextField1 As TextField End Class

In the subclasses again rename all the controls then set their super to that class and initialize those properties in Open

Class DesktopWebContainer inherits MyContainerSuper Private Control innerTextField1 As TextField Event Open() TextField1 = innerTextField1 End Event End Class

And finally type WebContainer1 as MyContainerSuper.

That super class is just demonstrating the idea but a problem with it is the properties are publicly assignable. Instead make them computed properties with only a getter, or just a method.

Class MyContainerSuper inherits WebContainer Private Property mTextField1 As TextField Function TextField1() As TextField return mTextField1 End Function End Class

This seems like a lot of work and it is. The situation arises because controls on a form are not inheritable and the controls are already named. In a loosely typed interpreted language like javascript you can do what you want to do, but a strongly typed compiled language like Xojo requires this extra work to map things out.

But I wonder if you really need 2 subclasses with the same controls? Could you have a single subclass where it’s behavior is controlled by a controller. Or just store a flag and code it different where needed.

Class MyDualContainer inherits WebContainer Control TextField1 As TextField Private Property controller As MyController Sub Constructor(kind As integer) if kind = 1 then controller = DesktopController elseif kind = 2 then controller = MobileController end End Sub End Class

Wayne,

I looked at your demo project. Thanks. It’s a different approach that I’ve been experimenting with. I need to digest it more. But it doesn’t seem to address the problem I have of existing Methods/Handlers needing to access controls on each WebContainer, unless I rewrite the code in those Methods/Handler to take into account which container is embedded, line by line.

Tim,

Yep. I would need a super of both WebContainers, but it would need to have all them controls of each WebContainer for my Methods/Handlers to work. I tried to have one of the two WebContainer be a subclass of the other, but that had the problems outlined by Will.

Will,

In your last example, though I’m not sure what you mean by a Controller there, reading it over gave me an idea. Perhaps it’s what you mean by a Controller. I can just have just one WebContainer instead of two, and have code move the controls of the second one around on the screen when it’s called to be embedded. Maybe that’s what you mean by using a Controller.

Otherwise I can keep the two WebContainers and just rewrite the Methods/Handlers so that they fork depending on which WebContainer is embedded. This last thing is what I was trying to avoid when I started this conversation, but it might be easier than writing code that moves all the controls around on the screen when the second WebContainer is called to be embedded, because rewriting those Methods/Handlers would mainly just involve putting it a big If/Then in each, and using Find/Replace in the second fork of each If/Then statement.

This is something that ideally would be addressed earlier in the design phase. Now you’re refactoring code, which is always more difficult. Added to that, your code breaks encapsulation, which makes it even harder.

Perhaps the best approach would be to rename the controls and add methods that simply return a reference to the control. Eg., rename TextField1 to TextField1a and add a method named TextField1:

return TextField1a 

You will have to – there is no other way. Even if you use introspection, you will need a Select Case or If/Else somewhere:

[code]Class MyWebContainer Inherits WebContainer

Function TextField1() As WebTextField
Dim ti As Introspection.TypeInfo = Introspection.GetType(Self)
Dim pis() As Introspection.PropertyInfo = ti.GetProperties()
For i As Integer = 0 To pis.Ubound
Dim pi As Introspection.PropertyInfo = pis(i)
Select Case pi.Name
Case “TextField1”
Return pi.Value(Self)
Case …

End
End
End

End Class

Class DesktopWebContainer Inherits MyWebContainer
Private TextField1 As WebTextField
End

Class Mobile WebContainer Inherits MyWebContainer
Private TextField1 As WebTextField
End[/code]

Right now, I’m finding limiting this to just a single WebContainer is working out. I’m using a Method that, when called for a mobile platform, reshapes the DesktopWebContainer and moves controls to better places for a mobile device. This way I don’t have to touch my existing Methods/Handlers that access controls on the WebContainer. I have a Mobile WebContainer in the IDE just for reference in figuring where to place different controls. It is never called.

Thanks so much for all the patience extended here to an obvious newbie to Xojo, and OOP in general. My extensive experience is with Filemaker. Xojo is my first OOP language.

What it sounds like you need is an interface

OK so what IS an interface ?
Its a way to say “this thing promises to implement this bunch of methods”
So in this case your DesktopWebontainer and MobileWbeContainer can be used interchangeably

So lets give an example

Dogs can make noise
And cars can make noise
And clouds dont make noise (for the sake of argument they dont)

But its unlikely you would have Clouds & Cars & Dog inherit from some common parent class

So if you want to generically have a bunch of objects (maybe an array of objects) how can you make sure you only try to make the ones that can make noise make their noise

So lets define an interface - NoiseMaker

    Interface NoiseMaker
              Sub MakeNoise()
   end interface

You’ll notice there’s NO code in there - thats deliberate
OK so an interface is a promise by any class that implements the interface to have ALL the methods of the interface exactly the same way

So if we have Cars we can do

   Class Car
     implements  NoiseMaker
        Sub MakeNoise()
                msgbox "beep beep"
        end sub
   End Class

And we could do similarly for Dogs

   Class Dog
     implements  NoiseMaker
        Sub MakeNoise()
                msgbox "bark bark"
        end sub
    End Class

But we would not have clouds implement it as they are not noise makers (for this example)

   Class Cloud

Some things you should notice

  1. each class that says “implements NoiseMaker” has the method defined by NoiseMaker - MakeNoise
  2. they might have the same method name BUT they have different implementations - dogs bark, cars beep

Ok so now we have all out classes set up - IF we tried to do something like

     dim listOfObjects() as Object

     ListOfObjects.append new Car
     ListOfObjects.append new Cloud
     ListOfObjects.append new Dog

     for each o as Object in ListOfObjects
           o.MakeNoise
    next

you would find this would not work. Why not ?

  1. “Object” doesnt make noise - it doesnt implement our interface
  2. even if that code kind of was OK - Clouds definitely do not make noise

So how to fix this ?
Well we can ask “if this thing a noise maker” and if it is then ask it to make its noise
Like so

     dim listOfObjects() as Object

     ListOfObjects.append new Car
     ListOfObjects.append new Cloud
     ListOfObjects.append new Dog

     for each o as Object in ListOfObjects
           if o IsA NoiseMaker then
                 dim tmp as NoiseMaker = NoiseMaker( o )
                 tmp.MakeNoise
          end if
    next
  1. note that we ASKED if the thing in question was a NoiseMaker
  2. that when we found one that was we wrote code to treat that object AS a noise maker and asked it to make its noise

Now this is a tad long winded BUT in you case you might have an interface for “GenericContainer” and then DesktopWebContainer & MobileWebContainer both implement that and you can then deal with either one equally as a GenericContainer - or whatever you name your intrface

This is a very handy way to abstract things

What I was picturing originally is that the Desktop and Mobile containers have the same or mostly the same UI but have different internal code. Their code wouldn’t be the same because that’s duplicated code. So there must be custom code in each class that you’re trying to preserve while somehow getting them used polymorphically in other code.

Reading a little more deeply I get the sense there’s no actual code in the containers, it’s all done through adding a different set of handlers. Like you just wanted two layouts so you made two containers, laid them out in the designer, and now you’re trying to use them interchangeably.

I think this is a very good solution, assuming it really is only about layout. It’d make an interesting feature request to allow a single Container to have multiple layouts designable in the IDE and a mechanism to choose which one an instance will use. Instead you have to position the secondary layout yourself, exactly that method you have.

The main downside is any redesign requires manually updating the positioning method. This could be automated: export a container as xml and write an app to process that and generate positioning code which is pasted back in. But then implementing an interface might be less work and allows for hassle free layout changes.

Here’s a sneaky little cheat to make implementing an interface easier. The problem is you have to rename all those controls and then implement the interface methods returning those very renamed controls. All because of a naming conflict. But the conflict happens because when you put ‘TextField1 As WebTextField’ in a container it’s creating the method ‘Function TextField1() As WebTextField’ behind the scenes. The trick is to create and add the interface but don’t implement any methods for it, let those behind the scenes methods satisfy the interface. This behavior is undocumented afaik and could/will change in the future, just a funky little thing I noticed.

[code]Interface MyInterface
Function TextField1() As WebTextField
Function Slider1() As WebSlider
End Interface

Class DesktopContainer Inherits WebContainer Implements MyInterface
Control TextField1 As WebTextField
Control Slider1 As WebSlider
End Class

//…

Property WebContainer1 As MyInterface

//…

WebContainer1 = aDesktopInstance
WebContainer1.TextField1.Text = “boo”
WebContainer1.Slider1.Value = 50[/code]

That’s when I was thinking each container had it’s own code. That code would be moved into controller classes so you could have 1 container that just calls through to it’s controller.

Interface MyInterface Function TextField1() As WebTextField Function Slider1() As WebSlider End Interface

It looks to me like I’m supposed to populate this Interface with Controls. I’m not clear on how to do that. I assume I need to put that code somewhere in the Navigator.