summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs')
-rw-r--r--Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs1526
1 files changed, 1526 insertions, 0 deletions
diff --git a/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs
new file mode 100644
index 00000000..65a78276
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs
@@ -0,0 +1,1526 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using System.Globalization;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using CategoryAttribute = NUnit.Framework.CategoryAttribute;
+using DescriptionAttribute = NUnit.Framework.DescriptionAttribute;
+using System.Threading.Tasks;
+using Xamarin.Forms.Internals;
+using System.Diagnostics;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+ [TestFixture]
+ public class TypedBindingUnitTests : BindingBaseUnitTests
+ {
+ [SetUp]
+ public override void Setup()
+ {
+ base.Setup();
+ log = new Logger();
+
+ Device.PlatformServices = new MockPlatformServices();
+ Log.Listeners.Add(log);
+ }
+
+ [TearDown]
+ public override void TearDown()
+ {
+ base.TearDown();
+ Device.PlatformServices = null;
+ Log.Listeners.Remove(log);
+ }
+
+ protected override BindingBase CreateBinding(BindingMode mode = BindingMode.Default, string stringFormat = null)
+ {
+ return new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ })
+ {
+ Mode = mode,
+ StringFormat= stringFormat
+ };
+ }
+
+ [Test]
+ public void InvalidCtor()
+ {
+ Assert.Throws<ArgumentNullException>(() => new TypedBinding<MockViewModel, string>(null, (mvm, s) => mvm.Text = s, null), "Allowed null getter");
+ }
+
+ [Test, NUnit.Framework.Category("[Binding] Set Value")]
+ public void ValueSetOnOneWayWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), null, propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ public void ValueSetOnOneWayToSourceWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), value, propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property changed");
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ public void ValueSetOnTwoWayWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnOneWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ string original = (string)bindable.GetValue(property);
+ const string value = "value";
+ viewmodel.Model.Model.Text = value;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource");
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnTwoWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Model.Model.Text,
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnOneWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model[1],
+ (cmvm, s) => cmvm.Model.Model[1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model [1] = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ string original = (string)bindable.GetValue(property);
+ const string value = "value";
+ viewmodel.Model.Model [1] = value;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource");
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnTwoWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model [1] = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Model.Model [1],
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedArrayPathOnTwoWayBinding(bool isDefault)
+ {
+ var viewmodel = new ComplexMockViewModel {
+ Array = new string [2]
+ };
+ viewmodel.Array [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Array [1],
+ (cmvm, s) => cmvm.Array [1] = s,
+ null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, new Binding("Array[1]", bindingMode));
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Array [1],
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithSelfPathOnOneWayBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm,
+ (cmvm, s) => cmvm = s,null){Mode = bindingMode};
+ const string value = "foo";
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = value;
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "value";
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(value, bindable.BindingContext,
+ "Source was updated from Target on OneWay binding");
+
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueDoesNotUpdateWithSelfPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm, (cmvm, s) => cmvm = s, null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "new value";
+
+ string original = (string)bindable.GetValue(property);
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource with self path");
+
+ const string newvalue2 = "new value 2";
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed on OneWayToSource with self path");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed on OneWayToSource with self path");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithSelfPathOnTwoWayBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm, (cmvm, s) => cmvm = s, null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = "value";
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "New Value";
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Self-path Source changed with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(BindingMode.OneWay)]
+ [TestCase(BindingMode.OneWayToSource)]
+ [TestCase(BindingMode.TwoWay)]
+ public void SourceAndTargetAreWeakComplexPath(BindingMode mode)
+ {
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value");
+
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = mode};
+
+ WeakReference weakViewModel = null, weakBindable = null;
+
+ int i=0;
+ Action create = null;
+ create = ()=>{
+ if (i++ < 1024){
+ create();
+ return;
+ }
+ MockBindable bindable = new MockBindable();
+
+ weakBindable = new WeakReference(bindable);
+
+ ComplexMockViewModel viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+
+ weakViewModel = new WeakReference(viewmodel);
+
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ bindable.BindingContext = null;
+ };
+
+ create();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWay)
+ Assert.IsFalse(weakViewModel.IsAlive, "ViewModel wasn't collected");
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource)
+ Assert.IsFalse(weakBindable.IsAlive, "Bindable wasn't collected");
+ }
+
+ class TestConverter<TSource, TTarget> : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Assert.AreEqual(typeof(TTarget), targetType);
+ return System.Convert.ChangeType(value, targetType, CultureInfo.CurrentUICulture);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Assert.AreEqual(typeof(TSource), targetType);
+ return System.Convert.ChangeType(value, targetType, CultureInfo.CurrentUICulture);
+ }
+ }
+
+ [Test]
+ public void ValueConverter()
+ {
+ var converter = new TestConverter<string, int>();
+
+ var vm = new MockViewModel("1");
+ var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 0);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, bindable.GetValue(property));
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ public void ValueConverterBack()
+ {
+ var converter = new TestConverter<string, int>();
+
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 1, BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("1", vm.Text);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ class TestConverterParameter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return parameter;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return parameter;
+ }
+ }
+
+ [Test]
+ public void ValueConverterParameter()
+ {
+ var converter = new TestConverterParameter();
+
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "Bar", BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter, ConverterParameter = "Foo"};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("Foo", vm.Text);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ class TestConverterCulture : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return culture.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return culture.ToString();
+ }
+ }
+
+#if !WINDOWS_PHONE
+ [Test]
+ [SetUICulture("pt-PT")]
+ public void ValueConverterCulture()
+ {
+ var converter = new TestConverterCulture();
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "Bar", BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("pt-PT", vm.Text);
+ }
+#endif
+
+ [Test]
+ public void SelfBindingConverter()
+ {
+ var converter = new TestConverter<int, string>();
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "0");
+ var binding = new TypedBinding<int, int>(
+ mvm => mvm, (mvm, s) => mvm = s, null){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = 1;
+ bindable.SetBinding(property, binding);
+ Assert.AreEqual("1", bindable.GetValue(property));
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ internal class MultiplePropertyViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ int done;
+ public int Done {
+ get { return done; }
+ set {
+ done = value;
+ OnPropertyChanged();
+ OnPropertyChanged("Progress");
+ }
+ }
+
+ int total = 100;
+ public int Total {
+ get { return total; }
+ set {
+ if (total == value)
+ return;
+
+ total = value;
+ OnPropertyChanged();
+ OnPropertyChanged("Progress");
+ }
+ }
+
+ public float Progress {
+ get { return (float)done / total; }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChangedEventHandler handler = PropertyChanged;
+ if (handler != null)
+ handler(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ internal class MultiplePropertyBindable
+ : BindableObject
+ {
+ public static readonly BindableProperty ValueProperty = BindableProperty.Create ("Value", typeof(float), typeof(MultiplePropertyBindable), 0f);
+
+ public float Value {
+ get { return (float)GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ public static readonly BindableProperty DoneProperty = BindableProperty.Create("Done", typeof(int), typeof(MultiplePropertyBindable), 0);
+
+ public int Done {
+ get { return (int)GetValue(DoneProperty); }
+ set { SetValue(DoneProperty, value); }
+ }
+ }
+
+ [Test]
+ public void MultiplePropertyUpdates()
+ {
+ var mpvm = new MultiplePropertyViewModel();
+
+ var bindable = new MultiplePropertyBindable();
+ var progressBinding = new TypedBinding<MultiplePropertyViewModel, float>(vm => vm.Progress, null, new [] {
+ new Tuple<Func<MultiplePropertyViewModel, object>, string> (vm=>vm, "Progress"),
+ }){Mode = BindingMode.OneWay};
+ var doneBinding = new TypedBinding<MultiplePropertyViewModel, int>(vm => vm.Done, (vm,d)=>vm.Done=d, new [] {
+ new Tuple<Func<MultiplePropertyViewModel, object>, string> (vm=>vm, "Done"),
+ }){Mode = BindingMode.OneWayToSource};
+
+ bindable.SetBinding(MultiplePropertyBindable.ValueProperty, progressBinding);
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty, doneBinding);
+ bindable.BindingContext = mpvm;
+
+ bindable.Done = 5;
+
+ Assert.AreEqual(5, mpvm.Done);
+ Assert.AreEqual(0.05f, mpvm.Progress);
+ Assert.AreEqual(5, bindable.Done);
+ Assert.AreEqual(0.05f, bindable.Value);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ [Description("When part of a complex path can not be evaluated during an update, bindables should return to their default value.")]
+ public void NullInPathUsesDefaultValue()
+ {
+ var vm = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "foo bar");
+
+ var bindable = new MockBindable();
+ var binding = new TypedBinding<ComplexMockViewModel, string>(cvm => cvm.Model.Text, (cvm, t) => cvm.Model.Text = t, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm.Model, "Text")
+ }){Mode = BindingMode.OneWay};
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ vm.Model = null;
+
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property));
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ [Description("When part of a complex path can not be evaluated during an update, bindables should return to their default value.")]
+ public void NullContextUsesDefaultValue()
+ {
+ var vm = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "vm value"
+ }
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "foo bar");
+ var binding = new TypedBinding<ComplexMockViewModel, string>(cvm => cvm.Model.Text, (cvm, t) => cvm.Model.Text = t, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm.Model, "Text")
+ }){Mode = BindingMode.OneWay};
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assume.That(bindable.GetValue(property), Is.EqualTo(vm.Model.Text));
+
+ bindable.BindingContext = null;
+
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property));
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ [Description("OneWay bindings should not double apply on source updates.")]
+ public void OneWayBindingsDontDoubleApplyOnSourceUpdates()
+ {
+ var vm = new ComplexMockViewModel();
+
+ var bindable = new MockBindable();
+ var binding = new TypedBinding<ComplexMockViewModel, int>(cmvm => cmvm.QueryCount, null, null){Mode = BindingMode.OneWay};
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty,binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, vm.count);
+
+ bindable.BindingContext = null;
+
+ Assert.AreEqual(1, vm.count, "Source property was queried on an unset");
+
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(2, vm.count, "Source property was queried multiple times on a reapply");
+ }
+
+ [Test]
+ [Description("When there are multiple bindings, an update in one should not cause the other to udpate.")]
+ public void BindingsShouldNotTriggerOtherBindings()
+ {
+ var vm = new ComplexMockViewModel();
+
+ var bindable = new MockBindable();
+ var qcbinding = new TypedBinding<ComplexMockViewModel, int>(cmvm => cmvm.QueryCount, null, null){Mode = BindingMode.OneWay};
+ var textBinding = new TypedBinding<ComplexMockViewModel, string>(cmvm => cmvm.Text, null, null){Mode = BindingMode.OneWay};
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty, qcbinding);
+ bindable.SetBinding(MockBindable.TextProperty, textBinding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, vm.count);
+
+ vm.Text = "update";
+
+ Assert.AreEqual(1, vm.count, "Source property was queried due to a different binding update.");
+ }
+
+ internal class DerivedViewModel
+ : MockViewModel
+ {
+ public override string Text {
+ get { return base.Text + "2"; }
+ set { base.Text = value; }
+ }
+ }
+
+ [Test]
+ [Description("The most derived version of a property should always be called.")]
+ public void MostDerviedPropertyOnContextSwitchOfSimilarType()
+ {
+ var vm = new MockViewModel { Text = "text" };
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = vm;
+ var binding = new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, new [] {
+ new Tuple<Func<MockViewModel, object>, string>(mvm=>mvm, "Text")
+ });
+ bindable.SetBinding(MockBindable.TextProperty, binding);
+
+ Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty));
+
+ bindable.BindingContext = vm = new DerivedViewModel { Text = "text" };
+
+ Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty));
+ }
+
+ [Test]
+ [Description("When binding with a multi-part path and part is null, no error should be thrown or logged")]
+ public void ChainedPartNull()
+ {
+ var bindable = new MockBindable { BindingContext = new ComplexMockViewModel() };
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Text,
+ (cmvm, s) => cmvm.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Text"),
+ });
+
+ Assert.That(() => bindable.SetBinding(MockBindable.TextProperty, binding), Throws.Nothing);
+ Assert.That(log.Messages.Count, Is.EqualTo(0), "An error was logged");
+ }
+
+ [Test]
+ public void SetBindingContextBeforeContextBindingAndInnerBindings()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAndInnerBindingBeforeContextBinding()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAfterContextBindingAndInnerBindings()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAfterInnerBindingsAndContextBinding()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+
+ label.SetBinding(Label.TextProperty, bindingItem1);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void Convert()
+ {
+ var slider = new Slider();
+ var vm = new MockViewModel { Text = "0.5" };
+ slider.BindingContext = vm;
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, null){Mode = BindingMode.TwoWay});
+
+ Assert.That(slider.Value, Is.EqualTo(0.5));
+
+ slider.Value = 0.9;
+
+ Assert.That(vm.Text, Is.EqualTo("0.9"));
+ }
+
+#if !WINDOWS_PHONE
+ [Test]
+ [SetCulture("pt-PT")]
+ [SetUICulture("pt-PT")]
+ public void ConvertIsCultureInvariant()
+ {
+ var slider = new Slider();
+ var vm = new MockViewModel { Text = "0.5" };
+ slider.BindingContext = vm;
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, null){Mode = BindingMode.TwoWay});
+
+ Assert.That(slider.Value, Is.EqualTo(0.5));
+
+ slider.Value = 0.9;
+
+ Assert.That(vm.Text, Is.EqualTo("0.9"));
+ }
+#endif
+
+ [Test]
+ public void FailToConvert()
+ {
+ var slider = new Slider();
+ slider.BindingContext = new ComplexMockViewModel { Model = new ComplexMockViewModel() };
+
+ Assert.That(() => {
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<ComplexMockViewModel, ComplexMockViewModel>(mvm => mvm.Model, null, null));
+ }, Throws.Nothing);
+
+ Assert.That(slider.Value, Is.EqualTo(Slider.ValueProperty.DefaultValue));
+ Assert.That(log.Messages.Count, Is.EqualTo(1), "No error logged");
+ }
+
+ class NullViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string Foo {
+ get;
+ set;
+ }
+
+ public string Bar {
+ get;
+ set;
+ }
+
+ public void SignalAllPropertiesChanged(bool useNull)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((useNull) ? null : String.Empty));
+ }
+ }
+
+ class MockBindable2 : MockBindable
+ {
+ public static readonly BindableProperty Text2Property = BindableProperty.Create("Text2", typeof(string), typeof(MockBindable2), "default", BindingMode.TwoWay);
+ public string Text2 {
+ get { return (string)GetValue(Text2Property); }
+ set { SetValue(Text2Property, value); }
+ }
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void NullPropertyUpdatesAllBindings(bool useStringEmpty)
+ {
+ var vm = new NullViewModel();
+ var bindable = new MockBindable2();
+ bindable.BindingContext = vm;
+ bindable.SetBinding(MockBindable.TextProperty, new TypedBinding<NullViewModel,string>(nvm => nvm.Foo, null, new [] {
+ new Tuple<Func<NullViewModel, object>, string>(nvm=>nvm,"Foo")
+ }));
+ bindable.SetBinding(MockBindable2.Text2Property, new TypedBinding<NullViewModel, string>(nvm => nvm.Bar, null, new [] {
+ new Tuple<Func<NullViewModel, object>, string>(nvm=>nvm,"Bar")
+ }));
+
+ vm.Foo = "Foo";
+ vm.Bar = "Bar";
+ Assert.That(() => vm.SignalAllPropertiesChanged(useNull: !useStringEmpty), Throws.Nothing);
+
+ Assert.That(bindable.Text, Is.EqualTo("Foo"));
+ Assert.That(bindable.Text2, Is.EqualTo("Bar"));
+ }
+
+ [TestCase]
+ public void BindingSourceOverContext()
+ {
+ var label = new Label();
+ label.BindingContext = "bindingcontext";
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ Assert.AreEqual("bindingcontext", label.Text);
+
+ var bindingSelfSource= new TypedBinding<string, string>(s => s, null, null){Source = "bindingsource"};
+ label.SetBinding(Label.TextProperty, bindingSelfSource);
+ Assert.AreEqual("bindingsource", label.Text);
+ }
+
+ class TestViewModel : INotifyPropertyChanged
+ {
+ event PropertyChangedEventHandler PropertyChanged;
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged {
+ add { PropertyChanged += value; }
+ remove { PropertyChanged -= value; }
+ }
+
+ public string Foo { get; set; }
+
+ public int InvocationListSize()
+ {
+ if (PropertyChanged == null)
+ return 0;
+ return PropertyChanged.GetInvocationList().Length;
+ }
+
+ public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ [Test]
+ public void BindingUnsubscribesForDeadTarget()
+ {
+ var viewmodel = new TestViewModel();
+
+ int i = 0;
+ Action create = null;
+ create = () => {
+ if (i++ < 1024) {
+ create();
+ return;
+ }
+
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, new TypedBinding<TestViewModel,string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ }));
+ button.BindingContext = viewmodel;
+ };
+
+ create();
+
+ Assume.That(viewmodel.InvocationListSize(), Is.EqualTo(1));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ viewmodel.OnPropertyChanged("Foo");
+
+ Assert.AreEqual(0, viewmodel.InvocationListSize());
+ }
+
+ [Test]
+ public void BindingDoesNotStayAliveForDeadTarget()
+ {
+ var viewModel = new TestViewModel();
+ WeakReference bindingRef = null;
+
+ int i = 0;
+ Action create = null;
+ create = () => {
+ if (i++ < 1024) {
+ create();
+ return;
+ }
+
+ var binding = new TypedBinding<TestViewModel, string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ });
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, binding);
+ button.BindingContext = viewModel;
+
+ bindingRef = new WeakReference(binding);
+ binding = null;
+ };
+
+ create();
+
+ Assume.That(viewModel.InvocationListSize(), Is.EqualTo(1));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ Assert.IsFalse(bindingRef.IsAlive, "Binding should not be alive!");
+ }
+
+ [Test]
+ public void BindingCreatesSingleSubscription()
+ {
+ TestViewModel viewmodel = new TestViewModel();
+ var binding = new TypedBinding<TestViewModel, string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ });
+
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, binding);
+ button.BindingContext = viewmodel;
+
+ Assert.That(viewmodel.InvocationListSize(), Is.EqualTo(1));
+ }
+
+ public class IndexedViewModel : INotifyPropertyChanged
+ {
+ Dictionary<string, object> dict = new Dictionary<string, object>();
+
+ [IndexerName("Item")]
+ public object this [string index] {
+ get { return dict [index]; }
+ set {
+ dict [index] = value;
+ OnPropertyChanged($"Item[{index}]");
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ [Test]
+ public void IndexedViewModelPropertyChanged()
+ {
+ var label = new Label();
+ var viewModel = new IndexedViewModel();
+
+ var binding = new TypedBinding<Tuple<IndexedViewModel, object>, object>(
+ vm => vm.Item1["Foo"],
+ (vm, s) => vm.Item1 ["Foo"] = s,
+ new [] {
+ new Tuple<Func<Tuple<IndexedViewModel, object>, object>, string>(vm=>vm, "Item1"),
+ new Tuple<Func<Tuple<IndexedViewModel, object>, object>, string>(vm=>vm.Item1, "Item[Foo]"),
+ });
+
+ label.BindingContext = new Tuple<IndexedViewModel, object>(viewModel, new object());
+ label.SetBinding(Label.TextProperty, binding);
+ Assert.AreEqual(null, label.Text);
+
+ viewModel ["Foo"] = "Baz";
+
+ Assert.AreEqual("Baz", label.Text);
+ }
+
+ [Test]
+ [Ignore]
+ public void SpeedTestApply()
+ {
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var vm0 = new MockViewModel { Text = "Foo" };
+ var vm1 = new MockViewModel { Text = "Bar" };
+ var bindable = new MockBindable();
+
+ var it = 100000;
+
+ BindingBase binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ });
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swtb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swtb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: null);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swtbh = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swtbh.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new Binding("Text");
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swsv = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.SetValue(property, (i % 2 == 0 ? vm0 : vm1).Text);
+ swsv.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ Assert.Fail($"Applying {it} Typedbindings\t\t\t: {swtb.ElapsedMilliseconds}ms.\nApplying {it} Typedbindings (without INPC)\t: {swtbh.ElapsedMilliseconds}ms.\nApplying {it} Bindings\t\t\t: {swb.ElapsedMilliseconds}ms.\nSetting {it} values\t\t\t\t: {swsv.ElapsedMilliseconds}ms.");
+ }
+
+ [Test]
+ [Ignore]
+ public void SpeedTestSetBC()
+ {
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var vm0 = new MockViewModel { Text = "Foo" };
+ var vm1 = new MockViewModel { Text = "Bar" };
+ var bindable = new MockBindable();
+
+ var it = 100000;
+
+ BindingBase binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ });
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swtb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swtb.Stop();
+ //Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: null);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swtbh = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swtbh.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new Binding("Text");
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swsv = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.SetValue(property, (i % 2 == 0 ? vm0 : vm1).Text);
+ swsv.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ Assert.Fail($"Setting BC for {it} Typedbindings\t\t\t: {swtb.ElapsedMilliseconds}ms.\nSetting BC for {it} Typedbindings (without INPC)\t: {swtbh.ElapsedMilliseconds}ms.\nSetting BC for {it} Bindings\t\t\t\t: {swb.ElapsedMilliseconds}ms.\nSetting {it} values\t\t\t\t\t: {swsv.ElapsedMilliseconds}ms.");
+ }
+ }
+}