Inconsistent behavior with UpdatePageLayout() when using tables with different column counts

Dear support team,

we’re encountering an inconsistent behavior when calling Aspose.Words.Document.UpdatePageLayout(), when the document contains 2 tables of different column counts back to back like so:

--------------------------------------------
| table 1 - single column                  |
--------------------------------------------
| table 2 - column  1 | table 2 - column 2 |
--------------------------------------------

The above representation is what we desire; currently we’re getting the following output though:

---------------------------
| table 1 - single column |
-----------------------------------------------
| table 2 - column  1     | table 2 - column 2 |
-----------------------------------------------

Both tables explicitly specify their Cell.CellFormat.PreferredWith values to add up to 100%; 100% for each cell of table 1 and 2x 50% for table 2.

Below is the source code of our test cases:

private static String Working_Directory = @".\";

private static Aspose.Words.Document BuildDocument()
{
    // Set Aspose words licence:
    //AsposeLicenceHelper.SetLicence_Words();

    var doc = new Aspose.Words.Document();
    var builder = new Aspose.Words.DocumentBuilder(doc);

    // First row with 100% width
    var table = builder.StartTable();
    var cell = builder.InsertCell();
    cell.CellFormat.PreferredWidth = Aspose.Words.Tables.PreferredWidth.FromPercent(100);
    builder.Write("row 1 - single cell");
    builder.EndTable();

    // Last row with 2 cells 50% width each
    table = builder.StartTable();
    cell = builder.InsertCell();
    cell.CellFormat.PreferredWidth = Aspose.Words.Tables.PreferredWidth.FromPercent(50);
    builder.Write("row 2 - cell 1");
    cell = builder.InsertCell();
    cell.CellFormat.PreferredWidth = Aspose.Words.Tables.PreferredWidth.FromPercent(50);
    builder.Write("row 2 - cell 2");
    builder.EndTable();

    return doc;
}

[Test]
public static void GenerateTableWithDifferentCellWidths_Test1()
{
    var doc = BuildDocument();

    doc.Save(Working_Directory + "Test1_working.docx", Aspose.Words.SaveFormat.Docx);
}

[Test]
public static void GenerateTableWithDifferentCellWidths_Test2()
{
    var doc = BuildDocument();

    doc.UpdatePageLayout();
    doc.Save(Working_Directory + "Test2_broken.docx", Aspose.Words.SaveFormat.Docx);
}

[Test]
public static void GenerateTableWithDifferentCellWidths_Test3()
{
    var doc = BuildDocument();

    doc.Save(Working_Directory + "Test3_working1.docx", Aspose.Words.SaveFormat.Docx);
    doc.UpdatePageLayout();
    doc.Save(Working_Directory + "Test3_working2.docx", Aspose.Words.SaveFormat.Docx);
}

[Test]
public static void GenerateTableWithDifferentCellWidths_Test4()
{
    var doc = BuildDocument();

    doc.UpdatePageLayout();
    doc.Save(Working_Directory + "Test4_broken1.docx", Aspose.Words.SaveFormat.Docx);
    doc.Save(Working_Directory + "Test4_broken2.docx", Aspose.Words.SaveFormat.Docx);
}

Although Test1 works as expected, our production code relies on calling doc.UpdatePageLayout(); for various other reasons, so this won’t be an acceptable solution to us.

Test 2 is the closest test case to our production code, which breaks by only calling doc.UpdatePageLayout();.

Testcases 3 and 4, we’ve stumbled upon by accident; but the results might still be relevant/helpful in your analysis. Both call doc.Save twice; but with different positioning of the doc.UpdatePageLayout();. Proper output in case 3 - for both doc.Save-calls, when you call the first doc.Save prior to doc.UpdatePageLayout(); and the second one after that. Calling doc.UpdatePageLayout(); in the beginning though breaks the output for both of the following doc.Save-calls.

The breaking change must have occurred in Aspose.Words 25.01, because 24.12 is the last version, where all of the above test cases yield the expected result.

Kind regards
Matthias

1 Like

@M.Heinz

Hi Matthias,

It sounds like you’re experiencing an issue with the UpdatePageLayout() method when dealing with tables of different column counts in your document. This inconsistency can indeed be frustrating, especially since it worked as expected in the previous version.

Cause: The issue may stem from the way the UpdatePageLayout() method interacts with the table properties and the timing of its execution.

