2

This leads on from this post.

This is supposed to be forming part of a UI API, but I've dropped the tabs and pages stuff from this example, as it's really just a structural issue. Apologies for the length, I've reduced it as much as I can.

Also just to be clear I know what the problem is. I'm just not sure if there's a clearer way of implementing this structure that avoids these errors being passed down to the API users.

public abstract class Container<TSub, TSub2>
    where TSub : Sub<TSub, TSub2>, new()
    where TSub2 : Sub2<TSub, TSub2>, new()
{
    private TSub sub;

    public Container()
    {
        this.sub = new TSub();
        this.sub.Container = this;
    }
}

public abstract class Sub<TSub, TSub2>
    where TSub : Sub<TSub, TSub2>, new()
    where TSub2 : Sub2<TSub, TSub2>, new()
{
    private Container<TSub, TSub2> container;
    private TSub2 sub2;

    public Sub()
    {
        this.sub2 = new TSub2();
    }

    public Container<TSub, TSub2> Container
    {
        get { return this.container; }
        internal protected set
        {
            this.container = value;
            this.sub2.Container = value;
        }
    }
}

public abstract class Sub2<TSub, TSub2>
    where TSub : Sub<TSub, TSub2>, new()
    where TSub2 : Sub2<TSub, TSub2>, new()
{
    public Sub2() { }

    public Container<TSub, TSub2> Container { get; internal protected set; }
}

public class DefaultContainer : Container<DefaultSub, DefaultSub2> { }

public class DefaultSub : Sub<DefaultSub, DefaultSub2> { }

public class DefaultSub2 : Sub2<DefaultSub, DefaultSub2> { }

So far, all that works exactly as expected. You can make a new DefaultContainer instance, and get at the strongly type DefaultSub and DefaultSub2 parts nested below.

You can also make an extended alternate container, sub and sub2, and that works fine as well.

The problem I now have lies here:

public class AlternateContainer : Container<AlternateSub, DefaultSub2> { }

public class AlternateSub : Sub<AlternateSub, DefaultSub2> { }

It's obvious what the problem is here - the generics part of the signature for AlternateContainer and AlternateSub (<AlternateSub, DefaultSub2>) don't match the signature of DefaultSub2 (<DefaultSub, DefaultSub2>).

However, while the API users often do override all 3 classes, they sometimes don't need to override 1 or 2 of them, and just want to use the default implementations for the others.

Obviously there are 2 fairly basic solutions - one being for the API users to simply include a blank class extending the default implementation with the correct generics, and another being to provide much of the default implementation with static classes and provide boilerplate.

What I'm finding from my test users though is most of them are writing the code as above, then getting several of these compiler errors

Error   1   The type 'ClassLibrary1.DefaultSub2' cannot be used as type parameter 'TSub2' in the generic type or method 'ClassLibrary1.Container<TSub,TSub2>'. There is no implicit reference conversion from 'ClassLibrary1.DefaultSub2' to 'ClassLibrary1.Sub2<ClassLibrary1.AlternateSub,ClassLibrary1.DefaultSub2>'.

They're then spending time digging about in the docs trying to work out the recommended fix.

Does anyone have any recommendations on how to provide these generic subclasses, yet make base implementations reusable?


UPDATE

The main use for this pattern is for a custom tab control. This provides the base implementation for the core functionality including mouse over and down event handling basic (over-ridable) skinning, etc. The developers who are using these classes are completely outside my control, so I don't know the full extent of everything they might want to do with these. Immediately it is clear that many want to overlay alert or status icons, and numbers or strings which can easily be updated.

The main tabs (Container) can provide strongly typed versions of the tabs (Sub) and tab pages (Sub2) very easily of course. However access to the following properties has also been requested:

Page.Tab
Tab.Page
    /* Must return the specific generic type for access to custom properties */

Of course, these are easy enough if the generic constraints are removed, but those constraints are needed to do things in the BASE implementation. Finally, these properties are also required:

Page.TabContainer
Tab.TabContainer
    /* This instance must include the <TTab, TPage> constraint */

The annoying thing is of course, I have all this working!! However, as it stands at the moment, if you want to make a custom container with default tabs and pages, you have to provide a boilerplate classes to do it, and it's just not very clear.

Also, it does just feel wrong. I just have this niggling feeling there's a much simpler, stronger way of expressing this sort of thing.

