Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic interfaces for semi-ad hoc report

Tags:

c#

generics

wcf

I'm trying to create a simple reporting tool, where a user can select from a set of KPI's, charts, aggregate functions and other parameters, click a button, after which a wcf service is called, which then returns a custom model with all data. This could then be displayed in an MVC/WPF application (could be both).

Since the users could be from several countries, I want to use data annotations to portrait all the numbers and headers in a fashion that is suited to the language and number formats the current user is used to.

The loading of the data and all that stuff is working just fine, no problem whatsoever. Also, I use data annotations, so all the language/culture-specific settings are taken care for. The issues start when I try to put all data into the model that I want to display to the user.

What I'm trying to do is have a Report class, that contains a collection of columns. Each column could be a list of int/double/... values. Now, since I'm dealing with WCF and the above explanation implies (as far as I understand) the use of generics, I assume I can use the [KnownType] or [ServiceKnownType] for the classes/wcf operations, while actually using a base type or interface as the return value. Never really tried this, but I found some good explanations that seem pretty logical to me, so I assume I won't have any big issues for this part (at least I hope not).

Right now, my interfaces are as such (simplified to focus on the actual problem I have):

public interface IReport<T> where T: IConvertible { ICollection<IColumn<T>> Columns { get; set; } }
public interface IColumn<T> where T: IConvertible { ICollection<IValue<T>> Values { get; set; } }
public interface IValue<T> where T: IConvertible { T Value { get; set; } }

Since the value in each column could be an int/double/..., I assume I must have an actual class just for the value (I don't think I can use a data annotation attribute on a collection type), as such:

public class IntValue: IValue<int>
{
    [DisplayFormat(DataFormatString = "{0:#,##0;-#,##0;'---'}", ApplyFormatInEditMode = true)]
    public int Value { get; set; }
}

Of course, that looks weird, since you could just make it a generic class Value that implements IValue and be done with it, but if I do the silly thing and make a class for each possible type (now that I type it out, that sounds really bad, I know), I can use the DisplayFormat attribute and don't have to worry about the way it will present itself to the user, it'll always be appropriate.

Now, for the classes that implement IColumn and IReport, that is simple:

public class Report<T>: IReport<T> where T: IConvertible 
{
    public ICollection<IColumn<T>> Columns { get; set; }
    public Report() { Columns=new List<IColumn<T>>(); }
}

public class Column<T>: IColumn<T> where T: IConvertible 
{
    public ICollection<IValue<T>> Values { get; set; }
    public Column() { Values = new List<IValue<T>>(); }
}

From the list of interfaces and classes, you will immediately see that this makes it impossible to have a report where some columns have other types. So it's not possible to create a report where some columns are int, some are double,... Since the generic constraint in IReport makes you specify a type, you are stuck with that for all columns, since it propagates down to the value of each column... And that is exactly what I want really.

I feel like I'm not getting anywhere, and probably missing something really simple, so a nudge in the right direction would be appreciated.

TL;DR: How do I get a generic collection in a non-generic type?

like image 406
Tom van Schaijk Avatar asked Oct 19 '22 18:10

Tom van Schaijk


1 Answers

Allright, I took inspiration from the suggested solutions and implemented a variation as below. I understand not wanting to use generics too much, but it still annoyed me. After all, I want columns (or values) of several types. It's what generics are there for. Also, I wanted to provide a built in mechanism to provide the formatting of the fields.

I left the IReport and IColumn interfaces pretty straightforward, but I do not refer to an IValue interface in the IColumn interface. Instead, I use an abstract class Value in which I define some of the base framework for the formatting and data retrieval (in string format that is).

In between the actual IntValue/DoubleValue and the Value baseclass, I added a generic Value class that implements the generic IValue interface that does nothing else than providing the Data field so I don't have to do it in the IntValue/DoubleValue classes, and implement the AsFormattedString method, which uses the normal ToString method using the Formatter that I create in the Value baseclass constructor.

The actual implementation of that formatter is provided in the IntValue/DoubleValue classes, and give the possibility to use a standard format that I already hard coded, or a custom one provided by the class user.

public interface IReport { ICollection<IColumn> Columns { get; set; } }
public interface IColumn { ICollection<Value> Values { get; set; } }

public interface IValue<T> where T: IConvertible { T Data { get; set; } }

public abstract class Value
{
    #region Formatting

    protected IFormatProvider Formatter { get; set; }
    protected abstract IFormatProvider GetFormatter();
    protected abstract string AsFormattedString();
    public override string ToString() { return AsFormattedString(); }

    #endregion

    public Value() { Formatter = GetFormatter(); }
}

public abstract class Value<T>: Value, IValue<T> where T: IConvertible
{
    #region IValue members

    public T Data { get; set; }

    #endregion

    #region Formatting

    protected override string AsFormattedString() { return Data.ToString(Formatter); }

    #endregion
}

public class IntValue: Value<int>
{
    public IntValue() { }
    public IntValue(string formatstring, int data) { Formatter = new IntFormatter(formatstring); Data = data; }

    #region Formatting

    protected override IFormatProvider GetFormatter() { return new IntFormatter(); }

    internal class IntFormatter: CustomFormatter
    {
        public IntFormatter() : this("{0:#,##0;-#,##0;'---'}") { }
        public IntFormatter(string formatstring) : base(formatstring) { }
    }

    #endregion
}

public class DoubleValue: Value<double>
{
    public DoubleValue() { }
    public DoubleValue(string formatstring, double data) { Formatter = new DoubleFormatter(formatstring); Data = data; }

    #region Formatting

    protected override IFormatProvider GetFormatter() { return new DoubleFormatter(); }

    internal class DoubleFormatter: CustomFormatter
    {
        public DoubleFormatter() : this("{0:0.#0;-0.#0;'---'}") { }
        public DoubleFormatter(string formatstring) : base(formatstring) { }
    }

    #endregion
}

public class ReportView: IReport
{
    public ICollection<IColumn> Columns { get; set; }
    public ReportView() { Columns = new List<IColumn>(); }
}

public class ReportColumn: IColumn
{
    public ICollection<Value> Values { get; set; }
    public ReportColumn() { Values = new List<Value>(); }
}

It's used as such:

    // Creating a report
    IReport report = new ReportView();

    // Adding columns
    IColumn mycolumn = new ReportColumn();
    mycolumn.Values.Add(new IntValue() { Data = 1 });
    mycolumn.Values.Add(new DoubleValue() { Data = 2.7 });
    mycolumn.Values.Add(new IntValue("{0:#,##0;-#,##0;'---'}", 15));
    mycolumn.Values.Add(new DoubleValue("{0:0.#0;-0.#0;'---'}", 2.9));
    report.Columns.Add(mycolumn);

    // Looping through each column, and get each value in the formatted form
    foreach(var column in report.Columns)
    {
        foreach(var value in column.Values) { value.ToString(); }
    }

If there is something to be added/corrected about this, I'd be glad to hear. I will check out the Visitor pattern that was hinted towards above by Binary Worrier, and test the entire setup. Let me know if I make silly or poor design choices pls! I will probably need to alter it left and right a bit to provide one single format for the entire column without having to provide it to each value, but the base framework is there I think.

like image 199
Tom van Schaijk Avatar answered Oct 31 '22 11:10

Tom van Schaijk