Solution: Here are a few steps you can take to troubleshoot and potentially resolve the issue:

  1. Check Table Properties: Ensure that both tables have their Cell.CellFormat.PreferredWidth set correctly. You mentioned that they are set to 100% for the first table and 50% for each cell in the second table, which is correct. However, double-check that there are no conflicting settings elsewhere in your document.

  2. Update Page Layout Timing: Since you noted that calling UpdatePageLayout() at different points in your test cases yields different results, try to isolate the timing of this call. It may help to call it after all modifications to the document are complete, just before saving.

  3. Version Compatibility: Since you identified that the issue arose after upgrading to version 25.01, consider reverting to 24.12 for your production code if feasible. This can help confirm if the new version introduced the inconsistency.

  4. Testing with Simplified Document: Create a minimal version of your document with just the two tables and see if the issue persists. This can help determine if other elements in your document are affecting the layout.

  5. Contact Support: If the issue continues, it may be beneficial to reach out to Aspose support directly with your findings. They can provide more targeted assistance, especially if this is a bug introduced in the latest version.

Next Steps: Please try the above steps and let me know if they help resolve the issue. If you continue to experience problems, feel free to share any additional details, and I can assist you further.

Best regards!

@Professionalize.Discourse Thanks for nothing - as usual :laughing:.

@M.Heinz
We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): WORDSNET-28535

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

@alexey.noskov
We’re currently considering rolling back our Aspose.Words version to prevent this issue.

As we’ve unfortunately experienced performance-issues earlier this year (with the version 25.02) version due to this bug, I’d like to ask, if you know which Aspose.Words version would be the latest, that’s not affected by the infinite-loop bug, but also still generates the tables without any issue (=> max. version 24.12).

@M.Heinz The problem you have mentioned occurred because of an infinite loop introduced after implanting character spacing control feature. This feature has been introduced in 25.2 version. So there should not be the problem in versions prior 25.2 version.

Thanks so much for your information!

1 Like

@M.Heinz The issue occurs because the your code relies on a trick that merges consecutive table into a single table, and the trick happens to produce an acceptable result in some scenarios, but not others. You should modify your code.

The code actually creates not one but two consecutive tables. There is MS Word behavior that merges such tables into a single table. Aspose.Words tries to imitate that behavior.
The logic that merges the tables relies on known table column widths. The problematic tables are auto-fit tables, and column widths are normally computed when building document layout (as actual column widths depend on the content metrics not known at the document model level). Here lies the problem:

When the document is saved to docx without updating document layout first, document validation logic sees that no column widths are computed by layout. The widths are needed to produce a valid docx. So validation codes employs a simplified version of table layout that does not work very well in general case, but produces some column widths that are better than none. The logic is applied to each table independently, before the tables are merged. As the tables are very simple, the computed widths are enough to produce the expected result when the tables are combined and saved to .docx.
If document layout is built first, validation code does not try to compute table column widths independently. The tables are just merged using the default cell width for all cells, producing a jagged table where the cell in row 1 spans only the first column. As the cell has preferred width 100%, the column occupies most of the available space.
The table that layout operates on is equivalent to a table generated by the following code. Basically it is your code but instead of two consecutive tables it generates two rows in a single table. Otherwise, the code is the same:

// First row with 100% width
Table table = builder.StartTable();
Cell cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(100);
builder.Write("row 1 - single cell");
builder.EndRow();

// Last row with 2 cells 50% width each
// table = builder.StartTable();
cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(50);
builder.Write("row 2 - cell 1");
cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(50);
builder.Write("row 2 - cell 2");
builder.EndTable();

Using this code with your tests will consistently generate a jagged table, both with or without layout update, as the code does not employ “consecutive table merge trick”.

It sounds complex, because it actually is complex.

In order to get the desired result, the code should indicate that the cell in row 1 spans 2 columns. This can be done by adding a merged cell. The below code will consistently generate the desired table for all your tests, as it does not rely on “consecutive table merge trick”:

// First row with 100% width
Table table = builder.StartTable();
Cell cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(100);
builder.Write("row 1 : 2 merged cells");
cell.CellFormat.HorizontalMerge = CellMerge.First;

// Add a merged cell to maintain the regular table structure.
cell = builder.InsertCell();
cell.CellFormat.HorizontalMerge = CellMerge.Previous;

builder.EndRow();

// Last row with 2 cells 50% width each
cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(50);
builder.Write("row 2 - cell 1");
cell = builder.InsertCell();
cell.CellFormat.PreferredWidth = PreferredWidth.FromPercent(50);
builder.Write("row 2 - cell 2");
builder.EndTable();

@alexey.noskov What do you mean by the following statement?

I can update the table.AutoFit to any available enum-value and the results are only getting worse, if I’m not using the default (presumably AutoFitBehavior.AutoFitToContents).

