-1
\$\begingroup\$

I'm working over project for my studies. It's a simple program about brewing coffee. I'm thinking about solution for showing single parameter in GUI.

I have a source brew parameter class:

public abstract class CoffeeParam { /* It's empty */ }
public class CoffeeParam<T> : CoffeeParam, INotifyPropertyChanged
{
    public string ParamName { get => _paramName; set { _paramName = value; InvokePropertyChanged(); } }
    public T Value { get => _value; set { _value = value; InvokePropertyChanged(); } }
    public string ParamUnit { get => _paramUnit; set { _paramUnit = value; InvokePropertyChanged(); } }
    private bool IsDescription { get => _isDescription; set { _isDescription = value; InvokePropertyChanged(); } }

    private bool _isDescription;
    private T _value;
    private string _paramName;
    private string _paramUnit;

    #region PropertyChanged
    private void InvokePropertyChanged([CallerMemberName]string propName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion
}

And the GUI class, where I try to reveal the type of CoffeeParam:

public class MyUserControl1 : UserControl
{
   public MyUserContro1(CoffeeParam coffeeParam)
   {
      Control cntr = null;
      if (coffeeParam is CoffeeParam<double>)
      {
        // creating controls for double input
      } else if (coffeeParam is CoffeeParam<int>)
      {
        // creating controls for int input
      }
      // rest of init code
   }
}

An important thing is that the CoffeeParam<type> may be almost everything (date, time, custom class).

My question is: How can I organize the GUI code to make it's close for modifications and open for extensions?

Actually, new param is forcing edition of the GUI class.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ The downvote is for the ellipsis ... in your code and the close-vote for the same reason. \$\endgroup\$
    – t3chb0t
    Dec 10, 2017 at 8:22
  • \$\begingroup\$ Is it better now? \$\endgroup\$
    – Mikołaj
    Dec 10, 2017 at 12:57
  • \$\begingroup\$ Not really. The comments about creating controls still qualify the code as sub/pseudocode and thus off-topic. Why did you remove these parts in the first place? \$\endgroup\$
    – t3chb0t
    Dec 10, 2017 at 13:00
  • \$\begingroup\$ Because in my opinion details of implementations are not important and the question do not relay of them - i'm asking about something like adding a new "if else" statement, not about creating controls. Removing these lines allows me to ask a question as compressed as possible. \$\endgroup\$
    – Mikołaj
    Dec 10, 2017 at 13:09
  • \$\begingroup\$ On Code Review, you put your full code up for review. Otherwise, we'll be giving answers and you'll tell us you can't implement the advice because of module X that you hadn't shown before. We don't accept that. Please take a look at the help center. \$\endgroup\$
    – Mast
    Dec 10, 2017 at 23:52

1 Answer 1

-2
\$\begingroup\$

You could create a factory class for controls

public class CoffeeParamControlFactory
{
    private readonly Dictionary<Type, Func<CoffeeParam, Control>> _creators =
        new Dictionary<Type, Func<CoffeeParam, Control>> {
            [typeof(double)] = p => {
                var tb = new TextBox();
                //TODO: configure
                return tb;
            },
            [typeof(int)] = p => {
                var tb = new TextBox();
                //TODO: configure
                return tb;
            },
        };

    public Control CreateAndConfigure(CoffeeParam coffeeParam)
    {
        var t = coffeeParam.GetType();
        if (!t.IsGenericType) {
            throw new ArgumentException("Must pass a paramter of type CoffeeParam<T>");
        }
        var genericParamType = t.GetGenericArguments()[0];
        Control control = _creators[genericParamType](coffeeParam);
        //TODO: additional configurations
        return control;
    }
}

Of course, you will have to make a change at some place, when a new type of parameter is required. But like this, you have a factory class whose main responsibility is to handle exactly this.

I would also move all the members of CoffeParam<T> that are not dependent on T to the abstract base class CoffeParam. This allows you to do more with the parameters without having to know the actual generic type argument.


Generics are great. But they are not dynamic, since the generic type parameters are resolved at compile time. They are of little use, if your scenario is dynamic and you must cast the concrete type at runtime. Therefore, it can be helpful to be less generic. I suggest the following alternate approach, where you first declare a non-generic and a generic interface. In C#, you can implement an interface member explicitly. This hides the member unless you access it through this interface. This allows us to declare two Value properties, one of type object implemented explicitly through ICoffeParam, and one of type T implemented implicitly.

I also make CoffeeParam<T> abstract and implement concrete parameters like DoubleCoffeeParam. This allows you to let such a parameter implement a logic very specific to a certain type of values. I also declare a separate interface for the control creation, since it not directly tied to the parameter functionality.

interface IControlCreator
{
    Control CreateControl();
}

The non-generic parameter interface

interface ICoffeParam
{
    string ParamName { get;  set; }
    object Value { get; set; }
    string ParamUnit { get; set; }
    bool IsDescription { get; set; }
}

The generic parameter interface

interface ICoffeParam<T> : ICoffeParam
{
    new T Value { get; set; }
}

The abstract base class is now generic

public abstract class CoffeeParam<T> : ICoffeParam<T>, INotifyPropertyChanged
{
    public string ParamName { get => _paramName; set { _paramName = value; InvokePropertyChanged(); } }

    object ICoffeParam.Value { get => _value; set { _value = (T)value; InvokePropertyChanged(); } }
    public T Value { get => _value; set { _value = value; InvokePropertyChanged(); } }

    public string ParamUnit { get => _paramUnit; set { _paramUnit = value; InvokePropertyChanged(); } }
    public bool IsDescription { get => _isDescription; set { _isDescription = value; InvokePropertyChanged(); } }

    // Fields ...

    // PropertyChanged implementation ...
}

As an example, a parameter class for doubles

public class DoubleCoffeeParam : CoffeeParam<double>, IControlCreator
{
    public Control CreateControl()
    {
        var tb = new TextBox();
        //TODO: Configure for double
        return tb;
    }
}

It is not generic. There is little you can do with a generic value anyway. e.g. you can't add 2 of them.

public CoffeeParam<T> Add<T>(CoffeeParam<T> x, CoffeeParam<T> y)
{
    //                                  NOT POSSIBLE!
    return new CoffeeParam<T>{ Value = x.Value + y.Value };
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ I down-voted your answer because while I agree with your points about generics, proposed refactoring is very unorthodox and violates MVVM by leaking controls to viewmodel layer. IMHO, a better approach would be to use existing WPF mechanisms for linking views to viewmodels: DataTemplate and DataTempateSelector. This way CoffeeParam will be able to stay UI-agnostic. \$\endgroup\$
    – Nikita B
    Dec 10, 2017 at 17:43

Not the answer you're looking for? Browse other questions tagged or ask your own question.