2020-04-22 10:57:06 +03:00
|
|
|
|
using System;
|
2020-09-29 13:15:44 +03:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
2020-04-22 10:57:06 +03:00
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Linq;
|
2020-09-29 13:15:44 +03:00
|
|
|
|
using System.Runtime.CompilerServices;
|
2020-04-22 10:57:06 +03:00
|
|
|
|
using System.Text;
|
2020-09-29 13:15:44 +03:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using NUnit.Framework;
|
|
|
|
|
using Xamarin.Forms;
|
2020-04-22 10:57:06 +03:00
|
|
|
|
|
|
|
|
|
namespace Xamarin.Forms.Core.UnitTests
|
|
|
|
|
{
|
|
|
|
|
[TestFixture]
|
|
|
|
|
public class MultiBindingTests : BaseTestFixture
|
|
|
|
|
{
|
|
|
|
|
const string c_Fallback = "First Middle Last";
|
|
|
|
|
const string c_TargetNull = "No Name Given";
|
|
|
|
|
|
|
|
|
|
[SetUp]
|
|
|
|
|
public override void Setup()
|
|
|
|
|
{
|
|
|
|
|
base.Setup();
|
|
|
|
|
Device.PlatformServices = new MockPlatformServices();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TearDown]
|
|
|
|
|
public override void TearDown()
|
|
|
|
|
{
|
|
|
|
|
base.TearDown();
|
|
|
|
|
Device.PlatformServices = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
2020-06-10 21:39:45 +03:00
|
|
|
|
public void TestChildOneWayOnMultiTwoWay()
|
2020-04-22 10:57:06 +03:00
|
|
|
|
{
|
|
|
|
|
var group = new GroupViewModel();
|
|
|
|
|
var stack = new StackLayout
|
|
|
|
|
{
|
|
|
|
|
BindingContext = group.Person1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
string oldName = group.Person1.FullName;
|
|
|
|
|
string oldFirstName = group.Person1.FirstName;
|
|
|
|
|
string oldMiddleName = group.Person1.MiddleName;
|
|
|
|
|
string oldLastName = group.Person1.LastName;
|
|
|
|
|
|
|
|
|
|
var label = new Label();
|
|
|
|
|
label.SetBinding(Label.TextProperty, new MultiBinding
|
|
|
|
|
{
|
|
|
|
|
Bindings = new Collection<BindingBase>
|
|
|
|
|
{
|
|
|
|
|
new Binding(nameof(PersonViewModel.FirstName), mode: BindingMode.OneWay),
|
|
|
|
|
new Binding(nameof(PersonViewModel.MiddleName)),
|
|
|
|
|
new Binding(nameof(PersonViewModel.LastName)),
|
|
|
|
|
},
|
|
|
|
|
Converter = new StringConcatenationConverter(),
|
|
|
|
|
Mode = BindingMode.TwoWay,
|
|
|
|
|
});
|
|
|
|
|
stack.Children.Add(label);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(oldName, label.Text);
|
|
|
|
|
Assert.AreEqual(oldName, group.Person1.FullName);
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
label.SetValueCore(Label.TextProperty, $"{oldFirstName.ToUpper()} {oldMiddleName} {oldLastName.ToUpper()}", Internals.SetValueFlags.None);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual($"{oldFirstName} {oldMiddleName} {oldLastName.ToUpper()}", group.Person1.FullName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestRelativeSources()
|
|
|
|
|
{
|
|
|
|
|
// Self
|
|
|
|
|
var entry1 = new Entry()
|
|
|
|
|
{
|
|
|
|
|
FontFamily = "Courier New",
|
|
|
|
|
FontSize = 12,
|
|
|
|
|
FontAttributes = FontAttributes.Italic
|
|
|
|
|
};
|
|
|
|
|
entry1.SetBinding(Entry.TextProperty,
|
|
|
|
|
new MultiBinding
|
|
|
|
|
{
|
|
|
|
|
Bindings = new Collection<BindingBase>
|
|
|
|
|
{
|
|
|
|
|
new Binding(nameof(Entry.FontFamily), source: RelativeBindingSource.Self),
|
|
|
|
|
new Binding(nameof(Entry.FontSize), source: RelativeBindingSource.Self),
|
|
|
|
|
new Binding(nameof(Entry.FontAttributes), source: RelativeBindingSource.Self),
|
|
|
|
|
},
|
|
|
|
|
Converter = new StringConcatenationConverter()
|
|
|
|
|
});
|
|
|
|
|
Assert.AreEqual("Courier New 12 Italic", entry1.Text);
|
|
|
|
|
// Our unit test's ConvertBack should throw an exception below because the desired
|
|
|
|
|
// return types aren't all strings
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.Throws<Exception>(() => entry1.SetValueCore(Entry.TextProperty, "Arial 12 Italic", Internals.SetValueFlags.None));
|
2020-04-22 10:57:06 +03:00
|
|
|
|
|
|
|
|
|
// FindAncestor and FindAncestorBindingContext
|
|
|
|
|
// are already tested in TestNestedMultiBindings
|
|
|
|
|
TestNestedMultiBindings();
|
|
|
|
|
|
|
|
|
|
// TemplatedParent
|
|
|
|
|
var templ = new ControlTemplate(typeof(ExpanderControlTemplate));
|
|
|
|
|
var expander = new ExpanderControl
|
|
|
|
|
{
|
|
|
|
|
ControlTemplate = templ,
|
|
|
|
|
Content = new Label { Text = "Content" },
|
|
|
|
|
IsEnabled = true,
|
|
|
|
|
IsExpanded = true
|
|
|
|
|
};
|
|
|
|
|
var cp = expander.Children[0].LogicalChildren[1] as ContentPresenter;
|
|
|
|
|
Assert.IsTrue(cp.IsVisible);
|
|
|
|
|
expander.IsEnabled = false;
|
|
|
|
|
Assert.IsFalse(cp.IsVisible);
|
|
|
|
|
expander.IsEnabled = true;
|
|
|
|
|
Assert.IsTrue(cp.IsVisible);
|
|
|
|
|
expander.IsExpanded = false;
|
|
|
|
|
Assert.IsFalse(cp.IsVisible);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestNestedMultiBindings()
|
|
|
|
|
{
|
|
|
|
|
var group = new GroupViewModel();
|
|
|
|
|
var stack = new StackLayout
|
|
|
|
|
{
|
|
|
|
|
BindingContext = group
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var checkBox = new CheckBox();
|
|
|
|
|
checkBox.SetBinding(
|
|
|
|
|
CheckBox.IsCheckedProperty,
|
2020-09-29 13:15:44 +03:00
|
|
|
|
new MultiBinding
|
|
|
|
|
{
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Bindings = {
|
|
|
|
|
new MultiBinding {
|
|
|
|
|
Bindings = {
|
2020-04-22 10:57:06 +03:00
|
|
|
|
new Binding(nameof(PersonViewModel.IsOver16)),
|
|
|
|
|
new Binding(nameof(PersonViewModel.HasPassedTest)),
|
|
|
|
|
new Binding(nameof(PersonViewModel.IsSuspended), converter: new Inverter()),
|
|
|
|
|
new Binding(
|
|
|
|
|
nameof(GroupViewModel.SuspendAll),
|
|
|
|
|
converter: new Inverter(),
|
|
|
|
|
source: new RelativeBindingSource(
|
|
|
|
|
RelativeBindingSourceMode.FindAncestorBindingContext,
|
|
|
|
|
ancestorType: typeof(GroupViewModel)))
|
|
|
|
|
},
|
|
|
|
|
Converter = new AllTrueMultiConverter()
|
|
|
|
|
},
|
|
|
|
|
new Binding(nameof(PersonViewModel.IsMonarch)),
|
|
|
|
|
new Binding(
|
|
|
|
|
$"{nameof(Element.BindingContext)}.{nameof(GroupViewModel.PardonAllSuspensions)}",
|
|
|
|
|
source: new RelativeBindingSource(RelativeBindingSourceMode.FindAncestor, typeof(StackLayout))),
|
|
|
|
|
},
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Converter = new AnyTrueMultiConverter(),
|
2020-06-16 10:24:33 +03:00
|
|
|
|
FallbackValue = "false", //use a string literal here to test xaml conversion
|
2020-04-22 10:57:06 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ^^
|
|
|
|
|
// CanDrive = (IsOver16 && HasPassedTest && !IsSuspended && !Group.SuspendAll) || IsMonarch || Group.PardonAllSuspensions
|
|
|
|
|
|
|
|
|
|
checkBox.BindingContext = group.Person5;
|
|
|
|
|
stack.Children.Add(checkBox);
|
|
|
|
|
|
|
|
|
|
// Monarch can do whatever she wants
|
|
|
|
|
Assert.IsTrue(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// ... Until being deposed after a coup
|
|
|
|
|
group.Person5.IsMonarch = false;
|
|
|
|
|
Assert.IsFalse(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// After passing test she can drive again
|
|
|
|
|
group.Person5.HasPassedTest = true;
|
|
|
|
|
Assert.IsTrue(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// Martial law declared; no one can drive
|
|
|
|
|
group.SuspendAll = true;
|
|
|
|
|
Assert.IsFalse(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// Martial law is over
|
|
|
|
|
group.SuspendAll = false;
|
|
|
|
|
Assert.IsTrue(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// But she got in an accident and now can't drive again
|
|
|
|
|
group.Person5.IsSuspended = true;
|
|
|
|
|
Assert.IsFalse(checkBox.IsChecked);
|
|
|
|
|
|
|
|
|
|
// The new PM has pardoned everyone after the end of the rebellion
|
|
|
|
|
group.PardonAllSuspensions = true;
|
|
|
|
|
Assert.IsTrue(checkBox.IsChecked);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestConverterReturnValues()
|
|
|
|
|
{
|
|
|
|
|
var group = new GroupViewModel();
|
|
|
|
|
var stack = new StackLayout
|
|
|
|
|
{
|
|
|
|
|
BindingContext = group
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
string oldName, oldFirstName, oldMiddleName, oldLastName, newLabelText;
|
|
|
|
|
|
|
|
|
|
// "Convert" return values
|
|
|
|
|
oldName = group.Person1.FullName;
|
|
|
|
|
oldFirstName = group.Person1.FirstName;
|
|
|
|
|
var label1 = GenerateNameLabel(nameof(group.Person1), BindingMode.TwoWay);
|
|
|
|
|
stack.Children.Add(label1);
|
|
|
|
|
group.Person1.FirstName = "DoNothing";
|
|
|
|
|
Assert.AreEqual(oldName, label1.Text);
|
|
|
|
|
Assert.AreEqual("DoNothing", group.Person1.FirstName);
|
|
|
|
|
|
|
|
|
|
group.Person1.FirstName = "UnsetValue";
|
|
|
|
|
Assert.AreEqual(c_Fallback, label1.Text);
|
|
|
|
|
Assert.AreEqual("UnsetValue", group.Person1.FirstName);
|
|
|
|
|
|
|
|
|
|
group.Person1.FirstName = "null";
|
|
|
|
|
Assert.AreEqual(c_TargetNull, label1.Text);
|
|
|
|
|
Assert.AreEqual("null", group.Person1.FirstName);
|
|
|
|
|
|
|
|
|
|
// "ConvertBack" return values
|
|
|
|
|
oldName = group.Person2.FullName;
|
|
|
|
|
oldFirstName = group.Person2.FirstName;
|
|
|
|
|
oldMiddleName = group.Person2.MiddleName;
|
|
|
|
|
oldLastName = group.Person2.LastName;
|
|
|
|
|
|
|
|
|
|
var label2 = GenerateNameLabel(nameof(group.Person2), BindingMode.TwoWay);
|
|
|
|
|
stack.Children.Add(label2);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
label2.SetValueCore(Label.TextProperty, $"DoNothing {oldMiddleName} {oldLastName.ToUpper()}", Internals.SetValueFlags.None);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual($"{oldFirstName} {oldMiddleName} {oldLastName.ToUpper()}", group.Person2.FullName);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.AreEqual($"DoNothing {oldMiddleName} {oldLastName.ToUpper()}", label2.Text);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
|
|
|
|
|
label2.Text = oldName;
|
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
|
|
|
|
Assert.AreEqual(oldName, label2.Text);
|
|
|
|
|
// Any UnsetValue prevents any changes to source but target accepts value
|
2020-06-10 21:39:45 +03:00
|
|
|
|
label2.SetValueCore(Label.TextProperty, $"{oldFirstName.ToUpper()} UnsetValue {oldLastName}");
|
|
|
|
|
Assert.AreEqual($"{oldFirstName.ToUpper()} {oldMiddleName} {oldLastName}", group.Person2.FullName);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual($"{oldFirstName.ToUpper()} UnsetValue {oldLastName}", label2.Text);
|
|
|
|
|
|
|
|
|
|
label2.Text = oldName;
|
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
|
|
|
|
Assert.AreEqual(oldName, label2.Text);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
label2.SetValueCore(Label.TextProperty, "null");
|
2020-04-22 10:57:06 +03:00
|
|
|
|
// Returning null prevents changes to source but target accepts value
|
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
|
|
|
|
Assert.AreEqual("null", label2.Text);
|
|
|
|
|
|
|
|
|
|
// Insufficient memebrs in ConvertBack array don't affect remaining
|
|
|
|
|
label2.Text = oldName;
|
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
|
|
|
|
Assert.AreEqual(oldName, label2.Text);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
label2.SetValueCore(Label.TextProperty, $"Duck Duck", Internals.SetValueFlags.None);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual($"Duck Duck {oldLastName}", group.Person2.FullName);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.AreEqual($"Duck Duck", label2.Text);
|
2020-09-29 13:15:44 +03:00
|
|
|
|
|
2020-04-22 10:57:06 +03:00
|
|
|
|
// Too many members are no problem either
|
|
|
|
|
label2.Text = oldName;
|
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
2020-09-29 13:15:44 +03:00
|
|
|
|
label2.SetValueCore(Label.TextProperty, oldName + " Extra", Internals.SetValueFlags.None);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual(oldName, group.Person2.FullName);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.AreEqual(oldName + " Extra", label2.Text);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
//[Test]
|
|
|
|
|
//public void TestEfficiency()
|
|
|
|
|
//{
|
|
|
|
|
// var group = new GroupViewModel();
|
|
|
|
|
// var stack = new StackLayout
|
|
|
|
|
// {
|
|
|
|
|
// BindingContext = group.Person1
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
// string oldName = group.Person1.FullName;
|
|
|
|
|
|
|
|
|
|
// var converter = new StringConcatenationConverter();
|
|
|
|
|
|
|
|
|
|
// var label = new Label();
|
|
|
|
|
// label.SetBinding(Label.TextProperty, new MultiBinding
|
|
|
|
|
// {
|
|
|
|
|
// Bindings = new Collection<BindingBase>
|
|
|
|
|
// {
|
|
|
|
|
// new Binding(nameof(PersonViewModel.FirstName)),
|
|
|
|
|
// new Binding(nameof(PersonViewModel.MiddleName)),
|
|
|
|
|
// new Binding(nameof(PersonViewModel.LastName)),
|
|
|
|
|
// },
|
|
|
|
|
// Converter = converter,
|
|
|
|
|
// Mode = BindingMode.TwoWay,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// // Initial binding should result in 1 Convert, no ConvertBack's
|
|
|
|
|
// Assert.AreEqual(1, converter.Converts);
|
|
|
|
|
// Assert.AreEqual(0, converter.ConvertBacks);
|
|
|
|
|
|
|
|
|
|
// // Parenting results in bctx change; should be 1 additional Convert, no ConvertBack's
|
|
|
|
|
// stack.Children.Add(label);
|
|
|
|
|
// Assert.AreEqual(group.Person1.FullName, label.Text);
|
|
|
|
|
// Assert.AreEqual(2, converter.Converts);
|
|
|
|
|
// Assert.AreEqual(0, converter.ConvertBacks);
|
|
|
|
|
|
|
|
|
|
// // Source change results in 1 additional Convert, no ConvertBack's
|
|
|
|
|
// group.Person1.FirstName = group.Person1.FullName.ToUpper();
|
|
|
|
|
// Assert.AreEqual(3, converter.Converts);
|
|
|
|
|
// Assert.AreEqual(0, converter.ConvertBacks);
|
|
|
|
|
|
|
|
|
|
// // Target change results in 1 ConvertBack, one additional Convert
|
|
|
|
|
// label.Text = oldName;
|
|
|
|
|
// Assert.AreEqual(oldName, group.Person1.FullName);
|
|
|
|
|
// Assert.AreEqual(4, converter.Converts);
|
|
|
|
|
// Assert.AreEqual(1, converter.ConvertBacks);
|
|
|
|
|
//}
|
2020-04-22 10:57:06 +03:00
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestBindingModes()
|
|
|
|
|
{
|
|
|
|
|
var group = new GroupViewModel();
|
|
|
|
|
var stack = new StackLayout
|
|
|
|
|
{
|
|
|
|
|
BindingContext = group
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
string oldName = group.Person1.FullName;
|
|
|
|
|
var label1W = GenerateNameLabel(nameof(group.Person1), BindingMode.OneWay);
|
|
|
|
|
stack.Children.Add(label1W);
|
|
|
|
|
Assert.AreEqual(group.Person1.FullName, label1W.Text);
|
|
|
|
|
label1W.SetValueCore(Label.TextProperty, "don't change source", Internals.SetValueFlags.None);
|
|
|
|
|
Assert.AreEqual(oldName, group.Person1.FullName);
|
|
|
|
|
|
|
|
|
|
var label2W = GenerateNameLabel(nameof(group.Person2), BindingMode.TwoWay);
|
|
|
|
|
stack.Children.Add(label2W);
|
|
|
|
|
Assert.AreEqual(group.Person2.FullName, label2W.Text);
|
|
|
|
|
label2W.Text = group.Person2.FullName.ToUpper();
|
|
|
|
|
Assert.AreEqual(group.Person2.FullName.ToUpper(), label2W.Text);
|
|
|
|
|
|
|
|
|
|
oldName = group.Person3.FullName;
|
|
|
|
|
var label1WTS = GenerateNameLabel(nameof(group.Person3), BindingMode.OneWayToSource);
|
|
|
|
|
stack.Children.Add(label1WTS);
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.AreEqual(Label.TextProperty.DefaultValue, label1WTS.Text);
|
|
|
|
|
label1WTS.SetValueCore(Label.TextProperty, oldName, Internals.SetValueFlags.None);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
Assert.AreEqual(oldName, label1WTS.Text);
|
|
|
|
|
Assert.AreEqual(oldName, group.Person3.FullName);
|
|
|
|
|
|
|
|
|
|
oldName = group.Person4.FullName;
|
|
|
|
|
var label1T = GenerateNameLabel(nameof(group.Person4), BindingMode.OneTime);
|
|
|
|
|
stack.Children.Add(label1T);
|
|
|
|
|
Assert.AreEqual(group.Person4.FullName, label1T.Text);
|
|
|
|
|
group.Person4.FirstName = "Do";
|
|
|
|
|
group.Person4.MiddleName = "Not";
|
|
|
|
|
group.Person4.LastName = "Update";
|
|
|
|
|
// changing source values should not trigger update
|
|
|
|
|
Assert.AreEqual(oldName, label1T.Text);
|
|
|
|
|
Assert.AreEqual("Do Not Update", group.Person4.FullName);
|
|
|
|
|
group.Person4 = group.Person1;
|
|
|
|
|
// changing the bctx should trigger update
|
|
|
|
|
Assert.AreEqual(group.Person1.FullName, label1T.Text);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
[Test]
|
|
|
|
|
public void TestStringFormat()
|
|
|
|
|
{
|
|
|
|
|
var property = BindableProperty.Create("foo", typeof(string), typeof(MockBindable), null);
|
|
|
|
|
var bindable = new MockBindable();
|
2020-09-29 13:15:44 +03:00
|
|
|
|
var multibinding = new MultiBinding
|
|
|
|
|
{
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Bindings = {
|
|
|
|
|
new Binding ("foo"),
|
|
|
|
|
new Binding ("bar"),
|
|
|
|
|
new Binding ("baz"),
|
|
|
|
|
},
|
|
|
|
|
StringFormat = "{0} - {1} - {2}"
|
|
|
|
|
};
|
2020-09-29 13:15:44 +03:00
|
|
|
|
Assert.DoesNotThrow(() => bindable.SetBinding(property, multibinding));
|
|
|
|
|
Assert.DoesNotThrow(() => bindable.BindingContext = new { foo = "FOO", bar = 42, baz = "BAZ" });
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Assert.That(bindable.GetValue(property), Is.EqualTo("FOO - 42 - BAZ"));
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-15 10:42:53 +03:00
|
|
|
|
[Test]
|
|
|
|
|
public void TestConverterWithStringFormat()
|
|
|
|
|
{
|
|
|
|
|
var property = BindableProperty.Create("foo", typeof(string), typeof(MockBindable), null);
|
|
|
|
|
var bindable = new MockBindable();
|
2020-10-16 18:35:11 +03:00
|
|
|
|
var multibinding = new MultiBinding
|
|
|
|
|
{
|
2020-10-15 10:42:53 +03:00
|
|
|
|
Bindings = {
|
|
|
|
|
new Binding ("foo"),
|
|
|
|
|
new Binding ("bar", stringFormat: "{0:000}"),
|
|
|
|
|
new Binding ("baz")
|
|
|
|
|
},
|
|
|
|
|
Converter = new StringConcatenationConverter(),
|
|
|
|
|
StringFormat = "Hello {0}"
|
|
|
|
|
};
|
2020-10-16 18:35:11 +03:00
|
|
|
|
Assert.DoesNotThrow(() => bindable.SetBinding(property, multibinding));
|
|
|
|
|
Assert.DoesNotThrow(() => bindable.BindingContext = new { foo = "FOO", bar = 42, baz = "BAZ" });
|
2020-10-15 10:42:53 +03:00
|
|
|
|
Assert.That(bindable.GetValue(property), Is.EqualTo("Hello FOO 042 BAZ"));
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-22 10:57:06 +03:00
|
|
|
|
private Label GenerateNameLabel(string person, BindingMode mode)
|
|
|
|
|
{
|
|
|
|
|
var label = new Label();
|
|
|
|
|
label.SetBinding(Label.TextProperty, new MultiBinding
|
|
|
|
|
{
|
|
|
|
|
Bindings = new Collection<BindingBase>
|
|
|
|
|
{
|
|
|
|
|
new Binding(nameof(PersonViewModel.FirstName)),
|
|
|
|
|
new Binding(nameof(PersonViewModel.MiddleName)),
|
|
|
|
|
new Binding(nameof(PersonViewModel.LastName)),
|
|
|
|
|
},
|
|
|
|
|
Converter = new StringConcatenationConverter(),
|
|
|
|
|
Mode = mode,
|
|
|
|
|
FallbackValue = c_Fallback,
|
|
|
|
|
TargetNullValue = c_TargetNull
|
|
|
|
|
});
|
|
|
|
|
label.SetBinding(Label.BindingContextProperty, new Binding(person));
|
|
|
|
|
return label;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class Inverter : IValueConverter
|
|
|
|
|
{
|
|
|
|
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
bool? b = value as bool?;
|
|
|
|
|
if (b == null)
|
|
|
|
|
return false;
|
|
|
|
|
return !b.Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
return Convert(value, targetType, parameter, culture);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class AllTrueMultiConverter : IMultiValueConverter
|
|
|
|
|
{
|
|
|
|
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (values == null || !targetType.IsAssignableFrom(typeof(bool)))
|
|
|
|
|
// Return UnsetValue to use the binding FallbackValue
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
foreach (var value in values)
|
|
|
|
|
{
|
|
|
|
|
if (!(value is bool b))
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
else if (!b)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (!(value is bool b) || targetTypes.Any(t => !t.IsAssignableFrom(typeof(bool))))
|
|
|
|
|
// Return null to indicate conversion back is not possible
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (b)
|
|
|
|
|
return targetTypes.Select(t => (object)true).ToArray();
|
|
|
|
|
else
|
|
|
|
|
// Can't convert back from false because of ambiguity
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class AnyTrueMultiConverter : IMultiValueConverter
|
|
|
|
|
{
|
|
|
|
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (values == null || !targetType.IsAssignableFrom(typeof(bool)))
|
|
|
|
|
// Return UnsetValue to use the binding FallbackValue
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
foreach (var value in values)
|
|
|
|
|
{
|
|
|
|
|
if (!(value is bool b))
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
else if (b)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (!(value is bool b) || targetTypes.Any(t => !t.IsAssignableFrom(typeof(bool))))
|
|
|
|
|
// Return null to indicate conversion back is not possible
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (!b)
|
|
|
|
|
return targetTypes.Select(t => (object)false).ToArray();
|
|
|
|
|
else
|
|
|
|
|
// Can't convert back from true because of ambiguity
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class StringConcatenationConverter : IMultiValueConverter
|
|
|
|
|
{
|
|
|
|
|
public int Converts { get; private set; }
|
|
|
|
|
|
|
|
|
|
public int ConvertBacks { get; private set; }
|
|
|
|
|
|
|
|
|
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
Converts++;
|
|
|
|
|
|
|
|
|
|
if (values is null)
|
|
|
|
|
return null;
|
2020-06-10 21:39:45 +03:00
|
|
|
|
string separator = parameter as string ?? " ";
|
2020-04-22 10:57:06 +03:00
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
if (values.All(v => string.IsNullOrEmpty(v as string)))
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
|
|
|
|
|
foreach (var value in values)
|
|
|
|
|
{
|
|
|
|
|
if (value as string == "DoNothing")
|
|
|
|
|
return Binding.DoNothing;
|
|
|
|
|
if (value as string == "UnsetValue")
|
|
|
|
|
return BindableProperty.UnsetValue;
|
|
|
|
|
if (value as string == "null")
|
|
|
|
|
return null;
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
if (i != 0 && separator != null)
|
|
|
|
|
sb.Append(separator);
|
2020-04-22 10:57:06 +03:00
|
|
|
|
sb.Append(value?.ToString());
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
ConvertBacks++;
|
|
|
|
|
|
|
|
|
|
string s = value as string;
|
|
|
|
|
if (s == "null" || string.IsNullOrEmpty(s))
|
|
|
|
|
return null;
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
string separator = parameter as string ?? " ";
|
2020-04-22 10:57:06 +03:00
|
|
|
|
|
2020-09-29 13:15:44 +03:00
|
|
|
|
if (!targetTypes.All(t => t == typeof(object)) && !targetTypes.All(t => t == typeof(string)))
|
2020-04-22 10:57:06 +03:00
|
|
|
|
// Normally we'd return null but throw exception just for unit test to catch
|
|
|
|
|
throw new Exception("Invalid targetTypes");
|
|
|
|
|
|
2020-06-10 21:39:45 +03:00
|
|
|
|
var array = s.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries).Cast<object>().ToArray();
|
2020-04-22 10:57:06 +03:00
|
|
|
|
for (int i = 0; i < array.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var str = array[i] as string;
|
|
|
|
|
if (str == "null")
|
|
|
|
|
array[i] = null;
|
|
|
|
|
if (str == "UnsetValue")
|
|
|
|
|
array[i] = BindableProperty.UnsetValue;
|
|
|
|
|
if (str == "DoNothing")
|
|
|
|
|
array[i] = Binding.DoNothing;
|
|
|
|
|
}
|
|
|
|
|
return array;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class GroupViewModel : INotifyPropertyChanged
|
|
|
|
|
{
|
|
|
|
|
PersonViewModel _person1 = new PersonViewModel
|
|
|
|
|
{
|
|
|
|
|
FirstName = "Gaius",
|
|
|
|
|
MiddleName = "Julius",
|
|
|
|
|
LastName = "Caesar",
|
|
|
|
|
IsOver16 = true,
|
|
|
|
|
HasPassedTest = false,
|
|
|
|
|
IsSuspended = false
|
|
|
|
|
};
|
|
|
|
|
PersonViewModel _person2 = new PersonViewModel
|
|
|
|
|
{
|
|
|
|
|
FirstName = "William",
|
|
|
|
|
MiddleName = "Henry",
|
|
|
|
|
LastName = "Gates",
|
|
|
|
|
IsOver16 = true,
|
|
|
|
|
HasPassedTest = true,
|
|
|
|
|
IsSuspended = false
|
|
|
|
|
};
|
|
|
|
|
PersonViewModel _person3 = new PersonViewModel
|
|
|
|
|
{
|
|
|
|
|
FirstName = "John",
|
|
|
|
|
MiddleName = "Fitzgerald",
|
|
|
|
|
LastName = "Kennedy",
|
|
|
|
|
IsOver16 = true,
|
|
|
|
|
HasPassedTest = true,
|
|
|
|
|
IsSuspended = true
|
|
|
|
|
};
|
|
|
|
|
PersonViewModel _person4 = new PersonViewModel
|
|
|
|
|
{
|
|
|
|
|
FirstName = "Harry",
|
|
|
|
|
MiddleName = "James",
|
|
|
|
|
LastName = "Potter",
|
|
|
|
|
HasPassedTest = true,
|
|
|
|
|
IsOver16 = false,
|
|
|
|
|
IsSuspended = false
|
|
|
|
|
};
|
|
|
|
|
PersonViewModel _person5 = new PersonViewModel
|
|
|
|
|
{
|
|
|
|
|
FirstName = "Queen",
|
|
|
|
|
MiddleName = "Elizabeth",
|
|
|
|
|
LastName = "II",
|
|
|
|
|
HasPassedTest = false,
|
|
|
|
|
IsOver16 = true,
|
|
|
|
|
IsSuspended = false,
|
|
|
|
|
IsMonarch = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public PersonViewModel Person1
|
|
|
|
|
{
|
|
|
|
|
get => _person1;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_person1 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PersonViewModel Person2
|
|
|
|
|
{
|
|
|
|
|
get => _person2;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_person2 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PersonViewModel Person3
|
|
|
|
|
{
|
|
|
|
|
get => _person3;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_person3 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PersonViewModel Person4
|
|
|
|
|
{
|
|
|
|
|
get => _person4;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_person4 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PersonViewModel Person5
|
|
|
|
|
{
|
|
|
|
|
get => _person5;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_person5 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _PardonAllSuspensions;
|
|
|
|
|
public bool PardonAllSuspensions
|
|
|
|
|
{
|
|
|
|
|
get => _PardonAllSuspensions;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_PardonAllSuspensions = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _SuspendAll;
|
|
|
|
|
public bool SuspendAll
|
|
|
|
|
{
|
|
|
|
|
get => _SuspendAll;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_SuspendAll = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
|
|
|
|
|
|
void OnPropertyChanged([CallerMemberName] string name = null)
|
|
|
|
|
{
|
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class PersonViewModel : INotifyPropertyChanged
|
|
|
|
|
{
|
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
|
|
|
|
|
|
string _FirstName;
|
|
|
|
|
public string FirstName
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _FirstName;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_FirstName != value)
|
|
|
|
|
{
|
|
|
|
|
_FirstName = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string _MiddleName;
|
|
|
|
|
public string MiddleName
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _MiddleName;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_MiddleName != value)
|
|
|
|
|
{
|
|
|
|
|
_MiddleName = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string _LastName;
|
|
|
|
|
public string LastName
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _LastName;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_LastName != value)
|
|
|
|
|
{
|
|
|
|
|
_LastName = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string FullName => FirstName + " " + MiddleName + " " + LastName;
|
|
|
|
|
|
|
|
|
|
public bool CanDrive => IsOver16 && HasPassedTest && !IsSuspended;
|
|
|
|
|
|
|
|
|
|
bool _IsOver16;
|
|
|
|
|
public bool IsOver16
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _IsOver16;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_IsOver16 != value)
|
|
|
|
|
{
|
|
|
|
|
_IsOver16 = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _HasPassedTest;
|
|
|
|
|
public bool HasPassedTest
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _HasPassedTest;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_HasPassedTest != value)
|
|
|
|
|
{
|
|
|
|
|
_HasPassedTest = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _IsSuspended;
|
|
|
|
|
public bool IsSuspended
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _IsSuspended;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_IsSuspended != value)
|
|
|
|
|
{
|
|
|
|
|
_IsSuspended = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _IsMonarch;
|
|
|
|
|
public bool IsMonarch
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _IsMonarch;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (_IsMonarch != value)
|
|
|
|
|
{
|
|
|
|
|
_IsMonarch = value;
|
|
|
|
|
OnPropertyChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnPropertyChanged([CallerMemberName] string name = null)
|
|
|
|
|
{
|
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[ContentProperty(nameof(Content))]
|
|
|
|
|
public class ExpanderControl : TemplatedView
|
|
|
|
|
{
|
|
|
|
|
#region bool IsExpanded dependency property
|
|
|
|
|
public static readonly BindableProperty IsExpandedProperty = BindableProperty.Create(
|
|
|
|
|
"IsExpanded",
|
|
|
|
|
typeof(bool),
|
|
|
|
|
typeof(ExpanderControl),
|
|
|
|
|
true);
|
|
|
|
|
public bool IsExpanded
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return (bool)GetValue(IsExpandedProperty);
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
SetValue(IsExpandedProperty, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region object Content dependency property
|
|
|
|
|
public static BindableProperty ContentProperty = BindableProperty.Create(
|
|
|
|
|
"Content",
|
|
|
|
|
typeof(View),
|
|
|
|
|
typeof(ExpanderControl),
|
|
|
|
|
null);
|
|
|
|
|
public View Content
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return (View)GetValue(ContentProperty);
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
SetValue(ContentProperty, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ExpanderControlTemplate : Grid
|
|
|
|
|
{
|
|
|
|
|
public ExpanderControlTemplate()
|
|
|
|
|
{
|
|
|
|
|
this.RowDefinitions = new RowDefinitionCollection
|
|
|
|
|
{
|
|
|
|
|
new RowDefinition { Height = new GridLength(0, GridUnitType.Auto)},
|
|
|
|
|
new RowDefinition { Height = new GridLength(1, GridUnitType.Star)}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var expander = new Button { Text = "^" };
|
|
|
|
|
Grid.SetRow(expander, 0);
|
|
|
|
|
this.Children.Add(expander);
|
|
|
|
|
|
|
|
|
|
var cp = new ContentPresenter();
|
|
|
|
|
Grid.SetRow(cp, 1);
|
|
|
|
|
cp.SetBinding(ContentPresenter.ContentProperty, new Binding(
|
|
|
|
|
nameof(ExpanderControl.Content),
|
|
|
|
|
source: new RelativeBindingSource(RelativeBindingSourceMode.TemplatedParent)));
|
|
|
|
|
cp.SetBinding(ContentPresenter.IsVisibleProperty, new MultiBinding
|
|
|
|
|
{
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Bindings = {
|
2020-04-22 10:57:06 +03:00
|
|
|
|
new Binding(nameof(ExpanderControl.IsEnabled), source: RelativeBindingSource.TemplatedParent),
|
|
|
|
|
new Binding(nameof(ExpanderControl.IsExpanded), source: RelativeBindingSource.TemplatedParent)
|
|
|
|
|
},
|
2020-06-10 21:39:45 +03:00
|
|
|
|
Converter = new AllTrueMultiConverter(),
|
|
|
|
|
FallbackValue = false
|
2020-04-22 10:57:06 +03:00
|
|
|
|
});
|
|
|
|
|
this.Children.Add(cp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|