Thanks for reading all this! Any further ideas?


UPDATE 2

I have another possible way to avoid passing the generics down, but it's missing required functionality really:

using System;
using System.Collections.Generic;
using System.Linq;

public abstract class Container2<TTab, TPage>
    where TTab : Tab, new()
    where TPage : TabPage, new()
{
    private Dictionary<int, TTab> index;
    private Dictionary<TTab, TPage> tabs;

    public Container2()
    {
        this.index = new Dictionary<int, TTab>();
        this.tabs = new Dictionary<TTab, TPage>();
    }

    public TTab this[int index] { get { return this.GetTab(index); } }

    public void AddTab(string text)
    {
        int index = this.tabs.Count;
        TTab tab = new TTab();
        TPage page = new TPage();

        tab.Text = text;
        tab.Page = page;

        this.tabs.Add(tab, page);
        this.index.Add(index, tab);
    }

    public TPage GetPage(int index) { return this.tabs[this.index[index]]; }

    public TPage GetPage(TTab tab) { return this.tabs[tab]; }

    public TTab GetTab(int index) { return this.index[index]; }

    public TTab GetTab(TPage page) { return this.tabs.FirstOrDefault(x => x.Value == page).Key; }
}

public abstract class Tab
{
    private TabPage page;
    private string text;

    public Tab()
    {
    }

    public TabPage Page { get { return this.page; } internal set { this.page = value; } }

    public string Text { get { return this.text; } set { this.text = value; } }
}

public abstract class TabPage
{
    private Tab tab;

    public TabPage()
    {
    }

    public Tab Tab { get { return this.tab; } }
}

That links the tab to the page and vice-versa in their base implementations only. If you want your extended page or tab type, you have to go through the container. That all works OK, however there is no way to maintain a link from the tab or the page up to the container, meaning you have to pass the reference to both about.

So right now I either have the ability to hold a link to the container, and little cause to use it, OR no link back to the container, along with the absolute requirement to use it.

I think I'm nearly ready to call this one way or the other, but I just want to try and pick the "best" pattern for this. Once it's bedded into production it's going to be all but impossible to replace.

Any last ideas? Anyone?

Community
  • 1
  • 1
Octopoid
  • 3,507
  • 5
  • 22
  • 43
  • Once you close the generic type by making `DefaultSub2` inherit from `Sub2`, the type of TSub and TSub2 in your abstract class are set. There isn't a way to have a class inherit from DefaultSub2 in a way that will change the type of a variable in the abstract class to the type you want unless DefaultSub2 is an open generic. – Grax32 Jan 16 '15 at 14:44
  • Yeah, I know what the problem is, and it makes sense as to why this doesn't work. However I can't have the classes as open generics, and there's a lot of base functionality which I need to bring through. Ideally I'd like to supply a `Container` class which uses `DefaultSub2` but of course I hit the same issue. As I say, I understand the problem, but from an API standpoint the structure seems overly confusing to consume at present. – Octopoid Jan 16 '15 at 15:41
  • Honestly, seeing this self referencing generic implementation triggers my code smell sensor. Are these generic arguments just for keeping around metadata? Perhaps this can simply be reworked with a better pattern to avoid the generic problem. – Tejs Jan 16 '15 at 19:21
  • I couldn't agree more, having to pass the constrained type arguments down around all over the place feels wrong, but whatever other pattern I try I either end up with type gotcha's or having to do a lot of type casting all over the place. Possibly removing the example use case for the pattern has not entirely helped here - I'll update the question. – Octopoid Jan 19 '15 at 09:03
  • 1
    Is there a good reason why the API needs to be strongly typed? You won't be able to strongly type tab controls with tabs of different types anyway, and what's wrong with users storing typed references to their pages, while delegating the display and selection to your code? Surely they can keep track of their own data instead of relying on view-related code to do so... – Pieter Witvoet Jan 20 '15 at 10:38
  • That's certainly what I'd do. I think the main thing is that they're storing references to one and wanting to access the other, strongly typed, rather than storing references to both. Some seem to want to store the tab, others the page. Different extended types is a good point, I think at present all of the testing has been done around a single extended type. With all this in mind, I think the latest version is probably the way to go - then they can either extend from the strong generic version, or just extend from the default impl and cast/store references. – Octopoid Jan 20 '15 at 11:31

0 Answers0