1

I have a problem with implementing generic user interface interaction.

I have different classes that contain data each for particular interface element. So every UserInterfaceElementComponent has data only useful for him - all data inherit from UserInterfaceElementComponentData. For example, UserInterfaceElementRightCorner only can use data of type UserInterfaceElementComponentDataRightCorner.

The problem is in generic part - I have class UserInterfaceManager that has a generic method:

public static void TransferDataToUIController<T>(T userInterfaceElementComponentData) where T: UserInterfaceElementComponentData
{
    _userInterfaceController.TransferUIDataToComponent(userInterfaceElementComponentData);
}

So from any class I can send it any data that inherits from UserInterfaceElementComponentData. And it asks controller to transfer this data to particular UserInterfaceElementComponent by geting its enum type:

public void TransferUIDataToComponent<T>(T userInterfaceElementComponentData) where T: UserInterfaceElementComponentData
{
    this._userInterfaceElements[(int)userInterfaceElementComponentData.ElementComponentType].AcceptUserInterfaceElementComponentData(userInterfaceElementComponentData);
}

So it gets particular UI component from this array this._userInterfaceElements that corresponds to this data type. Then it calls to accept data that was given to him.

public class UserInterfaceElementRightCorner : UserInterfaceElementComponent {

    public override void AcceptUserInterfaceElementComponentData(UserInterfaceElementComponentData userInterfaceElementComponentData)
    {
        ((UserInterfaceElementComponentDataRightCorner)userInterfaceElementComponentData)._planetInfoPanel.gameObject.SetActive(true);
    }
}

Though need to cast this object to corresponding type to get this particular data, so I could use it in that class in way I need, but I would like to avoid object cast because it will be called more than 10000 times per second.

I don't mind changin the whole architecture, I just need to pass object as parameter and it should already be the type I need. Though with generics it seems impossible.

Otherwise, I will have to create a new method for every type in 2 classes and compare types. In general I need to pass particular data type to particular client without casting and with 1-2 methods.

5 Answers5

2

