From 94501c6ea266e8420493a2adc28a021a66bb10c1 Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:54:37 +0100 Subject: [PATCH 1/7] shared validation rules and resources --- .../Properties/Resources.Designer.cs | 9 +++ .../Properties/Resources.en-US.resx | 3 + .../CoreNodeModels/Properties/Resources.resx | 3 + .../CoreNodeModelsWpf/InputValidationRules.cs | 76 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs diff --git a/src/Libraries/CoreNodeModels/Properties/Resources.Designer.cs b/src/Libraries/CoreNodeModels/Properties/Resources.Designer.cs index 33037e201de..8afcea7d831 100644 --- a/src/Libraries/CoreNodeModels/Properties/Resources.Designer.cs +++ b/src/Libraries/CoreNodeModels/Properties/Resources.Designer.cs @@ -479,6 +479,15 @@ public static string DateTimeDescription { } } + /// + /// Looks up a localized string similar to The input must match the Date Time format.. + /// + public static string DateTimeNodeInputInvalidFormat { + get { + return ResourceManager.GetString("DateTimeNodeInputInvalidFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Validates the data type of an input and returns it. /// diff --git a/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx b/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx index c219535bbc0..1450d015c92 100644 --- a/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx +++ b/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx @@ -584,6 +584,9 @@ The input must be numeric. + + The input must match the Date Time format. + Select a Color from the palette diff --git a/src/Libraries/CoreNodeModels/Properties/Resources.resx b/src/Libraries/CoreNodeModels/Properties/Resources.resx index 2dd8afe3279..959ea2304b9 100644 --- a/src/Libraries/CoreNodeModels/Properties/Resources.resx +++ b/src/Libraries/CoreNodeModels/Properties/Resources.resx @@ -584,6 +584,9 @@ The input must be numeric. + + The input must match the Date Time format. + Select a Color from the palette diff --git a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs new file mode 100644 index 00000000000..77e640f1bcf --- /dev/null +++ b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Windows.Controls; +using CoreNodeModels.Properties; +using Dynamo.Configuration; + +namespace CoreNodeModelsWpf +{ + /// + /// Accepts invariant numeric doubles/longs. + /// + public class NumericValidationRule : ValidationRule + { + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + var text = value as string; + if (IsNumeric(text)) return ValidationResult.ValidResult; + + return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); + } + + internal static bool IsNumeric(string value) + { + if (string.IsNullOrWhiteSpace(value)) return false; + + return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out _) || long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _); + } + } + + /// + /// Accepts Int64 integers only. Non-numeric and overflow both fail (Error bubble via DynamoTextBox). + /// + public class Integer64ValidationRule : ValidationRule + { + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + var text = value as string; + if (string.IsNullOrWhiteSpace(text)) + { + return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); + } + + try + { + Convert.ToInt64(text, CultureInfo.InvariantCulture); + return ValidationResult.ValidResult; + } + catch (FormatException) + { + return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); + } + catch (OverflowException) + { + return new ValidationResult(false, Resources.IntegerSliderInfoMessage); + } + } + } + + /// + /// Accepts PreferenceSettings.DefaultDateFormat only. + /// + public class DateTimeValidationRule : ValidationRule + { + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + var text = value as string; + if (DateTime.TryParseExact(text, PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) + { + return ValidationResult.ValidResult; + } + + return new ValidationResult(false, Resources.DateTimeNodeInputInvalidFormat); + } + } +} From b3fff83cabf93573fdd0d301d786fc2fa31616a0 Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:04:41 +0100 Subject: [PATCH 2/7] show error bubbles on incorrect values - error bubble closes on undo --- .../CoreNodeModels/Input/DateTime.cs | 38 +++++++++++++++-- .../CoreNodeModels/Input/DoubleSlider.cs | 5 +++ .../CoreNodeModels/Input/IntegerSlider.cs | 7 +++- .../Controls/DateTimeInputControl.xaml | 14 +++---- .../Controls/DateTimeInputControl.xaml.cs | 24 ++++++++--- .../Controls/DynamoSlider.xaml | 12 ++---- .../Controls/DynamoSlider.xaml.cs | 41 +++++++++++++++++-- .../NodeViewCustomizations/DoubleInput.cs | 37 ++--------------- .../NodeViewCustomizations/DoubleSlider.cs | 2 + .../NodeViewCustomizations/IntegerSlider.cs | 2 + .../CoreNodeModelsWpf/SliderViewModel.cs | 4 ++ .../NodeViewCustomizationTests.cs | 2 +- 12 files changed, 124 insertions(+), 64 deletions(-) diff --git a/src/Libraries/CoreNodeModels/Input/DateTime.cs b/src/Libraries/CoreNodeModels/Input/DateTime.cs index daadb8d2d5e..2a4cf6444b6 100644 --- a/src/Libraries/CoreNodeModels/Input/DateTime.cs +++ b/src/Libraries/CoreNodeModels/Input/DateTime.cs @@ -1,10 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Globalization; using Dynamo.Configuration; +using Dynamo.Graph; using Dynamo.Graph.Nodes; using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Xml; namespace CoreNodeModels.Input { @@ -92,6 +94,30 @@ public override IEnumerable BuildOutputAst(List - @@ -18,7 +18,8 @@ - - + Style="{StaticResource SZoomFadeTextBox}" > + - - + + diff --git a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs index 041cc62e01f..f96973fafae 100644 --- a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs +++ b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs @@ -1,5 +1,7 @@ -using System.Windows; -using System.Windows.Forms; +using CoreNodeModelsWpf.Converters; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; using UserControl = System.Windows.Controls.UserControl; namespace CoreNodeModelsWpf.Controls @@ -12,15 +14,25 @@ public partial class DateTimeInputControl : UserControl public DateTimeInputControl() { InitializeComponent(); + Loaded += DateTimeInputControl_Loaded; } - private void ButtonBase_OnClick(object sender, RoutedEventArgs e) + private void DateTimeInputControl_Loaded(object sender, RoutedEventArgs e) { - var picker = new DateTimePicker + var binding = new System.Windows.Data.Binding(nameof(CoreNodeModels.Input.DateTime.Value)) { - Format = DateTimePickerFormat.Time, - ShowUpDown = true, + Mode = BindingMode.TwoWay, + Converter = new StringToDateTimeConverter(), + UpdateSourceTrigger = UpdateSourceTrigger.Explicit, + NotifyOnValidationError = false }; + var rule = new DateTimeValidationRule + { + ValidationStep = ValidationStep.RawProposedValue + }; + binding.ValidationRules.Add(rule); + DateTimeTb.BindToProperty(binding); + Validation.SetErrorTemplate(DateTimeTb, null); } } } diff --git a/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml b/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml index d4b104af0e7..227fe34dedc 100644 --- a/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml +++ b/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml @@ -150,8 +150,7 @@ Background="{StaticResource MidGreyBrush}" BorderBrush="#4A4A4A" BorderThickness="1" - Foreground="#bcbcbc" - Text="{Binding MinText, Mode=OneWay, UpdateSourceTrigger=Explicit}" /> + Foreground="#bcbcbc" /> + Foreground="#bcbcbc" /> + Foreground="#bcbcbc" /> @@ -201,8 +198,7 @@ BorderThickness="1" TextWrapping="NoWrap" Foreground="{StaticResource PrimaryCharcoal100Brush}" - Opacity="1" - Text="{Binding ValueText, Mode=OneWay}"> + Opacity="1"> diff --git a/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml.cs b/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml.cs index b363143f9dc..6f2a4427884 100644 --- a/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml.cs +++ b/src/Libraries/CoreNodeModelsWpf/Controls/DynamoSlider.xaml.cs @@ -1,11 +1,14 @@ -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Input; -using System.Windows.Shapes; using Dynamo.Graph.Nodes; using Dynamo.Graph.Workspaces; +using Dynamo.Nodes; using Dynamo.UI; using Dynamo.ViewModels; +using System; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Shapes; namespace CoreNodeModelsWpf.Controls { @@ -31,6 +34,36 @@ public DynamoSlider(NodeModel model, IViewModelView nodeUI) } + public void BindValidatedTextBoxes(Func ruleFactory) + { + BindField(ValTb, "ValueText", ruleFactory()); + BindField(MinTb, "MinText", ruleFactory()); + BindField(MaxTb, "MaxText", ruleFactory()); + BindField(StepTb, "StepText", ruleFactory()); + } + + private static ValidationRule CloneStep(ValidationRule template) + { + template.ValidationStep = ValidationStep.RawProposedValue; + return template; + } + + private static void BindField(DynamoTextBox textBox, string propertyName, ValidationRule validationRule) + { + var binding = new Binding(propertyName) + { + Mode = BindingMode.TwoWay, + UpdateSourceTrigger = UpdateSourceTrigger.Explicit, + NotifyOnValidationError = false + }; + + validationRule.ValidationStep = ValidationStep.RawProposedValue; + binding.ValidationRules.Add(validationRule); + + textBox.BindToProperty(binding); + Validation.SetErrorTemplate(textBox, null); + } + #region Event Handlers private void Slider_OnDragStarted(object sender, DragStartedEventArgs e) diff --git a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleInput.cs b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleInput.cs index 1b371a80632..5a789cf8c1c 100644 --- a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleInput.cs +++ b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleInput.cs @@ -2,45 +2,13 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using System.Windows.Media; using CoreNodeModels.Input; -using CoreNodeModels.Properties; using Dynamo.Controls; using Dynamo.Nodes; using Dynamo.Wpf; namespace CoreNodeModelsWpf.Nodes -{ - internal class NumericValidationRule : ValidationRule - { - //if the string can be parsed to a common numeric type return true - internal bool validateInput(string value) - { - double doubleVal; - long longVal; - - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleVal) - || long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out longVal)) - { - return true; - } - return false; - } - - public override ValidationResult Validate(object value, CultureInfo cultureInfo) - { - - if (!validateInput(value as string)) - { - return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); - } - else - { - return new ValidationResult(true, null); - } - } - } - +{ public class DoubleInputNodeViewCustomization : INodeViewCustomization { public void CustomizeView(DoubleInput nodeModel, NodeView nodeView) @@ -67,8 +35,9 @@ public void CustomizeView(DoubleInput nodeModel, NodeView nodeView) Source = nodeModel, UpdateSourceTrigger = UpdateSourceTrigger.Explicit }; + var numericalValidation = new NumericValidationRule(); - numericalValidation.ValidationStep = ValidationStep.ConvertedProposedValue; + numericalValidation.ValidationStep = ValidationStep.RawProposedValue; textToValueBinding.ValidationRules.Add(numericalValidation); tb.BindToProperty(textToValueBinding); Validation.SetErrorTemplate(tb, null); diff --git a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleSlider.cs b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleSlider.cs index 681939421a2..6bff84ffa3d 100644 --- a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleSlider.cs +++ b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/DoubleSlider.cs @@ -14,6 +14,8 @@ public void CustomizeView(DoubleSlider model, NodeView nodeView) DataContext = new SliderViewModel(model) }; + slider.BindValidatedTextBoxes(() => new NumericValidationRule()); + nodeView.inputGrid.Children.Add(slider); } diff --git a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs index 778cce2563a..4b2d1b95634 100644 --- a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs +++ b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs @@ -29,6 +29,8 @@ public void CustomizeView(IntegerSlider64Bit model, NodeView nodeView) DataContext = new SliderViewModel(model) }; + slider.BindValidatedTextBoxes(() => new Integer64ValidationRule()); + nodeView.inputGrid.Children.Add(slider); } diff --git a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs index ff1d6915139..8dbffef9a88 100644 --- a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs @@ -20,21 +20,25 @@ public class SliderViewModel : NotificationObject where T : IComparable public string MaxText { get { return SliderBase.ConvertNumberToString(model.Max); } + set { } } public string MinText { get { return SliderBase.ConvertNumberToString(model.Min); } + set { } } public string StepText { get { return SliderBase.ConvertNumberToString(model.Step); } + set { } } public string ValueText { get { return SliderBase.ConvertNumberToString(model.Value); } + set { } } public T Max diff --git a/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs b/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs index bc81292876c..84d07b049e5 100644 --- a/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs +++ b/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs @@ -7,7 +7,7 @@ using System.Windows.Controls; using CoreNodeModels.Input; using CoreNodeModelsWpf.Controls; -using CoreNodeModelsWpf.Nodes; +using CoreNodeModelsWpf; using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Graph.Nodes.CustomNodes; From 729848339b88fdf1af734e1b455394dc5f727923 Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:03:57 +0100 Subject: [PATCH 3/7] dateTime fix and clearing error bubbles on integerSlider --- src/DynamoCoreWpf/Controls/DynamoTextBox.cs | 1 + .../CoreNodeModels/Input/DateTime.cs | 20 ++++++++++++++++++- .../Properties/Resources.Designer.cs | 11 +++++++++- .../Properties/Resources.en-US.resx | 7 +++++-- .../CoreNodeModels/Properties/Resources.resx | 5 ++++- .../Controls/DateTimeInputControl.xaml.cs | 4 +--- .../CoreNodeModelsWpf/InputValidationRules.cs | 2 +- 7 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/DynamoCoreWpf/Controls/DynamoTextBox.cs b/src/DynamoCoreWpf/Controls/DynamoTextBox.cs index fd2560a6007..e5ba4fe151b 100644 --- a/src/DynamoCoreWpf/Controls/DynamoTextBox.cs +++ b/src/DynamoCoreWpf/Controls/DynamoTextBox.cs @@ -223,6 +223,7 @@ private void UpdateDataSource(bool recordForUndo) if (expr.HasValidationError && nvm != null) { + nvm.NodeModel.ClearErrorsAndWarnings(); nvm.NodeModel.Error(expr.ValidationError.ErrorContent as string); } } diff --git a/src/Libraries/CoreNodeModels/Input/DateTime.cs b/src/Libraries/CoreNodeModels/Input/DateTime.cs index 2a4cf6444b6..711e0dc9b20 100644 --- a/src/Libraries/CoreNodeModels/Input/DateTime.cs +++ b/src/Libraries/CoreNodeModels/Input/DateTime.cs @@ -25,6 +25,23 @@ private DateTime(IEnumerable inPorts, IEnumerable outPorts ShouldDisplayPreviewCore = false; } + public string ValueText + { + get + { return Value.ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture); } + set { } + } + + public override System.DateTime Value + { + get { return base.Value; } + set + { + base.Value = value; + RaisePropertyChanged(nameof(ValueText)); + } + } + /// /// Constructor /// @@ -96,7 +113,8 @@ public override IEnumerable BuildOutputAst(List - /// Looks up a localized string similar to The input must match the Date Time format.. + /// Looks up a localized string similar to The input must match the Date Time format and be a valid date/time.. /// public static string DateTimeNodeInputInvalidFormat { get { @@ -987,6 +987,15 @@ public static string IntegerSliderInfoMessage { } } + /// + /// Looks up a localized string similar to The input must be an integer.. + /// + public static string IntegerSliderInputMustBeInteger { + get { + return ResourceManager.GetString("IntegerSliderInputMustBeInteger", resourceCulture); + } + } + /// /// Looks up a localized string similar to Produces integer values. /// diff --git a/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx b/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx index 1450d015c92..c9700c491b6 100644 --- a/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx +++ b/src/Libraries/CoreNodeModels/Properties/Resources.en-US.resx @@ -585,8 +585,11 @@ The input must be numeric. - The input must match the Date Time format. - + The input must match the Date Time format and be a valid date/time. + + + The input must be an integer. + Select a Color from the palette diff --git a/src/Libraries/CoreNodeModels/Properties/Resources.resx b/src/Libraries/CoreNodeModels/Properties/Resources.resx index 959ea2304b9..a155dceea97 100644 --- a/src/Libraries/CoreNodeModels/Properties/Resources.resx +++ b/src/Libraries/CoreNodeModels/Properties/Resources.resx @@ -585,7 +585,10 @@ The input must be numeric. - The input must match the Date Time format. + The input must match the Date Time format and be a valid date/time. + + + The input must be an integer. Select a Color from the palette diff --git a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs index f96973fafae..c15330ebf98 100644 --- a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs +++ b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml.cs @@ -1,4 +1,3 @@ -using CoreNodeModelsWpf.Converters; using System.Windows; using System.Windows.Controls; using System.Windows.Data; @@ -19,10 +18,9 @@ public DateTimeInputControl() private void DateTimeInputControl_Loaded(object sender, RoutedEventArgs e) { - var binding = new System.Windows.Data.Binding(nameof(CoreNodeModels.Input.DateTime.Value)) + var binding = new System.Windows.Data.Binding(nameof(CoreNodeModels.Input.DateTime.ValueText)) { Mode = BindingMode.TwoWay, - Converter = new StringToDateTimeConverter(), UpdateSourceTrigger = UpdateSourceTrigger.Explicit, NotifyOnValidationError = false }; diff --git a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs index 77e640f1bcf..b4f8149562d 100644 --- a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs +++ b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs @@ -48,7 +48,7 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo) } catch (FormatException) { - return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); + return new ValidationResult(false, Resources.IntegerSliderInputMustBeInteger); } catch (OverflowException) { From 3239245b340b07e67ead67ad113de108db624aa9 Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:30:17 +0100 Subject: [PATCH 4/7] cleanup --- .../CoreNodeModels/Input/BaseTypes.cs | 5 +++ .../CoreNodeModels/Input/DateTime.cs | 29 ++++++++++++----- .../CoreNodeModels/Input/DoubleSlider.cs | 4 +++ .../CoreNodeModels/Input/IntegerSlider.cs | 14 +-------- .../CoreNodeModels/Input/SliderBase.cs | 18 ----------- .../Controls/DateTimeInputControl.xaml | 1 - .../Controls/DateTimeInputControl.xaml.cs | 2 ++ .../Controls/DynamoSlider.xaml.cs | 31 +++++++++++-------- .../CoreNodeModelsWpf/InputValidationRules.cs | 8 ++--- .../NodeViewCustomizations/IntegerSlider.cs | 2 ++ .../CoreNodeModelsWpf/SliderViewModel.cs | 3 ++ 11 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/Libraries/CoreNodeModels/Input/BaseTypes.cs b/src/Libraries/CoreNodeModels/Input/BaseTypes.cs index 826b33c064a..5c47d677e92 100644 --- a/src/Libraries/CoreNodeModels/Input/BaseTypes.cs +++ b/src/Libraries/CoreNodeModels/Input/BaseTypes.cs @@ -337,6 +337,8 @@ protected override void DeserializeCore(XmlElement element, SaveContext context) { base.DeserializeCore(element, context); //Base implementation must be called + ClearErrorsAndWarnings(); + foreach ( XmlNode subNode in element.ChildNodes.Cast() @@ -344,6 +346,9 @@ XmlNode subNode in { Value = subNode.Attributes[0].Value; } + + // Value's equality guard can skip notify, force UI to drop uncommitted invalid text + RaisePropertyChanged(nameof(Value)); } #endregion diff --git a/src/Libraries/CoreNodeModels/Input/DateTime.cs b/src/Libraries/CoreNodeModels/Input/DateTime.cs index 711e0dc9b20..3dd179c451f 100644 --- a/src/Libraries/CoreNodeModels/Input/DateTime.cs +++ b/src/Libraries/CoreNodeModels/Input/DateTime.cs @@ -25,10 +25,17 @@ private DateTime(IEnumerable inPorts, IEnumerable outPorts ShouldDisplayPreviewCore = false; } + /// + /// Display text for the DateTime input. Bound TwoWay so WPF validation can run; + /// the setter is intentionally empty — commits go through UpdateModelValueCommand. + /// + [JsonIgnore] public string ValueText { get { return Value.ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture); } + // Required for TwoWay binding / ValidateWithoutUpdate. Do not write the model here — + // DynamoTextBox commits via UpdateModelValueCommand; a real setter would double-commit. set { } } @@ -116,17 +123,27 @@ protected override bool UpdateValueCore(UpdateValueParams updateValueParams) if (updateValueParams.PropertyName == nameof(Value) || updateValueParams.PropertyName == nameof(ValueText)) { - if (TryParseDateTime(updateValueParams.PropertyValue, out var parsed)) + if (!TryParseDateTime(updateValueParams.PropertyValue, out var parsed)) { - ClearErrorsAndWarnings(); - Value = System.DateTime.SpecifyKind(parsed, DateTimeKind.Utc); + Error(Properties.Resources.DateTimeNodeInputInvalidFormat); + return false; } + + ClearErrorsAndWarnings(); + Value = System.DateTime.SpecifyKind(parsed, DateTimeKind.Utc); return true; } return base.UpdateValueCore(updateValueParams); } - internal static bool TryParseDateTime(string text, out System.DateTime parsed) + /// + /// Parses using + /// and the invariant culture. + /// + /// The date/time string to parse. + /// When this method returns, the parsed value if successful; otherwise default. + /// Return true if matches the expected format; otherwise false. + public static bool TryParseDateTime(string text, out System.DateTime parsed) { return System.DateTime.TryParseExact( text, @@ -138,9 +155,7 @@ internal static bool TryParseDateTime(string text, out System.DateTime parsed) protected override System.DateTime DeserializeValue(string val) { - System.DateTime result; - result = System.DateTime.TryParseExact(val, PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out result) ? - result : PreferenceSettings.DynamoDefaultTime; + var result = TryParseDateTime(val, out var parsed) ? parsed : PreferenceSettings.DynamoDefaultTime; return System.DateTime.SpecifyKind(result, DateTimeKind.Utc); } diff --git a/src/Libraries/CoreNodeModels/Input/DoubleSlider.cs b/src/Libraries/CoreNodeModels/Input/DoubleSlider.cs index eefdbece61c..5266198c08a 100644 --- a/src/Libraries/CoreNodeModels/Input/DoubleSlider.cs +++ b/src/Libraries/CoreNodeModels/Input/DoubleSlider.cs @@ -122,6 +122,7 @@ protected override bool UpdateValueCore(UpdateValueParams updateValueParams) return true; // UpdateValueCore handled. case "Max": case "MaxText": + ClearErrorsAndWarnings(); Max = ConvertStringToDouble(value); return true; // UpdateValueCore handled. case "Value": @@ -185,6 +186,9 @@ protected override void DeserializeCore(XmlElement element, SaveContext context) } } + // Value's equality guard can skip notify, force UI to drop uncommitted invalid text + RaisePropertyChanged(nameof(Value)); + break; } } diff --git a/src/Libraries/CoreNodeModels/Input/IntegerSlider.cs b/src/Libraries/CoreNodeModels/Input/IntegerSlider.cs index e1309bef205..546f9f79edd 100644 --- a/src/Libraries/CoreNodeModels/Input/IntegerSlider.cs +++ b/src/Libraries/CoreNodeModels/Input/IntegerSlider.cs @@ -291,7 +291,7 @@ public IntegerSlider64Bit() } // If the value field in the slider has a number greater than - // long.Maxvalue (or MinValue), the value will be changed to long.MaxValue (or MinValue) + // long.MaxValue (or MinValue), the value will be changed to long.MaxValue (or MinValue) // The property setter is overridden here to update the UI, in case the value is changed. public override long Value { @@ -338,18 +338,6 @@ protected override bool UpdateValueCore(UpdateValueParams updateValueParams) return base.UpdateValueCore(updateValueParams); } - private void UpdateNodeInfo(string value) - { - if (IsValueInt64(value)) - { - ClearInfoMessages(); - } - else - { - Info(Resources.IntegerSliderInfoMessage, true); - } - } - public override IEnumerable BuildOutputAst(List inputAstNodes) { var rhs = AstFactory.BuildIntNode(Value); diff --git a/src/Libraries/CoreNodeModels/Input/SliderBase.cs b/src/Libraries/CoreNodeModels/Input/SliderBase.cs index 9ffac94039c..f9c0feae0d8 100644 --- a/src/Libraries/CoreNodeModels/Input/SliderBase.cs +++ b/src/Libraries/CoreNodeModels/Input/SliderBase.cs @@ -160,23 +160,5 @@ protected static long ConvertStringToInt64(string value) } return result; } - - /// - /// check if the value is within int64 range - /// - /// - /// - protected static bool IsValueInt64(string value) - { - try - { - var result = Convert.ToInt64(value); - return true; - } - catch (OverflowException) - { - return false; - } - } } } \ No newline at end of file diff --git a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml index f9bc99b1612..51a052b6c51 100644 --- a/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml +++ b/src/Libraries/CoreNodeModelsWpf/Controls/DateTimeInputControl.xaml @@ -1,7 +1,6 @@  nodeUI) } - public void BindValidatedTextBoxes(Func ruleFactory) + /// + /// Installs the Text bindings for the value, min, max and step text boxes. + /// + /// + /// Optional factory, invoked once per text box. It must return a new rule instance on + /// every call - a shared instance would let one field's validation state leak into another. + /// Pass null to bind without validation. + /// + public void BindValidatedTextBoxes(Func ruleFactory = null) { - BindField(ValTb, "ValueText", ruleFactory()); - BindField(MinTb, "MinText", ruleFactory()); - BindField(MaxTb, "MaxText", ruleFactory()); - BindField(StepTb, "StepText", ruleFactory()); - } - - private static ValidationRule CloneStep(ValidationRule template) - { - template.ValidationStep = ValidationStep.RawProposedValue; - return template; + BindField(ValTb, "ValueText", ruleFactory?.Invoke()); + BindField(MinTb, "MinText", ruleFactory?.Invoke()); + BindField(MaxTb, "MaxText", ruleFactory?.Invoke()); + BindField(StepTb, "StepText", ruleFactory?.Invoke()); } private static void BindField(DynamoTextBox textBox, string propertyName, ValidationRule validationRule) @@ -57,8 +59,11 @@ private static void BindField(DynamoTextBox textBox, string propertyName, Valida NotifyOnValidationError = false }; - validationRule.ValidationStep = ValidationStep.RawProposedValue; - binding.ValidationRules.Add(validationRule); + if (validationRule != null) + { + validationRule.ValidationStep = ValidationStep.RawProposedValue; + binding.ValidationRules.Add(validationRule); + } textBox.BindToProperty(binding); Validation.SetErrorTemplate(textBox, null); diff --git a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs index b4f8149562d..a6d16950f58 100644 --- a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs +++ b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs @@ -1,9 +1,7 @@ using System; -using System.Diagnostics; using System.Globalization; using System.Windows.Controls; using CoreNodeModels.Properties; -using Dynamo.Configuration; namespace CoreNodeModelsWpf { @@ -33,6 +31,8 @@ internal static bool IsNumeric(string value) /// public class Integer64ValidationRule : ValidationRule { + private const NumberStyles IntegerStyles = NumberStyles.Integer | NumberStyles.AllowThousands; + public override ValidationResult Validate(object value, CultureInfo cultureInfo) { var text = value as string; @@ -43,7 +43,7 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo) try { - Convert.ToInt64(text, CultureInfo.InvariantCulture); + long.Parse(text, IntegerStyles, CultureInfo.InvariantCulture); return ValidationResult.ValidResult; } catch (FormatException) @@ -65,7 +65,7 @@ public class DateTimeValidationRule : ValidationRule public override ValidationResult Validate(object value, CultureInfo cultureInfo) { var text = value as string; - if (DateTime.TryParseExact(text, PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) + if (CoreNodeModels.Input.DateTime.TryParseDateTime(text, out _)) { return ValidationResult.ValidResult; } diff --git a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs index 4b2d1b95634..7b355fe8b99 100644 --- a/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs +++ b/src/Libraries/CoreNodeModelsWpf/NodeViewCustomizations/IntegerSlider.cs @@ -14,6 +14,8 @@ public void CustomizeView(IntegerSlider model, NodeView nodeView) DataContext = new SliderViewModel(model) }; + slider.BindValidatedTextBoxes(); + nodeView.inputGrid.Children.Add(slider); } diff --git a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs index 8dbffef9a88..5d67c5da74c 100644 --- a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs @@ -17,6 +17,9 @@ public class SliderViewModel : NotificationObject where T : IComparable { private SliderBase model; + // These text setters are intentionally empty. Bindings are TwoWay so + // ValidateWithoutUpdate() can run; DynamoTextBox writes the model via + // UpdateModelValueCommand. Implementing these would double-commit on every edit. public string MaxText { get { return SliderBase.ConvertNumberToString(model.Max); } From bfdee342c82fa6c1b61520a2b69eb120caa272fc Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:25:36 +0100 Subject: [PATCH 5/7] regression tests --- .../InputValidationErrorBubbleTests.cs | 296 ++++++++++++++++++ .../NodeViewCustomizationTests.cs | 77 ++++- 2 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 test/DynamoCoreWpf3Tests/InputValidationErrorBubbleTests.cs diff --git a/test/DynamoCoreWpf3Tests/InputValidationErrorBubbleTests.cs b/test/DynamoCoreWpf3Tests/InputValidationErrorBubbleTests.cs new file mode 100644 index 00000000000..97d82273242 --- /dev/null +++ b/test/DynamoCoreWpf3Tests/InputValidationErrorBubbleTests.cs @@ -0,0 +1,296 @@ +using CoreNodeModels.Input; +using CoreNodeModels.Properties; +using CoreNodeModelsWpf.Controls; +using Dynamo.Configuration; +using Dynamo.Controls; +using Dynamo.Graph.Nodes; +using Dynamo.Models; +using Dynamo.Nodes; +using Dynamo.Utilities; +using DynamoCoreWpfTests.Utility; +using NUnit.Framework; +using System.Globalization; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Data; +using DateTimeNode = CoreNodeModels.Input.DateTime; + +namespace DynamoCoreWpfTests +{ + public class InputValidationErrorBubbleTests : DynamoTestUIBase + { + protected override void GetLibrariesToPreload(System.Collections.Generic.List libraries) + { + libraries.Add("VMDataBridge.dll"); + libraries.Add("DSCoreNodes.dll"); + base.GetLibrariesToPreload(libraries); + } + + private static DynamoTextBox ValueBox(NodeView nodeView) + { + // Number / DateTime: single box. Sliders: ValTb. + var named = nodeView.ChildrenOfType() + .FirstOrDefault(tb => tb.Name == "ValTb" || tb.Name == "DateTimeTb"); + return named ?? nodeView.inputGrid.ChildrenOfType().First(); + } + + private static int ErrorCount(NodeModel node) => node.NodeInfos.Count(i => i.State == ElementState.Error); + + private NodeView AddAndGetView(NodeModel node) + { + Model.AddNodeToCurrentWorkspace(node, true); + DispatcherUtil.DoEventsLoop(() => View.NodeViewsInFirstWorkspace().Any(nv => nv.ViewModel.NodeLogic.GUID == node.GUID), timeoutSeconds: 5); + return NodeViewWithGuid(node.GUID.ToString()); + } + + private void CommitText(DynamoTextBox box, string text) + { + box.Text = text; // triggers DynamoTextBox.UpdateDataSource(recordForUndo: true) + DispatcherUtil.DoEvents(); + } + + #region Number (DoubleInput) + + [Test] + public void WhenNumberGetsInvalidInputThenKeepsValueShowsErrorKeepsTextAndSkipsUndo() + { + var node = new DoubleInput(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "1"); + CommitText(box, "2"); + CommitText(box, "K"); + + Assert.AreEqual("2", node.Value); + Assert.AreEqual("K", box.Text); + Assert.IsTrue(node.IsInErrorState); + Assert.AreEqual(1, ErrorCount(node)); + Assert.AreEqual(Resources.NumberNodeInputMustBeNumeric, + node.NodeInfos.Single(i => i.State == ElementState.Error).Message); + + Model.CurrentWorkspace.Undo(); + DispatcherUtil.DoEvents(); + + // Invalid "K" was not recorded — undo reverses 1→2, not 2→K. + Assert.AreEqual("1", node.Value); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + Assert.AreEqual("1.000", box.Text); + } + + [Test] + public void WhenNumberGetsValidInputAfterErrorThenClearsBubbleAndUpdatesValue() + { + var node = new DoubleInput(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "5"); + CommitText(box, "K"); + Assert.IsTrue(node.IsInErrorState); + + CommitText(box, "7"); + + Assert.AreEqual("7", node.Value); + Assert.AreEqual("7.000", box.Text); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + } + + [Test] + public void WhenNumberSwapsInvalidInputsThenShowsOnlyLatestError() + { + var node = new DoubleInput(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "1"); + CommitText(box, "K"); + CommitText(box, "m"); + + Assert.AreEqual("1", node.Value); + Assert.AreEqual("m", box.Text); + Assert.AreEqual(1, ErrorCount(node)); + Assert.AreEqual(Resources.NumberNodeInputMustBeNumeric, + node.NodeInfos.Single(i => i.State == ElementState.Error).Message); + } + + [Test] + public void WhenNumberUndoesAfterErrorOnSameValueThenClearsBubble() + { + // Repro: Value setter early-returns when restored string equals current; + // DeserializeCore must clear Infos unconditionally. + var node = new DoubleInput(); + AddAndGetView(node); + + Model.ExecuteCommand(new DynamoModel.UpdateModelValueCommand( + Model.CurrentWorkspace.Guid, node.GUID, nameof(DoubleInput.Value), "5")); + Model.ExecuteCommand(new DynamoModel.UpdateModelValueCommand( + Model.CurrentWorkspace.Guid, node.GUID, nameof(DoubleInput.Value), "5")); + + var view = NodeViewWithGuid(node.GUID.ToString()); + CommitText(ValueBox(view), "K"); + Assert.IsTrue(node.IsInErrorState); + + Model.CurrentWorkspace.Undo(); + DispatcherUtil.DoEvents(); + + Assert.AreEqual("5", node.Value); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + } + + #endregion + + #region Integer Slider 64-bit + + [Test] + public void WhenIntegerSlider64GetsInvalidInputThenKeepsValueShowsErrorAndSkipsUndo() + { + var node = new IntegerSlider64Bit(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "1"); + CommitText(box, "2"); + CommitText(box, "K"); + + Assert.AreEqual(2L, node.Value); + Assert.AreEqual("K", box.Text); + Assert.IsTrue(node.IsInErrorState); + Assert.AreEqual(1, ErrorCount(node)); + + Model.CurrentWorkspace.Undo(); + DispatcherUtil.DoEvents(); + + Assert.AreEqual(1L, node.Value); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual("1", box.Text); + } + + [Test] + public void WhenIntegerSlider64GetsThousandsGroupedInputThenAcceptsValue() + { + var node = new IntegerSlider64Bit { Max = 10000 }; + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "2,500"); + + Assert.AreEqual(2500L, node.Value); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + } + + [Test] + public void WhenIntegerSlider64OverflowsThenShowsRangeErrorNotStacked() + { + var node = new IntegerSlider64Bit(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "1"); + CommitText(box, "m"); + Assert.AreEqual(Resources.IntegerSliderInputMustBeInteger, + node.NodeInfos.Single(i => i.State == ElementState.Error).Message); + + CommitText(box, "9223372036854775808"); // long.MaxValue + 1 + + Assert.AreEqual(1L, node.Value); + Assert.AreEqual(1, ErrorCount(node)); + Assert.AreEqual(Resources.IntegerSliderInfoMessage, + node.NodeInfos.Single(i => i.State == ElementState.Error).Message); + } + + #endregion + + #region Double Slider + + [Test] + public void WhenDoubleSliderGetsInvalidInputThenKeepsValueShowsErrorAndClearsOnValid() + { + var node = new CoreNodeModels.Input.DoubleSlider(); + var view = AddAndGetView(node); + var box = ValueBox(view); + + CommitText(box, "3.5"); + CommitText(box, "abc"); + + Assert.AreEqual(3.5, node.Value, 1e-9); + Assert.AreEqual("abc", box.Text); + Assert.IsTrue(node.IsInErrorState); + + CommitText(box, "4.25"); + + Assert.AreEqual(4.25, node.Value, 1e-9); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + } + + #endregion + + #region DateTime + + [Test] + public void WhenDateTimeGetsInvalidInputThenKeepsValueShowsErrorAndClearsOnUndo() + { + var node = new DateTimeNode + { + // DefaultDateFormat carries only minutes, so we need a value that round-trips + // exactly through serialize/undo. UtcNow would lose seconds and ticks. + Value = new System.DateTime(2015, 5, 30, 5, 30, 0, System.DateTimeKind.Utc) + }; + var original = node.Value; + var view = AddAndGetView(node); + var box = ValueBox(view); + + var valid = new System.DateTime(2020, 12, 8, 12, 0, 0, System.DateTimeKind.Utc); + CommitText(box, valid.ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture)); + Assert.AreEqual(valid, node.Value); + + CommitText(box, "not-a-date"); + + Assert.AreEqual(valid, node.Value); + Assert.AreEqual("not-a-date", box.Text); + Assert.IsTrue(node.IsInErrorState); + Assert.AreEqual(Resources.DateTimeNodeInputInvalidFormat, + node.NodeInfos.Single(i => i.State == ElementState.Error).Message); + + Model.CurrentWorkspace.Undo(); + DispatcherUtil.DoEvents(); + + Assert.AreEqual(original, node.Value); + Assert.IsFalse(node.IsInErrorState); + Assert.AreEqual(0, ErrorCount(node)); + } + + #endregion + + #region Legacy IntegerSlider (32-bit) binding regression + + [Test] + public void WhenIntegerSliderCustomizedThenTextBoxesAreBoundAndPopulated() + { + var node = new CoreNodeModels.Input.IntegerSlider64Bit { Value = 41, Min = 0, Max = 100, Step = 1 }; + var view = AddAndGetView(node); + var slider = view.ChildrenOfType().First(); + + foreach (var name in new[] { "ValTb", "MinTb", "MaxTb", "StepTb" }) + { + var tb = (DynamoTextBox)slider.FindName(name); + Assert.IsNotNull(tb, $"{name} missing"); + Assert.IsNotNull( + BindingOperations.GetBindingExpression(tb, TextBox.TextProperty), + $"{name} has no Text binding"); + } + + Assert.AreEqual("41", ((DynamoTextBox)slider.FindName("ValTb")).Text); + Assert.AreEqual("0", ((DynamoTextBox)slider.FindName("MinTb")).Text); + Assert.AreEqual("100", ((DynamoTextBox)slider.FindName("MaxTb")).Text); + Assert.AreEqual("1", ((DynamoTextBox)slider.FindName("StepTb")).Text); + } + + #endregion + } +} diff --git a/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs b/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs index 84d07b049e5..b7272f38286 100644 --- a/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs +++ b/test/DynamoCoreWpf3Tests/NodeViewCustomizationTests.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Windows; -using System.Windows.Controls; using CoreNodeModels.Input; -using CoreNodeModelsWpf.Controls; using CoreNodeModelsWpf; +using CoreNodeModelsWpf.Controls; +using Dynamo.Configuration; using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Graph.Nodes.CustomNodes; @@ -17,6 +11,13 @@ using Dynamo.Utilities; using DynamoCoreWpfTests.Utility; using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; namespace DynamoCoreWpfTests { @@ -503,6 +504,66 @@ public void INodeViewCustomizationCheckUsingReflectionIsCorrect() //this assembly contains some builtin nodeviewcustomizations and this methd should return true. Assert.IsFalse(NodeModelAssemblyLoader.ContainsNodeViewCustomizationType(dyncoreAssem)); } + + + + + + [Test] + [Category("UnitTests")] + public static void WhenInteger64InputIsNonIntegerThenValidationFails() + { + var rule = new Integer64ValidationRule(); + Assert.IsFalse(rule.Validate("m", CultureInfo.InvariantCulture).IsValid); + Assert.IsFalse(rule.Validate("1.5", CultureInfo.InvariantCulture).IsValid); + Assert.IsFalse(rule.Validate("", CultureInfo.InvariantCulture).IsValid); + Assert.IsFalse(rule.Validate(" ", CultureInfo.InvariantCulture).IsValid); + } + + [Test] + [Category("UnitTests")] + public static void WhenInteger64InputIsValidOrGroupedThenValidationPasses() + { + var rule = new Integer64ValidationRule(); + Assert.IsTrue(rule.Validate("0", CultureInfo.InvariantCulture).IsValid); + Assert.IsTrue(rule.Validate("2500", CultureInfo.InvariantCulture).IsValid); + Assert.IsTrue(rule.Validate("2,500", CultureInfo.InvariantCulture).IsValid); + Assert.IsTrue(rule.Validate("-2,500", CultureInfo.InvariantCulture).IsValid); + } + + [Test] + [Category("UnitTests")] + public static void WhenInteger64InputOverflowsThenValidationFailsWithRangeMessage() + { + var rule = new Integer64ValidationRule(); + var result = rule.Validate("9223372036854775808", CultureInfo.InvariantCulture); // long.MaxValue + 1 + Assert.IsFalse(result.IsValid); + Assert.AreEqual(CoreNodeModels.Properties.Resources.IntegerSliderInfoMessage, result.ErrorContent); + } + + [Test] + [Category("UnitTests")] + public static void WhenDateTimeInputMatchesDefaultFormatThenValidationPasses() + { + var rule = new DateTimeValidationRule(); + var text = new System.DateTime(2000, 1, 1, 12, 0, 0) + .ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture); + Assert.IsTrue(rule.Validate(text, CultureInfo.InvariantCulture).IsValid); + Assert.IsTrue(CoreNodeModels.Input.DateTime.TryParseDateTime(text, out _)); + } + + [Test] + [Category("UnitTests")] + public static void WhenDateTimeInputIsInvalidThenValidationFails() + { + var rule = new DateTimeValidationRule(); + var result = rule.Validate("not-a-date", CultureInfo.InvariantCulture); + Assert.IsFalse(result.IsValid); + Assert.AreEqual( + CoreNodeModels.Properties.Resources.DateTimeNodeInputInvalidFormat, + result.ErrorContent); + Assert.IsFalse(CoreNodeModels.Input.DateTime.TryParseDateTime("not-a-date", out _)); + } } } From 8f0633968010600d512a748d004171bfdc51e28e Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:51:08 +0100 Subject: [PATCH 6/7] sonarQube - 'value' keyword used in property set accessor declarations --- src/Libraries/CoreNodeModels/Input/DateTime.cs | 9 +++------ .../CoreNodeModelsWpf/SliderViewModel.cs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Libraries/CoreNodeModels/Input/DateTime.cs b/src/Libraries/CoreNodeModels/Input/DateTime.cs index 3dd179c451f..63ad0dcdfd9 100644 --- a/src/Libraries/CoreNodeModels/Input/DateTime.cs +++ b/src/Libraries/CoreNodeModels/Input/DateTime.cs @@ -27,16 +27,13 @@ private DateTime(IEnumerable inPorts, IEnumerable outPorts /// /// Display text for the DateTime input. Bound TwoWay so WPF validation can run; - /// the setter is intentionally empty — commits go through UpdateModelValueCommand. + /// the setter intentionally discard the value — commits go through UpdateModelValueCommand. /// [JsonIgnore] public string ValueText { - get - { return Value.ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture); } - // Required for TwoWay binding / ValidateWithoutUpdate. Do not write the model here — - // DynamoTextBox commits via UpdateModelValueCommand; a real setter would double-commit. - set { } + get => Value.ToString(PreferenceSettings.DefaultDateFormat, CultureInfo.InvariantCulture); + set => _ = value; } public override System.DateTime Value diff --git a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs index 5d67c5da74c..1527f25df50 100644 --- a/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/SliderViewModel.cs @@ -17,31 +17,31 @@ public class SliderViewModel : NotificationObject where T : IComparable { private SliderBase model; - // These text setters are intentionally empty. Bindings are TwoWay so + // These text setters intentionally discard the value. Bindings are TwoWay so // ValidateWithoutUpdate() can run; DynamoTextBox writes the model via // UpdateModelValueCommand. Implementing these would double-commit on every edit. public string MaxText { - get { return SliderBase.ConvertNumberToString(model.Max); } - set { } + get => SliderBase.ConvertNumberToString(model.Max); + set => _ = value; } public string MinText { - get { return SliderBase.ConvertNumberToString(model.Min); } - set { } + get => SliderBase.ConvertNumberToString(model.Min); + set => _ = value; } public string StepText { - get { return SliderBase.ConvertNumberToString(model.Step); } - set { } + get => SliderBase.ConvertNumberToString(model.Step); + set => _ = value; } public string ValueText { - get { return SliderBase.ConvertNumberToString(model.Value); } - set { } + get => SliderBase.ConvertNumberToString(model.Value); + set => _ = value; } public T Max From 7674c24e4d348137a1147a7b868857dbb64228dc Mon Sep 17 00:00:00 2001 From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:10:08 +0100 Subject: [PATCH 7/7] Integer64ValidationRule update after sonarQube's comment --- .../CoreNodeModelsWpf/InputValidationRules.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs index a6d16950f58..aff81084a1f 100644 --- a/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs +++ b/src/Libraries/CoreNodeModelsWpf/InputValidationRules.cs @@ -1,5 +1,5 @@ -using System; using System.Globalization; +using System.Numerics; using System.Windows.Controls; using CoreNodeModels.Properties; @@ -41,19 +41,14 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo) return new ValidationResult(false, Resources.NumberNodeInputMustBeNumeric); } - try + if (long.TryParse(text, IntegerStyles, CultureInfo.InvariantCulture, out _)) { - long.Parse(text, IntegerStyles, CultureInfo.InvariantCulture); return ValidationResult.ValidResult; } - catch (FormatException) - { - return new ValidationResult(false, Resources.IntegerSliderInputMustBeInteger); - } - catch (OverflowException) - { - return new ValidationResult(false, Resources.IntegerSliderInfoMessage); - } + + return BigInteger.TryParse(text, IntegerStyles, CultureInfo.InvariantCulture, out _) + ? new ValidationResult(false, Resources.IntegerSliderInfoMessage) + : new ValidationResult(false, Resources.IntegerSliderInputMustBeInteger); } }