I’ve tried setting the AutoFit behavior to the only non-AutoFit* value (for both tables), but that broke even those cases, which were working previously:

table.AutoFit(AutoFitBehavior.FixedColumnWidths); // new code
table.PreferredWidth = PreferredWidth.FromPercent(100);  // new code
builder.EndTable();

Other than that, your suggested solution unfortunately doesn’t satisfy our expectations for two reasons:

  1. Up until (and including) Aspose.Words 24.12, our code was working just fine and did yield the expected result of correctly auto-merging the the tables without any of our intervention required.
  2. Of course, the sample I’ve submitted to you was highly simplified. In reality our customers are coming up with the total number of rows/tables and an arbitrary number of columns/cells for each table/row.
    Simply imagine having 3 tables with 1, 2 and 3 cells respectively. To have those cells take up 100%, 50% and 33% respectively, I’d need to have 6 cells total in each row; first row merging all 6 cells into one; second row merging cells 1-3 and 4-6 and for the final row merge cells 1+2, 3+4 and 5+6.
    And this issue only worsens if you have 1, 2 and 5 cells per row or increase the number of rows with unique amounts of cell counts even more…

Why would we need to deal with this manually now, when it was working just fine previously?

We’re fine with the table(s) taking up 100% of the page’s width (except for the margins of course) and we’re setting the cell widths for each row to 100% / number_of_cells anyways; thus we wouldn’t need any dynamic width computation here.

Are there any other settings, that we’re missing right now, that would help us achieve this outcome?

Kind regards

@M.Heinz

I mean that the table cell width are not explicitly specified in your code.

I will forward your concerns to our development team.

For now, as a workaround, I can suggest only to save/open document to DOCX before rendering to force Aspose.Words to perform document validation and merge the tables.

I’m a bit confused:

  • For one, the comment on the cell.CellFormat.Width property basically states, that it’s not recommended to be used as a setter.
  • Secondly, setting e.g. cell.CellFormat.Width = PreferredWidth.FromPercent(50).Value; doesn’t seem to work either. Maybe because the Width property is expecting a size in points!? But how’d I convert 50% in the respective point count?

Could you please give me a hint as to how to set the width of cells explicitly?

@M.Heinz

This comment is applicable for existing table cells in document created by MS Word. But in your case you are generating tables from scratch. So you can safely use CellFormat.Width property to set cell width.

You can calculate absolute value in points. For example see the following code:

PageSetup ps = doc.FirstSection.PageSetup;
double pageWidth = ps.PageWidth - ps.LeftMargin - ps.RightMargin;

// Calculate width of cell that is 50% of page width.
double cellWidth = pageWidth * 0.5;

I’m sorry, but I still don’t see as to how AutoFitBehavior.FixedColumnWidths’ changes anything regarding this issue:

I’ve added the fixed column width statements and set all the cell.Widths accordingly, but still the output generates as described initially. (Only difference being, that it now exceeds the page’s width.)

private static Aspose.Words.Document BuildDocument()
{
    // Set Aspose words licence:
    AsposeLicenceHelper.SetLicence_Words();

    var doc = new Aspose.Words.Document();
    var builder = new DocumentBuilder(doc);

    Aspose.Words.PageSetup ps = doc.FirstSection.PageSetup;
    double pageWidth = ps.PageWidth - ps.LeftMargin - ps.RightMargin;

    // First row with 100% width
    var table = builder.StartTable();
    var cell = builder.InsertCell();
    table.AutoFit(AutoFitBehavior.FixedColumnWidths);
    cell.CellFormat.Width = pageWidth * 1; // 100%
    builder.Write("row 1 - only cell");
    builder.EndTable();

    // Second row with 2 cells 50% width each
    table = builder.StartTable();
    cell = builder.InsertCell();
    table.AutoFit(AutoFitBehavior.FixedColumnWidths);
    cell.CellFormat.Width = pageWidth * 0.5; // 50%
    builder.Write("row 2 - cell 1");
    cell = builder.InsertCell();
    cell.CellFormat.Width = pageWidth * 0.5; // 50%
    builder.Write("row 2 - cell 2");
    builder.EndTable();

    return doc;
}

// The test cases remain unchanged.

And replacing the following snippet by builder.EndRow(); doesn’t make any difference, as long, as I’m not generating the proper amount of cells per table/row; which in this (highly simplified) case would be 2 (and configuring them as merged in the first table/row to yield the desired output).

    builder.EndTable();

    // Second row with 2 cells 50% width each
    table = builder.StartTable();

