Passing a generic <TObject> class to a form
Asked Answered
C

5

10

I can't seem to find out the answer to this through searching, so here goes....

I know that I can pass Class objects generically to other classes by utilising this type of code:

public class ClsGeneric<TObject> where TObject : class
{
    public TObject GenericType { get; set; }
}

Then constructing in this way:

ClsGeneric<MyType> someName = new ClsGeneric<MyType>()

However, I have an application that requires me to open a Form and somehow pass in the generic type for use in that form. I am trying to be able to re-use this form for many different Class types.

Does anyone know if that's possible and if so how?

I've experimented a bit with the Form constructor, but to no avail.

Many thanks in advance, Dave

UPDATED: A Clarification on what the outcome I am trying to achieve is

enter image description here

UPDATED: 4th AUG, I've moved on a little further, but I offer a bounty for the solution. Here is what I have now:

interface IFormInterface
{
    DialogResult ShowDialog();
}


public class FormInterface<TObject> : SubForm, IFormInterface where TObject : class
{ }

public partial class Form1 : Form
{
    private FormController<Parent> _formController;

    public Form1()
    {
        InitializeComponent();
            _formController = new FormController<Parent>(this.btnShowSubForm, new DataController<Parent>(new MeContext()));   
    }
}

public class FormController<TObject> where TObject : class
{
    private DataController<TObject> _dataController;
    public FormController(Button btn, DataController<TObject> dataController)
    {
        _dataController = dataController;
        btn.Click += new EventHandler(btnClick);
    }

    private void btnClick(object sender, EventArgs e)
    {
        showSubForm("Something");
    }

    public void showSubForm(string className)
    {
        //I'm still stuck here because I have to tell the interface the Name of the Class "Child", I want to pass <TObject> here.
        // Want to pass in the true Class name to FormController from the MainForm only, and from then on, it's generic.

        IFormInterface f2 = new FormInterface<Child>();
        f2.ShowDialog();
    }
}

class MeContext : DbContext
{
    public MeContext() : base(@"data source=HAZEL-PC\HAZEL_SQL;initial catalog=MCL;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework") { }
    public DbSet<Parent> Child { get; set; }
}

public class DataController<TObject> where TObject : class
{
    protected DbContext _context;

    public DataController(DbContext context)
    {
        _context = context;
    }
}

public class Parent
{
    string Name { get; set; }
    bool HasChildren { get; set; }
    int Age { get; set; }
}

public class Child
{
    string Name { get; set; }
    int Age { get; set; }
}
Clubbable answered 2/8, 2017 at 13:34 Comment(13)
You wan to pass in a generic type to your form and have your form instantiate that type?Gladden
"somehow pass in the generic type for use in that form" - what use? How is this type going to be used?Ogdon
Minor note: Don't prefix your class names with Cls, that's very old school behaviour and not recommended.Araxes
If you want to instantiate T in your generic class you need your constraint to be where T : class, new()Gladden
Sinatr - The Form will control a ListBox picker for data stored in a database. I want to be able to re-use the form for all classes that have the same behaviour, and just run my Data Controller class which uses <TObject> to update/delete/Add etc.Clubbable
DavidG: I am old school ;-) but thanks for the tip.Clubbable
Stuart - The form will be opened from inside a Class that uses <TObject> - this Class doesn't know what <TObject> is - it's instantiated by the main form, which told it. Ideally, I want to pass <TObject> to the new sub-form and use it inside. Thanks.Clubbable
why would you need generic if you want to reuse the form for many classes? Stick with object, or use an interface, and pass the instance via constructor. You might want to check how DataSource property is implemented in databound components, or even WPFs' DataContext approach.Galaxy
Alex - Thanks. Perhaps my use of the word "generic" is a little wrong, forgive me. I have a Data Controller Class I made for Entity Framework, which is instantiated with <TObject>. So, in my sub-Form, I need to be able to pass <TObject> to it (which was itself passed from the previous Class). I hope this makes sense. I don't think a plain "object" will work in this case, however, using an Interface is a good tip, thanks.Clubbable
Aha, that's a bit different from the original question, I believe. There could be another approach in that case - pass an interface IProvider implementing factory method GetObject<T> to sub-form, and call it when needed to obtain a generic class instance. This will allow to avoid having any generic-related stuff inside top-level form, and still be type-safe at the point of GetObject's invocation.Galaxy
Alex - Sounds ideal. Could I humbly ask for a code sample if you have time please?Clubbable
I don't understand what are you trying to achieve. Code looks very bad :/Nordrheinwestfalen
I really don't understand why you need generics here. What does the sub form do with the TObject anyway?Raybourne
P
3

