diff --git a/src/Framework/Framework/Controls/TemplateHost.cs b/src/Framework/Framework/Controls/TemplateHost.cs index c55b0c6e78..d83a8871f9 100644 --- a/src/Framework/Framework/Controls/TemplateHost.cs +++ b/src/Framework/Framework/Controls/TemplateHost.cs @@ -3,7 +3,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls { @@ -18,13 +21,38 @@ public class TemplateHost : DotvvmControl /// Gets or sets the template that will be rendered inside this control. /// [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.Attribute, Required = true)] - public ITemplate? ContentTemplate { get; set; } + public ITemplate? Template + { + get { return (ITemplate?)GetValue(TemplateProperty); } + set { SetValue(TemplateProperty, value); } + } + public static readonly DotvvmProperty TemplateProperty + = DotvvmProperty.Register(c => c.Template, null); + protected internal override void OnLoad(IDotvvmRequestContext context) { - ContentTemplate?.BuildContent(context, this); + var placeHolder = new PlaceHolder(); + Template.NotNull("TemplateHost.Template is required").BuildContent(context, placeHolder); + + // validate data context of the passed template + var myDataContext = this.GetDataContextType()!; + if (!CheckChildrenDataContextStackEquality(myDataContext, placeHolder.Children)) + { + throw new DotvvmControlException(this, "Passing templates into markup controls or to controls which change the binding context, is not supported!"); + } + + Children.Add(placeHolder); + base.OnLoad(context); } + + private bool CheckChildrenDataContextStackEquality(DataContextStack desiredDataContext, DotvvmControlCollection children) + { + return children.Select(c => c.GetDataContextType()) + .Where(t => t != null) + .All(t => Equals(t, desiredDataContext)); + } } } diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 0f25ec348a..2f7e400686 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -43,6 +43,9 @@ + + + diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index f9167a2710..0e50972e07 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -28,6 +28,7 @@ using DotVVM.Samples.Common.ViewModels.FeatureSamples.JavascriptTranslation; using DotVVM.Samples.Common.Views.FeatureSamples.PostbackAbortSignal; using DotVVM.Samples.Common.ViewModels.FeatureSamples.BindingVariables; +using DotVVM.Samples.Common.Views.ControlSamples.TemplateHost; namespace DotVVM.Samples.BasicSamples { @@ -195,8 +196,10 @@ private static void AddControls(DotvvmConfiguration config) config.Markup.AddMarkupControl("cc", "RecursiveTextRepeater2", "Views/FeatureSamples/PostBack/RecursiveTextRepeater2.dotcontrol"); config.Markup.AddMarkupControl("cc", "ModuleControl", "Views/FeatureSamples/ViewModules/ModuleControl.dotcontrol"); config.Markup.AddMarkupControl("cc", "Incrementer", "Views/FeatureSamples/ViewModules/Incrementer.dotcontrol"); + config.Markup.AddMarkupControl("cc", "TemplatedListControl", "Views/ControlSamples/TemplateHost/TemplatedListControl.dotcontrol"); + config.Markup.AddMarkupControl("cc", "TemplatedMarkupControl", "Views/ControlSamples/TemplateHost/TemplatedMarkupControl.dotcontrol"); + config.Markup.AddCodeControls("cc", typeof(CompositeControlWithTemplate)); config.Markup.AddCodeControls("cc", typeof(Loader)); - config.Markup.AddMarkupControl("sample", "EmbeddedResourceControls_Button", "embedded://EmbeddedResourceControls/Button.dotcontrol"); config.Markup.AutoDiscoverControls(new DefaultControlRegistrationStrategy(config, "sample", "Views/")); diff --git a/src/Samples/Common/ViewModels/ControlSamples/TemplateHost/BasicViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/TemplateHost/BasicViewModel.cs new file mode 100644 index 0000000000..ef2efc6ae3 --- /dev/null +++ b/src/Samples/Common/ViewModels/ControlSamples/TemplateHost/BasicViewModel.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.ViewModels.ControlSamples.TemplateHost +{ + public class BasicViewModel : DotvvmViewModelBase + { + + public List ObjectList { get; set; } = new List() + { + new IntValue() { Value = 1 }, + new IntValue() { Value = 2 }, + new IntValue() { Value = 3 } + }; + + public IntValue CreateObject() + { + return new IntValue() { Value = 1 }; + } + } + + public class IntValue + { + public int Value { get; set; } + } +} + diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/Basic.dothtml b/src/Samples/Common/Views/ControlSamples/TemplateHost/Basic.dothtml new file mode 100644 index 0000000000..4f3f49d7d2 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/Basic.dothtml @@ -0,0 +1,44 @@ +@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.TemplateHost.BasicViewModel, DotVVM.Samples.Common + + + + + + + + + + +

TemplateHost

+ + + +

hello from template

+
+
+ + + + {{value: Value}} + + + + + + <%-- + +

hello from template

+
+
+ + + + {{value: Value}} + + + + --%> + + + + diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeControlWithTemplate.cs b/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeControlWithTemplate.cs new file mode 100644 index 0000000000..2cb6d98dba --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeControlWithTemplate.cs @@ -0,0 +1,24 @@ +using System; +using System.Text; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; + +namespace DotVVM.Samples.Common.Views.ControlSamples.TemplateHost +{ + public class CompositeControlWithTemplate : CompositeControl + { + + public static DotvvmControl GetContents( + ValueOrBinding headerText, + ITemplate contentTemplate + ) + { + return new HtmlGenericControl("fieldset") + .AppendChildren( + new HtmlGenericControl("legend", new TextOrContentCapability() { Text = headerText }), + new Framework.Controls.TemplateHost() { Template = contentTemplate } + ); + } + + } +} diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeListControlWithTemplate.cs b/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeListControlWithTemplate.cs new file mode 100644 index 0000000000..a263e255c6 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/CompositeListControlWithTemplate.cs @@ -0,0 +1,62 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Utils; + +namespace DotVVM.Samples.Common.Views.ControlSamples.TemplateHost +{ + public class CompositeListControlWithTemplate : CompositeControl + { + private readonly BindingCompilationService bindingCompilationService; + + public CompositeListControlWithTemplate(BindingCompilationService bindingCompilationService) + { + this.bindingCompilationService = bindingCompilationService; + } + + public IEnumerable GetContents( + IValueBinding dataSource, + + [ControlPropertyBindingDataContextChange("DataSource", order: 0)] + [CollectionElementDataContextChange(order: 1)] + ITemplate itemTemplate, + + ICommandBinding onCreateItem + ) + { + yield return new Framework.Controls.Repeater() { + ItemTemplate = new DelegateTemplate(_ => new HtmlGenericControl("div") + .AppendChildren( + new Framework.Controls.TemplateHost() { Template = itemTemplate }, + new HtmlGenericControl("p") + .AppendChildren( + new LinkButton() { Text = "Remove" } + .SetProperty( + ButtonBase.ClickProperty, + new CommandBindingExpression(bindingCompilationService, contexts => { + ((dynamic)dataSource.GetBindingValue(this)).Remove((dynamic)contexts[0]); + }, "564787DE-E882-4C2D-BA39-482D1AB8F0CD")) + ) + ) + ), + SeparatorTemplate = new DelegateTemplate(_ => new HtmlGenericControl("hr")) + } + .SetAttribute("class", "templated-list") + .SetProperty(ItemsControl.DataSourceProperty, dataSource); + + yield return new HtmlGenericControl("p") + .AppendChildren(new Button() { Text = "Add item" } + .SetProperty( + ButtonBase.ClickProperty, + new CommandBindingExpression(bindingCompilationService, contexts => { + var item = onCreateItem.BindingDelegate(this.GetDataContexts().ToArray(), this); + ((dynamic)dataSource.GetBindingValue(this)).Add(((dynamic)item)()); + }, "38921DE7-936D-4862-921A-5051DA0CAEB1"))); + } + + } +} diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.cs b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.cs new file mode 100644 index 0000000000..4f353b5775 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Controls; + +namespace DotVVM.Samples.Common.Views.ControlSamples.TemplateHost +{ + public class TemplatedListControl : DotvvmMarkupControl + { + + [MarkupOptions(AllowHardCodedValue = false, Required = true)] + public IEnumerable DataSource + { + get { return (IEnumerable)GetValue(DataSourceProperty); } + set { SetValue(DataSourceProperty, value); } + } + public static readonly DotvvmProperty DataSourceProperty + = DotvvmProperty.Register(c => c.DataSource, null); + + [ControlPropertyBindingDataContextChange(nameof(DataSource), order: 0)] + [CollectionElementDataContextChange(order: 1)] + [MarkupOptions(AllowBinding = false, Required = true, MappingMode = MappingMode.InnerElement)] + public ITemplate ItemTemplate + { + get { return (ITemplate)GetValue(ItemTemplateProperty); } + set { SetValue(ItemTemplateProperty, value); } + } + public static readonly DotvvmProperty ItemTemplateProperty + = DotvvmProperty.Register(c => c.ItemTemplate, null); + + [MarkupOptions(AllowHardCodedValue = false, Required = true)] + public ICommandBinding OnCreateItem + { + get { return (ICommandBinding)GetValue(OnCreateItemProperty); } + set { SetValue(OnCreateItemProperty, value); } + } + public static readonly DotvvmProperty OnCreateItemProperty + = DotvvmProperty.Register, TemplatedListControl>(c => c.OnCreateItem, null); + + + public void AddItem() + { + var item = OnCreateItem.BindingDelegate(this.GetDataContexts().ToArray(), this); + ((dynamic)DataSource).Add(((dynamic)item)()); + } + + public void RemoveItem(object item) + { + ((dynamic)DataSource).Remove((dynamic)item); + } + + } + +} + diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.dotcontrol b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.dotcontrol new file mode 100644 index 0000000000..f66babafb8 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedListControl.dotcontrol @@ -0,0 +1,21 @@ +@viewModel System.Object, mscorlib +@baseType DotVVM.Samples.Common.Views.ControlSamples.TemplateHost.TemplatedListControl, DotVVM.Samples.Common + + + +
+ + +

+ +

+
+
+ +
+
+
+ +

+ +

diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.cs b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.cs new file mode 100644 index 0000000000..2cc0bf837c --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; + +namespace DotVVM.Samples.Common.Views.ControlSamples.TemplateHost +{ + public class TemplatedMarkupControl : DotvvmMarkupControl + { + + public string HeaderText + { + get { return (string)GetValue(HeaderTextProperty); } + set { SetValue(HeaderTextProperty, value); } + } + public static readonly DotvvmProperty HeaderTextProperty + = DotvvmProperty.Register(c => c.HeaderText, null); + + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement, Required = true)] + public ITemplate ContentTemplate + { + get { return (ITemplate)GetValue(ContentTemplateProperty); } + set { SetValue(ContentTemplateProperty, value); } + } + public static readonly DotvvmProperty ContentTemplateProperty + = DotvvmProperty.Register(c => c.ContentTemplate, null); + + + } +} + diff --git a/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.dotcontrol b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.dotcontrol new file mode 100644 index 0000000000..ddc723f018 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/TemplateHost/TemplatedMarkupControl.dotcontrol @@ -0,0 +1,7 @@ +@viewModel System.Object, mscorlib +@baseType DotVVM.Samples.Common.Views.ControlSamples.TemplateHost.TemplatedMarkupControl, DotVVM.Samples.Common + +
+ {{value: _control.HeaderText}} + +
diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index eef49f8677..167511c5b0 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -135,6 +135,7 @@ public partial class SamplesRouteUrls public const string ControlSamples_SpaContentPlaceHolder_HistoryApi_Spa1Spa2Page = "ControlSamples/SpaContentPlaceHolder_HistoryApi/Spa1Spa2Page"; public const string ControlSamples_SpaContentPlaceHolder_HistoryApi_Spa2PageA = "ControlSamples/SpaContentPlaceHolder_HistoryApi/Spa2PageA"; public const string ControlSamples_SpaContentPlaceHolder_HistoryApi_Spa2PageB = "ControlSamples/SpaContentPlaceHolder_HistoryApi/Spa2PageB"; + public const string ControlSamples_TemplateHost_Basic = "ControlSamples/TemplateHost/Basic"; public const string ControlSamples_TextBox_IntBoundTextBox = "ControlSamples/TextBox/IntBoundTextBox"; public const string ControlSamples_TextBox_SelectAllOnFocus = "ControlSamples/TextBox/SelectAllOnFocus"; public const string ControlSamples_TextBox_SimpleDateBox = "ControlSamples/TextBox/SimpleDateBox"; diff --git a/src/Samples/Tests/Tests/Control/TemplateHostTests.cs b/src/Samples/Tests/Tests/Control/TemplateHostTests.cs new file mode 100644 index 0000000000..15f40d9aea --- /dev/null +++ b/src/Samples/Tests/Tests/Control/TemplateHostTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using Riganti.Selenium.Core; +using Xunit; +using Xunit.Abstractions; + +namespace DotVVM.Samples.Tests.Control +{ + public class TemplateHostTests : AppSeleniumTest + { + public TemplateHostTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Control_TemplateHost_Basic() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_TemplateHost_Basic); + + AssertUI.TextEquals(browser.Single("fieldset legend"), "Form 1"); + AssertUI.TextEquals(browser.Single("fieldset p"), "hello from template"); + + var items = browser.FindElements(".templated-list div"); + items.ThrowIfDifferentCountThan(3); + + // increment item + AssertUI.TextEquals(items[0].Single("big"), "1"); + items[0].ElementAt("input[type=button]", 1).Click(); + AssertUI.TextEquals(items[0].Single("big"), "0"); + + // remove item + items[0].Single("a").Click(); + browser.WaitFor(() => { + items = browser.FindElements(".templated-list div"); + items.ThrowIfDifferentCountThan(2); + }, 2000); + AssertUI.TextEquals(items[0].Single("big"), "2"); + + // add item + browser.Last("input[type=button]").Click(); + browser.WaitFor(() => { + items = browser.FindElements(".templated-list div"); + items.ThrowIfDifferentCountThan(3); + }, 2000); + }); + } + } +}