Skip to content

Support ContentPlaceHolder inside CompositeControl templates (deferred master page composition)#2017

Draft
Copilot wants to merge 7 commits intomainfrom
copilot/support-contentplaceholders-in-templates
Draft

Support ContentPlaceHolder inside CompositeControl templates (deferred master page composition)#2017
Copilot wants to merge 7 commits intomainfrom
copilot/support-contentplaceholders-in-templates

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 12, 2026

ContentPlaceHolder couldn't be used inside CompositeControl templates because GetContents runs in the Load phase, after master page composition (which runs during BuildView, before Init).

Changes

Framework

  • DefaultDotvvmViewBuilder: Instead of throwing immediately when a ContentPlaceHolder ID is not found during PerformMasterPageComposition, unmatched Content controls are deferred into a shared List<PendingMasterPageComposition> stored on the root DotvvmView. Works correctly across nested master page hierarchies.

  • ContentPlaceHolder.OnInit: Added ResolvePendingComposition() — when a ContentPlaceHolder is added to the tree (including as a missed lifecycle event during Load), it traverses ancestors to find the pending list and performs the deferred composition. Uses FindLastIndex so that when the same ContentPlaceHolderID is reused at multiple master page levels, each placeholder correctly matches its corresponding content (outermost placeholder ↔ outermost content).

  • DotvvmPresenter: Added ValidateMasterPageComposition() called after the Load phase on both GET and postback paths. If any Content controls remain unmatched at that point, a descriptive exception is thrown.

  • Internal: Added PendingMasterPageCompositionsProperty and PendingMasterPageComposition (holds the Content, its DataContextStack, and master page file for error messages).

Usage Example

{{!-- Root.dotmaster --}}
<cc:TemplateContainerControl>
    <ContentTemplate>
        <dot:ContentPlaceHolder ID="MainContent" />  {{!-- now works inside templates --}}
    </ContentTemplate>
</cc:TemplateContainerControl>

Where TemplateContainerControl is a CompositeControl that instantiates the template in GetContents (Load phase).

Samples & Tests

  • Added LateContentPlaceHolders feature sample with a 3-level master page hierarchy (Root.dotmasterNested.dotmasterContent.dothtml) where each ContentPlaceHolder lives inside a CompositeControl template.
  • Added NestedSameId.dotmaster + SharedIdContent.dothtml — demonstrates and tests the case where the same ContentPlaceHolderID ("RootContent") is reused in both the root and the middle master page.
  • Added ContentWithDefault.dothtml — tests that when no Content is provided for a ContentPlaceHolder, its own default children are rendered.
  • Added MismatchedContent.dothtml — a content page with a Content whose ContentPlaceHolderID has no match anywhere, verifying the post-Load error is thrown.
  • Added Selenium tests for all four cases in MasterPageTests.

Copilot AI and others added 2 commits April 12, 2026 14:25
…ster page composition)

Agent-Logs-Url: https://github.com/riganti/dotvvm/sessions/ca479579-7532-44bd-96e4-d8638c2d5130

Co-authored-by: tomasherceg <5599524+tomasherceg@users.noreply.github.com>
Agent-Logs-Url: https://github.com/riganti/dotvvm/sessions/ca479579-7532-44bd-96e4-d8638c2d5130

Co-authored-by: tomasherceg <5599524+tomasherceg@users.noreply.github.com>
Copilot AI changed the title [WIP] Allow ContentPlaceHolder usage in templates for master pages Support ContentPlaceHolder inside CompositeControl templates (deferred master page composition) Apr 12, 2026
Copilot AI requested a review from tomasherceg April 12, 2026 14:31
tomasherceg and others added 2 commits April 12, 2026 17:03
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
@tomasherceg
Copy link
Copy Markdown
Member

I fixed some build errors.
@copilot Can you add another sample that would use the same ContentPlaceHolderId in the root and in the middle master page?
Also, we should test the case that if no content is specified for a ContentPlaceHolder, its own content would be used as default.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

I fixed some build errors.
@copilot Can you add another sample that would use the same ContentPlaceHolderId in the root and in the middle master page?
Also, we should test the case that if no cont...

Done in commit c2ec3b6. Here's what was added:

Same ContentPlaceHolderID in root and nested master (NestedSameId.dotmaster + SharedIdContent.dothtml): Both Root.dotmaster and NestedSameId.dotmaster use ID="RootContent". The content page fills the innermost one.

This also required a bug fix: when the same ID appears at multiple levels, the pending list contains multiple entries with the same ID. The original FindIndex would pick the wrong (inner) entry for the outer placeholder. Changed to FindLastIndex so the outermost ContentPlaceHolder correctly matches the outermost Content (items are added inner-first, so the outermost is last).

Default content (ContentWithDefault.dothtml): Uses Nested.dotmaster but provides no Content for the NestedContent placeholder. Added default children to that placeholder in Nested.dotmaster (<p data-ui="default-nested-content">Default nested content</p>). The default content is rendered when no matching Content is found (the Children.Clear() only happens when a pending composition is matched).

Selenium tests added for both cases.

});
}

[Fact]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move these tests to a separate class (LateContentPlaceHoldersTests) to keep the current convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 84eb337. All four LateContentPlaceHolders tests have been moved to a new LateContentPlaceHoldersTests class in LateContentPlaceHoldersTests.cs, and MasterPageTests now contains only the original nested master pages test.

…rsTests class

Agent-Logs-Url: https://github.com/riganti/dotvvm/sessions/d304bd6d-2bdb-4b99-97c5-89777d4241b4

Co-authored-by: tomasherceg <5599524+tomasherceg@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<dot:ContentPlaceHolder> should be supported even inside templates

2 participants