I think you can add a new type argument to FormController:

public class FormController<TParent, TChild>
  where TParent : class
  where TChild : class 
{
    ...

    public void showSubForm(string className)
    {
        IFormInterface f2 = new FormInterface<TChild>();
        f2.ShowDialog();
    }
}
Palladin answered 5/8, 2017 at 8:34 Comment(1)
Many thanks for taking the time to offer a solution, much appreciated. I'm going to look at all the solutions I've received and will respond in the next few days.Clubbable
S
11

Maybe you've tried this, but you can create a custom class:

public class GenericForm<TObject> : Form where TObject : class
{
    // Here you can do whatever you want,
    // exactly like the example code in the
    // first lines of your question
    public TObject GenericType { get; set; }

    public GenericForm()
    {
        // To show that this actually works,
        // I'll handle the Paint event, because
        // it is executed AFTER the window is shown.
        Paint += GenericForm_Paint;
    }

    private void GenericForm_Paint(object sender, EventArgs e)
    {
        // Let's print the type of TObject to see if it worked:
        MessageBox.Show(typeof(TObject).ToString());
    }
}

If you create an instance of it like that:

var form = new GenericForm<string>();
form.Show();

The result is:

enter image description here

Going further, you can create an instance of type TObject from within the GenericForm class, using the Activator class:

GenericType = (TObject)Activator.CreateInstance(typeof(TObject));

In this example, since we know that is a string, we also know that it should throw an exception because string does not have a parameterless constructor. So, let's use the char array (char[]) constructor instead:

GenericType = (TObject)Activator.
         CreateInstance(typeof(TObject), new char[] { 'T', 'e', 's', 't' });

MessageBox.Show(GenericType as string);

The result:

enter image description here

Let's do the homework then. The following code should achieve what you want to do.

public class Parent
{
    string Name { get; set; }
    bool HasChildren { get; set; }
    int Age { get; set; }
}

public class Child
{
    string Name { get; set; }
    int Age { get; set; }
}

public class DataController<TObject> where TObject : class
{
    protected DbContext _context;

    public DataController(DbContext context)
    {
        _context = context;
    }
}

public class FormController<TObject> where TObject : class
{
    private DataController<TObject> _dataController;

    public FormController(Button btn, DataController<TObject> dataController)
    {
        _dataController = dataController;
        btn.Click += new EventHandler(btnClick);
    }

    private void btnClick(object sender, EventArgs e)
    {
        GenericForm<TObject> form = new GenericForm<TObject>();
        form.ShowDialog();
    }
}

public class GenericForm<TObject> : Form where TObject : class
{
    public TObject GenericType { get; set; }

    public GenericForm()
    {
        Paint += GenericForm_Paint;
    }

    private void GenericForm_Paint(object sender, EventArgs e)
    {
        MessageBox.Show(typeof(TObject).ToString());

        // If you want to instantiate:
        GenericType = (TObject)Activator.CreateInstance(typeof(TObject));
    }
}

However, looking to your current example, you have two classes, Parent and Child. If I understand correctly, those are the only possibilities to be the type of TObject.

If that is the case, then the above code will explode if someone pass a string as the type parameter (when the execution reaches Activator.CreateInstance) - with a runtime exception (because string does not have a parameterless constructor):

enter image description here

To protect your code against that, we can inherit an interface in the possible classes. This will result in a compile time exception, which is preferable:

enter image description here

The code is as follows.

// Maybe you should give a better name to this...
public interface IAllowedParamType { }

// Inherit all the possible classes with that
public class Parent : IAllowedParamType
{
    string Name { get; set; }
    bool HasChildren { get; set; }
    int Age { get; set; }
}

public class Child : IAllowedParamType
{
    string Name { get; set; }
    int Age { get; set; }
}

// Filter the interface on the 'where'
public class DataController<TObject> where TObject : class, IAllowedParamType
{
    protected DbContext _context;

    public DataController(DbContext context)
    {
        _context = context;
    }
}

public class FormController<TObject> where TObject : class, IAllowedParamType
{
    private DataController<TObject> _dataController;

    public FormController(Button btn, DataController<TObject> dataController)
    {
        _dataController = dataController;
        btn.Click += new EventHandler(btnClick);
    }

    private void btnClick(object sender, EventArgs e)
    {
        GenericForm<TObject> form = new GenericForm<TObject>();
        form.ShowDialog();
    }
}

