csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
953 lines
30 KiB
953 lines
30 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Reactive.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Data;
|
|
using Avalonia.Data.Converters;
|
|
using Avalonia.Threading;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Markup.UnitTests.Data
|
|
{
|
|
public class BindingTests
|
|
{
|
|
[Fact]
|
|
public void OneWay_Binding_Should_Be_Set_Up()
|
|
{
|
|
var source = new Source { Foo = "foo" };
|
|
var target = new TextBlock { DataContext = source };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.OneWay,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
source.Foo = "bar";
|
|
Assert.Equal("bar", target.Text);
|
|
target.Text = "baz";
|
|
Assert.Equal("bar", source.Foo);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoWay_Binding_Should_Be_Set_Up()
|
|
{
|
|
var source = new Source { Foo = "foo" };
|
|
var target = new TextBlock { DataContext = source };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.TwoWay,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
source.Foo = "bar";
|
|
Assert.Equal("bar", target.Text);
|
|
target.Text = "baz";
|
|
Assert.Equal("baz", source.Foo);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect()
|
|
{
|
|
var source = new WeakRefSource { Foo = null };
|
|
var target = new TestControl { DataContext = source };
|
|
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.TwoWay
|
|
};
|
|
|
|
target.Bind(TestControl.ValueProperty, binding);
|
|
|
|
var ref1 = AssignValue(target, "ref1");
|
|
|
|
Assert.Equal(ref1.Target, source.Foo);
|
|
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
|
|
var ref2 = AssignValue(target, "ref2");
|
|
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
|
|
target.Value = null;
|
|
|
|
Assert.Null(source.Foo);
|
|
}
|
|
|
|
private class DummyObject : ICloneable
|
|
{
|
|
private readonly string _val;
|
|
|
|
public DummyObject(string val)
|
|
{
|
|
_val = val;
|
|
}
|
|
|
|
public object Clone()
|
|
{
|
|
return new DummyObject(_val);
|
|
}
|
|
|
|
protected bool Equals(DummyObject other)
|
|
{
|
|
return string.Equals(_val, other._val);
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (ReferenceEquals(null, obj))
|
|
return false;
|
|
if (ReferenceEquals(this, obj))
|
|
return true;
|
|
if (obj.GetType() != this.GetType())
|
|
return false;
|
|
return Equals((DummyObject)obj);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return (_val != null ? _val.GetHashCode() : 0);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static WeakReference AssignValue(TestControl source, string val)
|
|
{
|
|
var obj = new DummyObject(val);
|
|
|
|
source.Value = obj;
|
|
|
|
return new WeakReference(obj);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneTime_Binding_Should_Be_Set_Up()
|
|
{
|
|
var source = new Source { Foo = "foo" };
|
|
var target = new TextBlock { DataContext = source };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.OneTime,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
source.Foo = "bar";
|
|
Assert.Equal("foo", target.Text);
|
|
target.Text = "baz";
|
|
Assert.Equal("bar", source.Foo);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneTime_Binding_Releases_Subscription_If_DataContext_Set_Later()
|
|
{
|
|
var target = new TextBlock();
|
|
var source = new Source { Foo = "foo" };
|
|
|
|
target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime));
|
|
target.DataContext = source;
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, source.SubscriberCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneWayToSource_Binding_Should_Be_Set_Up()
|
|
{
|
|
var source = new Source { Foo = "foo" };
|
|
var target = new TextBlock { DataContext = source, Text = "bar" };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.OneWayToSource,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
Assert.Equal("bar", source.Foo);
|
|
target.Text = "baz";
|
|
Assert.Equal("baz", source.Foo);
|
|
source.Foo = "quz";
|
|
Assert.Equal("baz", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneWayToSource_Binding_Should_React_To_DataContext_Changed()
|
|
{
|
|
var target = new TextBlock { Text = "bar" };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.OneWayToSource,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
var source = new Source { Foo = "foo" };
|
|
target.DataContext = source;
|
|
|
|
Assert.Equal("bar", source.Foo);
|
|
target.Text = "baz";
|
|
Assert.Equal("baz", source.Foo);
|
|
source.Foo = "quz";
|
|
Assert.Equal("baz", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneWayToSource_Binding_Should_Not_StackOverflow_With_Null_Value()
|
|
{
|
|
// Issue #2912
|
|
var target = new TextBlock { Text = null };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.OneWayToSource,
|
|
};
|
|
|
|
target.Bind(TextBox.TextProperty, binding);
|
|
|
|
var source = new Source { Foo = "foo" };
|
|
target.DataContext = source;
|
|
|
|
Assert.Null(source.Foo);
|
|
|
|
// When running tests under NCrunch, NCrunch replaces the standard StackOverflowException
|
|
// with its own, which will be caught by our code. Detect the stackoverflow anyway, by
|
|
// making sure the target property was only set once.
|
|
Assert.Equal(2, source.FooSetCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Default_BindingMode_Should_Be_Used()
|
|
{
|
|
var source = new Source { Foo = "foo" };
|
|
var target = new TwoWayBindingTest { DataContext = source };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
};
|
|
|
|
target.Bind(TwoWayBindingTest.TwoWayProperty, binding);
|
|
|
|
Assert.Equal("foo", target.TwoWay);
|
|
source.Foo = "bar";
|
|
Assert.Equal("bar", target.TwoWay);
|
|
target.TwoWay = "baz";
|
|
Assert.Equal("baz", source.Foo);
|
|
}
|
|
|
|
[Fact]
|
|
public void DataContext_Binding_Should_Use_Parent_DataContext()
|
|
{
|
|
var parentDataContext = Mock.Of<IHeadered>(x => x.Header == (object)"Foo");
|
|
|
|
var parent = new Decorator
|
|
{
|
|
Child = new Control(),
|
|
DataContext = parentDataContext,
|
|
};
|
|
|
|
var binding = new Binding
|
|
{
|
|
Path = "Header",
|
|
};
|
|
|
|
parent.Child.Bind(Control.DataContextProperty, binding);
|
|
|
|
Assert.Equal("Foo", parent.Child.DataContext);
|
|
|
|
parentDataContext = Mock.Of<IHeadered>(x => x.Header == (object)"Bar");
|
|
parent.DataContext = parentDataContext;
|
|
Assert.Equal("Bar", parent.Child.DataContext);
|
|
}
|
|
|
|
[Fact]
|
|
public void DataContext_Binding_Should_Track_Parent()
|
|
{
|
|
var parent = new Decorator
|
|
{
|
|
DataContext = new { Foo = "foo" },
|
|
};
|
|
|
|
var child = new Control();
|
|
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
};
|
|
|
|
child.Bind(Control.DataContextProperty, binding);
|
|
|
|
Assert.Null(child.DataContext);
|
|
parent.Child = child;
|
|
Assert.Equal("foo", child.DataContext);
|
|
}
|
|
|
|
[Fact]
|
|
public void DataContext_Binding_Should_Produce_Correct_Results()
|
|
{
|
|
var viewModel = new { Foo = "bar" };
|
|
var root = new Decorator
|
|
{
|
|
DataContext = viewModel,
|
|
};
|
|
|
|
var child = new Control();
|
|
var values = new List<object>();
|
|
|
|
child.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x));
|
|
child.Bind(Control.DataContextProperty, new Binding("Foo"));
|
|
|
|
// When binding to DataContext and the source isn't found, the binding should produce
|
|
// null rather than UnsetValue in order to not propagate incorrect DataContexts from
|
|
// parent controls while things are being set up. This logic is implemented in
|
|
// `UntypedBindingExpressionBase.PublishValue`.
|
|
Assert.True(child.IsSet(Control.DataContextProperty));
|
|
|
|
root.Child = child;
|
|
|
|
Assert.Equal(new[] { null, "bar" }, values);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Return_FallbackValue_When_Path_Not_Resolved()
|
|
{
|
|
var target = new TextBlock();
|
|
var source = new Source();
|
|
var binding = new Binding
|
|
{
|
|
Source = source,
|
|
Path = "BadPath",
|
|
FallbackValue = "foofallback",
|
|
};
|
|
|
|
target.Bind(TextBlock.TextProperty, binding);
|
|
|
|
Assert.Equal("foofallback", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Return_FallbackValue_When_Invalid_Source_Type()
|
|
{
|
|
var target = new ProgressBar();
|
|
var source = new Source { Foo = "foo" };
|
|
var binding = new Binding
|
|
{
|
|
Source = source,
|
|
Path = "Foo",
|
|
FallbackValue = 42,
|
|
};
|
|
|
|
target.Bind(ProgressBar.ValueProperty, binding);
|
|
|
|
Assert.Equal(42, target.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Return_TargetNullValue_When_Value_Is_Null()
|
|
{
|
|
var target = new TextBlock();
|
|
var source = new Source { Foo = null };
|
|
|
|
var binding = new Binding
|
|
{
|
|
Source = source,
|
|
Path = "Foo",
|
|
TargetNullValue = "(null)",
|
|
};
|
|
|
|
target.Bind(TextBlock.TextProperty, binding);
|
|
|
|
Assert.Equal("(null)", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_Path_Should_Bind_To_DataContext()
|
|
{
|
|
var target = new TextBlock { DataContext = "foo" };
|
|
var binding = new Binding();
|
|
|
|
target.Bind(TextBlock.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_Path_Should_Bind_To_DataContext()
|
|
{
|
|
var target = new TextBlock { DataContext = "foo" };
|
|
var binding = new Binding { Path = string.Empty };
|
|
|
|
target.Bind(TextBlock.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dot_Path_Should_Bind_To_DataContext()
|
|
{
|
|
var target = new TextBlock { DataContext = "foo" };
|
|
var binding = new Binding { Path = "." };
|
|
|
|
target.Bind(TextBlock.TextProperty, binding);
|
|
|
|
Assert.Equal("foo", target.Text);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests a problem discovered with ListBox with selection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// - Items is bound to DataContext first, followed by say SelectedIndex
|
|
/// - When the ListBox is removed from the logical tree, DataContext becomes null (as it's
|
|
/// inherited)
|
|
/// - This changes Items to null, which changes SelectedIndex to null as there are no
|
|
/// longer any items
|
|
/// - However, the news that DataContext is now null hasn't yet reached the SelectedIndex
|
|
/// binding and so the unselection is sent back to the ViewModel
|
|
/// </remarks>
|
|
[Fact]
|
|
public void Should_Not_Write_To_Old_DataContext()
|
|
{
|
|
var vm = new OldDataContextViewModel();
|
|
var target = new OldDataContextTest();
|
|
|
|
var fooBinding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
Mode = BindingMode.TwoWay,
|
|
};
|
|
|
|
var barBinding = new Binding
|
|
{
|
|
Path = "Bar",
|
|
Mode = BindingMode.TwoWay,
|
|
};
|
|
|
|
// Bind Foo and Bar to the VM.
|
|
target.Bind(OldDataContextTest.FooProperty, fooBinding);
|
|
target.Bind(OldDataContextTest.BarProperty, barBinding);
|
|
target.DataContext = vm;
|
|
|
|
// Make sure the control's Foo and Bar properties are read from the VM
|
|
Assert.Equal(1, target.GetValue(OldDataContextTest.FooProperty));
|
|
Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
|
|
|
|
// Set DataContext to null.
|
|
target.DataContext = null;
|
|
|
|
// Foo and Bar are no longer bound so they return 0, their default value.
|
|
Assert.Equal(0, target.GetValue(OldDataContextTest.FooProperty));
|
|
Assert.Equal(0, target.GetValue(OldDataContextTest.BarProperty));
|
|
|
|
// The problem was here - DataContext is now null, setting Foo to 0. Bar is bound to
|
|
// Foo so Bar also gets set to 0. However the Bar binding still had a reference to
|
|
// the VM and so vm.Bar was set to 0 erroneously.
|
|
Assert.Equal(1, vm.Foo);
|
|
Assert.Equal(2, vm.Bar);
|
|
}
|
|
|
|
[Fact]
|
|
public void AvaloniaObject_this_Operator_Accepts_Binding()
|
|
{
|
|
var target = new ContentControl
|
|
{
|
|
DataContext = new { Foo = "foo" }
|
|
};
|
|
|
|
target[!ContentControl.ContentProperty] = new Binding("Foo");
|
|
|
|
Assert.Equal("foo", target.Content);
|
|
}
|
|
|
|
[Fact]
|
|
public void StyledProperty_SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
|
|
{
|
|
var viewModel = new TestStackOverflowViewModel()
|
|
{
|
|
Value = 50
|
|
};
|
|
|
|
var target = new StyledPropertyClass();
|
|
|
|
target.Bind(StyledPropertyClass.DoubleValueProperty,
|
|
new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
|
|
|
|
var child = new StyledPropertyClass();
|
|
|
|
child.Bind(StyledPropertyClass.DoubleValueProperty,
|
|
new Binding("DoubleValue")
|
|
{
|
|
Mode = BindingMode.TwoWay,
|
|
Source = target
|
|
});
|
|
|
|
Assert.Equal(1, viewModel.SetterInvokedCount);
|
|
|
|
//here in real life stack overflow exception is thrown issue #855 and #824
|
|
target.DoubleValue = 51.001;
|
|
|
|
Assert.Equal(2, viewModel.SetterInvokedCount);
|
|
|
|
double expected = 51;
|
|
|
|
Assert.Equal(expected, viewModel.Value);
|
|
Assert.Equal(expected, target.DoubleValue);
|
|
Assert.Equal(expected, child.DoubleValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
|
|
{
|
|
var viewModel = new TestStackOverflowViewModel()
|
|
{
|
|
Value = 50
|
|
};
|
|
|
|
var target = new DirectPropertyClass();
|
|
|
|
target.Bind(DirectPropertyClass.DoubleValueProperty, new Binding("Value")
|
|
{
|
|
Mode = BindingMode.TwoWay,
|
|
Source = viewModel
|
|
});
|
|
|
|
var child = new DirectPropertyClass();
|
|
|
|
child.Bind(DirectPropertyClass.DoubleValueProperty,
|
|
new Binding("DoubleValue")
|
|
{
|
|
Mode = BindingMode.TwoWay,
|
|
Source = target
|
|
});
|
|
|
|
Assert.Equal(1, viewModel.SetterInvokedCount);
|
|
|
|
//here in real life stack overflow exception is thrown issue #855 and #824
|
|
target.DoubleValue = 51.001;
|
|
|
|
Assert.Equal(2, viewModel.SetterInvokedCount);
|
|
|
|
double expected = 51;
|
|
|
|
Assert.Equal(expected, viewModel.Value);
|
|
Assert.Equal(expected, target.DoubleValue);
|
|
Assert.Equal(expected, child.DoubleValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void Combined_OneTime_And_OneWayToSource_Bindings_Should_Release_Subscriptions()
|
|
{
|
|
var target1 = new TextBlock();
|
|
var target2 = new TextBlock();
|
|
var root = new Panel { Children = { target1, target2 } };
|
|
var source = new Source { Foo = "foo" };
|
|
|
|
using (target1.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)))
|
|
using (target2.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneWayToSource)))
|
|
{
|
|
root.DataContext = source;
|
|
}
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, source.SubscriberCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Binding_Can_Resolve_Property_From_IReflectableType_Type()
|
|
{
|
|
var source = new DynamicReflectableType { ["Foo"] = "foo" };
|
|
var target = new TwoWayBindingTest { DataContext = source };
|
|
var binding = new Binding
|
|
{
|
|
Path = "Foo",
|
|
};
|
|
|
|
target.Bind(TwoWayBindingTest.TwoWayProperty, binding);
|
|
|
|
Assert.Equal("foo", target.TwoWay);
|
|
source["Foo"] = "bar";
|
|
Assert.Equal("bar", target.TwoWay);
|
|
target.TwoWay = "baz";
|
|
Assert.Equal("baz", source["Foo"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Binding_To_Types_Should_Work()
|
|
{
|
|
var type = typeof(string);
|
|
var textBlock = new TextBlock() { DataContext = type };
|
|
using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name")))
|
|
{
|
|
Assert.Equal("String", textBlock.Text);
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public void Binding_Producing_Default_Value_Should_Result_In_Correct_Priority()
|
|
{
|
|
var defaultValue = StyledPropertyClass.NullableDoubleProperty.GetDefaultValue(typeof(StyledPropertyClass));
|
|
|
|
var vm = new NullableValuesViewModel() { NullableDouble = defaultValue };
|
|
var target = new StyledPropertyClass();
|
|
|
|
target.Bind(StyledPropertyClass.NullableDoubleProperty, new Binding(nameof(NullableValuesViewModel.NullableDouble)) { Source = vm });
|
|
|
|
Assert.Equal(BindingPriority.LocalValue, target.GetDiagnosticInternal(StyledPropertyClass.NullableDoubleProperty).Priority);
|
|
Assert.Equal(defaultValue, target.GetValue(StyledPropertyClass.NullableDoubleProperty));
|
|
}
|
|
|
|
[Fact]
|
|
public void Binding_Non_Nullable_ValueType_To_Null_Reverts_To_Default_Value()
|
|
{
|
|
var source = new NullableValuesViewModel { NullableDouble = 42 };
|
|
var target = new StyledPropertyClass();
|
|
var binding = new Binding(nameof(source.NullableDouble)) { Source = source };
|
|
|
|
target.Bind(StyledPropertyClass.DoubleValueProperty, binding);
|
|
Assert.Equal(42, target.DoubleValue);
|
|
|
|
source.NullableDouble = null;
|
|
|
|
Assert.Equal(12.3, target.DoubleValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void Binding_Nullable_ValueType_To_Null_Sets_Value_To_Null()
|
|
{
|
|
var source = new NullableValuesViewModel { NullableDouble = 42 };
|
|
var target = new StyledPropertyClass();
|
|
var binding = new Binding(nameof(source.NullableDouble)) { Source = source };
|
|
|
|
target.Bind(StyledPropertyClass.NullableDoubleProperty, binding);
|
|
Assert.Equal(42, target.NullableDouble);
|
|
|
|
source.NullableDouble = null;
|
|
|
|
Assert.Null(target.NullableDouble);
|
|
}
|
|
|
|
[Fact]
|
|
public void OneWayToSource_Binding_Does_Not_Override_TwoWay_Binding()
|
|
{
|
|
// Issue #2983
|
|
var target1 = new TextBlock();
|
|
var target2 = new TextBlock { Text = "OneWayToSource" };
|
|
var source = new Source { Foo = "foo" };
|
|
var root = new Panel
|
|
{
|
|
DataContext = source,
|
|
Children = { target1, target2 }
|
|
};
|
|
|
|
target1.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.TwoWay));
|
|
target2.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneWayToSource));
|
|
|
|
Assert.Equal("OneWayToSource", source.Foo);
|
|
|
|
target1.Text = "TwoWay";
|
|
|
|
Assert.Equal("TwoWay", source.Foo);
|
|
}
|
|
|
|
[Fact]
|
|
public void Target_Undoing_Property_Change_During_TwoWay_Binding_Does_Not_Cause_StackOverflow()
|
|
{
|
|
var source = new TestStackOverflowViewModel { BoolValue = true };
|
|
var target = new TwoWayBindingTest();
|
|
|
|
source.ResetSetterInvokedCount();
|
|
|
|
// The AlwaysFalse property is set to false in the PropertyChanged callback. Ensure
|
|
// that binding it to an initial `true` value with a two-way binding does not cause a
|
|
// stack overflow.
|
|
target.Bind(
|
|
TwoWayBindingTest.AlwaysFalseProperty,
|
|
new Binding(nameof(TestStackOverflowViewModel.BoolValue))
|
|
{
|
|
Mode = BindingMode.TwoWay,
|
|
});
|
|
|
|
target.DataContext = source;
|
|
|
|
Assert.Equal(1, source.SetterInvokedCount);
|
|
Assert.False(source.BoolValue);
|
|
Assert.False(target.AlwaysFalse);
|
|
}
|
|
|
|
private class StyledPropertyClass : AvaloniaObject
|
|
{
|
|
public static readonly StyledProperty<double> DoubleValueProperty =
|
|
AvaloniaProperty.Register<StyledPropertyClass, double>(nameof(DoubleValue), 12.3);
|
|
|
|
public double DoubleValue
|
|
{
|
|
get => GetValue(DoubleValueProperty);
|
|
set => SetValue(DoubleValueProperty, value);
|
|
}
|
|
|
|
public static StyledProperty<double?> NullableDoubleProperty =
|
|
AvaloniaProperty.Register<StyledPropertyClass, double?>(nameof(NullableDoubleProperty), -1);
|
|
|
|
public double? NullableDouble
|
|
{
|
|
get => GetValue(NullableDoubleProperty);
|
|
set => SetValue(NullableDoubleProperty, value);
|
|
}
|
|
}
|
|
|
|
private class DirectPropertyClass : AvaloniaObject
|
|
{
|
|
public static readonly DirectProperty<DirectPropertyClass, double> DoubleValueProperty =
|
|
AvaloniaProperty.RegisterDirect<DirectPropertyClass, double>(
|
|
nameof(DoubleValue),
|
|
o => o.DoubleValue,
|
|
(o, v) => o.DoubleValue = v);
|
|
|
|
private double _doubleValue;
|
|
public double DoubleValue
|
|
{
|
|
get => _doubleValue;
|
|
set => SetAndRaise(DoubleValueProperty, ref _doubleValue, value);
|
|
}
|
|
}
|
|
|
|
private class NullableValuesViewModel : INotifyPropertyChanged
|
|
{
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
private double? _nullableDouble;
|
|
public double? NullableDouble
|
|
{
|
|
get => _nullableDouble; set
|
|
{
|
|
_nullableDouble = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NullableDouble)));
|
|
}
|
|
}
|
|
}
|
|
|
|
private class TestStackOverflowViewModel : INotifyPropertyChanged
|
|
{
|
|
public int SetterInvokedCount { get; private set; }
|
|
|
|
public const int MaxInvokedCount = 1000;
|
|
|
|
private bool _boolValue;
|
|
private double _value;
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
public bool BoolValue
|
|
{
|
|
get => _boolValue;
|
|
set
|
|
{
|
|
if (_boolValue != value)
|
|
{
|
|
_boolValue = value;
|
|
SetterInvokedCount++;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BoolValue)));
|
|
}
|
|
} }
|
|
|
|
public double Value
|
|
{
|
|
get => _value;
|
|
set
|
|
{
|
|
if (_value != value)
|
|
{
|
|
SetterInvokedCount++;
|
|
if (SetterInvokedCount < MaxInvokedCount)
|
|
{
|
|
_value = (int)value;
|
|
if (_value > 75)
|
|
_value = 75;
|
|
if (_value < 25)
|
|
_value = 25;
|
|
}
|
|
else
|
|
{
|
|
_value = value;
|
|
}
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ResetSetterInvokedCount() => SetterInvokedCount = 0;
|
|
}
|
|
|
|
private class TwoWayBindingTest : Control
|
|
{
|
|
public static readonly StyledProperty<bool> AlwaysFalseProperty =
|
|
AvaloniaProperty.Register<StyledPropertyClass, bool>(nameof(AlwaysFalse));
|
|
public static readonly StyledProperty<string> TwoWayProperty =
|
|
AvaloniaProperty.Register<TwoWayBindingTest, string>(
|
|
"TwoWay",
|
|
defaultBindingMode: BindingMode.TwoWay);
|
|
|
|
public bool AlwaysFalse
|
|
{
|
|
get => GetValue(AlwaysFalseProperty);
|
|
set => SetValue(AlwaysFalseProperty, value);
|
|
}
|
|
|
|
public string TwoWay
|
|
{
|
|
get => GetValue(TwoWayProperty);
|
|
set => SetValue(TwoWayProperty, value);
|
|
}
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
if (change.Property == AlwaysFalseProperty)
|
|
SetCurrentValue(AlwaysFalseProperty, false);
|
|
}
|
|
}
|
|
|
|
public class Source : INotifyPropertyChanged
|
|
{
|
|
private PropertyChangedEventHandler _propertyChanged;
|
|
private string _foo;
|
|
|
|
public string Foo
|
|
{
|
|
get => _foo;
|
|
set
|
|
{
|
|
_foo = value;
|
|
++FooSetCount;
|
|
RaisePropertyChanged();
|
|
}
|
|
}
|
|
|
|
public int FooSetCount { get; private set; }
|
|
|
|
|
|
public int SubscriberCount { get; private set; }
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged
|
|
{
|
|
add { _propertyChanged += value; ++SubscriberCount; }
|
|
remove { _propertyChanged += value; --SubscriberCount; }
|
|
}
|
|
|
|
private void RaisePropertyChanged([CallerMemberName] string prop = "")
|
|
{
|
|
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
|
}
|
|
}
|
|
|
|
public class WeakRefSource : INotifyPropertyChanged
|
|
{
|
|
private WeakReference<object> _foo;
|
|
|
|
public object Foo
|
|
{
|
|
get
|
|
{
|
|
if (_foo == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (_foo.TryGetTarget(out object target))
|
|
{
|
|
if (target is ICloneable cloneable)
|
|
{
|
|
return cloneable.Clone();
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
set
|
|
{
|
|
_foo = new WeakReference<object>(value);
|
|
|
|
RaisePropertyChanged();
|
|
}
|
|
}
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
private void RaisePropertyChanged([CallerMemberName] string prop = "")
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
|
}
|
|
}
|
|
|
|
private class OldDataContextViewModel
|
|
{
|
|
public int Foo { get; set; } = 1;
|
|
public int Bar { get; set; } = 2;
|
|
}
|
|
|
|
private class TestControl : Control
|
|
{
|
|
public static readonly DirectProperty<TestControl, object> ValueProperty =
|
|
AvaloniaProperty.RegisterDirect<TestControl, object>(
|
|
nameof(Value),
|
|
o => o.Value,
|
|
(o, v) => o.Value = v);
|
|
|
|
private object _value;
|
|
|
|
public object Value
|
|
{
|
|
get => _value;
|
|
set => SetAndRaise(ValueProperty, ref _value, value);
|
|
}
|
|
}
|
|
|
|
private class OldDataContextTest : Control
|
|
{
|
|
public static readonly StyledProperty<int> FooProperty =
|
|
AvaloniaProperty.Register<OldDataContextTest, int>("Foo");
|
|
|
|
public static readonly StyledProperty<int> BarProperty =
|
|
AvaloniaProperty.Register<OldDataContextTest, int>("Bar");
|
|
|
|
public OldDataContextTest()
|
|
{
|
|
this.Bind(BarProperty, this.GetObservable(FooProperty));
|
|
}
|
|
}
|
|
|
|
private class InheritanceTest : Decorator
|
|
{
|
|
public static readonly StyledProperty<int> BazProperty =
|
|
AvaloniaProperty.Register<InheritanceTest, int>(nameof(Baz), defaultValue: 6, inherits: true);
|
|
|
|
public int Baz
|
|
{
|
|
get => GetValue(BazProperty);
|
|
set => SetValue(BazProperty, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|