diff --git a/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs b/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs index 95a0941035..7138d78314 100644 --- a/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs +++ b/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs @@ -50,9 +50,14 @@ public ParametrizedCode(string code, OperatorPrecedence precedence = default) // TODO(exyi): add WriteTo(StringBuilder) /// - /// Converts this to string and assigns all parameters using `parameterAssignment`. If there is any missing, exception is thrown. + /// Converts this to string and assigns all parameters using `parameterAssignment`. /// + /// Thrown when some parameter is not assigned and has no default value. public string ToString(Func parameterAssignment) => ToString(parameterAssignment, out var _); + /// + /// Converts this to string and assigns all parameters using `parameterAssignment`. + /// + /// Thrown when some parameter is not assigned and has no default value. public string ToString(Func parameterAssignment, out bool allIsDefault) { allIsDefault = true; @@ -205,7 +210,7 @@ private CodeParameterAssignment[] FindAssignment(Func EnumerateAllParameters() } } + public record MissingAssignmentException(CodeParameterInfo Parameter, ParametrizedCode FullCode): RecordExceptions.RecordException + { + public override string Message => $"Assignment of parameter '{Parameter.Parameter}' was not found."; + } + /// /// Builder class with reasonably fast Add operation. Use Build method to convert it to immutable ParametrizedCode /// diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index e0438393ea..5a40e00242 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; +using FastExpressionCompiler; using Newtonsoft.Json; namespace DotVVM.Framework.Controls @@ -263,18 +264,26 @@ options.KoContext is object ? string SubstituteArguments(ParametrizedCode parametrizedCode) { - return parametrizedCode.ToString(p => - p == JavascriptTranslator.CurrentElementParameter ? options.ElementAccessor : - p == CommandBindingExpression.CurrentPathParameter ? CodeParameterAssignment.FromIdentifier(getContextPath(control)) : - p == CommandBindingExpression.ControlUniqueIdParameter ? uniqueControlId?.GetParametrizedJsExpression(control) ?? CodeParameterAssignment.FromLiteral("") : - p == JavascriptTranslator.KnockoutContextParameter ? knockoutContext : - p == JavascriptTranslator.KnockoutViewModelParameter ? viewModel : - p == CommandBindingExpression.OptionalKnockoutContextParameter ? optionalKnockoutContext : - p == CommandBindingExpression.CommandArgumentsParameter ? options.CommandArgs ?? default : - p == CommandBindingExpression.PostbackHandlersParameter ? CodeParameterAssignment.FromIdentifier(getHandlerScript()) : - p == CommandBindingExpression.AbortSignalParameter ? abortSignal : - default - ); + try + { + return parametrizedCode.ToString(p => + p == JavascriptTranslator.CurrentElementParameter ? options.ElementAccessor : + p == CommandBindingExpression.CurrentPathParameter ? CodeParameterAssignment.FromIdentifier(getContextPath(control)) : + p == CommandBindingExpression.ControlUniqueIdParameter ? uniqueControlId?.GetParametrizedJsExpression(control) ?? CodeParameterAssignment.FromLiteral("") : + p == JavascriptTranslator.KnockoutContextParameter ? knockoutContext : + p == JavascriptTranslator.KnockoutViewModelParameter ? viewModel : + p == CommandBindingExpression.OptionalKnockoutContextParameter ? optionalKnockoutContext : + p == CommandBindingExpression.CommandArgumentsParameter ? options.CommandArgs ?? default : + p == CommandBindingExpression.PostbackHandlersParameter ? CodeParameterAssignment.FromIdentifier(getHandlerScript()) : + p == CommandBindingExpression.AbortSignalParameter ? abortSignal : + default + ); + } + catch (ParametrizedCode.MissingAssignmentException e) when (e.Parameter.Parameter == CommandBindingExpression.CommandArgumentsParameter) + { + var returnType = expression.GetProperty(ErrorHandlingMode.ReturnNull)?.Type; + throw new DotvvmControlException(control, $"The binding {expression} of type {returnType?.ToCode(stripNamespace: true) ?? "?"} requires arguments, but none were provided to the KnockoutHelper.GenerateClientPostback method.", innerException: e) { RelatedBinding = expression }; + } } } diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 09852a5a42..fc40ba614e 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -118,6 +118,9 @@ + + + diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index 04dac2a63f..11bde4c862 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -267,6 +267,8 @@ private static void AddControls(DotvvmConfiguration config) config.Markup.AddCodeControls("cc", typeof(Loader)); config.Markup.AddMarkupControl("sample", "EmbeddedResourceControls_Button", "embedded://EmbeddedResourceControls/Button.dotcontrol"); config.Markup.AddMarkupControl("cc", "NodeControl", "Views/ControlSamples/HierarchyRepeater/NodeControl.dotcontrol"); + config.Markup.AddMarkupControl("cc", "CommandAsProperty", "Views/FeatureSamples/MarkupControl/CommandAsProperty.dotcontrol"); + config.Markup.AddMarkupControl("cc", "CommandAsPropertyWrapper", "Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.dotcontrol"); config.Markup.AutoDiscoverControls(new DefaultControlRegistrationStrategy(config, "sample", "Views/")); diff --git a/src/Samples/Common/ViewModels/FeatureSamples/MarkupControl/CommandAsPropertyPageViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/MarkupControl/CommandAsPropertyPageViewModel.cs new file mode 100644 index 0000000000..21f0a92112 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/MarkupControl/CommandAsPropertyPageViewModel.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.MarkupControl +{ + public class CommandAsPropertyPageViewModel : DotvvmViewModelBase + { + + public List Items { get; set; } = new() { + new ItemModel() { Name = "One", IsTrue = true }, + new ItemModel() { Name = "Two", IsTrue = false }, + new ItemModel() { Name = "Three", IsTrue = true } + }; + + public ItemModel SelectedItem { get; set; } + + public Task MyFunction(string name, bool isTrue) + { + SelectedItem = new ItemModel() { Name = name, IsTrue = isTrue }; + return Task.CompletedTask; + } + + + public class ItemModel + { + public string Name { get; set; } + public bool IsTrue { get; set; } + + } + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.cs b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.cs new file mode 100644 index 0000000000..bba02836e9 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; + +namespace DotVVM.Samples.Common.Views.FeatureSamples.MarkupControl +{ + public class CommandAsProperty : DotvvmMarkupControl + { + + public Func Click + { + get => (Func)GetValue(ClickProperty)!; + set => SetValue(ClickProperty, value); + } + public static readonly DotvvmProperty ClickProperty + = DotvvmProperty.Register, CommandAsProperty>(c => c.Click, null); + + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.dotcontrol b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.dotcontrol new file mode 100644 index 0000000000..6aa0b43198 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsProperty.dotcontrol @@ -0,0 +1,6 @@ +@viewModel System.String, mscorlib +@baseType DotVVM.Samples.Common.Views.FeatureSamples.MarkupControl.CommandAsProperty, DotVVM.Samples.Common + +<%-- both options are legit --%> + + diff --git a/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyPage.dothtml b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyPage.dothtml new file mode 100644 index 0000000000..7ca44388c9 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyPage.dothtml @@ -0,0 +1,33 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.MarkupControl.CommandAsPropertyPageViewModel, DotVVM.Samples.Common + + + + + + + + + + +