But as stated previously: I’m not eager to generate, configure and properly merge the cells, when dealing with e.g. rows of 3, 4 and 5 columns/cells respectively.

@M.Heinz table.AutoFit(AutoFitBehavior.FixedColumnWidths); method should be called when the table is already built, i.e. after builder.EndTable();. this method recalculates table cell widths according to the specified AutoFitBehavior. So calling this method before finishing the table is not quite correct.

@alexey.noskov Thanks so far; this works now identical for all test cases:

private static Aspose.Words.Document BuildDocument()
{
    // Set Aspose words licence:
    AsposeLicenceHelper.SetLicence_Words();

    var doc = new Aspose.Words.Document();
    var builder = new DocumentBuilder(doc);

    Aspose.Words.PageSetup ps = doc.FirstSection.PageSetup;
    double pageWidth = ps.PageWidth - ps.LeftMargin - ps.RightMargin;

    var table = builder.StartTable();

    for (int rowIdx = 1; rowIdx <= 5; rowIdx++)
    {
        for (var cellIdx = 1; cellIdx <= rowIdx; cellIdx++)
        {
            var cell = builder.InsertCell();
            cell.CellFormat.Width = pageWidth / rowIdx; // total width divided by number of cells per row
            builder.Write($"row {rowIdx} - cell {cellIdx}");
        }
        builder.EndRow();
    }

    builder.EndTable();
    table.AutoFit(AutoFitBehavior.FixedColumnWidths);

    return doc;
}

Well, kind of… :rofl: Care to explain, what happens in row #4?

|1...................................|
|1................|2.................|
|1..........|2...........|3..........|
|1........|2......|3........|4...............|
|1.....|2.....|3......|4.....|5......|

Test1_working.docx (7.4 KB)

@M.Heinz Feedback from development team:

The behavior you rely on is an undocumented side affect of a rather strange and probably unintentional sequence of steps happening when saving a table constructed by their code to .docx.

You have a point though that manually computing cell merges in your scenario is not practical.

We suggest to modify your code as follows to specify the required actions explicitly.

The code below will produce the desired results without the need for manual cell merge or grid span assignment:

public static void Test28535()
{
    Document doc = new Document();
    DocumentBuilder builder = new DocumentBuilder(doc);

    // Make a table with a single row and 3 cells.
    // The cells will have widths computed from their preferred widths assigned.
    Table firstTable = Test28535AddTable(builder, 3);
    // 5 cells:
    Table t5 = Test28535AddTable(builder, 5);
    // 4 cells:
    Table t4 = Test28535AddTable(builder, 4);

    // Copy all tables rows to a single table.
    MergeTablesPrimitive(firstTable, t5);
    MergeTablesPrimitive(firstTable, t4);

    // This will produce correct cell column counts from the cell widths
    // computed when individual tables were created.
    firstTable.AutoFit(AutoFitBehavior.FixedColumnWidths);
    // All cells will get fixed preferred width
    // computed from the current cell width.
    // Cell preferred in percent units will be replaced with absolute values.
    // Also, table width will be determined by cell widths.
    // The table will not have auto-fit layout (cell widths will not adapt to contents).

    // (Optional) This will make the resulting table 100% again.
    firstTable.PreferredWidth = PreferredWidth.FromPercent(100.0);
    // The table will be resized on rendering or saving to docx.
    // Pdf will match docx output both with or without the above call.

    // Saving order will not matter.
    doc.Save("28535.aw.pdf");
    doc.Save("28535.aw.docx");
}

private static Table Test28535AddTable(DocumentBuilder builder, int columnCount)
{
    // Add the specified number of cells with the same preferred width.
    PreferredWidth preferred = PreferredWidth.FromPercent(100.0 / columnCount);

    Table table = builder.StartTable();
    for (int i = 0; i < columnCount; ++i)
    {
        Cell cell = builder.InsertCell();
        cell.CellFormat.PreferredWidth = preferred;

        // This is just for readability, not needed for the case to work.
        builder.Write(string.Format("{0}", preferred));
    }
    builder.EndTable();

    // This will assign equal cell widths according to the preferred widths.
    table.AutoFit(AutoFitBehavior.AutoFitToWindow);
    // The widths will not match MS Word precisely,
    // but it is actually what is needed to preserve their relations
    // with cells in other rows.

    return table;
}

private static void MergeTablesPrimitive(Table remaining, Table merged)
{
    // Just move all table rows from the second table to the end of the first.
    while (merged.FirstChild != null)
        remaining.AppendChild(merged.FirstChild);

    // And remove the empty table.
    merged.Remove();
}

