7

Ok, I've thumped on this idea all day now, and I have reached the part where I admit I just flat out don't know. It's possible that what I'm doing is just stupid and there is a better way, but this is where my thinking has brought me.

I am attempting to use a generic method to load forms in WinForms:

protected void LoadForm<T>(ref T formToShow, bool autoLoaded) where T : FormWithWorker, new()
{
    // Do some stuff
}

The forms are loaded by a ToolStripMenuItem (either through the selection of the item or using the Open Windows menu item). They are lazy-loaded, so there are fields for the forms within the MDI parent, but they are null until they are needed. I have a common method used for ToolStripMenuItem_Click that handles all of the menu item clicks. The method has no real way of knowing which form is being called for except that the name of the ToolStripMenuItem matches a pattern chosen for the form class names they correspond to. So, using the name of the ToolStripMenuItem, I can divine the name of the type of form being requested and the name of the private field allocated to store the reference for that form.

Using that, I can either use a growing/contracting switch statement with hard-coded types and string matches to call method with the specific type set (undesirable), or I can use Reflection to get the field and create the instance of the type. The problem to me is, System.Activator.CreateInstance provides an ObjectHandler that can't be cast to the types that I need. Here is a snippet of what I have so far:

string formName = "_form" + ((ToolStripMenuItem)sender).Name.Replace("ToolStripMenuItem", "");
string formType = formName.Substring(1);

FieldInfo fi = this.GetType().GetField(formName, BindingFlags.NonPublic | BindingFlags.Instance);

FormWithWorker formToLoad = (FormWithWorker)fi.GetValue(this);
if (formToLoad == null)
{
    formToLoad = (????)System.Activator.CreateInstance("MyAssemblyName", formType);
}

this.LoadForm(ref formToLoad, false);
fi.SetValue(this, formToLoad);

I know the string name of the type that goes in for (????) but at compile-time I do not know the type because it changes. I have tried a bunch of ways to get this cast/instantiation to work, but none have been successful. I would very much like to know if it's possible to perform such a cast knowing the type only as a string. I tried using Type.GetType(string, string) to perform the cast, but the compiler didn't like it. If someone has a different idea on how to load the forms dynamically because I'm just doing it stupidly, please let me know about it.

Joel Etherton
  • 37,325
  • 10
  • 89
  • 104

4 Answers4

13

This problem is usually resolved by casting to a common base class or interface of all potential types.

In C# 4, you can also assign it to a dynamic variable to hold the return value and call arbitrary methods on it. The methods will be late bound. However, I prefer to stick to the former solution whenever possible.

Mehrdad Afshari
  • 414,610
  • 91
  • 852
  • 789
  • Up vote. I've done something very similar using an interface and it works nicely. –  Jan 28 '11 at 19:19
  • I tried this method out, but the problem I encountered was that the dynamic type it ended up assigning was the base class and not the actual class, so when calling the SetValue method, it threw a type-casting exception. – Joel Etherton Feb 01 '11 at 11:41
  • @Joel as the other answer points out, you should use the overload that returns the object itself. Apparently, the overload you are using is useful for activating .NET remoting objects. – Mehrdad Afshari Feb 01 '11 at 12:01
  • I came in this morning with a fresh mind on the topic, and the reason I couldn't get @Andras Vass's answer to work was that I was calling with the wrong namespace/type in CreateInstance. Once I realized that mistake, his idea worked splendidly. I did +1 your answer, though, because it was something that made a lot of sense to me. – Joel Etherton Feb 01 '11 at 12:10
5

You'd be better off with the other overload that takes a Type and using e.g. Type.GetType(string).

FormWithWorker formToLoad = (FormWithWorker)fi.GetValue(this);
if (formToLoad == null)
{
    formToLoad =
      (FormWithWorker)System.Activator.CreateInstance(Type.GetType("MyNamespace.MyFormType"));
}
Andras Vass
  • 11,478
  • 1
  • 37
  • 49
  • I remember at one point this post mentioned specifically the use of the `.UnWrap()` method on `System.Activator.CreateInstance`. It doesn't say it now, but it should because the solution to my problem was to declare the private field with the base class's type and then call `CreateInstance("namespace","type").Unwrap()`. This created the proper type and allowed the method to properly instantiate. – Joel Etherton Feb 01 '11 at 12:06
  • why is this useful if you had to cast it ? – Leandro Bardelli Jul 25 '19 at 23:40
2

According to what you have, FormWithWorker must be (at least) as base class of the type you are instantiating, so you can do this:

FormWithWorker formToLoad = (FormWithWorker)fi.GetValue(this);
if (formToLoad == null)
{
    formToLoad = (FormWithWorker)System.Activator.CreateInstance("MyAssemblyName", formType);
}
Mark Avenius
  • 13,679
  • 6
  • 42
  • 50
  • I tried this, but I received a casting error due to the `System.Activator.CreateInstance` method returning an ObjectHandler. I just read @Andras Vass's idea of calling `Unwrap()` and combining the 2 is what I'll be trying next. – Joel Etherton Jan 28 '11 at 19:29
0

While a common interface is one way to approach this problem, interfaces aren't practical for all scenerioes. The decision above is one of going with a factory pattern (switch statement - concrete class selection) or use reflection. There's a stack post that tackles this problem. I believe you can directly apply this to your issue:

Method Factory - case vs. reflection

Community
  • 1
  • 1
P.Brian.Mackey
  • 43,228
  • 68
  • 238
  • 348