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...