I think you need some means of binding a component to the type of data it accepts. Otherwise, how do you know they match up (other than by parsing the name and using your human intuition, which a computer can't do)?

This can be done with generics and inheritance, like this:

abstract class MyComponentData
{
    abstract public string ElementComponentType { get; }
}
class MyComponentData1 : MyComponentData
{
    override public string ElementComponentType { get { return "Type1"; } }
}
class MyComponentData2 : MyComponentData
{
    override public string ElementComponentType { get { return "Type2"; } }
}

abstract class MyComponentClass
{
}
class MyComponentClass<T> : MyComponentClass where T: MyComponentData
{
    public void AcceptData(T input)
    {
        //Do something with the data
    }
}

With this scheme, it is required to declare what type of data a particular component will accept as part of the declaration of the generic class MyComponentClass. So if you have a component that can handle MyComponentData1, you have to declare it as var c = new MyComponentClass<MyComponentData1>(). Thus any instance of a component is bound to a specific data type.

To initialize your lookup list:

Dictionary<string, MyComponentClass> _userInterfaceElements = new Dictionary<string, MyComponentClass>();
_userInterfaceElements.Add("Type1", new MyComponentClass<MyComponentData1>());
_userInterfaceElements.Add("Type2", new MyComponentClass<MyComponentData2>());

And to dispatch an incoming request for a component, passing the data, using the table:

static void TransferUIDataToComponent<T>(T data) where T: MyComponentData
{
    MyComponentClass<T> c = _userInterfaceElements[data.ElementComponentType] as MyComponentClass<T>;
    c.AcceptData(data);
}

But... hey.... we don't actually need the table any more! So you can just do this:

static void TransferUIDataToComponent<T>(T data) where T: MyComponentData
{
    var c = new MyComponentClass<T>();
    c.AcceptData(data);
}

Meaning you can get rid of _userInterfaceElements, get rid of ElementComponentType, and get rid of the abstract MyComponentClass that I added (we only needed it for _userInterfaceElements so we could store any component). And notice we have completely removed the need for any cast.

Then, to call the dispatch:

var data1 = new MyComponentData1();
TransferUIDataToComponent<MyComponentData1>(data1);

var data2 = new MyComponentData2();
TransferUIDataToComponent<MyComponentData2>(data2);
John Wu
  • 26,955
1

What @JohnWu outlines is 'regular' polymorphism: ie implement a common abstraction across all implementation types to avoid casting. If you can do that, do that.

But depending on your scenario, sometimes this doesn't work. What you seem to be asking for in your question is double-dispatch: you want the type of both the class implementing the interface and the underlying type of the parameter to control dispatch binding (so you end up in a method with a strongly typed parameter).

If this is what you are after, you can't have it. But you can cheat: the Vistor pattern enables you to implement these kind of arrangements using 'regular' polymorphic dispatch, based on the fact that you end up doing a single dispatch twice. You pass the target to the parameter's interface, and the parameter passes himself, strongly-typed, to the target.

Eric Lippert wrote a really good blog post about this which makes much more sense than the explanation above: https://ericlippert.com/2015/05/04/wizards-and-warriors-part-three/

piers7
  • 169
1

Wow dude, you hated my other solution. Guess I didn't understand your NFRs properly.

Here's another solution that keeps the list (so no micro-instantiations required) and still has no casts. Hope you're happy.

The idea here is that we remove from the base class any method prototypes that need to be type-specific; after all, the base class has no idea what its derived type is. Instead we implement a common Apply method which accepts the controller as an argument. This is a type of dependency injection, but at the method level. The controller is serving double duty as a sort of factory, which can return existing, strongly-typed components. Each data class knows how to find its partner component using generics.

Once the component is obtained, it's a simple matter of a call to the AcceptUserInterfaceElementComponentData method which is now type-specific and therefore can have strongly-typed arguments.

I did get rid of the enum... using an enum to identify type seemed like a code smell to me.

abstract public class UserInterfaceElementComponentData
{
    abstract public void Apply(UserInterfaceController controller);
}

public class UserInterfaceElementComponentDataRightCorner : UserInterfaceElementComponentData
{
    public string TypeSpecificExample
    {
        get
        {
            return "I am a UserInterfaceElementComponentDataRightCorner!!! I'm even hardcoded!!!";
        }
    }

    public override void Apply(UserInterfaceController controller)
    {
        var component = controller.ResolveComponent<UserInterfaceElementRightCorner>();
        component.AcceptUserInterfaceElementComponentData(this);
    }
}


abstract public class UserInterfaceElementComponent
{
}


public class UserInterfaceElementRightCorner : UserInterfaceElementComponent
{
    public void AcceptUserInterfaceElementComponentData(UserInterfaceElementComponentDataRightCorner userInterfaceElementComponentData)
    {
        Console.WriteLine("Hey I'm doing sonmething with a specific, uncast data class.  Here's proof: " + userInterfaceElementComponentData.TypeSpecificExample);
    }
}

public class UserInterfaceController
{
    List<UserInterfaceElementComponent> _userInterfaceElements = new List<UserInterfaceElementComponent>();

    public UserInterfaceController()
    {
        _userInterfaceElements.Add( new UserInterfaceElementRightCorner());
    }
    public void TransferUIDataToComponent<T>(T userInterfaceElementComponentData) where T : UserInterfaceElementComponentData
    {
        userInterfaceElementComponentData.Apply(this);
    }

    public T ResolveComponent<T>()
    {
        var result =  _userInterfaceElements.OfType<T>().FirstOrDefault();
        if (result == null) throw new InvalidOperationException(string.Format("Component {0} not found", typeof(T).FullName));
        return result;
    }
}
static class CastlessDispatch
{
    static UserInterfaceController _userInterfaceController = new UserInterfaceController();
    public static void TransferDataToUIController<T>(T userInterfaceElementComponentData) where T : UserInterfaceElementComponentData
    {
        _userInterfaceController.TransferUIDataToComponent(userInterfaceElementComponentData);
    }
}
John Wu
  • 26,955
1

One of the limitations of the c# language is that there is only one type identifier for an array or list, so every element must be the same type, or derive from the same type. If it is derived, the only way to get the subtype out of the array is to cast it. This is true no matter how clever you are with generics.

Here is a solution that may seem a little ugly but it solves the casting problem by getting away from the generics and initializing type-safe pointers that can be used without any casting.

I kept the list, to keep you happy, but it doesn't do anything in my example.

abstract public class UserInterfaceElementComponentData
{
    abstract public void Apply(UserInterfaceController controller);
}

public class UserInterfaceElementComponentDataRightCorner : UserInterfaceElementComponentData
{
    public string TypeSpecificExample
    {
        get
        {
            return "I am a UserInterfaceElementComponentDataRightCorner!!! I'm even hardcoded!!!";
        }
    }

    public override void Apply(UserInterfaceController controller)
    {
        var component = controller.UserInterfaceElementRightCorner;
        component.AcceptUserInterfaceElementComponentData(this);
    }
}


abstract public class UserInterfaceElementComponent
{
}


public class UserInterfaceElementRightCorner : UserInterfaceElementComponent
{
    public void AcceptUserInterfaceElementComponentData(UserInterfaceElementComponentDataRightCorner userInterfaceElementComponentData)
    {
        Console.WriteLine("Hey I'm doing something with a specific, uncast data class.  Here's proof: " + userInterfaceElementComponentData.TypeSpecificExample);
    }
}

public class UserInterfaceController
{
    List<UserInterfaceElementComponent> _userInterfaceElements = new List<UserInterfaceElementComponent>();
    private UserInterfaceElementRightCorner _userInterfaceElementRightCorner;

    public UserInterfaceController()
    {
        _userInterfaceElementRightCorner = new UserInterfaceElementRightCorner());
        _userInterfaceElements.Add( _userInterfaceElementRightCorner );
    }
    public UserInterfaceElementRightCorner UserInterfaceElementRightCorner
    {
        get { return _userInterfaceElementRightCorner; }
    }
    public void TransferUIDataToComponent(UserInterfaceElementComponentData userInterfaceElementComponentData)
    {
        userInterfaceElementComponentData.Apply(this);
    }
}
static class CastlessDispatch
{
    static UserInterfaceController _userInterfaceController = new UserInterfaceController();
    public static void TransferDataToUIController(UserInterfaceElementComponentData userInterfaceElementComponentData)
    {
        _userInterfaceController.TransferUIDataToComponent(userInterfaceElementComponentData);
    }
}
John Wu
  • 26,955