Pass as command

+ +
+ +

Pass as static command

+ +
+ + <%--

Pass as value

+ +
--%> + + <%--

Pass as resource

+ +
--%> + +

Selected item: {{value: Name}}, {{value: IsTrue}}

+ + + + + diff --git a/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.cs b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.cs new file mode 100644 index 0000000000..2772994a13 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; + +namespace DotVVM.Samples.Common.Views.FeatureSamples.MarkupControl +{ + public class CommandAsPropertyWrapper : DotvvmMarkupControl + { + + public Func Click + { + get { return (Func)GetValue(ClickProperty); } + set { SetValue(ClickProperty, value); } + } + public static readonly DotvvmProperty ClickProperty + = DotvvmProperty.Register, CommandAsPropertyWrapper>(c => c.Click, null); + + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.dotcontrol b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.dotcontrol new file mode 100644 index 0000000000..7aa8969c17 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/MarkupControl/CommandAsPropertyWrapper.dotcontrol @@ -0,0 +1,25 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.MarkupControl.CommandAsPropertyPageViewModel, DotVVM.Samples.Common +@baseType DotVVM.Samples.Common.Views.FeatureSamples.MarkupControl.CommandAsPropertyWrapper, DotVVM.Samples.Common + + +
+
+

Passed as command

+ +
+
+

Passed as static command

+ +
+ + <%-- remove after we fix the lambda conversion --%> +
+

Passed as value

+ +
+
+

Passed as resource

+ +
+
+
diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index fe2388d721..84518eb640 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -281,6 +281,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_Localization_Localization_FormatString = "FeatureSamples/Localization/Localization_FormatString"; public const string FeatureSamples_Localization_Localization_NestedPage_Type = "FeatureSamples/Localization/Localization_NestedPage_Type"; public const string FeatureSamples_MarkupControl_ComboBoxDataSourceBoundToStaticCollection = "FeatureSamples/MarkupControl/ComboBoxDataSourceBoundToStaticCollection"; + public const string FeatureSamples_MarkupControl_CommandAsPropertyPage = "FeatureSamples/MarkupControl/CommandAsPropertyPage"; public const string FeatureSamples_MarkupControl_CommandBindingInDataContextWithControlProperty = "FeatureSamples/MarkupControl/CommandBindingInDataContextWithControlProperty"; public const string FeatureSamples_MarkupControl_CommandBindingInRepeater = "FeatureSamples/MarkupControl/CommandBindingInRepeater"; public const string FeatureSamples_MarkupControl_CommandPropertiesInMarkupControl = "FeatureSamples/MarkupControl/CommandPropertiesInMarkupControl"; diff --git a/src/Samples/Tests/Tests/Feature/MarkupControlTests.cs b/src/Samples/Tests/Tests/Feature/MarkupControlTests.cs index ef51f5007f..f034ca4ce6 100644 --- a/src/Samples/Tests/Tests/Feature/MarkupControlTests.cs +++ b/src/Samples/Tests/Tests/Feature/MarkupControlTests.cs @@ -409,5 +409,29 @@ public void Feature_MarkupControl_MarkupDeclaredProperties() browser.WaitFor(() => AssertUI.InnerTextEquals(browser.First("[data-ui=counter]"), "2"), 2000); }); } + + [Fact] + public void Feature_MarkupControl_CommandAsPropertyPage() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_MarkupControl_CommandAsPropertyPage); + + var lists = browser.FindElements("div[data-ui=button-list]"); + for (var j = 0; j < 4; j++) + { + for (var i = 0; i < lists.Count; i++) + { + lists[i].ElementAt("input[type=button]", j).Click(); + AssertUI.InnerTextEquals(browser.Single("p[data-ui=result]"), (i % 3) switch { + 0 => "Selected item: One, true", + 1 => "Selected item: Two, false", + _ => "Selected item: Three, true" + }); + + i++; + } + } + }); + } } }