Referencing controls, variables etc. in a form known only by its name as a string
I will have a large number of forms in my application. I want to store their names in a database, and when I want to open one of them, I simply want to use the name of the form. It's not straightforward, but I've come across solutions like this and this and this, where you use this sort of construction:
Code:
Dim frmNewForm_Type As Type = Type.GetType("your_assembly.type_name")
frmNewForm = CType(Activator.CreateInstance(frmNewForm_Type), Form)
.
Although this works as far as simply opening a form, I want to open the form with parameters, such as an ID number for a particular record. However, it seems you cannot continue the above with because the compiler rightly complains that intID isn't a know property of frmNewForm.
In short, I want to create a procedure something like
Code:
Public Sub OpenFormInstance(strFormName As String, intID As Integer)
, calling it like this:
Code:
OpenFormInstance("KittenForm", DataGridView.CurrentRow.Cells(0).Value)
that will open a form and load up the data for a particular record.
I have created a function with a huge "case" statement containing a large number of clauses like
Code:
Case "KittenForm"
Dim FormInstance As New KittenForm
FormInstance.intID = intID
FormInstance.MdiParent = MDI
FormInstance.Show()
The above case statement works, but with a huge number of form, is obviously a maintenance nightmare, and also offends me.
Any suggestions for a better solution?
Re: Referencing controls, variables etc. in a form known only by its name as a string
Normally, you'd use the constructor, but that seems excluded in this case. Would it be acceptable to have one more line? For example, you could have a method called something like Initialize, which took whatever parameters you needed, and simply call that immediately after you create the form.
Re: Referencing controls, variables etc. in a form known only by its name as a string
It is wrong to say you can't call a constructor with parameters via Activator.CreateInstance(). There are a lot of overloads, but for example this overload takes a list of arguments and will try to find a constructor that matches.
The trick is, if all of your forms have different constructors, and you need to get different things from the database based on the constructor, you're going to have to write some extra code per-type to figure out what to do. And if you go that far, you may as well just start manually calling the constructor.
Setting properties in a general manner is harder. Don't do it. For example, look at your KittenForm. It needs an "International National Tree" ID in order to be properly constructed (Or at least, that's what I think 'int' stands for, clearly if it were just an "ID" you'd have called it that, because there's no other sensible type for an ID.) What you need is something that us architecture types call a "Factory Method". Its job is to construct other types. We have a lot of time-tested techniques for handling how we deal with these, and our toolkit got a lot more broad in C# 7. Unfortunately, VB developers have historically attacked Microsoft when new features arrive, so the tools most relevant to your problem aren't even on the VB roadmap. Womp womp.
So there's two scenarios.
One is "I need to construct forms I didn't even write". That involves a lot more thinking and I'm not going to touch it unless you say that's precisely what you need.
The easier case is, "I need to construct forms that I wrote and are in assemblies I'm directly referencing." We can work with this case. If you're used to working with Interfaces, you can do a lot more with it. If you're not, you have to work with slightly more primitive tools. I'm going to stick to "not comfortable with Interfaces" because it's less explanation and easier to describe if I already know some specific things.
What you do in this case is go ahead and make a type for the things that the form needs. So you might make this:
Code:
Public Class KittenFormParameters
Public ReadOnly Property Id As Integer
Public Sub New(ByVal id As Integer)
Me.Id = id
End Sub
End Class
Now we need a type that can take a string name or some other token, and associate that name with a function that can create the appropriate type based on that input. Clear case for a Dictionary, but we need a way to say "a function that can create a Form given something that represents its parameters". That's a Dictionary(Of String, Func(Of Object, Form)) when we're using primitive tools. That is, "A class that associates a String with a Function that takes one Object parameter and returns a Form."
If we give a function like that the KittenFormParameters, it can cast to the right type, create our KittenForm, pass it the right parameters, then return it. Or: if we make KittenForm have this:
Code:
Public Sub New(ByVal params As KittenFormParameters)
We could do even less work. Let's do the "more work" way for illustration, as it's more flexible.
So we could end up with a class like this:
Code:
Public Class FormFactory
Private _creatorLookup As New Dictionary(Of String, Object)()
Public Sub New()
_creatorLookup("KittenForm") = AddressOf CreateKittenForm
End Sub
Public Function GetForm(ByVal formToken As String, ByVal params As Object) As Form
Dim creator As Func(Of String, Object)
If _creatorLookup.TryGetValue(formToken, creator) Then
Return creator(params)
Else
' Error: the string you got isn't a known form.
End If
End Function
Private Function CreateKittenForm(ByVal params As Object) As Form
Dim castParams = DirectCast(params, KittenFormParameters)
Return New KittenForm(castParams)
End Sub
End Class
Is it tedious if you have "many forms"? Yes. That's why interfaces are a good tool. We could also write this class, if you wrote the constructor (Sub New) that I discussed above:
Code:
Public Class FormFactory
Private _formTypeLookup As New Dictionary(Of String, Type)()
Public Sub New()
_formTypeLookup("KittenForm") = GetType(KittenForm)
End Sub
Public Function GetForm(ByVal formToken As String, ByVal params As Object) As Form
Dim formType As Func(Of String, Type)
If _formTypeLookup.TryGetValue(formToken, formType) Then
Dim result = Activator.CreateInstance(formType, params)
Return CType(result, Form)
Else
' Error: the string you got isn't a known form.
End If
End Function
End Class
There are ways to make that fancier and still do specific things for each form, but they tend to be things that only make sense in the context of their specific problem. The gist is always:
- If you can pick some "common ancestor" for the kinds of Forms you want to work with, that lets you treat many different Forms as "the same type".
- Interfaces can be "a common ancestor" and are most convenient because any class can implement multiples.
- Form is always a common ancestor for Forms.
- You can cast from "a common ancestor" to any type you want, but should only do so within the context of a method that is specific for that form.
The last point there is one I could bicker with people about, but I think when we work in code like this everything works better if the purpose of our code is to get more and more specific until there's no reason to expect a cast to fail as opposed to "a giant Select..Case". Pulling that off takes very careful thought.
That's why it's generally "better" to not write code in such a generic manner. It's much harder to deal with this problem.