public class GenericForm<TObject> : Form where TObject : class, IAllowedParamType
{
    public TObject GenericType { get; set; }

    public GenericForm()
    {
        Paint += GenericForm_Paint;
    }

    private void GenericForm_Paint(object sender, EventArgs e)
    {
        MessageBox.Show(typeof(TObject).ToString());

        // If you want to instantiate:
        GenericType = (TObject)Activator.CreateInstance(typeof(TObject));
    }
}

UPDATE

As RupertMorrish noted, you can still compile the following code:

public class MyObj : IAllowedParamType
{
    public int Id { get; set; }

    public MyObj(int id)
    {
        Id = id;
    }
}

And that should still rise an exception, because you just removed the implicit parameterless constructor. Of course, if you know what you are doing, this is hard to happen, however we can forbidden this by using new() on the 'where' type filtering - while also getting rid of the Activator.CreateInstance stuff.

The entire code:

// Maybe you should give a better name to this...
public interface IAllowedParamType { }

// Inherit all the possible classes with that
public class Parent : IAllowedParamType
{
    string Name { get; set; }
    bool HasChildren { get; set; }
    int Age { get; set; }
}

public class Child : IAllowedParamType
{
    string Name { get; set; }
    int Age { get; set; }
}

// Filter the interface on the 'where'
public class DataController<TObject> where TObject : new(), IAllowedParamType
{
    protected DbContext _context;

    public DataController(DbContext context)
    {
        _context = context;
    }
}

public class FormController<TObject> where TObject : new(), IAllowedParamType
{
    private DataController<TObject> _dataController;

    public FormController(Button btn, DataController<TObject> dataController)
    {
        _dataController = dataController;
        btn.Click += new EventHandler(btnClick);
    }

    private void btnClick(object sender, EventArgs e)
    {
        GenericForm<TObject> form = new GenericForm<TObject>();
        form.ShowDialog();
    }
}

public class GenericForm<TObject> : Form where TObject : new(), IAllowedParamType
{
    public TObject GenericType { get; set; }

    public GenericForm()
    {
        Paint += GenericForm_Paint;
    }

    private void GenericForm_Paint(object sender, EventArgs e)
    {
        MessageBox.Show(typeof(TObject).ToString());

        // If you want to instantiate:
        GenericType = new TObject();
    }
}
Sense answered 4/8, 2017 at 19:45 Comment(6)
Many thanks for taking the time to offer a solution, much appreciated. I'm going to look at all the solutions I've received and will respond in the next few days.Clubbable
/// (because string does not have a parameterless constructor) You can restrict TObject to types that have a parameterless constructor by specifying ... where TObject : IAllowedParamType, new() [new() implies class. You can leave it in if you like]Quin
@RupertMorrish Good idea. But the problem is, any type that is not expected in the business logic of the application (even some that have parameterless constructors) should not be allowed, which can be solved using a common interface. Still, nice mention of new(), +1.Sense
Right, I was assuming you'd have the interface, too. new() is in addition to that, as you can't specify a constructor in an interface.Quin
If you specify new(), then it won't compile. learn.microsoft.com/en-us/dotnet/csharp/language-reference/…Quin
Let us continue this discussion in chat.Quin
P
3

I think you can add a new type argument to FormController:

public class FormController<TParent, TChild>
  where TParent : class
  where TChild : class 
{
    ...

    public void showSubForm(string className)
    {
        IFormInterface f2 = new FormInterface<TChild>();
        f2.ShowDialog();
    }
}
Palladin answered 5/8, 2017 at 8:34 Comment(1)
Many thanks for taking the time to offer a solution, much appreciated. I'm going to look at all the solutions I've received and will respond in the next few days.Clubbable
H
2

So as I understand it, you want a Form<T> to open upon some action in the MainForm, with your MainForm using a FormController, as a manager of all your forms, relaying the generic type information to your Form<T>. Furthermore, the instantiated object of your Form<T> class should request an instance of a DatabaseController<T> class from your FormController.

If that is the case, the following attempt might work:

MainForm receives a reference to the FormController instance upon constructor initialization or has another way to interact with the FormController, e.g. a CommonService of which both know, etc.

This allows MainForm to call a generic method of the FormController to create and show a new Form object:

void FormController.CreateForm<T> () 
{
    Form<T> form = new Form<T>();
    form.Show();
    // Set potential Controller states if not stateless
    // Register forms, etc.
}

with Form<T> along the lines of:

