Extending a table with extra columns

We promised to add some interesting and advanced samples on how the new PageBuilder can be extended to your needs.

Our third custom development example makes it possible to dynamically add columns to an existing table, depending on the data available in your database structure.

Suppose you are building a product that contains a description, an image, and a table. The table contains the SKUS of the product (each row is a SKU record). The columns in the table represent fields of these SKU records.

Take a look at following previews. The left image is an itemGroup built with the out of the box PageBuilder. As you can see there are three Field columns in the table, because the template contained three columns.
The image on the right is the result of the custom engine we will discuss below. The table now consists of 3 extra columns, added by the custom code.
Let's take a look how to accomplish this.

      

We are building ItemGroups, so what we need is a custom ItemGroupBuilder:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Adam.Core;
using Adam.PageBuilder.Core.Build;

namespace ExtendedModelEngine
{
    class ExtendedModelItemGroupBuilder : ItemGroupBuilder
    {
        public ExtendedModelItemGroupBuilder(Application application)
            : base(application)
        {
            // Pass the ItemGroupBuilder, so we can access its properties from 
            // within the ResolveAction.
            RecordResolveAction = new ExtendedModelResolveAction(application, this);
        }
    }
}

For this functionality we will add additional information while resolving, so we need to override the RecordResolveAction.

We want to override the OnResolve method with the TableTarget of this action. As there is only one table in our design, we do not need to check if this is the correct table.
It's important to know that we want to add our additional columns, after the whole table was filled. Therefore, the OnResolve method of the base class must be called first.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Adam.Core;
using Adam.Core.Fields;
using Adam.Core.Records;
using Adam.Tools.ExceptionHandler;
using Adam.DocMaker.Core;
using Adam.PageBuilder.Core.Extensions;
using Adam.PageBuilder.Core.Resolve;

namespace ExtendedModelEngine
{
    private class ExtendedModelResolveAction : RecordResolveAction
    {
        private readonly ExtendedModelItemGroupBuilder _builder;
        private string _linkFieldName;
        private string _caseFieldName;

        public ExtendedModelResolveAction(Application application, ExtendedModelItemGroupBuilder builder)
            : base(application)
        {
            _builder = builder;
        }

        protected override void OnResolve(ResolveTarget<Table> tableTarget)
        {
            base.OnResolve(tableTarget);

            _linkFieldName = XDocument.Parse(_builder.Tag).Elements("linkField").Single().Value;
            _caseFieldName = XDocument.Parse(_builder.Tag).Elements("caseField").Single().Value;
            FieldDefinitionHelper helper = new FieldDefinitionHelper(App);

            // Select all body rows
            Table table = tableTarget.Item;

            // Find the new column titles and the rows that must be filled in.
            SortedDictionary<string, List<int>> extraColumns = FindNewColumnsInformation(table);

            // Create and fill the new columns.
            CreateNewColumns(table, extraColumns);

            // Adjust the width of all columns.
            AdjustColumnWidths(table, extraColumns.Count);
        }
    }
}

We have added the possibility to provide your custom ItemGroupBuilder with a Tag. This tag (a String) can contain anything you want. For this demo we have chosen for a xml format that contains all fieldNames necessary for extending our table. Let's take a look to the xml structure:

XML
1
2
3
4
<fields>
  <linkField>Cases</linkField>
  <caseField>CaseName</caseField>
</fields>

The first field 'Cases' is a RecordLinkField. The product record contains this field, and has 0, 1, 2, 3 or 4 links. Each link has the TextField 'CaseName'. We will use the name of the case as the header of a new column. Suppose there are 4 different cases (maximum 4 links), and the possibilities are Gigbag, Soft case, Hard case and Deluxe case. Each guitar model (SKUs) may have a case, or can have more than 1 available cases. When no model of this guitar has a particular case, the column will not be shown. In the example above, you can see that this guitar has no model with a Deluxe casing.
When a model has a case available, this will be shown by a symbol in the corresponding row/column.

As you can see in the above code sample, there are three submethods. SortedDictionary will collect all information necessary for adding the columns. Which columns are needed, and what models support each case.
CreateNewColumns will create the necessary columns and enter the correct information.
AdjustColumnsWidths recalculates the width of the columns. This is needed because when a column is added in InDesign, the table will not get bigger, but the available table space will be devided between the number of columns. We want to give all the columns our own width, because the new columns need less space than the existing columns.

