Davide Andrea
Davide Andrea

Reputation: 1413

Many custom user controls implementing same interface; is there a way to avoid duplicating same code in each?

Sorry, but I was not able to find an answer to this question in StackOverflow, even though this must be a duplicate. I do know that multiple inheritance is not possible.

My application has 50 classes of custom user controls, all of which implement my IParamConfig Interface.

The IParamConfig defines 5 properties and 8 methods.

Of those custom user controls, some inherit from TextBox, some from Button, some from CheckBox, some from ComboBox, etc.

I could create a ParamConfig class that implements the Methods and Properties of IParamConfig; but I know that I cannot have each of my custom user controls classes inherit from both the ParamConfig and TextBox or some other user control class.

Therefore, I am forced to duplicate the implementation of the Methods and Properties of IParamConfig multiple times.

For example, I created an abstract class AbtrParamConfigTextBox that inherits from TextBox and implements the Methods and Properties of IParamConfig; then, I created many TextBox-like custom user controls that inherit from this abstract class.

Similarly, I created an abstract class AbtrParamConfigComboBox that inherits from ComboBox and that also implements the Methods and Properties of IParamConfig.

Custom user controls inherit from various standard user controls

In total, I had to duplicate the common code 12 times (boxes in red in the picture).

I'd love it if I could have the common code appear only once.

For example, I'd love it if I could have all the custom user controls inherit from the same class (ParamConfig ).

All custom user controls inherit from a single class

enter image description here

I tried doing that by creating a ParamConfig class that inherits from UserControl, and then creating a ParamConfigTextBox that inherits from ParamConfig, that includes a single TextBox.

Single Parent class

But that resulted in awkward workarounds, because I no longer have direct access to the Properties of the TextBox in ParamConfigTextBox, so I have to duplicate them in ParamConfigTextBox, and add code to copy the properties from ParamConfigTextBox to its TextBox. Ugly.

Question:

Am I stuck in having to duplicate the common code 12 times?

Should I pursue the common class approach inheriting from UserControl?

Or is there a cleaner solution?

EDIT: A screenshot of the User Interface, as requested. enter image description here It has ~100 tabs, each full of user controls to configure the product.

EDIT:

Interface:

    // Functions
    string BmsParamName { get; } // Name of the parameter in the BMS's memory associated with this control
    void InitControlOnLoad(); // Initialize the control after load
    void ClearCtrl(); // Clear this control
    uint BitsUsedMask(); // Return the mask for the bits used in memory 
    void ShowValue16(ushort paramWord, bool isBadData); // Update the value or state displayed by this control
    void ShowValue32(uint paramValue32, bool isBadData); // Update the value or state displayed by this control
    uint NoOfBmsMemWords(); // Return the number of BMS memory words that this set uses
    void ShowParamArray(uint[] paramValues, uint bmsMemOfst, bool isBadData); // Update the value or state displayed by this control
    ushort GetEntry16(ushort configWord); // Receive the full word of configuration data and return it after replacing those bits that this configuration control is responsible for
    uint GetEntry32(); // Return the value in this control as a 16 bit unsigned integer
    ushort[] GetEntryArray(); // Get the set of configuration words for this configuration control

Properties:

    #region Properties (Protected properties, available to inherited classes)

    // Name of BMS variable
    protected string bmsParamName = ""; 
    [DefaultValueAttribute(0), Category("_Vinci"),
    Description("Name of the variable in the BMS's memory associated with this control")]
    public string BmsParamName
    {
        get { return bmsParamName; }
        set { bmsParamName = value; }
    }

    // Conversion factor
    protected float conversionFactor = 1F;
    [DefaultValueAttribute(1),
     Description("To convert the BMS value to the value shown in this setting, divide by this factor"),
     Category("_Vinci")]
    public float ConversionFactor
    {
        get { return conversionFactor; }
        set { conversionFactor = value; }
    }

    // Width of the data
    protected VinciForm.DataWidth dataWidth = VinciForm.DataWidth.Data16; 
    [DefaultValueAttribute(VinciForm.DataWidth.Data16),
     Description("Whether the data in the BMS fill an entire 32 bit word, a 16 bit Word, just the top 8 bits (MSB), the lower 8 bits (LSB), or specific bits"),
     Category("_Vinci")]
    public VinciForm.DataWidth DataWidth
    {
        get { return dataWidth; }
        set
        {
            dataWidth = value;
            firstBitNo = 0u;
            switch (dataWidth)
            {
                case VinciForm.DataWidth.Data32:
                    noOfBits = 32u;
                    break;

                case VinciForm.DataWidth.Data16:
                    noOfBits = 16u;
                    break;

                case VinciForm.DataWidth.Data8LSB:
                    noOfBits = 8u;
                    break;

                case VinciForm.DataWidth.Data8MSB:
                    firstBitNo = 8u;
                    noOfBits = 8u;
                    break;

                case VinciForm.DataWidth.DataBits:
                    noOfBits = 16u;
                    break;
            }
        }
    }

    // Number of the least significant bit used
    protected uint firstBitNo = 0u; 
    [DefaultValueAttribute(0), Category("_Vinci"),
    Description("Number of the least significant bit used")]
    public uint FirstBitNo
    {
        get { return firstBitNo; }
        set { firstBitNo = value; }
    }

    // Number of bits used
    protected uint noOfBits = 16u; 
    [DefaultValueAttribute(3), Category("_Vinci"),
    Description("Number of bits used")]
    public uint NoOfBits
    {
        get { return noOfBits; }
        set { noOfBits = value; }
    }

    // Display format: unsigned, signed or hex
    protected VinciForm.DisplayFormat displayFrmtCode = VinciForm.DisplayFormat.UnsignedFormat; 
    [DefaultValueAttribute(VinciForm.DataWidth.Data16),
     Description("Display format: unsigned, signed or hex"),
     Category("_Vinci")]
    public VinciForm.DisplayFormat DisplayFormat
    {
        get { return displayFrmtCode; }
        set { displayFrmtCode = value; }
    }

    // Format string
    protected string displayFrmtStr = ""; 
    [DefaultValueAttribute(""),
     Description("Display format string (e.g.: F4 for 4 decimal places); leave blank for automatic"),
     Category("_Vinci")]
    public virtual string DisplayFrmtStr
    {
        get { return displayFrmtStr; }
        set { displayFrmtStr = value; }
    }

    #endregion

