using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using NUnit.Framework; using CategoryAttribute=NUnit.Framework.CategoryAttribute; using DescriptionAttribute=NUnit.Framework.DescriptionAttribute; namespace Xamarin.Forms.Core.UnitTests { [TestFixture] public class BindingUnitTests : BindingBaseUnitTests { class Logger : LogListener { public IReadOnlyList Messages { get { return messages; } } public override void Warning (string category, string message) { messages.Add ("[" + category + "] " + message); } readonly List messages = new List(); } Logger log; [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, string stringFormat = null) { return new Binding ("Text", mode, stringFormat: stringFormat); } [Test] public void Ctor() { const string path = "Foo"; var binding = new Binding (path, BindingMode.OneWayToSource); Assert.AreEqual (path, binding.Path); Assert.AreEqual (BindingMode.OneWayToSource, binding.Mode); } [Test] public void InvalidCtor() { Assert.Throws (() => new Binding (null), "Allowed null Path"); Assert.Throws (() => new Binding (String.Empty), "Allowed empty path"); Assert.Throws (() => new Binding (" "), "Allowed whitespace path"); Assert.Throws (() => new Binding ("Path", (BindingMode)Int32.MaxValue), "Allowed invalid value for BindingMode"); } [Test] [Description ("You should get an exception when trying to change a binding after it's been applied")] public void ChangeBindingAfterApply() { var property = BindableProperty.Create (w => w.Foo, null); var binding = new Binding { Path = "Text" }; var vm = new MockViewModel { Text = "Bar" }; var bo = new MockBindable { BindingContext = vm }; bo.SetBinding (property, binding); Assert.That (() => binding.Path = "path", Throws.InvalidOperationException); Assert.That (() => binding.Converter = null, Throws.InvalidOperationException); Assert.That (() => binding.ConverterParameter = new object(), Throws.InvalidOperationException); } [Test] public void NullPathIsSelf() { var property = BindableProperty.Create (w => w.Foo, null); var binding = new Binding(); var bo = new MockBindable { BindingContext = "Foo" }; bo.SetBinding (property, binding); Assert.That (bo.GetValue (property), Is.EqualTo ("Foo")); } class DoubleViewModel : MockViewModel { public double Value { get; set; } } [Test] public void StringFormatNonStringType() { var property = BindableProperty.Create (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 (w=>w.Text, null); var binding = new Binding ("Text"); bindable.SetBinding (property, binding); var bindable2 = new MockBindable(); bindable2.BindingContext = new MockViewModel(); Assert.Throws (() => bindable2.SetBinding (property, binding), "Binding allowed reapplication with a different context"); } class ComplexPropertyNamesViewModel : MockViewModel { public string Foo_Bar { get; set; } public string @if { get; set; } /*public string P̀ः०‿ { get; set; }*/ public string _UnderscoreStart { get; set; } } [Category ("[Binding] Complex paths")] [TestCase ("Foo_Bar")] [TestCase ("if")] //TODO FIXME [TestCase ("P̀ः०‿")] [TestCase ("_UnderscoreStart")] public void ComplexPropertyNames (string propertyName) { var vm = new ComplexPropertyNamesViewModel(); vm.GetType().GetProperty (propertyName).SetValue (vm, "Value"); var binding = new Binding (propertyName); var bindable = new MockBindable { BindingContext = vm }; bindable.SetBinding (MockBindable.TextProperty, binding); 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 (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 (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 (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 ( [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 (w=>w.Text, null, propertyDefault); var binding = new Binding ("Model.Model.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.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 { } } }; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWayToSource; if (isDefault) { propertyDefault = BindingMode.OneWayToSource; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, defaultValue: value, defaultBindingMode: propertyDefault); var binding = new Binding ("Model.Model.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.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 (w=>w.Text, "default value", propertyDefault); var binding = new Binding ("Model.Model.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.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()); } class Outer { public Outer (Inner inner) { PropertyWithPublicSetter = inner; PropertyWithPrivateSetter = inner; } public Inner PropertyWithPublicSetter { get; set; } public Inner PropertyWithPrivateSetter { get; private set; } } class Inner { public Inner (string property) { GetSetProperty = property; } public string GetSetProperty { get; set; } } [Test, Category ("[Binding] Complex paths")] public void BindingWithNoPublicSetterOnParent ( [Values (true, false)] bool setContextFirst, [Values (BindingMode.OneWay, BindingMode.TwoWay)] BindingMode bindingmode, [Values (true, false)] bool usePrivateSetter) { var value = "FooBar"; var property = BindableProperty.Create (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 (); if (setContextFirst) { bindable.BindingContext = viewmodel; bindable.SetBinding (property, binding); } else { bindable.SetBinding (property, binding); bindable.BindingContext = viewmodel; } Assert.AreEqual (value, viewmodel.PropertyWithPublicSetter.GetSetProperty); Assert.AreEqual (value, bindable.GetValue (property)); if (bindingmode == BindingMode.TwoWay) { var updatedValue = "Qux"; bindable.SetValue (property, updatedValue); Assert.AreEqual (updatedValue, bindable.GetValue (property)); Assert.AreEqual (updatedValue, viewmodel.PropertyWithPublicSetter.GetSetProperty); } Assert.That (log.Messages.Count, Is.EqualTo (0), "An error was logged: " + log.Messages.FirstOrDefault()); } [Test, Category ("[Binding] Indexed paths")] public void ValueSetOnOneWayWithIndexedPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; var viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { Model = new ComplexMockViewModel() } }; viewmodel.Model.Model[1] = value; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWay; if (isDefault) { propertyDefault = BindingMode.OneWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, null, propertyDefault); var binding = new Binding ("Model.Model[1]", 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[1], "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] Indexed paths")] public void ValueSetOnOneWayWithSelfIndexedPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; var viewmodel = new ComplexMockViewModel(); viewmodel[1] = value; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWay; if (isDefault) { propertyDefault = BindingMode.OneWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, null, propertyDefault); var binding = new Binding (".[1]", 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[1], "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] Indexed paths")] public void ValueSetOnOneWayWithIndexedPathArrayBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Bar"; var viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { Array = new [] { "Foo", "Bar" } } }; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWay; if (isDefault) { propertyDefault = BindingMode.OneWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, null, propertyDefault); var binding = new Binding ("Model.Array[1]", 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.Array[1], "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] Indexed paths")] public void ValueSetOnOneWayWithIndexedSelfPathArrayBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "bar"; string[] context = new[] { "foo", value }; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWay; if (isDefault) { propertyDefault = BindingMode.OneWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, null, propertyDefault); var binding = new Binding (".[1]", bindingMode); var bindable = new MockBindable(); if (setContextFirst) { bindable.BindingContext = context; bindable.SetBinding (property, binding); } else { bindable.SetBinding (property, binding); bindable.BindingContext = context; } Assert.AreEqual (value, context[1], "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] Indexed paths")] public void ValueSetOnOneWayToSourceWithIndexedPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; var viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { Model = new ComplexMockViewModel() } }; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWayToSource; if (isDefault) { propertyDefault = BindingMode.OneWayToSource; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, defaultValue: value, defaultBindingMode: propertyDefault); var binding = new Binding ("Model.Model[1]", 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[1], "BindingContext property did not change"); Assert.That (log.Messages.Count, Is.EqualTo (0), "An error was logged: " + log.Messages.FirstOrDefault()); } [Test, Category ("[Binding] Indexed paths")] public void ValueSetOnTwoWayWithIndexedPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; var viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { Model = new ComplexMockViewModel() } }; viewmodel.Model.Model[1] = value; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.TwoWay; if (isDefault) { propertyDefault = BindingMode.TwoWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, "default value", propertyDefault); var binding = new Binding ("Model.Model[1]", 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[1], "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] Indexed paths")] public void ValueSetOnTwoWayWithIndexedArrayPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; var viewmodel = new ComplexMockViewModel { Model = new ComplexMockViewModel { Array = new string[2] } }; viewmodel.Model.Array[1] = value; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.TwoWay; if (isDefault) { propertyDefault = BindingMode.TwoWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, "default value", propertyDefault); var binding = new Binding ("Model.Array[1]", 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.Array[1], "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] Indexed paths")] public void ValueSetOnTwoWayWithIndexedArraySelfPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { const string value = "Foo"; string[] viewmodel = new [] { "bar", value }; BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.TwoWay; if (isDefault) { propertyDefault = BindingMode.TwoWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, "default value", propertyDefault); var binding = new Binding (".[1]", 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[1], "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] Self paths")] public void ValueSetOnOneWayWithSelfPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWay; if (isDefault) { propertyDefault = BindingMode.OneWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, null, propertyDefault); var binding = new Binding (".", bindingMode); const string value = "value"; var bindable = new MockBindable(); if (setContextFirst) { bindable.BindingContext = value; bindable.SetBinding (property, binding); } else { bindable.SetBinding (property, binding); bindable.BindingContext = value; } Assert.AreEqual (value, bindable.BindingContext, "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] Self paths")] public void ValueNotSetOnOneWayToSourceWithSelfPathBinding ( [Values (true, false)] bool isDefault) { BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.OneWayToSource; if (isDefault) { propertyDefault = BindingMode.OneWayToSource; bindingMode = BindingMode.Default; } const string value = "Foo"; var property = BindableProperty.Create (w=>w.Text, defaultValue: value, defaultBindingMode: propertyDefault); var binding = new Binding (".", bindingMode); var bindable = new MockBindable(); Assert.IsNull (bindable.BindingContext); bindable.SetBinding (property, binding); Assert.AreEqual (value, bindable.GetValue (property), "Target property changed"); Assert.IsNull (bindable.BindingContext, "BindingContext changed with self-path binding"); Assert.That (log.Messages.Count, Is.EqualTo (0), "An error was logged: " + log.Messages.FirstOrDefault()); } [Test, Category ("[Binding] Self paths")] public void ValueSetOnTwoWayWithSelfPathBinding ( [Values (true, false)] bool setContextFirst, [Values (true, false)] bool isDefault) { BindingMode propertyDefault = BindingMode.OneWay; BindingMode bindingMode = BindingMode.TwoWay; if (isDefault) { propertyDefault = BindingMode.TwoWay; bindingMode = BindingMode.Default; } var property = BindableProperty.Create (w=>w.Text, "default value", propertyDefault); var binding = new Binding (".", bindingMode); const string value = "Foo"; var bindable = new MockBindable(); if (setContextFirst) { bindable.BindingContext = value; bindable.SetBinding (property, binding); } else { bindable.SetBinding (property, binding); bindable.BindingContext = value; } Assert.AreEqual (value, bindable.BindingContext, "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] 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 (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 (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 (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) { 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model.Text", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model.Text", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model.Text", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model[1]", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model[1]", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var bindable = new MockBindable(); bindable.BindingContext = viewmodel; bindable.SetBinding (property, new Binding ("Model.Model[1]", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); 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 (w=>w.Text, "default value", propertyDefault); const string value = "foo"; var bindable = new MockBindable(); bindable.BindingContext = value; bindable.SetBinding (property, new Binding (".", bindingMode)); 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 (w=>w.Text, "default value", propertyDefault); var binding = new Binding (".", 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 (w=>w.Text, "default value", propertyDefault); var binding = new Binding (".", 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()); } [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 (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 (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 (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 (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), "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 (w=>w.Text, "default value", BindingMode.OneWay); var binding = new Binding ("Text", 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"); } 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 (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"); } // 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) { if (i++ < 1024) { HackAroundMonoSucking (i, property, binding, out weakViewModel, out weakBindable); 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; } class TestConverter : 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(); var vm = new MockViewModel { Text = "1" }; var property = BindableProperty.Create (w=>w.TargetInt, 0); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Text", converter: converter)); 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(); var vm = new MockViewModel(); var property = BindableProperty.Create (w=>w.TargetInt, 1, BindingMode.OneWayToSource); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Text", converter: converter)); 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 (w=>w.Text, "Bar", BindingMode.OneWayToSource); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Text", converter: converter, converterParameter: "Foo")); 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 (w=>w.Text, "Bar", BindingMode.OneWayToSource); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Text", converter: converter)); bindable.BindingContext = vm; Assert.AreEqual ("pt-PT", vm.Text); } #endif [Test] public void SelfBindingConverter() { var converter = new TestConverter (); var property = BindableProperty.Create (w => w.Text, "0"); var bindable = new MockBindable (); bindable.BindingContext = 1; bindable.SetBinding (property, new Binding (Binding.SelfPath, converter:converter)); 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 (b => b.Value, 0f); public float Value { get { return (float)GetValue (ValueProperty); } set { SetValue (ValueProperty, value); } } public static readonly BindableProperty DoneProperty = BindableProperty.Create (b => b.Done, 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(); bindable.SetBinding (MultiplePropertyBindable.ValueProperty, new Binding ("Progress", BindingMode.OneWay)); bindable.SetBinding (MultiplePropertyBindable.DoneProperty, new Binding ("Done", BindingMode.OneWayToSource)); 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 (w => w.Text, "foo bar"); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Model.Text", BindingMode.OneWay)); 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 (w => w.Text, "foo bar"); var bindable = new MockBindable(); bindable.SetBinding (property, new Binding ("Model.Text", BindingMode.OneWay)); 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(); bindable.SetBinding (MultiplePropertyBindable.DoneProperty, new Binding ("QueryCount", BindingMode.OneWay)); 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(); bindable.SetBinding (MultiplePropertyBindable.DoneProperty, new Binding ("QueryCount", BindingMode.OneWay)); bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text", BindingMode.OneWay)); 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; bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text")); Assert.AreEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty)); bindable.BindingContext = vm = new DerivedViewModel { Text = "text" }; Assert.AreEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty)); } internal class EmptyViewModel { } internal class DifferentViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; string text = "foo"; public string Text { get { return text; } } public string Text2 { set { text = value; } } public string PrivateSetter { get; private set; } } [Test] [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")); 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"); } [Test] public void Clone() { object param = new object(); var binding = new Binding (".", converter: new TestConverter(), 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); } [Test] public void PropertyMissingOneWay() { var bindable = new MockBindable { BindingContext = new MockViewModel() }; bindable.Text = "foo"; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Monkeys", BindingMode.OneWay)), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (bindable.Text, Is.EqualTo (MockBindable.TextProperty.DefaultValue)); } [Test] public void PropertyMissingOneWayToSource() { var bindable = new MockBindable { BindingContext = new MockViewModel() }; bindable.Text = "foo"; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Monkeys", BindingMode.OneWayToSource)), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (bindable.Text, Is.EqualTo (bindable.Text)); } [Test] public void PropertyMissingTwoWay() { var bindable = new MockBindable { BindingContext = new MockViewModel() }; bindable.Text = "foo"; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Monkeys")), Throws.Nothing); // The first error is for the initial binding, the second is for reflecting the update back to the default value Assert.That (log.Messages.Count, Is.EqualTo (2), "An error was not logged"); Assert.That (bindable.Text, Is.EqualTo (MockBindable.TextProperty.DefaultValue)); } [Test] public void GetterMissingTwoWay() { var bindable = new MockBindable { BindingContext = new DifferentViewModel() }; bindable.Text = "foo"; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text2")), Throws.Nothing); Assert.That (bindable.Text, Is.EqualTo (MockBindable.TextProperty.DefaultValue)); Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "Text2", "Xamarin.Forms.Core.UnitTests.BindingUnitTests+DifferentViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); Assert.That (((DifferentViewModel) bindable.BindingContext).Text, Is.EqualTo (MockBindable.TextProperty.DefaultValue)); } [Test] public void BindingAppliesAfterGetterPreviouslyMissing() { var bindable = new MockBindable { BindingContext = new EmptyViewModel() }; bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text")); bindable.BindingContext = new MockViewModel { Text = "Foo" }; Assert.That (bindable.Text, Is.EqualTo ("Foo")); Assert.That (log.Messages.Count, Is.Not.GreaterThan (1), "Too many errors were logged"); Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "Text", "Xamarin.Forms.Core.UnitTests.BindingUnitTests+EmptyViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); } [Test] public void SetterMissingTwoWay() { var bindable = new MockBindable { BindingContext = new DifferentViewModel() }; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text")), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "Text", "Xamarin.Forms.Core.UnitTests.BindingUnitTests+DifferentViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); Assert.That (() => bindable.SetValueCore (MockBindable.TextProperty, "foo"), Throws.Nothing); } [Test] public void PrivateSetterTwoWay() { var bindable = new MockBindable { BindingContext = new DifferentViewModel() }; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("PrivateSetter")), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged"); Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "PrivateSetter", "Xamarin.Forms.Core.UnitTests.BindingUnitTests+DifferentViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); Assert.That (() => bindable.SetValueCore (MockBindable.TextProperty, "foo"), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (2), "An error was not logged"); Assert.That (log.Messages[1], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage, "PrivateSetter", "Xamarin.Forms.Core.UnitTests.BindingUnitTests+DifferentViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); } [Test] public void PropertyNotFound() { var bindable = new MockBindable { BindingContext = new MockViewModel() }; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("MissingProperty")), Throws.Nothing); 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.MockViewModel", "Xamarin.Forms.Core.UnitTests.MockBindable", "Text"))); } [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() }; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Model.Text")), Throws.Nothing); Assert.That (log.Messages.Count, Is.EqualTo (0), "An error was logged"); } [Test] public void PropertyNotFoundChained() { var bindable = new MockBindable { BindingContext = new ComplexMockViewModel { Model = new ComplexMockViewModel() } }; Assert.That (() => bindable.SetBinding (MockBindable.TextProperty, new Binding ("Model.MissingProperty")), Throws.Nothing); 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.MockBindable", "Text"))); Assert.That (bindable.Text, Is.EqualTo (MockBindable.TextProperty.DefaultValue)); } [Test] public void CreateBindingNull() { Assert.That (() => Binding.Create (null), Throws.InstanceOf()); } [Test] public void CreateBindingSimple() { Binding binding = Binding.Create (mvm => mvm.Text); Assert.IsNotNull (binding); Assert.AreEqual ("Text", binding.Path); } [Test] public void CreateBindingComplex() { Binding binding = Binding.Create (vm => vm.Model.Model.Text); Assert.IsNotNull (binding); Assert.AreEqual ("Model.Model.Text", binding.Path); } [Test] public void CreateBindingIndexed() { Binding binding = Binding.Create (vm => vm.Model.Model[5]); Assert.IsNotNull (binding); Assert.AreEqual ("Model.Model[5]", binding.Path); } [Test] public void CreateBindingIndexedNonConstant() { int x = 5; Assert.That ( () => Binding.Create (vm => vm.Model.Model[x]), Throws.ArgumentException); } internal class ReferenceTypeIndexerViewModel : MockViewModel { public string this [string value] { get { return value; } } } [Test] public void CreateBindingNullToIndexer() { Assert.That ( () => Binding.Create (vm => vm[null]), Throws.Nothing); } [Test] public void CreateBindingWithMethod() { Assert.That ( () => Binding.Create (vm => vm.DoStuff()), Throws.ArgumentException); } [Test] [Description ("Indexers are seen as methods, we don't want to get them confused with real methods.")] public void CreateBindingWithMethodArgument() { Assert.That ( () => Binding.Create (vm => vm.DoStuff (null)), Throws.ArgumentException); } object Method (MockViewModel vm) { return vm.Text; } [Test] public void CreateBindingMethod() { Func func = vm => vm.Text; Assert.That (() => Binding.Create (vm => func (vm)), Throws.ArgumentException); Assert.That (() => Binding.Create (vm => Method (vm)), Throws.ArgumentException); } [Test] public void CreateBindingPrivateIndexer() { Assert.That (() => Binding.Create (vm => vm[5]), Throws.ArgumentException); } class InternalIndexerViewModel { internal int this [int x] { get { return x; } } } [Test] public void CreateBindingInvalidExpression() { Assert.That (() => Binding.Create (vm => vm.Text + vm.Text), Throws.ArgumentException); Assert.That (() => Binding.Create (vm => 5), Throws.ArgumentException); Assert.That (() => Binding.Create (vm => null), Throws.ArgumentException); } [Test] public void SetBindingContextBeforeContextBindingAndInnerBindings () { var label = new Label (); var view = new StackLayout { Children = {label} }; view.BindingContext = new {item0 = "Foo", item1 = "Bar"}; label.SetBinding (BindableObject.BindingContextProperty, "item0"); label.SetBinding (Label.TextProperty, Binding.SelfPath); Assert.AreEqual ("Foo", label.Text); } [Test] public void SetBindingContextAndInnerBindingBeforeContextBinding () { var label = new Label (); var view = new StackLayout { Children = {label} }; view.BindingContext = new {item0 = "Foo", item1 = "Bar"}; label.SetBinding (Label.TextProperty, Binding.SelfPath); label.SetBinding (BindableObject.BindingContextProperty, "item0"); Assert.AreEqual ("Foo", label.Text); } [Test] public void SetBindingContextAfterContextBindingAndInnerBindings () { var label = new Label (); var view = new StackLayout { Children = {label} }; label.SetBinding (BindableObject.BindingContextProperty, "item0"); label.SetBinding (Label.TextProperty, Binding.SelfPath); view.BindingContext = new {item0 = "Foo", item1 = "Bar"}; Assert.AreEqual ("Foo", label.Text); } [Test] public void SetBindingContextAfterInnerBindingsAndContextBinding () { var label = new Label (); var view = new StackLayout { Children = {label} }; label.SetBinding (Label.TextProperty, Binding.SelfPath); label.SetBinding (BindableObject.BindingContextProperty, "item0"); view.BindingContext = new {item0 = "Foo", item1 = "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, "Text", 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, "Text", 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, "Model"); }, Throws.Nothing); Assert.That (slider.Value, Is.EqualTo (Slider.ValueProperty.DefaultValue)); } class NullViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public string Foo { get; set; } public string Bar { get; set; } public void SignalAllPropertiesChanged (bool useNull) { var changed = PropertyChanged; if (changed != null) changed (this, new PropertyChangedEventArgs ((useNull) ? null : String.Empty)); } } class MockBindable2 : MockBindable { public static readonly BindableProperty Text2Property = BindableProperty.Create ( b => b.Text2, "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, "Foo"); bindable.SetBinding (MockBindable2.Text2Property, "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"; label.SetBinding (Label.TextProperty, Binding.SelfPath); Assert.AreEqual ("bindingcontext", label.Text); label.SetBinding (Label.TextProperty, new Binding (Binding.SelfPath, source: "bindingsource")); 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 () { TestViewModel viewmodel = new TestViewModel(); int i = 0; Action create = null; create = () => { if (i++ < 1024) { create(); return; } var button = new Button (); button.SetBinding (Button.TextProperty, "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 BindingCreatesSingleSubscription () { TestViewModel viewmodel = new TestViewModel(); var button = new Button (); button.SetBinding (Button.TextProperty, "Foo"); button.BindingContext = viewmodel; Assert.That (viewmodel.InvocationListSize (), Is.EqualTo (1)); } public class IndexedViewModel : INotifyPropertyChanged { Dictionary dict = new Dictionary (); 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 (); //viewModel["Foo"] = "Bar"; label.BindingContext = new { Data = viewModel }; label.SetBinding (Label.TextProperty, "Data[Foo]"); Assert.AreEqual (null, label.Text); viewModel["Foo"] = "Baz"; Assert.AreEqual ("Baz", label.Text); } } }