From bbf11be8de9aa5ea37d8c1315d36df1fc423ecaf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jun 2024 06:06:29 +0200 Subject: [PATCH] Fix negation of null values. (#16101) * Add tests for binding negation operator. Expected results come from 11.0.x branch. * Unset and null need to be distinct. Also remove `ExpressionNode.Reset` as it was doing unneeded stuff and should just be the same as `SetSource(AvaloniaProperty.UnsetValue, null);`. * Make ExpressionNode.OnSourceChanged accept null. The previous design assumed that a source of `null` was an invalid input to all types of expression nodes. It turned out that there was a single exception to this rule: the `!` operator can in fact operate on a null value. With this new design we instead have to explicitly check for a null value in every override of `OnSourceChanged ` except in `LogicalNotNode`. Due to this change, we also have to distinguish between `null` and `(unset)` in `ExpressionNode.SetSource` as well. Fixes #16071 * Fix comment. --- src/Avalonia.Base/Data/BindingNotification.cs | 2 +- .../Data/Core/BindingExpression.cs | 14 +-- .../Core/ExpressionNodes/ArrayIndexerNode.cs | 5 +- .../AvaloniaPropertyAccessorNode.cs | 3 + .../ExpressionNodes/CollectionNodeBase.cs | 5 +- .../Core/ExpressionNodes/DataContextNode.cs | 5 +- .../Core/ExpressionNodes/ExpressionNode.cs | 49 ++++---- .../Core/ExpressionNodes/FuncTransformNode.cs | 5 +- .../LogicalAncestorElementNode.cs | 5 +- .../Core/ExpressionNodes/LogicalNotNode.cs | 4 +- .../Core/ExpressionNodes/MethodCommandNode.cs | 5 +- .../Core/ExpressionNodes/NamedElementNode.cs | 5 +- .../ExpressionNodes/ParentDataContextNode.cs | 5 +- .../ExpressionNodes/PropertyAccessorNode.cs | 5 +- .../DynamicPluginPropertyAccessorNode.cs | 5 +- .../Reflection/DynamicPluginStreamNode.cs | 5 +- .../Reflection/ReflectionIndexerNode.cs | 5 +- .../Reflection/ReflectionTypeCastNode.cs | 5 +- .../Data/Core/ExpressionNodes/StreamNode.cs | 5 +- .../ExpressionNodes/TemplatedParentNode.cs | 5 +- .../VisualAncestorElementNode.cs | 5 +- .../Xaml/BindingTests.cs | 111 ++++++++++++++++++ 22 files changed, 214 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index a3a2e0c2b0..39ef4374aa 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -178,7 +178,7 @@ namespace Avalonia.Data /// to . If is a /// then the value will first be extracted. /// - public static object? UpdateValue(object o, object value) + public static object? UpdateValue(object? o, object value) { if (o is BindingNotification n) { diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index c98650fa8e..ba0ce7bb9e 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -160,7 +160,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri var source = _nodes[0].Source; for (var i = 0; i < _nodes.Count; ++i) - _nodes[i].SetSource(null, null); + _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); _nodes[0].SetSource(source, null); } @@ -253,10 +253,6 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri _nodes[nodeIndex + 1].SetSource(value, dataValidationError); WriteTargetValueToSource(); } - else if (value is null) - { - OnNodeError(nodeIndex, "Value is null."); - } else { _nodes[nodeIndex + 1].SetSource(value, dataValidationError); @@ -273,11 +269,11 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri /// The error message. internal void OnNodeError(int nodeIndex, string error) { - // Set the source of all nodes after the one that errored to null. This needs to be done - // for each node individually because setting the source to null will not result in + // Set the source of all nodes after the one that errored to unset. This needs to be done + // for each node individually because setting the source to unset will not result in // OnNodeValueChanged or OnNodeError being called. for (var i = nodeIndex + 1; i < _nodes.Count; ++i) - _nodes[i].SetSource(null, null); + _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); if (_mode == BindingMode.OneWayToSource) return; @@ -394,7 +390,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri protected override void StopCore() { foreach (var node in _nodes) - node.Reset(); + node.SetSource(AvaloniaProperty.UnsetValue, null); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && TryGetTarget(out var target)) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs index a347a1ab72..514cf235ff 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs @@ -44,8 +44,11 @@ internal sealed class ArrayIndexerNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is Array array) SetValue(array.GetValue(_indexes)); else diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs index 09f4c9be26..266fbb884a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs @@ -38,6 +38,9 @@ internal sealed class AvaloniaPropertyAccessorNode : protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is AvaloniaObject newObject) { WeakEvents.AvaloniaPropertyChanged.Subscribe(newObject, this); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs index db8a8e8080..1c9c7d0294 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs @@ -22,8 +22,11 @@ internal abstract class CollectionNodeBase : ExpressionNode, UpdateValueOrSetError(sender); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + Subscribe(source); UpdateValue(source); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs index 14e21d4192..2aa14f12a2 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs @@ -4,8 +4,11 @@ namespace Avalonia.Data.Core.ExpressionNodes; internal sealed class DataContextNode : DataContextNodeBase { - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is IDataContextProvider && source is AvaloniaObject ao) { ao.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs index 8b53190f86..ee52308756 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; @@ -60,16 +61,6 @@ internal abstract class ExpressionNode BuildString(builder); } - /// - /// Resets the node to its uninitialized state when the is unsubscribed. - /// - public void Reset() - { - SetSource(null, null); - _source = null; - _value = AvaloniaProperty.UnsetValue; - } - /// /// Sets the owner binding. /// @@ -101,28 +92,26 @@ internal abstract class ExpressionNode /// public void SetSource(object? source, Exception? dataValidationError) { - var oldSource = Source; - - if (source == AvaloniaProperty.UnsetValue) - source = null; + if (_source?.TryGetTarget(out var oldSource) != true) + oldSource = AvaloniaProperty.UnsetValue; if (source == oldSource) return; - if (oldSource is not null) + if (oldSource is not null && oldSource != AvaloniaProperty.UnsetValue) Unsubscribe(oldSource); - _source = new(source); - - if (source is null) + if (source == AvaloniaProperty.UnsetValue) { - // If the source is null then the value is null. We explicitly do not want to call + // If the source is unset then the value is unset. We explicitly do not want to call // OnSourceChanged as we don't want to raise errors for subsequent nodes in the // binding change. + _source = null; _value = AvaloniaProperty.UnsetValue; } else { + _source = new(source); try { OnSourceChanged(source, dataValidationError); } catch (Exception e) { SetError(e); } } @@ -242,6 +231,26 @@ internal abstract class ExpressionNode } } + /// + /// Called from to validate that the source + /// is non-null and raise a node error if it is not. + /// + /// The expression node source. + /// + /// True if the source is non-null; otherwise, false. + /// + protected bool ValidateNonNullSource([NotNullWhen(true)] object? source) + { + if (source is null) + { + Owner?.OnNodeError(Index - 1, "Value is null."); + _value = null; + return false; + } + + return true; + } + /// /// When implemented in a derived class, subscribes to the new source, and updates the current /// . @@ -250,7 +259,7 @@ internal abstract class ExpressionNode /// /// Any data validation error reported by the previous expression node. /// - protected abstract void OnSourceChanged(object source, Exception? dataValidationError); + protected abstract void OnSourceChanged(object? source, Exception? dataValidationError); /// /// When implemented in a derived class, unsubscribes from the previous source. diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs index 7ad0b7ee97..1eb15d2469 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs @@ -21,8 +21,11 @@ internal sealed class FuncTransformNode : ExpressionNode // We don't have enough information to add anything here. } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + SetValue(_transform(source)); } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs index ccf6c76f90..b15f285867 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs @@ -56,8 +56,11 @@ internal sealed class LogicalAncestorElementNode : SourceNode return target is ILogical logical && logical.IsAttachedToLogicalTree; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is ILogical logical) { var locator = ControlLocator.Track(logical, _ancestorLevel, _ancestorType); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs index bb65ac16dd..8b1102b463 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs @@ -28,14 +28,12 @@ internal sealed class LogicalNotNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { var v = BindingNotification.ExtractValue(source); if (TryConvert(v, out var value)) - { SetValue(BindingNotification.UpdateValue(source, !value), dataValidationError); - } else SetError($"Unable to convert '{source}' to bool."); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs index 76ea564320..e0641f1461 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs @@ -39,8 +39,11 @@ internal sealed class MethodCommandNode : ExpressionNode, IWeakEventSubscriber

(source); if (_plugin.Start(reference, PropertyName) is { } accessor) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs index d1ecc13208..533520d35c 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs @@ -42,8 +42,11 @@ internal sealed class DynamicPluginPropertyAccessorNode : ExpressionNode, IPrope return _accessor?.SetValue(value, BindingPriority.LocalValue) ?? false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + var reference = new WeakReference(source); if (GetPlugin(source) is { } plugin && diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs index 70b0f710a2..f6c692e685 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs @@ -16,8 +16,11 @@ internal sealed class DynamicPluginStreamNode : ExpressionNode builder.Append('^'); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + var reference = new WeakReference(source); if (GetPlugin(reference) is { } plugin && diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs index 52526431dc..abc7d80744 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs @@ -52,8 +52,11 @@ internal sealed class ReflectionIndexerNode : CollectionNodeBase, ISettableNode return true; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + _indexes = null; if (GetIndexer(source.GetType(), out _getter, out _setter)) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs index c973d9d236..8b356603a6 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs @@ -19,8 +19,11 @@ internal sealed class ReflectionTypeCastNode : ExpressionNode builder.Append(')'); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (_targetType.IsInstanceOfType(source)) SetValue(source); else diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs index 19e5a58828..32ccc9e214 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs @@ -23,8 +23,11 @@ internal sealed class StreamNode : ExpressionNode, IObserver void IObserver.OnError(Exception error) { } void IObserver.OnNext(object? value) => SetValue(value); - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (_plugin.Start(new(source)) is { } accessor) { _subscription = accessor.Subscribe(this); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs index 6e81a01cee..3dca520fa7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs @@ -22,8 +22,11 @@ internal sealed class TemplatedParentNode : SourceNode throw new InvalidOperationException("Cannot find a StyledElement to get a TemplatedParent."); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is StyledElement newElement) { newElement.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs index 4bca2c8cb4..4cf27d8968 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs @@ -56,8 +56,11 @@ internal sealed class VisualAncestorElementNode : SourceNode return target is Visual visual && visual.IsAttachedToVisualTree; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is Visual visual) { var locator = VisualLocator.Track(visual, _ancestorLevel, _ancestorType); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 38b286fa39..84a3bf1fcf 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Globalization; using System.Reactive.Subjects; using Avalonia.Controls; @@ -426,11 +428,120 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Theory] + [MemberData(nameof(NegationData))] + public void Negating_Object_Returns_Correct_Value(object value, bool? expected) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var viewModel = new WindowViewModel { Object = value }; + + window.DataContext = viewModel; + window.ApplyTemplate(); + + Assert.Equal(expected, window.Tag); + } + } + + [Theory] + [MemberData(nameof(NegationData))] + public void Double_Negating_Object_Returns_Correct_Value(object value, bool? negated) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var viewModel = new WindowViewModel { Object = value }; + + window.DataContext = viewModel; + window.ApplyTemplate(); + + var expected = negated.HasValue ? !negated : null; + Assert.Equal(expected, window.Tag); + } + } + + [Theory] + [MemberData(nameof(NegationData))] + public void Negating_Object_Returns_Correct_Value_When_Bound_To_Bool(object value, bool? expected) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var viewModel = new WindowViewModel { Object = value }; + + window.DataContext = viewModel; + window.ApplyTemplate(); + + Assert.Equal(expected ?? false, window.IsVisible); + } + } + + [Theory] + [MemberData(nameof(NegationData))] + public void Double_Negating_Object_Returns_Correct_Value_When_Bound_To_Bool(object value, bool? negated) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var viewModel = new WindowViewModel { Object = value }; + + window.DataContext = viewModel; + window.ApplyTemplate(); + + var expected = negated.HasValue ? !negated : false; + Assert.Equal(expected, window.IsVisible); + } + } + + public static IEnumerable NegationData() + { + yield return new object[] { true, false }; + yield return new object[] { false, true }; + yield return new object[] { null, true }; + yield return new object[] { new object(), null }; + yield return new object[] { "foo", null }; + yield return new object[] { "true", false }; + yield return new object[] { "false", true }; + yield return new object[] { 0, true }; + yield return new object[] { 1, false }; + yield return new object[] { 2, false }; + yield return new object[] { -1, false }; + yield return new object[] { 0.0, true }; + yield return new object[] { 1.0, false }; + yield return new object[] { 2.0, false }; + yield return new object[] { -1.0, false }; + yield return new object[] { double.NaN, false }; + yield return new object[] { double.PositiveInfinity, false }; + yield return new object[] { double.NegativeInfinity, false }; + } + private class WindowViewModel { public bool ShowInTaskbar { get; set; } public string Greeting1 { get; set; } = "Hello"; public string Greeting2 { get; set; } = "World"; + public object Object { get; set; } } public class CultureAppender : IValueConverter