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);
}
}