Upvotes: 2

Views: 851

Answers (1)

Sefe
Sefe

Reputation: 14007

Your interface does not seem to access any properties of the implementing control (at least not the properties in your example). That means it should be fairly easy to encapsulate them in their own class and reuse it. Even if it would access some properties of your control, you could implement the interface in a separate class and use techniques such as callback methods to access the control specific part (can't give you an example, because for the methods you just gave us the interface in the post).

You can then use instances of this class in controls and thus save the controls the implementation and reuse code. That is actually what a lot of the common design patterns do (such as bridge, visitor or adapter): replace inheritance with composition.

To illustrate my point, I use a simplified version of your interface:

interface IParamConfig
{
    string BmsParamName
    {
        get;
        set;
    }

    float ConversionFactor
    {
        get;
        set;
    }
}

You would implement this in a separate helper class:

public class ParamConfig : IParamConfig
{
    private string bmsParamName = ""; 
    [DefaultValueAttribute(0),
    Description("Name of the variable in the BMS's memory associated with this control")]
    public string BmsParamName
    {
        get { return bmsParamName; }
        set { bmsParamName = value; }
    }

    // Conversion factor
    private float conversionFactor = 1F;
    [DefaultValueAttribute(1),
     Description("To convert the BMS value to the value shown in this setting, divide by this factor")]
    public float ConversionFactor
    {
        get { return conversionFactor; }
        set { conversionFactor = value; }
    }
}

I left some of the attributes for the property editor, even though it is a helper class. You'll see later why I did it. If you want to stay close to your current implementation, you can implement IParamConfig on your control and forward the functionality of the helper class:

public class ParamConfigTextBox : TextBox, IParamConfig
{
    private readonly ParamConfig paramConfig = new ParamConfig();

    [DefaultValueAttribute(0), Category("_Vinci"),
    Description("Name of the variable in the BMS's memory associated with this control")]
    public string BmsParamName
    {
        get { return paramConfig.BmsParamName; }
        set { paramConfig.BmsParamName = value; }
    }

    // Conversion factor
    [DefaultValueAttribute(1),
     Description("To convert the BMS value to the value shown in this setting, divide by this factor"),
     Category("_Vinci")]
    public float ConversionFactor
    {
        get { return paramConfig.ConversionFactor; }
        set { paramConfig.ConversionFactor = value; }
    }
}

Admittedly, these two properties seem to be a bit too simple to offload into a helper class, but you are going to have them all in there, also the more complex ones, so overall you will achieve the optimal code reusage.

The downside of this solution is that you still have to implement the entire interface in all your controls. The functionality is in your helper class, but you still have to do the footwork of implementation quite a few times. You can simplify your task by creating a provider interface, which is much easier to implement (this time it's the full interface, no simplified example):

interface IParamConfigProvider
{
    IParamConfig ParamConfig
    {
        get;
    }
}

public class ParamConfigTextBox : TextBox, IParamConfigProvider
{
    private readonly paramConfig = new ParamConfig();

    [Category("_Vinci"),
    Description("The param config properties")]
    public ParamConfig ParamConfig
    {
        get { return paramConfig; }
    }

    IParamConfig IParamConfigProvider.ParamConfig
    {
        get { return ParamConfig; }
    }
}

You can still navigate the proprties of your IParamConfig in your visual designer (remember that I kept some of the designer attributes? - now you know why: you'll still see the properties in the editor). That is the easiest way to implement this at control level, but the code that accesses IParamConfig would need to be changed to go through IParamConfigProvider. At the end it's your choice what serves you better.

Upvotes: 1

Related Questions