class Form<T> : Form where T : class
{
    DatabaseController<T> _dbController;
    Form(FormController formController)
    {
        _dbController = formController.CreateDatabaseController<T>();
    }
}

Now you have a couple of ways for the Form to receive a DatabaseController instance:

1. Your Form<T> receives a reference of the FormController or has another way to communicate with it to call a method along the lines of:
DatabaseController<T> FormController.CreateDatabaseController<T> () 
{
    return new DatabaseController<T>();
}

Your FormController does not need to be generic, otherwise you'd need a new FormController instance for every T there is. It just needs to supply a generic method.

  1. Your Form<T> receives an instance of the DatabaseController from the FormController upon constructor initialization:

    void FormController.CreateForm () { Form form = new Form(new DatabaseController()); form.Show(); }

with Form<T> being:

class Form<T> : Form where T : class
{
    DatabaseController<T> _dbController;
    Form(DatabaseController<T> controller) 
    {
         _dbController = controller;
    }
}

3. Like with 2 but your Form<T> and DatabaseController<T> provide static FactoryMethods to stay true to the Single Responsibility Priciple. e.g.:
public class Form<T> : Form where T : class
{
    private DatabaseController<T> _dbController;

    public static Form<T> Create<T>(DatabaseController<T> controller)
    {
        return new Form<T>(controller);
    }

    private Form(DatabaseController<T> controller) 
    {
         _dbController = controller;
    }
}

4. You can also use an IoC Container to register and receive instances of a specific type at runtime. Every Form<T> receives an instance of the IoC Container at runtime and requests its corresponding DatabaseController<T>. This allows you to better manage the lifetime of your controller and form objects within the application.
Hockey answered 7/8, 2017 at 10:9 Comment(1)
Many thanks for taking the time to offer a solution, much appreciated. I'm going to look at all the solutions I've received and will respond in the next few days.Clubbable
S
1

Well i'm not gonna go into the details here and will only suffice to some blueprints. In this scenario i'd use a combination of Unity constructor injection with a generic factory to handle the instantiation given the type in main form.

It's not that intricate, take a look at Unity documentation at Dependency Injection with Unity

The reason for picking Unity out of all DI containers is it was part of Enterprise Library from Microsoft itself and now continues to live on as an independent library in the form of Nugget. a friend of mine has recently ported Unity to .Net core, too. Simply put, it's hands down the most elaborate container available.

As for the factory i believe it's necessary because you don't wanna create a concrete lookup for handling all possible types, so it clearly has to be a generic factory. I'd advise you to make your factory a singleton and put it in whole another project, thereby separating your UI project from the models and both party will communicate through this DI bridge. you can even take a step further and process your model types using assembly reflection. sorry for being too general, but i really don't know how familiar are you with these patterns. It really worth taking some time and utilizing these patterns. in my humble opinion there is no escape from these maneuvers if you want a truly scalable software.

You can reach me in private if you're looking for hints on implementation of any of the above-mentioned strategies.

Sideboard answered 11/8, 2017 at 4:19 Comment(1)
Many thanks for taking the time to write that out. I'll definitely take a look at that as an option, and, no, I was not familiar prior to your post, so many thanks again.Clubbable
G
0

Try Factory method.

public interface IProvider
{
    T GetObject<T>();
}

Top-level form:

public class TopLevelForm : Form
{
    public TopLevelForm(IProvider provider):base()
    {
         _provider = provider;
    }

    private void ShowSecondForm()
    {
        var f2 = new SecondForm(provider);
        f2.Show();
    }
}

Second-level form:

public class SecondLevelForm : Form
{
    public SecondLevelForm(IProvider provider):base()
    {
         _data = provider.GetObject<MyEntity>();
    }
}

As for IProvider's implementation - there are plenty of methods, starting from the simpliest one, return new T();

Galaxy answered 2/8, 2017 at 14:13 Comment(4)
Super. I really appreciate that, and I'll give it a try.Clubbable
Sorry Alex, it's the other way round unfortunately, my SecondLevelForm does not know what <MyEntity> would be, only the top level form would. So this doesn't seem to work. Thanks, DavyClubbable
I've updated the original question with a diagram. Many thanks, DaveClubbable
I seem not to understand what exactly you're trying to achieve but from where I stand it looks more and more as DataSource property used for binding, and it is of object type. You might want to use that approach too, or use a shared interface to have some degree of type safety between MainForm and SubForm.Galaxy

© 2022 - 2024 — McMap. All rights reserved.