The output of the above snippet is attached:
28535.aw.docx (5.8 KB)
28535.aw.pdf (32.4 KB)

1 Like

@alexey.noskov Thanks, that’s great to be able to use the PreferredWidth again.

Unfortunately, the code from your development team has the same flaw as the code from my previous post. If you either replace t5 with a table having 2 columns or simply add on an other table with 2 columns, you’ll get a rather strange output, where one line will be longer than the others for no apparent reason. In this example, it’s the first row being longer; in my previous post, it was affecting the 4th row.

/// <summary>
/// Swapped t5 for a t2: Instead of 5 cells in that table, just 2 cells.
/// </summary>
public static Document Test28535_modified()
{
    Document doc = new Document();
    DocumentBuilder builder = new DocumentBuilder(doc);

    Table firstTable = Test28535AddTable(builder, 3);
    Table t2 = Test28535AddTable(builder, 2);
    Table t4 = Test28535AddTable(builder, 4);

    MergeTablesPrimitive(firstTable, t2);
    MergeTablesPrimitive(firstTable, t4);

    firstTable.AutoFit(AutoFitBehavior.FixedColumnWidths);
    firstTable.PreferredWidth = PreferredWidth.FromPercent(100.0);

    //doc.Save(Working_Directory + "28535.aw.pdf");
    doc.Save(Working_Directory + "28535.aw.docx");
}

28535.aw.docx (17.9 KB)

If you, again with your code, merge tables with 1…7 cells respectively in that order, it’ll be the last row sticking out.

What’s up with that?

@M.Heinz Unfortunately, I cannot reproduce the problem on my side. Your code produces the following output on my side: out.docx (7.3 KB)

Here is fool code used for testing:

/// <summary>
/// Swapped t5 for a t2: Instead of 5 cells in that table, just 2 cells.
/// </summary>
public static void Test28535_modified()
{
    Document doc = new Document();
    DocumentBuilder builder = new DocumentBuilder(doc);

    Table firstTable = Test28535AddTable(builder, 3);
    Table t2 = Test28535AddTable(builder, 2);
    Table t4 = Test28535AddTable(builder, 4);

    MergeTablesPrimitive(firstTable, t2);
    MergeTablesPrimitive(firstTable, t4);

    firstTable.AutoFit(AutoFitBehavior.FixedColumnWidths);
    firstTable.PreferredWidth = PreferredWidth.FromPercent(100.0);

    //doc.Save(Working_Directory + "28535.aw.pdf");
    doc.Save(@"C:\Temp\out.docx");
}

private static Table Test28535AddTable(DocumentBuilder builder, int columnCount)
{
    // Add the specified number of cells with the same preferred width.
    PreferredWidth preferred = PreferredWidth.FromPercent(100.0 / columnCount);

    Table table = builder.StartTable();
    for (int i = 0; i < columnCount; ++i)
    {
        Cell cell = builder.InsertCell();
        cell.CellFormat.PreferredWidth = preferred;

        // This is just for readability, not needed for the case to work.
        builder.Write(string.Format("{0}", preferred));
    }
    builder.EndTable();

    // This will assign equal cell widths according to the preferred widths.
    table.AutoFit(AutoFitBehavior.AutoFitToWindow);
    // The widths will not match MS Word precisely,
    // but it is actually what is needed to preserve their relations
    // with cells in other rows.

    return table;
}

private static void MergeTablesPrimitive(Table remaining, Table merged)
{
    // Just move all table rows from the second table to the end of the first.
    while (merged.FirstChild != null)
        remaining.AppendChild(merged.FirstChild);

    // And remove the empty table.
    merged.Remove();
}
1 Like

I can run any of the 3 public functions to get a result, with different row lengths.

I’ve tried several combinations of:

  • Aspose.Words 25.07 and 25.08
  • .NET Framework 4.8 and .NET 8
  • UnitTest and Console-application

None of the tested combinations has yielded me a good result.

These files are from a console application, Aspose.Words 25.08, .NET 8 setup on Win10:
28535.docx (17.9 KB)
28535_modified.docx (18.0 KB)
Test28535_modified_2_rows_per_table.docx (18.1 KB)

And the entire project for the console app:
TestConsole.zip (5.5 KB)

@M.Heinz On my side the result is different. With your code only 28535_modified.docx looks incorrect. After removing the following line:

firstTable.AutoFit(AutoFitBehavior.FixedColumnWidths);

All the tables look fine:
28535.docx (17.9 KB)
28535_modified.docx (18.0 KB)
Test28535_modified_2_rows_per_table.docx (18.0 KB)

1 Like