1

Fourth time is the charm

This is the same as my second answer (the one you initially checked) and I got rid of the <OfType> call and the enumerator.

The trick here was to define a contravariant interface that pairs components and their data. I have been meaning to learn more about contravariance so this was a fun exercise, thank you.

This may be a bit of messy code at this point, but I think I've proven it can be done, and you can use the same technique but maybe clean up the structure a bit.

public enum ElementComponentTypeEnum { RightCorner = 1 }

public interface IRelated<in T1, in T2>  
{ 
    void AcceptUserInterfaceElementComponentData(T2 t2);
}
abstract public class UserInterfaceElementComponentData
{
    abstract public ElementComponentTypeEnum ElementComponentType { get; }
    abstract public void Apply(UserInterfaceController controller);
}

public class UserInterfaceElementComponentDataRightCorner : UserInterfaceElementComponentData
{
    public string TypeSpecificString
    {
        get
        {
            return "I am a UserInterfaceElementComponentDataRightCorner!!! I'm even hardcoded!!!";
        }
    }
    override public ElementComponentTypeEnum ElementComponentType { get { return ElementComponentTypeEnum.RightCorner; } }

    public override void Apply(UserInterfaceController controller)
    {
        IRelated<UserInterfaceElementRightCorner, UserInterfaceElementComponentDataRightCorner> component = controller.ResolveComponent<UserInterfaceElementRightCorner,UserInterfaceElementComponentDataRightCorner>(this.ElementComponentType);
        component.AcceptUserInterfaceElementComponentData(this);
    }
}


abstract public class UserInterfaceElementComponent : IRelated<UserInterfaceElementComponent, UserInterfaceElementComponentData>
{
    public void AcceptUserInterfaceElementComponentData(UserInterfaceElementComponentData data) { }
}


public class UserInterfaceElementRightCorner : UserInterfaceElementComponent, IRelated<UserInterfaceElementRightCorner,UserInterfaceElementComponentDataRightCorner>
{
    public void AcceptUserInterfaceElementComponentData(UserInterfaceElementComponentDataRightCorner data) 
    {
        Console.WriteLine("Hey I'm doing sonmething with a specific, uncast data class.  Here's proof: " + data.TypeSpecificString);
    }

}

public class UserInterfaceController
{
    private UserInterfaceElementComponent[] _userInterfaceElements = new UserInterfaceElementComponent[10];

    public UserInterfaceController()
    {
        _userInterfaceElements[(int)ElementComponentTypeEnum.RightCorner] = new UserInterfaceElementRightCorner();
    }
    public void TransferUIDataToComponent<T>(T userInterfaceElementComponentData) where T : UserInterfaceElementComponentData
    {
        userInterfaceElementComponentData.Apply(this);
    }

    public IRelated<T1,T2> ResolveComponent<T1,T2>(ElementComponentTypeEnum elementComponentType) where T1 : UserInterfaceElementComponent where T2: UserInterfaceElementComponentData
    {
        IRelated<T1,T2> result = _userInterfaceElements[(int)elementComponentType];
        return result;
    }
}
static public class CastlessDispatch
{
    static UserInterfaceController _userInterfaceController = new UserInterfaceController();
    public static void TransferDataToUIController<T>(T userInterfaceElementComponentData) where T : UserInterfaceElementComponentData
    {
        _userInterfaceController.TransferUIDataToComponent(userInterfaceElementComponentData);
    }
}
John Wu
  • 26,955