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?