We start with collecting all necessary info. We are using a SortedDictionary with the case name as a key. In this way the columns will also be sorted by case name. The value of the SortedDictionary contains a List with the indices of body rows in the table. Only the models that have a specific case will be added in this list.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private SortedDictionary<string, List<int>> FindNewColumnsInformation(Table table)
{
    // Only select the body rows of the table
    Row[] bodyRows = table.Rows.Where(r => r.RowType == RowType.Body).ToArray();

    SortedDictionary<string, List<int>> newColumns = new SortedDictionary<string, List<int>>();
    for (int rowIndex = 0; rowIndex < bodyRows.Length; rowIndex++)
    {
        // The run must contain a record.
        Run run = (Run)bodyRows[rowIndex].Cells.First().Paragraphs.First().Items.First();
        Guid? linkId = run.GetExtractedRecordId();

        // Load the record and get the field value.
        Record record = new Record(App);
        record.Load(linkId.Value);

        FieldContainer modelsLinkField = record.Fields[_linkFieldName];
        RecordLinkField field = (RecordLinkField)modelsLinkField.MyLanguage;

        Record modelRecord = new Record(App);
        foreach (RecordLinkItem item in field.Children)
        {
            modelRecord.Load(item.RecordId);
            FieldContainer modelsField = modelRecord.Fields[_caseFieldName];

            string modelName = ((TextField)modelsField.MyLanguage).Value;
            // If our dictionary does not contain the new column, add to dictionary.
            if (!newColumns.ContainsKey(modelName))
            {
                newColumns.Add(modelName, new List<int>());
            }
            // Add the index of current row.
            newColumns[modelName].Add(rowIndex);
        }
    }
    return newColumns;
}

Once the information is complete, we can start by adding the columns to the table. For each key in the SortedDictionary a column is added. In every cell, a single Paragraph with a single Run must be created. A header Run contains the key, the Runs in a body row contain a special character. In this case a bullet in the 'Windings 2' font.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static void CreateNewColumns(Table table, SortedDictionary<string, List<int>> extraColumns)
{
    Row[] bodyRows = table.Rows.Where(r => r.RowType == RowType.Body).ToArray();

    Document document = table.OwnerDocument;
    foreach (string columnName in extraColumns.Keys)
    {
        table.AddColumn();

        // Fill the columnName in the header of the column.
        Row headerRow = table.Rows.Where(r => r.RowType == RowType.Header).Single();
        if (headerRow != null)
        {
            // As an example we are creating a paragraph from a template. In this way the formatting 
            // and the contents of the template will be copied to our new one. So we only need to replace
            // the value of the Run instead of creating it.
            Paragraph paragraph = document.CreateParagraph(headerRow.Cells.First().Paragraphs.First());
            ((Run)paragraph.Items.Single()).Value = columnName;
            headerRow.Cells.Last().Paragraphs.Add(paragraph);
        }

        // Every row in the column that contains this option is given a special Wingdings character.
        foreach (int rowIndex in extraColumns[columnName])
        {
            Paragraph paragraph = document.CreateParagraph();
            ParagraphFormatting formatting = bodyRows[rowIndex].Cells.First().Paragraphs.First().Formatting;
            paragraph.Formatting.Alignment = formatting.Alignment;

            Run run = document.CreateRun();
            run.Value = Char.ConvertFromUtf32('\u0050');
            run.Formatting.FontFamily = "Wingdings 2";
            run.Formatting.Bold = true;
            paragraph.Items.Add(run);

            bodyRows[rowIndex].Cells.Last().Paragraphs.Add(paragraph);
        }
    }
}

As a last step we need to resize the columns. The new columns are given a fixed width, the original columns are recalculated.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void AdjustColumnWidths(Table table, int columnCount)
{
    // The original columns need to be resized to fit the contents,
    // the new columns are given the value of 30 points.
    const int newColumnWidth = 30;
    int originalColumns = table.ColumnCount - columnCount;
    int newColumns = columnCount;

    double resizedColumnWidth = (table.Width - (newColumns * newColumnWidth)) / originalColumns;

    // Start with existing columns
    for (int i = 0; i < originalColumns; i++)
    {
        table.SetColumnWidth(i, resizedColumnWidth);
    }
    // New columns
    for (int i = originalColumns; i < table.ColumnCount; i++)
    {
        table.SetColumnWidth(i, newColumnWidth);
    }
}

This ends our third PageBuilder 4.0 Blog post. As a wrap up we'd like to remind you that you can also extend the Tag information in this code. It is by example possible to add the fontFamily and the character code to the xml to make it possible to change the 'bullets' in the columns. In this way different combinations can be tested with small effort...

Comments

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed