diff --git a/src/Framework/Framework/Controls/CheckableControlBase.cs b/src/Framework/Framework/Controls/CheckableControlBase.cs index e675b7422b..3a19b66b76 100644 --- a/src/Framework/Framework/Controls/CheckableControlBase.cs +++ b/src/Framework/Framework/Controls/CheckableControlBase.cs @@ -194,13 +194,15 @@ protected virtual void AddAttributesToInput(IHtmlWriter writer) } // handle enabled attribute - writer.AddKnockoutDataBind("enable", this, EnabledProperty, () => + var enabled = this.GetValueRaw(EnabledProperty); + if (enabled is IValueBinding enabledBinding) { - if (!Enabled) - { - writer.AddAttribute("disabled", "disabled"); - } - }); + writer.AddKnockoutDataBind("enable", this, enabledBinding); + } + if (false.Equals(KnockoutHelper.TryEvaluateValueBinding(this, enabled))) + { + writer.AddAttribute("disabled", "disabled"); + } if (!string.IsNullOrEmpty(InputCssClass)) { diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index db33cd9501..9073a7d142 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -256,16 +256,9 @@ protected virtual void AddVisibleAttributeOrBinding(in RenderState r, IHtmlWrite writer.AddKnockoutDataBind("visible", valueBinding.GetKnockoutBindingExpression(this)); } - try + if (false.Equals(KnockoutHelper.TryEvaluateValueBinding(this, v))) { - if (false.Equals(EvalPropertyValue(VisibleProperty, v))) - { - writer.AddAttribute("style", "display:none"); - } - } - catch (Exception) when (valueBinding is {}) - { - // suppress value binding errors + writer.AddAttribute("style", "display:none"); } } @@ -311,19 +304,15 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c private void AddCssClassesToRender(IHtmlWriter writer) { KnockoutBindingGroup cssClassBindingGroup = new KnockoutBindingGroup(); - foreach (var cssClass in CssClasses.Properties) + foreach (var (cssClass, rawValue) in CssClasses.RawValues) { - if (HasValueBinding(cssClass)) + if (rawValue is IValueBinding binding) { - cssClassBindingGroup.Add(cssClass.GroupMemberName, this, cssClass); + cssClassBindingGroup.Add(cssClass, this, binding); } - try - { - if (true.Equals(this.GetValue(cssClass))) - writer.AddAttribute("class", cssClass.GroupMemberName, append: true, appendSeparator: " "); - } - catch when (HasValueBinding(cssClass)) { } + if (true.Equals(KnockoutHelper.TryEvaluateValueBinding(this, rawValue))) + writer.AddAttribute("class", cssClass, append: true, appendSeparator: " "); } if (!cssClassBindingGroup.IsEmpty) writer.AddKnockoutDataBind("css", cssClassBindingGroup); @@ -332,24 +321,19 @@ private void AddCssClassesToRender(IHtmlWriter writer) private void AddCssStylesToRender(IHtmlWriter writer) { KnockoutBindingGroup? cssStylesBindingGroup = null; - foreach (var styleProperty in CssStyles.Properties) + foreach (var (style, rawValue) in CssStyles.RawValues) { - if (HasValueBinding(styleProperty)) + if (rawValue is IValueBinding binding) { if (cssStylesBindingGroup == null) cssStylesBindingGroup = new KnockoutBindingGroup(); - cssStylesBindingGroup.Add(styleProperty.GroupMemberName, this, styleProperty); + cssStylesBindingGroup.Add(style, this, binding); } - try + var value = KnockoutHelper.TryEvaluateValueBinding(this, rawValue)?.ToString(); + if (!string.IsNullOrEmpty(value)) { - var value = GetValue(styleProperty)?.ToString(); - if (!string.IsNullOrEmpty(value)) - { - writer.AddStyleAttribute(styleProperty.GroupMemberName, value!); - } + writer.AddStyleAttribute(style, value!); } - // suppress all errors when we have rendered the value binding anyway - catch when (HasValueBinding(styleProperty)) { } } if (cssStylesBindingGroup != null) diff --git a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs index 8c2e6bd58a..bd6a6eadbf 100644 --- a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs +++ b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs @@ -41,6 +41,12 @@ public void Add(string name, KnockoutBindingGroup nestedGroup) Add(name, nestedGroup.ToString()); } + public void Add(string name, DotvvmBindableObject contextControl, IValueBinding binding) + { + var expression = binding.GetKnockoutBindingExpression(contextControl); + Add(name, expression); + } + [Obsolete("Use Add or AddValue instead")] public virtual void Add(string name, string expression, bool surroundWithDoubleQuotes) { diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index d205d427b5..e0438393ea 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -15,6 +15,24 @@ namespace DotVVM.Framework.Controls { public static class KnockoutHelper { + /// If value is a binding, evaluates it. If the binding is a value binding, any thrown exceptions are suppressed + internal static object? TryEvaluateValueBinding(DotvvmBindableObject control, object? valueOrBinding) + { + if (valueOrBinding is IStaticValueBinding b) + { + try + { + return b.Evaluate(control); + } + catch when (b is IValueBinding) + { + return null; + } + } + return valueOrBinding; + } + + /// /// Adds the data-bind attribute to the next HTML element that is being rendered. The binding expression is taken from the specified property. If in server rendering mode, the binding is also not rendered. /// diff --git a/src/Framework/Framework/Controls/RadioButton.cs b/src/Framework/Framework/Controls/RadioButton.cs index db5c7a16f9..08cb94a92e 100644 --- a/src/Framework/Framework/Controls/RadioButton.cs +++ b/src/Framework/Framework/Controls/RadioButton.cs @@ -6,6 +6,7 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; using System.Collections.Generic; +using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Controls { @@ -18,12 +19,14 @@ public class RadioButton : CheckableControlBase /// Gets or sets whether the control is checked. /// [MarkupOptions(AllowHardCodedValue = false)] + [Obsolete("Checked property probably does not work as a reasonable person would expect. Use CheckedItem and CheckedValue instead.")] public bool Checked { get { return (bool)GetValue(CheckedProperty)!; } set { SetValue(CheckedProperty, value); } } + [Obsolete("Checked property probably does not work as a reasonable person would expect. Use CheckedItem and CheckedValue instead.")] public static readonly DotvvmProperty CheckedProperty = DotvvmProperty.Register(t => t.Checked, false); @@ -42,7 +45,6 @@ public object? CheckedItem /// /// Gets or sets an unique name of the radio button group. /// - [MarkupOptions(AllowBinding = false)] public string GroupName { get { return (string)GetValue(GroupNameProperty)!; } @@ -63,11 +65,14 @@ protected override void RenderInputTag(IHtmlWriter writer) protected virtual void RenderGroupNameAttribute(IHtmlWriter writer) { - var group = new KnockoutBindingGroup(); - group.Add("name", this, GroupNameProperty, () => { - writer.AddAttribute("name", GroupName); - }); - writer.AddKnockoutDataBind("attr", group); + var valueRaw = GetValueRaw(GroupNameProperty); + if (valueRaw is IValueBinding valueBinding) + { + writer.AddKnockoutDataBind("attr", new KnockoutBindingGroup { { "name", this, valueBinding } }); + } + var value = KnockoutHelper.TryEvaluateValueBinding(this, valueRaw); + if (value is not null) + writer.AddAttribute("name", (string)value); } protected virtual void RenderTypeAttribute(IHtmlWriter writer) @@ -78,26 +83,35 @@ protected virtual void RenderTypeAttribute(IHtmlWriter writer) protected virtual void RenderCheckedValueAttribute(IHtmlWriter writer) { - writer.AddKnockoutDataBind("checkedValue", this, CheckedValueProperty, () => { - var checkedValue = (CheckedValue ?? string.Empty).ToString(); - if (!string.IsNullOrEmpty(checkedValue)) - { - writer.AddKnockoutDataBind("checkedValue", KnockoutHelper.MakeStringLiteral(checkedValue)); - } - }); + var checkedValue = GetValueOrBinding(CheckedValueProperty); + if (!checkedValue.ValueIsNull()) + { + var checkedValueExpr = checkedValue.GetJsExpression(this); + writer.AddKnockoutDataBind("checkedValue", checkedValueExpr); + } RenderCheckedValueComparerAttribute(writer); } protected virtual void RenderCheckedAttribute(IHtmlWriter writer) { + if (!IsPropertySet(CheckedValueProperty)) + { + throw new DotvvmControlException(this, "The 'CheckedValue' of the RadioButton control must be set when CheckedItem is used. Remember that all RadioButtons with the same GroupName should be bound to the same property in the viewmodel."); + } var checkedItemBinding = GetValueBinding(CheckedItemProperty); - if (checkedItemBinding == null) + if (checkedItemBinding is null) { - writer.AddKnockoutDataBind("checked", this, CheckedProperty, () => { }); - if (!IsPropertySet(CheckedValueProperty)) +#pragma warning disable CS0618 // CheckedProperty is obsolete + var @checked = GetValueRaw(CheckedProperty); + if (@checked is IValueBinding checkedBinding) + { + writer.AddKnockoutDataBind("checked", this, checkedBinding); + } + if (true.Equals(KnockoutHelper.TryEvaluateValueBinding(this, @checked))) { - throw new DotvvmControlException(this, "The 'CheckedValue' of the RadioButton control must be set. Remember that all RadioButtons with the same GroupName have to be bound to the same property in the viewmodel."); + writer.AddAttribute("checked", ""); } +#pragma warning restore CS0618 } else { @@ -123,6 +137,36 @@ protected virtual void RenderCheckedAttribute(IHtmlWriter writer) control.GetValue(CheckedValueProperty)?.DothtmlNode ); } + + if (!control.Properties.ContainsKey(CheckedValueProperty)) + { + yield return new ControlUsageError( + "The 'CheckedValue' of the RadioButton control must be set when CheckedItem is used. Remember that all RadioButtons with the same GroupName should be bound to the same property in the viewmodel.", + control.GetValue(CheckedItemProperty)?.DothtmlNode + ); + } + +#pragma warning disable CS0618 // CheckedProperty is obsolete + if (control.Properties.ContainsKey(CheckedProperty)) + { + if (control.Properties.ContainsKey(CheckedItemProperty)) + { + yield return new ControlUsageError( + "The Checked and CheckedItem properties cannot be both used on the same RadioButton.", + control.GetValue(CheckedProperty)?.DothtmlNode, + control.GetValue(CheckedItemProperty)?.DothtmlNode + ); + } + } + + if (!control.Properties.ContainsKey(CheckedItemProperty) && !control.Properties.ContainsKey(CheckedProperty)) + { + yield return new ControlUsageError( + "The CheckedItem property must be set on the RadioButton.", + control.DothtmlNode + ); + } +#pragma warning restore CS0618 } } } diff --git a/src/Samples/Common/Views/FeatureSamples/FormControlsEnabled/FormControlsEnabled.dothtml b/src/Samples/Common/Views/FeatureSamples/FormControlsEnabled/FormControlsEnabled.dothtml index eb8960ce87..c1ed0584d2 100644 --- a/src/Samples/Common/Views/FeatureSamples/FormControlsEnabled/FormControlsEnabled.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/FormControlsEnabled/FormControlsEnabled.dothtml @@ -34,9 +34,9 @@
- - - + + +
@@ -64,9 +64,9 @@
- - - + + +
@@ -99,9 +99,9 @@
- - - + + +
@@ -112,4 +112,4 @@ - \ No newline at end of file + diff --git a/src/Tests/ControlTests/SimpleControlTests.cs b/src/Tests/ControlTests/SimpleControlTests.cs index c7ffb2756c..9da3cdb8c3 100644 --- a/src/Tests/ControlTests/SimpleControlTests.cs +++ b/src/Tests/ControlTests/SimpleControlTests.cs @@ -215,6 +215,34 @@ public async Task TextBox() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + [DataRow(RenderMode.Server)] + [DataRow(RenderMode.Client)] + public async Task RadioButton(RenderMode mode) + { + var r = await cth.RunPage(typeof(BasicTestViewModel), $$""" + + + + + + + + + + + + 0} Enabled=false CheckedValue={resource:true} /> + + + + """, + fileName: $"SimpleControls_RadioButton_{mode}.dothtml" + ); + + check.CheckString(r.OutputString, fileExtension: "html"); + } + [TestMethod] public async Task CommandBinding() { @@ -415,6 +443,8 @@ public class BasicTestViewModel: DotvvmViewModelBase public string UrlSuffix { get; set; } = "#something"; + public bool Bool { get; set; } = true; + public GridViewDataSet Customers { get; set; } = new GridViewDataSet() { RowEditOptions = { EditRowId = 1, diff --git a/src/Tests/ControlTests/testoutputs/SimpleControlTests.RadioButton.html b/src/Tests/ControlTests/testoutputs/SimpleControlTests.RadioButton.html new file mode 100644 index 0000000000..dcec3e0ee7 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/SimpleControlTests.RadioButton.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 803a7b58c2..f1fb1ffaa5 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -1239,8 +1239,7 @@ }, "GroupName": { "type": "System.String", - "defaultValue": "", - "onlyHardcoded": true + "defaultValue": "" } }, "DotVVM.Framework.Controls.RenderSettings": {