diff --git a/src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs b/src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs index 1759c9412c..7e25625a11 100644 --- a/src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs +++ b/src/Framework/Framework/Runtime/DefaultDotvvmViewBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using DotVVM.Framework.Binding; @@ -107,7 +108,7 @@ private void PerformMasterPageComposition(DotvvmView childPage, DotvvmView maste var placeHolders = GetMasterPageContentPlaceHolders(masterPage); // find contents - var contents = GetChildPageContents(childPage, placeHolders); + var (contents, auxControls) = GetChildPageContents(childPage, placeHolders); // perform the composition foreach (var content in contents) @@ -133,7 +134,10 @@ private void PerformMasterPageComposition(DotvvmView childPage, DotvvmView maste content.SetValue(Internal.MarkupFileNameProperty, childPage.GetValue(Internal.MarkupFileNameProperty)); content.SetValue(Internal.ReferencedViewModuleInfoProperty, childPage.GetValue(Internal.ReferencedViewModuleInfoProperty)); } - + + foreach (var control in auxControls) + masterPage.Children.Add(control); + // copy the directives from content page to the master page (except the @masterpage) masterPage.ViewModelType = childPage.ViewModelType; } @@ -160,11 +164,30 @@ private List GetMasterPageContentPlaceHolders(DotvvmControl /// /// Checks that the content page does not contain invalid content. /// - private List GetChildPageContents(DotvvmView childPage, List parentPlaceHolders) + private (List contents, List auxiliaryControls) GetChildPageContents(DotvvmView childPage, List parentPlaceHolders) { - // make sure that the body contains only whitespace and Content controls - var nonContentElements = - childPage.Children.Where(c => !((c is RawLiteral && ((RawLiteral)c).IsWhitespace) || (c is Content))); + // make sure that the body contains only Content controls (and whitespace and auxiliary controls) + var nonContentElements = new List(); + // controls which don't render anything and may be placed anywhere (RequireResource) + var auxiliaryControls = new List(); + foreach (var child in childPage.Children) + { + if (child is RawLiteral { IsWhitespace: true }) + { + // ignored + } + else if (child is RequiredResource) + { + child.Parent = null; // childPage view is discarded + auxiliaryControls.Add(child); + } + else if (child is Content) + { + // handled bellow + } + else + nonContentElements.Add(child); + } if (nonContentElements.Any()) { // show all error lines @@ -182,10 +205,10 @@ private List GetChildPageContents(DotvvmView childPage, List c.Parent != childPage) is {} invalidContent) { - throw new DotvvmControlException(invalidContent, "The control cannot be placed inside any control!"); + throw new DotvvmControlException(invalidContent, $"The control cannot be placed inside any control!"); } - return contents; + return (contents, auxiliaryControls); } } } diff --git a/src/Framework/Testing/ControlTestHelper.cs b/src/Framework/Testing/ControlTestHelper.cs index 13e1fad9b3..75ece29cce 100644 --- a/src/Framework/Testing/ControlTestHelper.cs +++ b/src/Framework/Testing/ControlTestHelper.cs @@ -134,7 +134,7 @@ public async Task RunPage( ClaimsPrincipal? user = null, CultureInfo? culture = null) { - if (!markup.Contains("\n{markup}\n{(renderResources ? "" : "")}\n"; } @@ -142,7 +142,7 @@ public async Task RunPage( { markup = "" + markup; } - if (!markup.Contains("\n{markup}"; } diff --git a/src/Tests/ControlTests/ServerSideStyleTests.cs b/src/Tests/ControlTests/ServerSideStyleTests.cs index a448cf161a..8ce8c00504 100644 --- a/src/Tests/ControlTests/ServerSideStyleTests.cs +++ b/src/Tests/ControlTests/ServerSideStyleTests.cs @@ -443,6 +443,67 @@ @viewModel int check.CheckString(r.FormattedHtml, fileExtension: "html"); } + + + [TestMethod] + public async Task AddResourceWithMasterPage() + { + var cth = createHelper(c => { + + c.Resources.Register("test-resource1", new InlineScriptResource("alert(1)")); + c.Resources.Register("test-resource2", new InlineScriptResource("alert(2)")); + c.Resources.Register("test-resource3", new InlineScriptResource("alert(3)")); + c.Resources.Register("test-resource4", new InlineScriptResource("alert(4)")); + c.Resources.Register("test-resource5", new InlineScriptResource("alert(5)")); + + c.Styles.RegisterRoot() + .AddRequiredResource(cx => { + return cx.Control.TreeRoot.Directives.TryGetValue("custom_resource_import", out var x) ? x.Select(d => d.Value).ToArray() : new string[0]; + }); + }); + + var r = await cth.RunPage(typeof(object), """ + + + real body + + + + """, + directives: """ + @masterPage master1.dotmaster + @custom_resource_import test-resource1 + """, + markupFiles: new Dictionary { + ["master1.dotmaster"] = """ + @viewModel object + @masterPage master2.dotmaster + @custom_resource_import test-resource2 + + + + + +
+ +
+
+ """, + ["master2.dotmaster"] = """ + @custom_resource_import test-resource3 + @viewModel object + + + + + + + + """ + }, renderResources: true); + check.CheckString(r.OutputString, fileExtension: "html"); + } + public class BasicTestViewModel: DotvvmViewModelBase { [Bind(Name = "int")] diff --git a/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.AddResourceWithMasterPage.html b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.AddResourceWithMasterPage.html new file mode 100644 index 0000000000..d3ebdf9598 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.AddResourceWithMasterPage.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + +
+ + real body + +
+ + + + + + + + + + + + +