C# List which could contain only image or string

155 views Asked by At

NET developers. Decided to make layer to extract some data from data source to build tables in some generated documents further.

Maybe question is simple, but I am confused a little bit.

The problem is - want to build table model which could contain string or image. At the moment I ended up with that set of classes, but I am not sure that is the proper way:

public class TableModel
{
    public long RowsCount { get; set; }

    public long ColumnsCount { get; set; }

    public List<List<DataUnitModel>> Table { get; set; } 
}

public class DataUnitModel
{
    public object Element { get; set; }

    public Type ElementType { get; set; }
}

It suppose to be working like that (pseudocode):

public void BuildTable(TableModel myTable)
{
    for (var i = 0; i < myTable.Rows)
    {
        for (var j = 0; j < myTable.Cols)
        {
            DocumentCurrentRange.Insert(myTable.Table[i][j]);
        }
    }
}     

UPD:

Data types - it could be jsut simple strings or jpg/png/bmp images

UPD2:

Txnks guys, realized that I dont need GetElement function here its useless.

3

There are 3 answers

3
vgru On BEST ANSWER

It appears you are taking a slightly wrong approach here, i.e. you are solving the wrong problem. Your "table" could simply be a List<List<object>>, and it would essentially carry the same information as your classes are carrying now (apart from the "number of columns", if the list is empty).

If I understood your question, each element type will be "rendered" in some way (as a report, or in a webpage, or converted to something else). If that's true, I would concentrate on defining what's common for a rendering step of all the elements. So, the end result should allow you to write something like:

var html = new StringBuilder();

foreach (var item in document.Items)
{
    // get the renderer for this item type
    var renderer = renderers[item.GetType()];

    // append to the report
    renderer.Append(html, item);
}

A simplified report model could be a list of paragraphs, tables, figures, something like:

<Document>

  <Paragraph>
     Some text
  </Paragraph>

  <Table>
     <HeaderRow>
        <Col>Num</Col> <Col>Name</Col> ...
     <HeaderRow>
     <Row>
        <Col>Num</Col> <Col>Name</Col> ...
     <Row>
  </Table>

  <Image Url="..." />

</Document>

Or perhaps each element will have a list of child elements too, but this is a simple example.

Your model would then be expressed with something like:

interface IDocument 
{
    List<IItem> Items { get; }
}

interface IItem
{
    // common properties which apply to all items
    // (if they don't apply to all items, they
    // shouldn't be here)

    bool CanBreakAcrossPages { get; }
}

interface ITextualItem : IItem
{
    string TextFont { get; }
    float TextSize { get; }
    ...
}

class ParagraphItem : ITextualItem
{
    public bool CanBreakAcrossPages { get; set; }
    public string TextFont { get; set; }
    public float TextSize { get; set; }
    string Text { get; set; }
}

... you get the idea

And then, independently, you might have a renderer interface like this:

// there are several ways to do this, but this is
// just a simple way to avoid casting all over the 
// code, and still have generics for better type safety

interface IHtmlRenderer
{
    void Append(StringBuilder html, object element);
}

interface IHtmlRenderer<T> : IHtmlRenderer where T : IItem
{
    void Append(StringBuilder html, T element);
}

abstract class BaseHtmlRenderer<T> : IHtmlRenderer<T> where T : IItem
{
    public void Append(StringBuilder html, object element)
    {
        // this is the only place where we will cast
        this.Append(html, (T)element);
    }

    abstract public void Append(StringBuilder html, T element);
}

Which then makes creating different renderers obvious:

class ParagraphRenderer : BaseHtmlRenderer<ParagraphItem>
{
    public override void Append(StringBuilder html, ParagraphItem element)
    {
        // do stuff
    }
}

class ImageRenderer : BaseHtmlRenderer<ImageItem>
{
    public override void Append(StringBuilder html, ImageItem element)
    {
        // do stuff
    }
}

And all you are left to do is create a mapping of renderers for each IItem type (item type -> renderer). This can even be done through reflection/dependency injection, so that everything will "just work" when you decide to create a new renderer type:

// this can easily be done automatically through reflection

var renderers = new Dictionary<Type, Action<StringBuilder, object>>();
renderers[typeof(ImageItem)] = new ParagraphRenderer().Append;
renderers[typeof(ParagraphItem)] = new ParagraphRenderer().Append;

And now we get to our desired usage:

var html = new StringBuilder();

foreach (var item in document.Items)
{
    // get the renderer for this item type
    var renderer = renderers[item.GetType()];

    // append to the report
    renderer.Append(html, item);
}

This brings you to table items, which are interesting in your case, because they can apparently contain images and paragraphs as table cells. This means you could define them something like:

class TableItem : ITextualItem
{
    // note the recursion here: 
    // ITableItem is an IItem, and it contains a list of IItems,
    // meaning you can even have nested tables inside a single table cell    

    public List<IItem> HeaderCols { get; }
    public List<List<IItem>> Rows { get; }
}

And then the renderer is again interesting because it's a composite (recursive) renderer:

public class TableRenderer : BaseHtmlRenderer<TableItem>
{
    // we need to be able to render individual cells
    private Dictionary<Type, Action<StringBuilder, object>> _renderers;

    public override void Append(StringBuilder html, TableItem element)
    {
        RenderHeaderRowStart(html);        
        foreach (var col in element.HeaderCols)
        {
            var cellRenderer = _renderers[col.GetType()];
            cellRenderer.Append(html);
        }
        RenderHeaderRowEnd(html);

        ...
    } 
}

Which means you need to pass the dictionary to the renderer constructor:

// renderers dictionary contains a TableRenderer, which also keeps
// a reference to this same dictionary
renderers[typeof(TableItem)] = new TableRenderer(renderers).Append;
8
Sefe On

The only thing that your extension method DataUnitModelExtensions.GetElement does is to make sure that the data contained in DataUnitModel.Element is either Image, string or null. Judging from your question, that is the only type of data this property can contain. So your extension method essentially does nothing useful. You are using two as casts in there which are great for typecasting, but then you return an object which makes your typecasts useless.

If you want to expose the content of the Element field, use two different properties that return Image and string. Since you are defing the DataUnitModel class yourself, there is no need for extension methods:

public class DataUnitModel
{
    public object Element { get; set; }

    public string ElementText { get { return Element as string; } }

    public Image ElementImage { get { return Element as Image; } }

    public Type ElementType { get; set; }
}

If you want to get the element type, you can use GetType for your ElementType property, although some simple bool properties would do the job:

public class DataUnitModel
{
    public object Element { get; set; }

    public string ElementText { get { return Element as string; } }

    public Image ElementImage { get { return Element as Image; } }

    public Type ElementType { get { Element != null ? Element.GetType() : null; }

    public bool IsElementText { get { return Element is string; } }

    public bool IsElementImage { get { return Element is Image; } }
}
1
Carra On

You could use an interface:

public interface IDataUnitModel{}
public class StringDataUnitModel : IDataUnitModel
{
  public string Value {get;set;}
}
public class ImageDataUnitModel : IDataUnitModel
{
  public Image Value {get;set;}
}
//...
public List<IDataUnitModel> list = new List<IDataUnitModel>();
list.Add(new StringDataUnitModel{Value = "ABC"});
list.Add(new ImageDataUnitModel());

And while retrieving you can check the type of object. You also can only put in images & strings.

If the table can only contain images or strings you could use generics.