diff options
author | Stephane Delcroix <stephane@delcroix.org> | 2016-11-15 20:39:48 +0100 |
---|---|---|
committer | Jason Smith <jason.smith@xamarin.com> | 2016-11-15 11:39:48 -0800 |
commit | a6bbed029c64d2d64b74eeb67e27a099abf70664 (patch) | |
tree | 551c3924c055e2d39592b3f1c726cca46924dd73 /Xamarin.Forms.Core.UnitTests | |
parent | 14e21dcebd4a706aaa5eed384b142957d84df002 (diff) | |
download | xamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.tar.gz xamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.tar.bz2 xamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.zip |
[XamlC] TypedBindings, some tests, a compiler, ... (#489)
Diffstat (limited to 'Xamarin.Forms.Core.UnitTests')
-rw-r--r-- | Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs | 546 | ||||
-rw-r--r-- | Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs | 701 | ||||
-rw-r--r-- | Xamarin.Forms.Core.UnitTests/MockViewModel.cs | 22 | ||||
-rw-r--r-- | Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs | 1526 | ||||
-rw-r--r-- | Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj | 1 |
5 files changed, 2145 insertions, 651 deletions
diff --git a/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs b/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs index dd0d2244..84addb7a 100644 --- a/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs +++ b/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs @@ -1,12 +1,80 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using NUnit.Framework; namespace Xamarin.Forms.Core.UnitTests { public abstract class BindingBaseUnitTests : BaseTestFixture { - protected abstract BindingBase CreateBinding (BindingMode mode, string stringFormat = null); + internal class Logger : LogListener + { + public IReadOnlyList<string> Messages { + get { return messages; } + } + + public override void Warning(string category, string message) + { + messages.Add("[" + category + "] " + message); + } + + readonly List<string> messages = new List<string>(); + } + + internal Logger log; + + protected abstract BindingBase CreateBinding (BindingMode mode = BindingMode.Default, string stringFormat = null); + + internal class ComplexMockViewModel : MockViewModel + { + public ComplexMockViewModel Model { + get { return model; } + set { + if (model == value) + return; + + model = value; + OnPropertyChanged("Model"); + } + } + + internal int count; + public int QueryCount { + get { return count++; } + } + + [IndexerName("Indexer")] + public string this [int v] { + get { return values [v]; } + set { + if (values [v] == value) + return; + + values [v] = value; + OnPropertyChanged("Indexer[" + v + "]"); + } + } + + public string [] Array { + get; + set; + } + + public object DoStuff() + { + return null; + } + + public object DoStuff(object argument) + { + return null; + } + + string [] values = new string [5]; + ComplexMockViewModel model; + } [Test] public void CloneMode() @@ -20,8 +88,7 @@ namespace Xamarin.Forms.Core.UnitTests [Test] public void StringFormat() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = CreateBinding (BindingMode.Default, "Foo {0}"); var vm = new MockViewModel { Text = "Bar" }; @@ -34,8 +101,7 @@ namespace Xamarin.Forms.Core.UnitTests [Test] public void StringFormatOnUpdate() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = CreateBinding (BindingMode.Default, "Foo {0}"); var vm = new MockViewModel { Text = "Bar" }; @@ -51,8 +117,7 @@ namespace Xamarin.Forms.Core.UnitTests [Description ("StringFormat should not be applied to OneWayToSource bindings")] public void StringFormatOneWayToSource() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = CreateBinding (BindingMode.OneWayToSource, "Foo {0}"); var vm = new MockViewModel { Text = "Bar" }; @@ -68,8 +133,7 @@ namespace Xamarin.Forms.Core.UnitTests [Description ("StringFormat should only be applied from from source in TwoWay bindings")] public void StringFormatTwoWay() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = CreateBinding (BindingMode.TwoWay, "Foo {0}"); var vm = new MockViewModel { Text = "Bar" }; @@ -86,8 +150,7 @@ namespace Xamarin.Forms.Core.UnitTests [Description ("You should get an exception when trying to change a binding after it's been applied")] public void ChangeAfterApply() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = CreateBinding (BindingMode.OneWay); var vm = new MockViewModel { Text = "Bar" }; @@ -97,6 +160,466 @@ namespace Xamarin.Forms.Core.UnitTests Assert.That (() => binding.Mode = BindingMode.OneWayToSource, Throws.InvalidOperationException); Assert.That (() => binding.StringFormat = "{0}", Throws.InvalidOperationException); } + + [Test] + public void StringFormatNonStringType() + { + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); + var binding = new Binding("Value", stringFormat: "{0:P2}"); + + var vm = new { Value = 0.95d }; + var bo = new MockBindable { BindingContext = vm }; + bo.SetBinding(property, binding); + + if (System.Threading.Thread.CurrentThread.CurrentCulture.Name == "tr-TR") + Assert.That(bo.GetValue(property), Is.EqualTo("%95,00")); + else + Assert.That(bo.GetValue(property), Is.EqualTo("95.00 %")); + } + + [Test] + public void ReuseBindingInstance() + { + var vm = new MockViewModel(); + + var bindable = new MockBindable(); + bindable.BindingContext = vm; + + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); + var binding = new Binding("Text"); + bindable.SetBinding(property, binding); + + var bindable2 = new MockBindable(); + bindable2.BindingContext = new MockViewModel(); + Assert.Throws<InvalidOperationException>(() => bindable2.SetBinding(property, binding), + "Binding allowed reapplication with a different context"); + } + + [Test, Category("[Binding] Set Value")] + public void ValueSetOnOneWay( + [Values(true, false)] bool setContextFirst, + [Values(true, false)] bool isDefault) + { + const string value = "Foo"; + var viewmodel = new MockViewModel { + Text = value + }; + + 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), null, propertyDefault); + var binding = CreateBinding(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.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] Set Value")] + public void ValueSetOnOneWayToSource( + [Values(true, false)] bool setContextFirst, + [Values(true, false)] bool isDefault) + { + const string value = "Foo"; + var viewmodel = new MockViewModel(); + + 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), defaultValue: value, defaultBindingMode: propertyDefault); + var binding = CreateBinding(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.Text, + "BindingContext property did not change"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [Test, Category("[Binding] Set Value")] + public void ValueSetOnTwoWay( + [Values(true, false)] bool setContextFirst, + [Values(true, false)] bool isDefault) + { + const string value = "Foo"; + var viewmodel = new MockViewModel { + Text = value + }; + + 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), defaultValue: "default value", defaultBindingMode: propertyDefault); + var binding = CreateBinding(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.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] Update Value")] + public void ValueUpdatedWithSimplePathOnOneWayBinding( + [Values(true, false)]bool isDefault) + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + viewmodel.Text = newvalue; + Assert.AreEqual(newvalue, bindable.GetValue(property), + "Bindable did not update on binding context property change"); + Assert.AreEqual(newvalue, viewmodel.Text, + "Source property changed when it shouldn't"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [Test, Category("[Binding] Update Value")] + public void ValueUpdatedWithSimplePathOnOneWayToSourceBinding( + [Values(true, false)]bool isDefault) + + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + string original = (string)bindable.GetValue(property); + const string value = "value"; + viewmodel.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.Text, + "Source property changed when it shouldn't"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [Test, Category("[Binding] Update Value")] + public void ValueUpdatedWithSimplePathOnTwoWayBinding( + [Values(true, false)]bool isDefault) + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + viewmodel.Text = newvalue; + Assert.AreEqual(newvalue, bindable.GetValue(property), + "Target property did not update change"); + Assert.AreEqual(newvalue, viewmodel.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.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()); + } + + [TestCase(true)] + [TestCase(false)] + public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayBinding(bool isDefault) + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + bindable.BindingContext = new MockViewModel(); + Assert.AreEqual(null, bindable.GetValue(property)); + + viewmodel.Text = newvalue; + Assert.AreEqual(null, bindable.GetValue(property), + "Target updated from old Source property change"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [TestCase(true)] + [TestCase(false)] + public void ValueUpdatedWithOldContextDoesNotUpdateWithTwoWayBinding(bool isDefault) + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + bindable.BindingContext = new MockViewModel(); + Assert.AreEqual(null, bindable.GetValue(property)); + + viewmodel.Text = newvalue; + Assert.AreEqual(null, bindable.GetValue(property), + "Target updated from old Source property change"); + + string original = viewmodel.Text; + + bindable.SetValue(property, newvalue); + Assert.AreEqual(original, viewmodel.Text, + "Source updated from old Target property change"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [TestCase(true)] + [TestCase(false)] + public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayToSourceBinding(bool isDefault) + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + 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 = CreateBinding(bindingMode); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + bindable.BindingContext = new MockViewModel(); + Assert.AreEqual(property.DefaultValue, bindable.GetValue(property)); + + viewmodel.Text = newvalue; + Assert.AreEqual(property.DefaultValue, bindable.GetValue(property), + "Target updated from old Source property change"); + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [Test] + public void BindingStaysOnUpdateValueFromBinding() + { + const string newvalue = "New Value"; + var viewmodel = new MockViewModel { + Text = "Foo" + }; + + var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), null); + var binding = CreateBinding(BindingMode.Default); + + var bindable = new MockBindable(); + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + viewmodel.Text = newvalue; + Assert.AreEqual(newvalue, bindable.GetValue(property)); + + const string newValue2 = "new value 2"; + viewmodel.Text = newValue2; + Assert.AreEqual(newValue2, bindable.GetValue(property)); + + Assert.That(log.Messages.Count, Is.EqualTo(0), + "An error was logged: " + log.Messages.FirstOrDefault()); + } + + [Test] + public void OneWayToSourceContextSetToNull() + { + var binding = new Binding("Text", BindingMode.OneWayToSource); + + MockBindable bindable = new MockBindable { + BindingContext = new MockViewModel() + }; + bindable.SetBinding(MockBindable.TextProperty, binding); + + Assert.That(() => bindable.BindingContext = null, Throws.Nothing); + } + + [Category("[Binding] Simple paths")] + [TestCase(BindingMode.OneWay)] + [TestCase(BindingMode.OneWayToSource)] + [TestCase(BindingMode.TwoWay)] + public void SourceAndTargetAreWeakWeakSimplePath(BindingMode mode) + { + var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", BindingMode.OneWay); + var binding = CreateBinding(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); + + MockViewModel viewmodel = new MockViewModel(); + weakViewModel = new WeakReference(viewmodel); + + bindable.BindingContext = viewmodel; + bindable.SetBinding(property, binding); + + Assume.That(() => bindable.BindingContext = null, Throws.Nothing); + }; + + create(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + 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"); + } + + [Test] + public void PropertyChangeBindingsOccurThroughMainThread() + { + var vm = new MockViewModel { Text = "text" }; + + var bindable = new MockBindable(); + var binding = CreateBinding(); + bindable.BindingContext = vm; + bindable.SetBinding(MockBindable.TextProperty, binding); + + bool mainThread = false; + Device.PlatformServices = new MockPlatformServices(invokeOnMainThread: a => mainThread = true); + + vm.Text = "updated"; + + Assert.IsTrue(mainThread, "Binding did not occur on main thread"); + Assert.AreNotEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty), "Binding was applied anyway through other means"); + } } [TestFixture] @@ -223,5 +746,6 @@ namespace Xamarin.Forms.Core.UnitTests Assert.That (() => BindingBase.TryGetSynchronizedCollection (null, out context), Throws.InstanceOf<ArgumentNullException>()); } + } } diff --git a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs index c28adda9..548e4ad5 100644 --- a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs +++ b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.ComponentModel; using System.Linq; @@ -16,24 +15,6 @@ namespace Xamarin.Forms.Core.UnitTests public class BindingUnitTests : BindingBaseUnitTests { - class Logger - : LogListener - { - public IReadOnlyList<string> Messages - { - get { return messages; } - } - - public override void Warning (string category, string message) - { - messages.Add ("[" + category + "] " + message); - } - - readonly List<string> messages = new List<string>(); - } - - Logger log; - [SetUp] public override void Setup() { @@ -52,7 +33,7 @@ namespace Xamarin.Forms.Core.UnitTests Log.Listeners.Remove (log); } - protected override BindingBase CreateBinding (BindingMode mode, string stringFormat = null) + protected override BindingBase CreateBinding(BindingMode mode = BindingMode.Default, string stringFormat = null) { return new Binding ("Text", mode, stringFormat: stringFormat); } @@ -87,9 +68,8 @@ namespace Xamarin.Forms.Core.UnitTests [Description ("You should get an exception when trying to change a binding after it's been applied")] public void ChangeBindingAfterApply() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - - var binding = new Binding { Path = "Text" }; + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); + var binding = (Binding)CreateBinding(BindingMode.Default, "Foo {0}"); var vm = new MockViewModel { Text = "Bar" }; var bo = new MockBindable { BindingContext = vm }; @@ -103,59 +83,13 @@ namespace Xamarin.Forms.Core.UnitTests [Test] public void NullPathIsSelf() { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable)); var binding = new Binding(); var bo = new MockBindable { BindingContext = "Foo" }; - bo.SetBinding (property, binding); - - Assert.That (bo.GetValue (property), Is.EqualTo ("Foo")); - } + bo.SetBinding(property, binding); - class DoubleViewModel - : MockViewModel - { - public double Value - { - get; - set; - } - } - - [Test] - public void StringFormatNonStringType() - { - var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null); - - var binding = new Binding ("Value", stringFormat: "{0:P2}"); - - var vm = new DoubleViewModel { Value = 0.95 }; - var bo = new MockBindable { BindingContext = vm }; - bo.SetBinding (property, binding); - - if (System.Threading.Thread.CurrentThread.CurrentCulture.Name == "tr-TR") - Assert.That (bo.GetValue (property), Is.EqualTo ("%95,00")); - else - Assert.That (bo.GetValue (property), Is.EqualTo ("95.00 %")); - } - - [Test] - public void ReuseBindingInstance() - { - var vm = new MockViewModel(); - - var bindable = new MockBindable(); - bindable.BindingContext = vm; - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null); - var binding = new Binding ("Text"); - bindable.SetBinding (property, binding); - - var bindable2 = new MockBindable(); - bindable2.BindingContext = new MockViewModel(); - Assert.Throws<InvalidOperationException> (() => bindable2.SetBinding (property, binding), - "Binding allowed reapplication with a different context"); + Assert.That(bo.GetValue(property), Is.EqualTo("Foo")); } class ComplexPropertyNamesViewModel @@ -204,118 +138,6 @@ namespace Xamarin.Forms.Core.UnitTests Assert.That (bindable.Text, Is.EqualTo ("Value")); } - [Test, Category ("[Binding] Simple paths")] - public void ValueSetOnOneWayWithSimplePathBinding ( - [Values (true, false)] bool setContextFirst, - [Values (true, false)] bool isDefault) - { - const string value = "Foo"; - var viewmodel = new MockViewModel { - Text = value - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWay; - if (isDefault) { - propertyDefault = BindingMode.OneWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null, propertyDefault); - - var binding = new Binding ("Text", 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.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] Simple paths")] - public void ValueSetOnOneWayToSourceWithSimplePathBinding ( - [Values (true, false)] bool setContextFirst, - [Values (true, false)] bool isDefault) - { - const string value = "Foo"; - var viewmodel = new MockViewModel(); - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWayToSource; - if (isDefault) { - propertyDefault = BindingMode.OneWayToSource; - bindingMode = BindingMode.Default; - } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, - defaultValue: value, defaultBindingMode: propertyDefault); - - var binding = new Binding ("Text", 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.Text, - "BindingContext property did not change"); - Assert.That (log.Messages.Count, Is.EqualTo (0), - "An error was logged: " + log.Messages.FirstOrDefault()); - } - - [Test, Category ("[Binding] Simple paths")] - public void ValueSetOnTwoWayWithSimplePathBinding ( - [Values (true, false)] bool setContextFirst, - [Values (true, false)] bool isDefault) - { - const string value = "Foo"; - var viewmodel = new MockViewModel { - Text = value - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.TwoWay; - if (isDefault) { - propertyDefault = BindingMode.TwoWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var binding = new Binding ("Text", 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.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 ValueSetOnOneWayWithComplexPathBinding ( @@ -338,9 +160,8 @@ namespace Xamarin.Forms.Core.UnitTests bindingMode = BindingMode.Default; } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null, propertyDefault); - - var binding = new Binding ("Model.Model.Text", bindingMode); + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), null, propertyDefault); + var binding = new Binding("Model.Model.Text", bindingMode); var bindable = new MockBindable(); if (setContextFirst) { @@ -379,10 +200,8 @@ namespace Xamarin.Forms.Core.UnitTests bindingMode = BindingMode.Default; } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, - defaultValue: value, defaultBindingMode: propertyDefault); - - var binding = new Binding ("Model.Model.Text", bindingMode); + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), value, propertyDefault); + var binding = new Binding("Model.Model.Text", bindingMode); var bindable = new MockBindable(); if (setContextFirst) { @@ -422,9 +241,8 @@ namespace Xamarin.Forms.Core.UnitTests bindingMode = BindingMode.Default; } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var binding = new Binding ("Model.Model.Text", bindingMode); + var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), "default value", propertyDefault); + var binding = new Binding("Model.Model.Text", bindingMode); var bindable = new MockBindable(); if (setContextFirst) { @@ -470,10 +288,10 @@ namespace Xamarin.Forms.Core.UnitTests [Values (true, false)] bool usePrivateSetter) { var value = "FooBar"; - var property = BindableProperty.Create<MockBindable, string> (w => w.Text, "default value", BindingMode.Default); - var binding = new Binding (usePrivateSetter? "PropertyWithPrivateSetter.GetSetProperty": "PropertyWithPublicSetter.GetSetProperty", bindingmode); - var viewmodel = new Outer (new Inner (value)); - var bindable = new MockBindable (); + var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", BindingMode.Default); + var binding = new Binding(usePrivateSetter ? "PropertyWithPrivateSetter.GetSetProperty" : "PropertyWithPublicSetter.GetSetProperty", bindingmode); + var viewmodel = new Outer(new Inner(value)); + var bindable = new MockBindable(); if (setContextFirst) { bindable.BindingContext = viewmodel; @@ -884,142 +702,32 @@ namespace Xamarin.Forms.Core.UnitTests bindingMode = BindingMode.Default; } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); + var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault); - var binding = new Binding (".", bindingMode); + var binding = new Binding(".", bindingMode); const string value = "Foo"; var bindable = new MockBindable(); if (setContextFirst) { bindable.BindingContext = value; - bindable.SetBinding (property, binding); + bindable.SetBinding(property, binding); } else { - bindable.SetBinding (property, binding); + bindable.SetBinding(property, binding); bindable.BindingContext = value; } - Assert.AreEqual (value, bindable.BindingContext, + Assert.AreEqual(value, bindable.BindingContext, "BindingContext property changed"); - Assert.AreEqual (value, bindable.GetValue (property), + Assert.AreEqual(value, bindable.GetValue(property), "Target property did not change"); - Assert.That (log.Messages.Count, Is.EqualTo (0), + Assert.That(log.Messages.Count, Is.EqualTo(0), "An error was logged: " + log.Messages.FirstOrDefault()); } - [Category ("[Binding] Simple paths")] - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithSimplePathOnOneWayBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWay; - if (isDefault) { - propertyDefault = BindingMode.OneWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, new Binding ("Text", bindingMode)); - - viewmodel.Text = newvalue; - Assert.AreEqual (newvalue, bindable.GetValue (property), - "Bindable did not update on binding context property change"); - Assert.AreEqual (newvalue, viewmodel.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] Simple paths")] - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithSimplePathOnOneWayToSourceBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWayToSource; - if (isDefault) { - propertyDefault = BindingMode.OneWayToSource; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, new Binding ("Text", bindingMode)); - - string original = (string)bindable.GetValue (property); - const string value = "value"; - viewmodel.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.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] Simple paths")] - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithSimplePathOnTwoWayBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.TwoWay; - if (isDefault) { - propertyDefault = BindingMode.TwoWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, new Binding ("Text", bindingMode)); - - viewmodel.Text = newvalue; - Assert.AreEqual (newvalue, bindable.GetValue (property), - "Target property did not update change"); - Assert.AreEqual (newvalue, viewmodel.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.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] Complex paths")] - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithComplexPathOnOneWayBinding (bool isDefault) + [Category("[Binding] Complex paths")] + [TestCase(true)] + [TestCase(false)] + public void ValueUpdatedWithComplexPathOnOneWayBinding(bool isDefault) { const string newvalue = "New Value"; var viewmodel = new ComplexMockViewModel { @@ -1373,316 +1081,68 @@ namespace Xamarin.Forms.Core.UnitTests bindingMode = BindingMode.Default; } - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); + var property = BindableProperty.Create<MockBindable, string>(w => w.Text, "default value", propertyDefault); - var binding = new Binding (".", bindingMode); + var binding = new Binding(".", bindingMode); var bindable = new MockBindable(); bindable.BindingContext = "value"; - bindable.SetBinding (property, binding); + bindable.SetBinding(property, binding); const string newvalue = "New Value"; bindable.BindingContext = newvalue; - Assert.AreEqual (newvalue, bindable.GetValue (property), + Assert.AreEqual(newvalue, bindable.GetValue(property), "Target property did not update change"); - Assert.AreEqual (newvalue, bindable.BindingContext, + 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, + bindable.SetValue(property, newvalue2); + Assert.AreEqual(newvalue, bindable.BindingContext, "Self-path Source changed with Target's change"); - Assert.AreEqual (newvalue2, bindable.GetValue (property), + 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()); - } - - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWay; - if (isDefault) { - propertyDefault = BindingMode.OneWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var binding = new Binding ("Text", bindingMode); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, binding); - - bindable.BindingContext = new MockViewModel(); - Assert.AreEqual (null, bindable.GetValue (property)); - - viewmodel.Text = newvalue; - Assert.AreEqual (null, bindable.GetValue (property), - "Target updated from old Source property change"); - Assert.That (log.Messages.Count, Is.EqualTo (0), - "An error was logged: " + log.Messages.FirstOrDefault()); - } - - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithOldContextDoesNotUpdateWithTwoWayBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.TwoWay; - if (isDefault) { - propertyDefault = BindingMode.TwoWay; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var binding = new Binding ("Text", bindingMode); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, binding); - - bindable.BindingContext = new MockViewModel(); - Assert.AreEqual (null, bindable.GetValue (property)); - - viewmodel.Text = newvalue; - Assert.AreEqual (null, bindable.GetValue (property), - "Target updated from old Source property change"); - - string original = viewmodel.Text; - - bindable.SetValue (property, newvalue); - Assert.AreEqual (original, viewmodel.Text, - "Source updated from old Target property change"); - Assert.That (log.Messages.Count, Is.EqualTo (0), - "An error was logged: " + log.Messages.FirstOrDefault()); - } - - [TestCase (true)] - [TestCase (false)] - public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayToSourceBinding (bool isDefault) - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - BindingMode propertyDefault = BindingMode.OneWay; - BindingMode bindingMode = BindingMode.OneWayToSource; - if (isDefault) { - propertyDefault = BindingMode.OneWayToSource; - bindingMode = BindingMode.Default; - } - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault); - - var binding = new Binding ("Text", bindingMode); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, binding); - - bindable.BindingContext = new MockViewModel(); - Assert.AreEqual (property.DefaultValue, bindable.GetValue (property)); - - viewmodel.Text = newvalue; - Assert.AreEqual (property.DefaultValue, bindable.GetValue (property), - "Target updated from old Source property change"); - Assert.That (log.Messages.Count, Is.EqualTo (0), - "An error was logged: " + log.Messages.FirstOrDefault()); - } - - [Test] - public void BindingStaysOnUpdateValueFromBinding() - { - const string newvalue = "New Value"; - var viewmodel = new MockViewModel { - Text = "Foo" - }; - - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null); - - var binding = new Binding ("Text"); - - var bindable = new MockBindable(); - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, binding); - - viewmodel.Text = newvalue; - Assert.AreEqual (newvalue, bindable.GetValue (property)); - - const string newValue2 = "new value 2"; - viewmodel.Text = newValue2; - Assert.AreEqual (newValue2, bindable.GetValue (property)); - - Assert.That (log.Messages.Count, Is.EqualTo (0), + Assert.That(log.Messages.Count, Is.EqualTo(0), "An error was logged: " + log.Messages.FirstOrDefault()); } - [Test] - public void OneWayToSourceContextSetToNull() - { - var binding = new Binding ("Text", BindingMode.OneWayToSource); - - MockBindable bindable = new MockBindable { - BindingContext = new MockViewModel() - }; - bindable.SetBinding (MockBindable.TextProperty, binding); - - Assert.That (() => bindable.BindingContext = null, Throws.Nothing); - } - - [Category ("[Binding] Simple paths")] - [TestCase (BindingMode.OneWay)] - [TestCase (BindingMode.OneWayToSource)] - [TestCase (BindingMode.TwoWay)] - public void SourceAndTargetAreWeakWeakSimplePath (BindingMode mode) + [Category("[Binding] Complex paths")] + [TestCase(BindingMode.OneWay)] + [TestCase(BindingMode.OneWayToSource)] + [TestCase(BindingMode.TwoWay)] + public void SourceAndTargetAreWeakComplexPath(BindingMode mode) { - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", BindingMode.OneWay); + var property = BindableProperty.Create<MockBindable, string>(w => w.Text, "default value"); - var binding = new Binding ("Text", mode); + var binding = new Binding("Model.Model[1]"); 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); - - MockViewModel viewmodel = new MockViewModel(); - weakViewModel = new WeakReference (viewmodel); - - bindable.BindingContext = viewmodel; - bindable.SetBinding (property, binding); - - Assume.That (() => bindable.BindingContext = null, Throws.Nothing); - }; - - create(); + HackAroundMonoSucking(0, property, binding, out weakViewModel, out weakBindable); GC.Collect(); GC.WaitForPendingFinalizers(); 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"); - } + Assert.IsFalse(weakViewModel.IsAlive, "ViewModel wasn't collected"); - internal class ComplexMockViewModel - : MockViewModel - { - public ComplexMockViewModel Model - { - get { return model; } - set - { - if (model == value) - return; - - model = value; - OnPropertyChanged ("Model"); - } - } - - internal int count; - public int QueryCount - { - get { return count++; } - } - - [IndexerName ("Indexer")] - public string this [int v] - { - get { return values[v]; } - set - { - if (values[v] == value) - return; - - values[v] = value; - OnPropertyChanged ("Indexer[" + v + "]"); - } - } - - public string[] Array - { - get; - set; - } - - public object DoStuff() - { - return null; - } - - public object DoStuff (object argument) - { - return null; - } - - string[] values = new string[5]; - ComplexMockViewModel model; - } - - [Category ("[Binding] Complex paths")] - [TestCase (BindingMode.OneWay)] - [TestCase (BindingMode.OneWayToSource)] - [TestCase (BindingMode.TwoWay)] - public void SourceAndTargetAreWeakComplexPath (BindingMode mode) - { - var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value"); - - var binding = new Binding ("Model.Model[1]"); - - WeakReference weakViewModel = null, weakBindable = null; - - HackAroundMonoSucking (0, property, binding, out weakViewModel, out weakBindable); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - - 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"); + Assert.IsFalse(weakBindable.IsAlive, "Bindable wasn't collected"); } // Mono doesn't handle the GC properly until the stack frame where the object is created is popped. // This means calling another method and not just using lambda as works in real .NET - void HackAroundMonoSucking (int i, BindableProperty property, Binding binding, out WeakReference weakViewModel, out WeakReference weakBindable) + void HackAroundMonoSucking(int i, BindableProperty property, Binding binding, out WeakReference weakViewModel, out WeakReference weakBindable) { if (i++ < 1024) { - HackAroundMonoSucking (i, property, binding, out weakViewModel, out weakBindable); + HackAroundMonoSucking(i, property, binding, out weakViewModel, out weakBindable); return; } MockBindable bindable = new MockBindable(); - weakBindable = new WeakReference (bindable); + weakBindable = new WeakReference(bindable); ComplexMockViewModel viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { @@ -1719,16 +1179,18 @@ namespace Xamarin.Forms.Core.UnitTests { var converter = new TestConverter<string, int>(); - var vm = new MockViewModel { Text = "1" }; - var property = BindableProperty.Create<MockBindable, int> (w=>w.TargetInt, 0); + var vm = new MockViewModel ("1"); + var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 0); + var binding = CreateBinding(); + ((Binding)binding).Converter = converter; var bindable = new MockBindable(); - bindable.SetBinding (property, new Binding ("Text", converter: converter)); + bindable.SetBinding(property, binding); bindable.BindingContext = vm; - Assert.AreEqual (1, bindable.GetValue (property)); + Assert.AreEqual(1, bindable.GetValue(property)); - Assert.That (log.Messages.Count, Is.EqualTo (0), + Assert.That(log.Messages.Count, Is.EqualTo(0), "An error was logged: " + log.Messages.FirstOrDefault()); } @@ -2035,71 +1497,50 @@ namespace Xamarin.Forms.Core.UnitTests string text = "foo"; - public string Text - { + public string Text { get { return text; } } - public string Text2 - { + public string Text2 { set { text = value; } } - public string PrivateSetter - { + public string PrivateSetter { get; private set; } } [Test] - [Description ("Paths should not distinguish types, a context change to a completely different type should work.")] + [Description("Paths should not distinguish types, a context change to a completely different type should work.")] public void DifferentContextTypeAccessedCorrectlyWithSamePath() { var vm = new MockViewModel { Text = "text" }; var bindable = new MockBindable(); bindable.BindingContext = vm; - bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text")); + bindable.SetBinding(MockBindable.TextProperty, new Binding("Text")); - Assert.AreEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty)); + Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty)); var dvm = new DifferentViewModel(); bindable.BindingContext = dvm; - Assert.AreEqual (dvm.Text, bindable.GetValue (MockBindable.TextProperty)); - } - - [Test] - public void PropertyChangeBindingsOccurThroughMainThread() - { - var vm = new MockViewModel { Text = "text" }; - - var bindable = new MockBindable(); - bindable.BindingContext = vm; - bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text")); - - bool mainThread = false; - Device.PlatformServices = new MockPlatformServices (invokeOnMainThread: a => mainThread = true); - - vm.Text = "updated"; - - Assert.IsTrue (mainThread, "Binding did not occur on main thread"); - Assert.AreNotEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty), "Binding was applied anyway through other means"); + Assert.AreEqual(dvm.Text, bindable.GetValue(MockBindable.TextProperty)); } [Test] public void Clone() { object param = new object(); - var binding = new Binding (".", converter: new TestConverter<string, int>(), converterParameter: param, stringFormat: "{0}"); + var binding = new Binding(".", converter: new TestConverter<string, int>(), converterParameter: param, stringFormat: "{0}"); var clone = (Binding)binding.Clone(); - Assert.AreSame (binding.Converter, clone.Converter); - Assert.AreSame (binding.ConverterParameter, clone.ConverterParameter); - Assert.AreEqual (binding.Mode, clone.Mode); - Assert.AreEqual (binding.Path, clone.Path); - Assert.AreEqual (binding.StringFormat, clone.StringFormat); + Assert.AreSame(binding.Converter, clone.Converter); + Assert.AreSame(binding.ConverterParameter, clone.ConverterParameter); + Assert.AreEqual(binding.Mode, clone.Mode); + Assert.AreEqual(binding.Path, clone.Path); + Assert.AreEqual(binding.StringFormat, clone.StringFormat); } [Test] @@ -2247,7 +1688,7 @@ namespace Xamarin.Forms.Core.UnitTests Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "MissingProperty", - "Xamarin.Forms.Core.UnitTests.BindingUnitTests+ComplexMockViewModel", + "Xamarin.Forms.Core.UnitTests.BindingBaseUnitTests+ComplexMockViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); @@ -2673,4 +2114,4 @@ namespace Xamarin.Forms.Core.UnitTests Assert.AreEqual ("Baz", label.Text); } } -} +}
\ No newline at end of file diff --git a/Xamarin.Forms.Core.UnitTests/MockViewModel.cs b/Xamarin.Forms.Core.UnitTests/MockViewModel.cs index da2f6f96..bdb3f7ef 100644 --- a/Xamarin.Forms.Core.UnitTests/MockViewModel.cs +++ b/Xamarin.Forms.Core.UnitTests/MockViewModel.cs @@ -3,28 +3,30 @@ using System.Runtime.CompilerServices; namespace Xamarin.Forms.Core.UnitTests { - internal class MockViewModel - : INotifyPropertyChanged + class MockViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; - string text; + public MockViewModel(string text=null) + { + _text = text; + } + + string _text; public virtual string Text { - get { return text; } + get { return _text; } set { - if (text == value) + if (_text == value) return; - text = value; + _text = value; OnPropertyChanged ("Text"); } } protected void OnPropertyChanged ([CallerMemberName] string propertyName = null) { - PropertyChangedEventHandler handler = PropertyChanged; - if (handler != null) - handler (this, new PropertyChangedEventArgs (propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName)); } } -} +}
\ No newline at end of file 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."); + } + } +} diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj index e4a36d1a..243cbce3 100644 --- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj +++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj @@ -180,6 +180,7 @@ <Compile Include="PinchGestureRecognizerTests.cs" /> <Compile Include="AppLinkEntryTests.cs" /> <Compile Include="NativeBindingTests.cs" /> + <Compile Include="TypedBindingUnitTests.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Xamarin.Forms.Core\Xamarin.Forms.Core.csproj"> |